Merge branch 'dev' into feat/old-mac-support
24
.github/dependabot.yaml
vendored
@ -1,17 +1,19 @@
|
|||||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem-
|
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem-
|
||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "cargo"
|
- package-ecosystem: 'cargo'
|
||||||
directory: "src-tauri"
|
directory: 'src-tauri'
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: 'weekly'
|
||||||
- package-ecosystem: "npm"
|
open-pull-requests-limit: 0
|
||||||
|
- package-ecosystem: 'npm'
|
||||||
directories:
|
directories:
|
||||||
- "/"
|
- '/'
|
||||||
- "core"
|
- 'core'
|
||||||
- "docs"
|
- 'docs'
|
||||||
- "extensions"
|
- 'extensions'
|
||||||
- "extensions/*"
|
- 'extensions/*'
|
||||||
- "web-app"
|
- 'web-app'
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: 'weekly'
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
|||||||
37
.github/workflows/autoqa-manual-trigger.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
name: Manual trigger AutoQA Test Runner
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
jan_app_url_windows:
|
||||||
|
description: 'URL to download Jan app for Windows (.exe)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'https://delta.jan.ai/nightly/Jan-nightly_0.6.5-758_x64-setup.exe'
|
||||||
|
jan_app_url_ubuntu:
|
||||||
|
description: 'URL to download Jan app for Ubuntu (.deb)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'https://delta.jan.ai/nightly/Jan-nightly_0.6.5-758_amd64.deb'
|
||||||
|
jan_app_url_macos:
|
||||||
|
description: 'URL to download Jan app for macOS (.dmg)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'https://delta.jan.ai/nightly/Jan-nightly_0.6.5-758_universal.dmg'
|
||||||
|
is_nightly:
|
||||||
|
description: 'Is this a nightly build?'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
call-autoqa-template:
|
||||||
|
uses: ./.github/workflows/autoqa-template.yml
|
||||||
|
with:
|
||||||
|
jan_app_windows_source: ${{ inputs.jan_app_url_windows }}
|
||||||
|
jan_app_ubuntu_source: ${{ inputs.jan_app_url_ubuntu }}
|
||||||
|
jan_app_macos_source: ${{ inputs.jan_app_url_macos }}
|
||||||
|
is_nightly: ${{ inputs.is_nightly }}
|
||||||
|
source_type: 'url'
|
||||||
|
secrets:
|
||||||
|
RP_TOKEN: ${{ secrets.RP_TOKEN }}
|
||||||
396
.github/workflows/autoqa-template.yml
vendored
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
name: Auto QA Test Runner Template
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
jan_app_windows_source:
|
||||||
|
description: 'Windows app source - can be URL or local path'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
jan_app_ubuntu_source:
|
||||||
|
description: 'Ubuntu app source - can be URL or local path'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
jan_app_macos_source:
|
||||||
|
description: 'macOS app source - can be URL or local path'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
is_nightly:
|
||||||
|
description: 'Is this a nightly build?'
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
source_type:
|
||||||
|
description: 'Source type: url or local'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
default: 'url'
|
||||||
|
artifact_name_windows:
|
||||||
|
description: 'Windows artifact name (only needed for local)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
artifact_name_ubuntu:
|
||||||
|
description: 'Ubuntu artifact name (only needed for local)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
artifact_name_macos:
|
||||||
|
description: 'macOS artifact name (only needed for local)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
secrets:
|
||||||
|
RP_TOKEN:
|
||||||
|
description: 'ReportPortal API token'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
windows:
|
||||||
|
runs-on: windows-11-nvidia-gpu
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEFAULT_JAN_APP_URL: 'https://catalog.jan.ai/windows/Jan-nightly_0.6.5-758_x64-setup.exe'
|
||||||
|
DEFAULT_IS_NIGHTLY: 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python 3.13
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
- name: Download artifact (if source_type is local)
|
||||||
|
if: inputs.source_type == 'local'
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact_name_windows }}
|
||||||
|
path: ${{ runner.temp }}/windows-artifact
|
||||||
|
|
||||||
|
- name: Clean existing Jan installations
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
.\autoqa\scripts\windows_cleanup.ps1 -IsNightly "${{ inputs.is_nightly }}"
|
||||||
|
|
||||||
|
- name: Download/Prepare Jan app
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
if ("${{ inputs.source_type }}" -eq "local") {
|
||||||
|
# Find the exe file in the artifact
|
||||||
|
$exeFile = Get-ChildItem -Path "${{ runner.temp }}/windows-artifact" -Recurse -Filter "*.exe" | Select-Object -First 1
|
||||||
|
if ($exeFile) {
|
||||||
|
Write-Host "✅ Found local installer: $($exeFile.FullName)"
|
||||||
|
Copy-Item -Path $exeFile.FullName -Destination "$env:TEMP\jan-installer.exe" -Force
|
||||||
|
Write-Host "✅ Installer copied to: $env:TEMP\jan-installer.exe"
|
||||||
|
# Don't set JAN_APP_PATH here - let the install script set it to the correct installed app path
|
||||||
|
echo "IS_NIGHTLY=${{ inputs.is_nightly }}" >> $env:GITHUB_ENV
|
||||||
|
} else {
|
||||||
|
Write-Error "❌ No .exe file found in artifact"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# Use the existing download script for URLs
|
||||||
|
.\autoqa\scripts\windows_download.ps1 `
|
||||||
|
-WorkflowInputUrl "${{ inputs.jan_app_windows_source }}" `
|
||||||
|
-WorkflowInputIsNightly "${{ inputs.is_nightly }}" `
|
||||||
|
-RepoVariableUrl "${{ vars.JAN_APP_URL }}" `
|
||||||
|
-RepoVariableIsNightly "${{ vars.IS_NIGHTLY }}" `
|
||||||
|
-DefaultUrl "$env:DEFAULT_JAN_APP_URL" `
|
||||||
|
-DefaultIsNightly "$env:DEFAULT_IS_NIGHTLY"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Install Jan app
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
.\autoqa\scripts\windows_install.ps1 -IsNightly "$env:IS_NIGHTLY"
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
working-directory: autoqa
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Run Auto QA Tests
|
||||||
|
working-directory: autoqa
|
||||||
|
shell: powershell
|
||||||
|
env:
|
||||||
|
RP_TOKEN: ${{ secrets.RP_TOKEN }}
|
||||||
|
ENABLE_REPORTPORTAL: 'true'
|
||||||
|
RP_ENDPOINT: 'https://reportportal.menlo.ai'
|
||||||
|
RP_PROJECT: 'default_personal'
|
||||||
|
MAX_TURNS: '50'
|
||||||
|
DELAY_BETWEEN_TESTS: '3'
|
||||||
|
LAUNCH_NAME: 'CI AutoQA Run Windows - ${{ github.run_number }} - ${{ github.ref_name }}'
|
||||||
|
run: |
|
||||||
|
.\scripts\run_tests.ps1 -JanAppPath "$env:JAN_APP_PATH" -ProcessName "$env:JAN_PROCESS_NAME" -RpToken "$env:RP_TOKEN"
|
||||||
|
|
||||||
|
- name: Cleanup after tests
|
||||||
|
if: always()
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
.\autoqa\scripts\windows_post_cleanup.ps1 -IsNightly "${{ inputs.is_nightly }}"
|
||||||
|
|
||||||
|
ubuntu:
|
||||||
|
runs-on: ubuntu-22-04-nvidia-gpu
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEFAULT_JAN_APP_URL: 'https://delta.jan.ai/nightly/Jan-nightly_0.6.4-728_amd64.deb'
|
||||||
|
DEFAULT_IS_NIGHTLY: 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python 3.13
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
- name: Download artifact (if source_type is local)
|
||||||
|
if: inputs.source_type == 'local'
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact_name_ubuntu }}
|
||||||
|
path: ${{ runner.temp }}/ubuntu-artifact
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
x11-utils \
|
||||||
|
python3-tk \
|
||||||
|
python3-dev \
|
||||||
|
wmctrl \
|
||||||
|
xdotool \
|
||||||
|
libnss3-dev \
|
||||||
|
libgconf-2-4 \
|
||||||
|
libxss1 \
|
||||||
|
libasound2 \
|
||||||
|
libxtst6 \
|
||||||
|
libgtk-3-0 \
|
||||||
|
libgbm-dev \
|
||||||
|
libxshmfence1 \
|
||||||
|
libxrandr2 \
|
||||||
|
libpangocairo-1.0-0 \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libcairo-gobject2 \
|
||||||
|
libgdk-pixbuf2.0-0 \
|
||||||
|
gnome-screenshot
|
||||||
|
|
||||||
|
- name: Setup script permissions
|
||||||
|
run: |
|
||||||
|
chmod +x autoqa/scripts/setup_permissions.sh
|
||||||
|
./autoqa/scripts/setup_permissions.sh
|
||||||
|
|
||||||
|
- name: Clean existing Jan installations
|
||||||
|
run: |
|
||||||
|
./autoqa/scripts/ubuntu_cleanup.sh
|
||||||
|
|
||||||
|
- name: Download/Prepare Jan app
|
||||||
|
run: |
|
||||||
|
if [ "${{ inputs.source_type }}" = "local" ]; then
|
||||||
|
# Find the deb file in the artifact
|
||||||
|
DEB_FILE=$(find "${{ runner.temp }}/ubuntu-artifact" -name "*.deb" -type f | head -1)
|
||||||
|
if [ -n "$DEB_FILE" ]; then
|
||||||
|
echo "✅ Found local installer: $DEB_FILE"
|
||||||
|
cp "$DEB_FILE" "/tmp/jan-installer.deb"
|
||||||
|
echo "✅ Installer copied to: /tmp/jan-installer.deb"
|
||||||
|
echo "JAN_APP_PATH=/tmp/jan-installer.deb" >> $GITHUB_ENV
|
||||||
|
echo "IS_NIGHTLY=${{ inputs.is_nightly }}" >> $GITHUB_ENV
|
||||||
|
if [ "${{ inputs.is_nightly }}" = "true" ]; then
|
||||||
|
echo "JAN_PROCESS_NAME=Jan-nightly" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "JAN_PROCESS_NAME=Jan" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ No .deb file found in artifact"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Use the existing download script for URLs
|
||||||
|
./autoqa/scripts/ubuntu_download.sh \
|
||||||
|
"${{ inputs.jan_app_ubuntu_source }}" \
|
||||||
|
"${{ inputs.is_nightly }}" \
|
||||||
|
"${{ vars.JAN_APP_URL_LINUX }}" \
|
||||||
|
"${{ vars.IS_NIGHTLY }}" \
|
||||||
|
"$DEFAULT_JAN_APP_URL" \
|
||||||
|
"$DEFAULT_IS_NIGHTLY"
|
||||||
|
|
||||||
|
# Set the correct environment variables for the test runner
|
||||||
|
echo "JAN_APP_PATH=/tmp/jan-installer.deb" >> $GITHUB_ENV
|
||||||
|
if [ "${{ inputs.is_nightly }}" = "true" ]; then
|
||||||
|
echo "JAN_PROCESS_NAME=Jan-nightly" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "JAN_PROCESS_NAME=Jan" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Jan app
|
||||||
|
run: |
|
||||||
|
./autoqa/scripts/ubuntu_install.sh "$IS_NIGHTLY"
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
working-directory: autoqa
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Run Auto QA Tests
|
||||||
|
working-directory: autoqa
|
||||||
|
env:
|
||||||
|
RP_TOKEN: ${{ secrets.RP_TOKEN }}
|
||||||
|
ENABLE_REPORTPORTAL: 'true'
|
||||||
|
RP_ENDPOINT: 'https://reportportal.menlo.ai'
|
||||||
|
RP_PROJECT: 'default_personal'
|
||||||
|
MAX_TURNS: '50'
|
||||||
|
DELAY_BETWEEN_TESTS: '3'
|
||||||
|
LAUNCH_NAME: 'CI AutoQA Run Ubuntu - ${{ github.run_number }} - ${{ github.ref_name }}'
|
||||||
|
run: |
|
||||||
|
./scripts/run_tests.sh "$JAN_APP_PATH" "$JAN_PROCESS_NAME" "$RP_TOKEN" "ubuntu"
|
||||||
|
|
||||||
|
- name: Cleanup after tests
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
./autoqa/scripts/ubuntu_post_cleanup.sh "$IS_NIGHTLY"
|
||||||
|
|
||||||
|
macos:
|
||||||
|
runs-on: macos-selfhosted-15-arm64
|
||||||
|
timeout-minutes: 60
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEFAULT_JAN_APP_URL: 'https://delta.jan.ai/nightly/Jan-nightly_0.6.4-728_universal.dmg'
|
||||||
|
DEFAULT_IS_NIGHTLY: 'true'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python 3.13
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
- name: Download artifact (if source_type is local)
|
||||||
|
if: inputs.source_type == 'local'
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact_name_macos }}
|
||||||
|
path: ${{ runner.temp }}/macos-artifact
|
||||||
|
|
||||||
|
- name: Setup script permissions
|
||||||
|
run: |
|
||||||
|
chmod +x autoqa/scripts/setup_permissions.sh
|
||||||
|
./autoqa/scripts/setup_permissions.sh
|
||||||
|
|
||||||
|
- name: Clean existing Jan installations
|
||||||
|
run: |
|
||||||
|
./autoqa/scripts/macos_cleanup.sh
|
||||||
|
|
||||||
|
- name: Download/Prepare Jan app
|
||||||
|
run: |
|
||||||
|
if [ "${{ inputs.source_type }}" = "local" ]; then
|
||||||
|
# Find the dmg file in the artifact
|
||||||
|
DMG_FILE=$(find "${{ runner.temp }}/macos-artifact" -name "*.dmg" -type f | head -1)
|
||||||
|
if [ -n "$DMG_FILE" ]; then
|
||||||
|
echo "✅ Found local installer: $DMG_FILE"
|
||||||
|
cp "$DMG_FILE" "/tmp/jan-installer.dmg"
|
||||||
|
echo "✅ Installer copied to: /tmp/jan-installer.dmg"
|
||||||
|
echo "JAN_APP_PATH=/tmp/jan-installer.dmg" >> $GITHUB_ENV
|
||||||
|
echo "IS_NIGHTLY=${{ inputs.is_nightly }}" >> $GITHUB_ENV
|
||||||
|
if [ "${{ inputs.is_nightly }}" = "true" ]; then
|
||||||
|
echo "PROCESS_NAME=Jan-nightly" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "PROCESS_NAME=Jan" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ No .dmg file found in artifact"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Use the existing download script for URLs
|
||||||
|
./autoqa/scripts/macos_download.sh \
|
||||||
|
"${{ inputs.jan_app_macos_source }}" \
|
||||||
|
"${{ inputs.is_nightly }}" \
|
||||||
|
"${{ vars.JAN_APP_URL }}" \
|
||||||
|
"${{ vars.IS_NIGHTLY }}" \
|
||||||
|
"$DEFAULT_JAN_APP_URL" \
|
||||||
|
"$DEFAULT_IS_NIGHTLY"
|
||||||
|
|
||||||
|
# Set the correct environment variables for the test runner
|
||||||
|
echo "JAN_APP_PATH=/tmp/jan-installer.dmg" >> $GITHUB_ENV
|
||||||
|
if [ "${{ inputs.is_nightly }}" = "true" ]; then
|
||||||
|
echo "PROCESS_NAME=Jan-nightly" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "PROCESS_NAME=Jan" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Jan app
|
||||||
|
run: |
|
||||||
|
./autoqa/scripts/macos_install.sh
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
echo "Installing system dependencies for macOS..."
|
||||||
|
|
||||||
|
# Check if Homebrew is available
|
||||||
|
if command -v brew >/dev/null 2>&1; then
|
||||||
|
echo "Homebrew is available"
|
||||||
|
|
||||||
|
# Install python-tk if not available
|
||||||
|
python3 -c "import tkinter" 2>/dev/null || {
|
||||||
|
echo "Installing python-tk via Homebrew..."
|
||||||
|
brew install python-tk || true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
echo "Homebrew not available, checking if tkinter works..."
|
||||||
|
python3 -c "import tkinter" || {
|
||||||
|
echo "⚠️ tkinter not available and Homebrew not found"
|
||||||
|
echo "This may cause issues with mouse control"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "System dependencies check completed"
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
cd autoqa
|
||||||
|
echo "Installing Python dependencies..."
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
echo "✅ Python dependencies installed"
|
||||||
|
|
||||||
|
- name: Setup ReportPortal environment
|
||||||
|
run: |
|
||||||
|
echo "Setting up ReportPortal environment..."
|
||||||
|
echo "RP_TOKEN=${{ secrets.RP_TOKEN }}" >> $GITHUB_ENV
|
||||||
|
echo "ReportPortal environment configured"
|
||||||
|
|
||||||
|
- name: Run E2E tests
|
||||||
|
env:
|
||||||
|
RP_TOKEN: ${{ secrets.RP_TOKEN }}
|
||||||
|
ENABLE_REPORTPORTAL: 'true'
|
||||||
|
RP_ENDPOINT: 'https://reportportal.menlo.ai'
|
||||||
|
RP_PROJECT: 'default_personal'
|
||||||
|
MAX_TURNS: '50'
|
||||||
|
DELAY_BETWEEN_TESTS: '3'
|
||||||
|
LAUNCH_NAME: 'CI AutoQA Run Macos - ${{ github.run_number }} - ${{ github.ref_name }}'
|
||||||
|
run: |
|
||||||
|
cd autoqa
|
||||||
|
echo "Starting E2E test execution..."
|
||||||
|
|
||||||
|
echo "Environment variables:"
|
||||||
|
echo "JAN_APP_PATH: $JAN_APP_PATH"
|
||||||
|
echo "PROCESS_NAME: $PROCESS_NAME"
|
||||||
|
echo "IS_NIGHTLY: $IS_NIGHTLY"
|
||||||
|
|
||||||
|
./scripts/run_tests.sh "$JAN_APP_PATH" "$PROCESS_NAME" "$RP_TOKEN" "macos"
|
||||||
|
|
||||||
|
- name: Cleanup after tests
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
./autoqa/scripts/macos_post_cleanup.sh
|
||||||
12
.github/workflows/jan-linter-and-test.yml
vendored
@ -114,10 +114,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Install tauri-driver dependencies
|
|
||||||
run: |
|
|
||||||
cargo install tauri-driver --locked
|
|
||||||
|
|
||||||
# Clean cache, continue on error
|
# Clean cache, continue on error
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@ -154,10 +150,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
- name: Install tauri-driver dependencies
|
|
||||||
run: |
|
|
||||||
cargo install tauri-driver --locked
|
|
||||||
|
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
shell: powershell
|
shell: powershell
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
@ -202,10 +194,6 @@ jobs:
|
|||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2 webkit2gtk-driver
|
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2 webkit2gtk-driver
|
||||||
|
|
||||||
- name: Install tauri-driver dependencies
|
|
||||||
run: |
|
|
||||||
cargo install tauri-driver --locked
|
|
||||||
|
|
||||||
- name: 'Cleanup cache'
|
- name: 'Cleanup cache'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
46
.github/workflows/jan-tauri-build-nightly.yaml
vendored
@ -223,3 +223,49 @@ jobs:
|
|||||||
RUN_ID=${{ github.run_id }}
|
RUN_ID=${{ github.run_id }}
|
||||||
COMMENT="This is the build for this pull request. You can download it from the Artifacts section here: [Build URL](https://github.com/${{ github.repository }}/actions/runs/${RUN_ID})."
|
COMMENT="This is the build for this pull request. You can download it from the Artifacts section here: [Build URL](https://github.com/${{ github.repository }}/actions/runs/${RUN_ID})."
|
||||||
gh pr comment $PR_URL --body "$COMMENT"
|
gh pr comment $PR_URL --body "$COMMENT"
|
||||||
|
|
||||||
|
# AutoQA trigger for S3 builds
|
||||||
|
trigger-autoqa-s3:
|
||||||
|
needs:
|
||||||
|
[
|
||||||
|
build-macos,
|
||||||
|
build-windows-x64,
|
||||||
|
build-linux-x64,
|
||||||
|
get-update-version,
|
||||||
|
set-public-provider,
|
||||||
|
sync-temp-to-latest,
|
||||||
|
]
|
||||||
|
if: needs.set-public-provider.outputs.public_provider == 'aws-s3'
|
||||||
|
uses: ./.github/workflows/autoqa-template.yml
|
||||||
|
with:
|
||||||
|
jan_app_windows_source: 'https://delta.jan.ai/nightly/Jan-nightly_${{ needs.get-update-version.outputs.new_version }}_x64-setup.exe'
|
||||||
|
jan_app_ubuntu_source: 'https://delta.jan.ai/nightly/Jan-nightly_${{ needs.get-update-version.outputs.new_version }}_amd64.deb'
|
||||||
|
jan_app_macos_source: 'https://delta.jan.ai/nightly/Jan-nightly_${{ needs.get-update-version.outputs.new_version }}_universal.dmg'
|
||||||
|
is_nightly: true
|
||||||
|
source_type: 'url'
|
||||||
|
secrets:
|
||||||
|
RP_TOKEN: ${{ secrets.RP_TOKEN }}
|
||||||
|
|
||||||
|
# AutoQA trigger for artifact builds
|
||||||
|
trigger-autoqa-artifacts:
|
||||||
|
needs:
|
||||||
|
[
|
||||||
|
build-macos,
|
||||||
|
build-windows-x64,
|
||||||
|
build-linux-x64,
|
||||||
|
get-update-version,
|
||||||
|
set-public-provider,
|
||||||
|
]
|
||||||
|
if: needs.set-public-provider.outputs.public_provider == 'none'
|
||||||
|
uses: ./.github/workflows/autoqa-template.yml
|
||||||
|
with:
|
||||||
|
jan_app_windows_source: '' # Not needed for artifacts
|
||||||
|
jan_app_ubuntu_source: '' # Not needed for artifacts
|
||||||
|
jan_app_macos_source: '' # Not needed for artifacts
|
||||||
|
is_nightly: true
|
||||||
|
source_type: 'local'
|
||||||
|
artifact_name_windows: 'jan-windows-${{ needs.get-update-version.outputs.new_version }}'
|
||||||
|
artifact_name_ubuntu: 'jan-linux-amd64-${{ needs.get-update-version.outputs.new_version }}-deb'
|
||||||
|
artifact_name_macos: 'jan-nightly-mac-universal-${{ needs.get-update-version.outputs.new_version }}.dmg'
|
||||||
|
secrets:
|
||||||
|
RP_TOKEN: ${{ secrets.RP_TOKEN }}
|
||||||
|
|||||||
@ -143,7 +143,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Build app
|
- name: Build app
|
||||||
run: |
|
run: |
|
||||||
make build-tauri
|
make build
|
||||||
|
|
||||||
APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep AppImage | head -1)
|
APP_IMAGE=./src-tauri/target/release/bundle/appimage/$(ls ./src-tauri/target/release/bundle/appimage/ | grep AppImage | head -1)
|
||||||
yarn tauri signer sign \
|
yarn tauri signer sign \
|
||||||
|
|||||||
@ -168,7 +168,7 @@ jobs:
|
|||||||
- name: Build app
|
- name: Build app
|
||||||
run: |
|
run: |
|
||||||
rustup target add x86_64-apple-darwin
|
rustup target add x86_64-apple-darwin
|
||||||
make build-tauri
|
make build
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
APP_PATH: '.'
|
APP_PATH: '.'
|
||||||
|
|||||||
@ -181,7 +181,7 @@ jobs:
|
|||||||
curl -L -o ./src-tauri/binaries/vcomp140.dll https://catalog.jan.ai/vcomp140.dll
|
curl -L -o ./src-tauri/binaries/vcomp140.dll https://catalog.jan.ai/vcomp140.dll
|
||||||
curl -L -o ./src-tauri/binaries/msvcp140_codecvt_ids.dll https://catalog.jan.ai/msvcp140_codecvt_ids.dll
|
curl -L -o ./src-tauri/binaries/msvcp140_codecvt_ids.dll https://catalog.jan.ai/msvcp140_codecvt_ids.dll
|
||||||
ls ./src-tauri/binaries
|
ls ./src-tauri/binaries
|
||||||
make build-tauri
|
make build
|
||||||
env:
|
env:
|
||||||
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
|
AZURE_KEY_VAULT_URI: ${{ secrets.AZURE_KEY_VAULT_URI }}
|
||||||
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|||||||
7
.gitignore
vendored
@ -50,4 +50,9 @@ src-tauri/resources/bin
|
|||||||
.opencode
|
.opencode
|
||||||
OpenCode.md
|
OpenCode.md
|
||||||
archive/
|
archive/
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
|
# auto qa
|
||||||
|
autoqa/trajectories
|
||||||
|
autoqa/recordings
|
||||||
|
autoqa/__pycache__
|
||||||
5
Makefile
@ -42,7 +42,6 @@ lint: install-and-build
|
|||||||
# Testing
|
# Testing
|
||||||
test: lint
|
test: lint
|
||||||
yarn test
|
yarn test
|
||||||
yarn test:e2e
|
|
||||||
|
|
||||||
# Builds and publishes the app
|
# Builds and publishes the app
|
||||||
build-and-publish: install-and-build
|
build-and-publish: install-and-build
|
||||||
@ -50,10 +49,6 @@ build-and-publish: install-and-build
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
build: install-and-build
|
build: install-and-build
|
||||||
yarn build
|
|
||||||
|
|
||||||
# Deprecated soon
|
|
||||||
build-tauri: install-and-build
|
|
||||||
yarn copy:lib
|
yarn copy:lib
|
||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
|
|||||||
319
autoqa/README.md
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
# E2E Test Runner with ReportPortal Integration
|
||||||
|
|
||||||
|
🚀 An automated end-to-end test runner for Jan application with ReportPortal integration, screen recording, and comprehensive test monitoring.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ **Automated Jan App Testing**: Automatically starts/stops Jan application
|
||||||
|
- 🖥️ **Auto Computer Server**: Automatically starts computer server in background
|
||||||
|
- 📹 **Screen Recording**: Records test execution for debugging
|
||||||
|
- 📊 **ReportPortal Integration**: Optional test results upload to ReportPortal
|
||||||
|
- 🔄 **Turn Monitoring**: Prevents infinite loops with configurable turn limits
|
||||||
|
- 🎯 **Flexible Configuration**: Command-line arguments and environment variables
|
||||||
|
- 🌐 **Cross-platform**: Windows, macOS, and Linux support
|
||||||
|
- 📁 **Test Discovery**: Automatically scans test files from directory
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Jan application installed
|
||||||
|
- Windows Sandbox (for computer provider)
|
||||||
|
- Computer server package installed
|
||||||
|
- Required Python packages (see requirements.txt)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd autoqa
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## For Windows and Linux
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ensure Jan application is installed in one of the default locations:
|
||||||
|
- Windows: `%LOCALAPPDATA%\Programs\jan\Jan.exe`
|
||||||
|
- macOS: `~/Applications/Jan.app/Contents/MacOS/Jan`
|
||||||
|
- Linux: `jan` (in PATH)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Local Development (No ReportPortal)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests in ./tests directory (auto-starts computer server)
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Run with custom test directory
|
||||||
|
python main.py --tests-dir "my_tests"
|
||||||
|
|
||||||
|
# Run with custom Jan app path
|
||||||
|
python main.py --jan-app-path "C:/Custom/Path/Jan.exe"
|
||||||
|
|
||||||
|
# Skip auto computer server start (if already running)
|
||||||
|
python main.py --skip-server-start
|
||||||
|
```
|
||||||
|
|
||||||
|
### With ReportPortal Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable ReportPortal with token
|
||||||
|
python main.py --enable-reportportal --rp-token "YOUR_API_TOKEN"
|
||||||
|
|
||||||
|
# Full ReportPortal configuration
|
||||||
|
python main.py \
|
||||||
|
--enable-reportportal \
|
||||||
|
--rp-endpoint "https://reportportal.example.com" \
|
||||||
|
--rp-project "my_project" \
|
||||||
|
--rp-token "YOUR_API_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Command Line Arguments
|
||||||
|
|
||||||
|
| Argument | Environment Variable | Default | Description |
|
||||||
|
| ----------------------- | --------------------- | ------------------------------- | ------------------------------------------------- |
|
||||||
|
| **Computer Server** |
|
||||||
|
| `--skip-server-start` | `SKIP_SERVER_START` | `false` | Skip automatic computer server startup |
|
||||||
|
| **ReportPortal** |
|
||||||
|
| `--enable-reportportal` | `ENABLE_REPORTPORTAL` | `false` | Enable ReportPortal integration |
|
||||||
|
| `--rp-endpoint` | `RP_ENDPOINT` | `https://reportportal.menlo.ai` | ReportPortal endpoint URL |
|
||||||
|
| `--rp-project` | `RP_PROJECT` | `default_personal` | ReportPortal project name |
|
||||||
|
| `--rp-token` | `RP_TOKEN` | - | ReportPortal API token (required when RP enabled) |
|
||||||
|
| **Jan Application** |
|
||||||
|
| `--jan-app-path` | `JAN_APP_PATH` | _auto-detected_ | Path to Jan application executable |
|
||||||
|
| `--jan-process-name` | `JAN_PROCESS_NAME` | `Jan.exe` | Jan process name for monitoring |
|
||||||
|
| **Model Configuration** |
|
||||||
|
| `--model-name` | `MODEL_NAME` | `ByteDance-Seed/UI-TARS-1.5-7B` | AI model name |
|
||||||
|
| `--model-base-url` | `MODEL_BASE_URL` | `http://10.200.108.58:1234/v1` | Model API endpoint |
|
||||||
|
| `--model-provider` | `MODEL_PROVIDER` | `oaicompat` | Model provider type |
|
||||||
|
| `--model-loop` | `MODEL_LOOP` | `uitars` | Agent loop type |
|
||||||
|
| **Test Execution** |
|
||||||
|
| `--max-turns` | `MAX_TURNS` | `30` | Maximum turns per test |
|
||||||
|
| `--tests-dir` | `TESTS_DIR` | `tests` | Directory containing test files |
|
||||||
|
| `--delay-between-tests` | `DELAY_BETWEEN_TESTS` | `3` | Delay between tests (seconds) |
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file or set environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Computer Server
|
||||||
|
SKIP_SERVER_START=false
|
||||||
|
|
||||||
|
# ReportPortal Configuration
|
||||||
|
ENABLE_REPORTPORTAL=true
|
||||||
|
RP_ENDPOINT=https://reportportal.example.com
|
||||||
|
RP_PROJECT=my_project
|
||||||
|
RP_TOKEN=your_secret_token
|
||||||
|
|
||||||
|
# Jan Application
|
||||||
|
JAN_APP_PATH=C:\Custom\Path\Jan.exe
|
||||||
|
JAN_PROCESS_NAME=Jan.exe
|
||||||
|
|
||||||
|
# Model Configuration
|
||||||
|
MODEL_NAME=gpt-4
|
||||||
|
MODEL_BASE_URL=https://api.openai.com/v1
|
||||||
|
MODEL_PROVIDER=openai
|
||||||
|
MODEL_LOOP=uitars
|
||||||
|
|
||||||
|
# Test Settings
|
||||||
|
MAX_TURNS=50
|
||||||
|
TESTS_DIR=e2e_tests
|
||||||
|
DELAY_BETWEEN_TESTS=5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Structure
|
||||||
|
|
||||||
|
### Test Files
|
||||||
|
|
||||||
|
- Test files should be `.txt` files containing test prompts
|
||||||
|
- Place test files in the `tests/` directory (or custom directory)
|
||||||
|
- Support nested directories for organization
|
||||||
|
|
||||||
|
Example test file (`tests/basic/login_test.txt`):
|
||||||
|
|
||||||
|
```
|
||||||
|
Test the login functionality of Jan application.
|
||||||
|
Navigate to login screen, enter valid credentials, and verify successful login.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
autoqa/
|
||||||
|
├── main.py # Main test runner
|
||||||
|
├── utils.py # Jan app utilities
|
||||||
|
├── test_runner.py # Test execution logic
|
||||||
|
├── screen_recorder.py # Screen recording functionality
|
||||||
|
├── reportportal_handler.py # ReportPortal integration
|
||||||
|
├── tests/ # Test files directory
|
||||||
|
│ ├── basic/
|
||||||
|
│ │ ├── login_test.txt
|
||||||
|
│ │ └── navigation_test.txt
|
||||||
|
│ └── advanced/
|
||||||
|
│ └── complex_workflow.txt
|
||||||
|
├── recordings/ # Screen recordings (auto-created)
|
||||||
|
├── trajectories/ # Agent trajectories (auto-created)
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests locally (auto-starts computer server)
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Get help
|
||||||
|
python main.py --help
|
||||||
|
|
||||||
|
# Run without auto-starting computer server
|
||||||
|
python main.py --skip-server-start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Custom configuration
|
||||||
|
python main.py \
|
||||||
|
--tests-dir "integration_tests" \
|
||||||
|
--max-turns 40 \
|
||||||
|
--delay-between-tests 10 \
|
||||||
|
--model-name "gpt-4"
|
||||||
|
|
||||||
|
# Environment + Arguments
|
||||||
|
ENABLE_REPORTPORTAL=true RP_TOKEN=secret python main.py --max-turns 50
|
||||||
|
|
||||||
|
# Different model provider
|
||||||
|
python main.py \
|
||||||
|
--model-provider "openai" \
|
||||||
|
--model-name "gpt-4" \
|
||||||
|
--model-base-url "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
# External computer server (skip auto-start)
|
||||||
|
SKIP_SERVER_START=true python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# GitHub Actions / CI environment
|
||||||
|
ENABLE_REPORTPORTAL=true \
|
||||||
|
RP_TOKEN=${{ secrets.RP_TOKEN }} \
|
||||||
|
MODEL_NAME=production-model \
|
||||||
|
MAX_TURNS=40 \
|
||||||
|
SKIP_SERVER_START=false \
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Computer Server Management
|
||||||
|
|
||||||
|
The test runner automatically manages the computer server:
|
||||||
|
|
||||||
|
### Automatic Server Management (Default)
|
||||||
|
|
||||||
|
- **Auto-start**: Computer server starts automatically in background thread
|
||||||
|
- **Auto-cleanup**: Server stops when main program exits (daemon thread)
|
||||||
|
- **Error handling**: Graceful fallback if server fails to start
|
||||||
|
|
||||||
|
### Manual Server Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If you prefer to manage computer server manually:
|
||||||
|
python -m computer_server # In separate terminal
|
||||||
|
|
||||||
|
# Then run tests without auto-start:
|
||||||
|
python main.py --skip-server-start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server Logs
|
||||||
|
|
||||||
|
```
|
||||||
|
2025-07-15 15:30:45 - INFO - Starting computer server in background...
|
||||||
|
2025-07-15 15:30:45 - INFO - Calling computer_server.run_cli()...
|
||||||
|
2025-07-15 15:30:45 - INFO - Computer server thread started
|
||||||
|
2025-07-15 15:30:50 - INFO - Computer server is running successfully
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
- **Console logs**: Detailed execution information
|
||||||
|
- **Screen recordings**: Saved to `recordings/` directory as MP4 files
|
||||||
|
- **Trajectories**: Agent interaction data in `trajectories/` directory
|
||||||
|
- **Local results**: Test results logged to console
|
||||||
|
|
||||||
|
### ReportPortal Integration
|
||||||
|
|
||||||
|
When enabled, results are uploaded to ReportPortal including:
|
||||||
|
|
||||||
|
- Test execution status (PASSED/FAILED)
|
||||||
|
- Screen recordings as attachments
|
||||||
|
- Detailed turn-by-turn interaction logs
|
||||||
|
- Error messages and debugging information
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Computer server startup failed**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install required dependencies
|
||||||
|
pip install computer_server
|
||||||
|
|
||||||
|
# Check if computer_server is available
|
||||||
|
python -c "import computer_server; print('OK')"
|
||||||
|
|
||||||
|
# Use manual server if auto-start fails
|
||||||
|
python main.py --skip-server-start
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Jan app not found**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Specify custom path
|
||||||
|
python main.py --jan-app-path "D:/Apps/Jan/Jan.exe"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Windows dependencies missing**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Windows-specific packages
|
||||||
|
pip install pywin32 psutil
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **ReportPortal connection failed**:
|
||||||
|
|
||||||
|
- Verify endpoint URL and token
|
||||||
|
- Check network connectivity
|
||||||
|
- Ensure project exists
|
||||||
|
|
||||||
|
5. **Screen recording issues**:
|
||||||
|
|
||||||
|
- Check disk space in `recordings/` directory
|
||||||
|
- Verify screen recording permissions
|
||||||
|
|
||||||
|
6. **Test timeouts**:
|
||||||
|
```bash
|
||||||
|
# Increase turn limit
|
||||||
|
python main.py --max-turns 50
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable detailed logging by modifying the logging level in `main.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
```
|
||||||
514
autoqa/main.py
Normal file
@ -0,0 +1,514 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import platform
|
||||||
|
from datetime import datetime
|
||||||
|
from computer import Computer
|
||||||
|
from reportportal_client import RPClient
|
||||||
|
from reportportal_client.helpers import timestamp
|
||||||
|
|
||||||
|
from utils import scan_test_files
|
||||||
|
from test_runner import run_single_test_with_timeout
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Platform detection
|
||||||
|
IS_WINDOWS = platform.system() == "Windows"
|
||||||
|
IS_LINUX = platform.system() == "Linux"
|
||||||
|
IS_MACOS = platform.system() == "Darwin"
|
||||||
|
|
||||||
|
def get_computer_config():
|
||||||
|
"""Get computer configuration based on platform"""
|
||||||
|
if IS_WINDOWS:
|
||||||
|
return {
|
||||||
|
"os_type": "windows"
|
||||||
|
}
|
||||||
|
elif IS_LINUX:
|
||||||
|
return {
|
||||||
|
"os_type": "linux"
|
||||||
|
}
|
||||||
|
elif IS_MACOS:
|
||||||
|
return {
|
||||||
|
"os_type": "macos"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Default fallback
|
||||||
|
logger.warning(f"Unknown platform {platform.system()}, using Linux config as fallback")
|
||||||
|
return {
|
||||||
|
"os_type": "linux"
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_default_jan_path():
|
||||||
|
"""Get default Jan app path based on OS"""
|
||||||
|
if IS_WINDOWS:
|
||||||
|
# Try multiple common locations on Windows
|
||||||
|
possible_paths = [
|
||||||
|
os.path.expanduser(r"~\AppData\Local\Programs\jan\Jan.exe"),
|
||||||
|
os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'jan', 'Jan.exe'),
|
||||||
|
os.path.join(os.environ.get('APPDATA', ''), 'jan', 'Jan.exe'),
|
||||||
|
r"C:\Program Files\jan\Jan.exe",
|
||||||
|
r"C:\Program Files (x86)\jan\Jan.exe"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Return first existing path, or first option as default
|
||||||
|
for path in possible_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
# If none exist, return the most likely default
|
||||||
|
return possible_paths[0]
|
||||||
|
|
||||||
|
elif IS_LINUX:
|
||||||
|
# Linux possible locations
|
||||||
|
possible_paths = [
|
||||||
|
"/usr/bin/Jan",
|
||||||
|
"/usr/local/bin/Jan",
|
||||||
|
os.path.expanduser("~/Applications/Jan/Jan"),
|
||||||
|
"/opt/Jan/Jan"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Return first existing path, or first option as default
|
||||||
|
for path in possible_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
# Default to nightly build path
|
||||||
|
return "/usr/bin/Jan"
|
||||||
|
|
||||||
|
elif IS_MACOS:
|
||||||
|
# macOS defaults
|
||||||
|
possible_paths = [
|
||||||
|
"/Applications/Jan.app/Contents/MacOS/Jan",
|
||||||
|
os.path.expanduser("~/Applications/Jan.app/Contents/MacOS/Jan")
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in possible_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
|
||||||
|
return possible_paths[0]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Unknown platform
|
||||||
|
return "jan"
|
||||||
|
|
||||||
|
def start_computer_server():
|
||||||
|
"""Start computer server in background thread"""
|
||||||
|
try:
|
||||||
|
logger.info("Starting computer server in background...")
|
||||||
|
|
||||||
|
# Import computer_server module
|
||||||
|
import computer_server
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Start server in a separate thread
|
||||||
|
def run_server():
|
||||||
|
try:
|
||||||
|
# Save original sys.argv to avoid argument conflicts
|
||||||
|
original_argv = sys.argv.copy()
|
||||||
|
|
||||||
|
# Override sys.argv for computer_server to use default args
|
||||||
|
sys.argv = ['computer_server'] # Reset to minimal args
|
||||||
|
|
||||||
|
# Use the proper entry point
|
||||||
|
logger.info("Calling computer_server.run_cli()...")
|
||||||
|
computer_server.run_cli()
|
||||||
|
logger.info("Computer server.run_cli() completed")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Computer server interrupted")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Computer server error: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
finally:
|
||||||
|
# Restore original sys.argv
|
||||||
|
try:
|
||||||
|
sys.argv = original_argv
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
server_thread = threading.Thread(target=run_server, daemon=True)
|
||||||
|
server_thread.start()
|
||||||
|
|
||||||
|
logger.info("Computer server thread started")
|
||||||
|
|
||||||
|
# Give server more time to start up
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Check if thread is still alive (server is running)
|
||||||
|
if server_thread.is_alive():
|
||||||
|
logger.info("Computer server is running successfully")
|
||||||
|
return server_thread
|
||||||
|
else:
|
||||||
|
logger.error("Computer server thread died unexpectedly")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"Cannot import computer_server module: {e}")
|
||||||
|
logger.error("Please install computer_server package")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting computer server: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_arguments():
|
||||||
|
"""Parse command line arguments"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="E2E Test Runner with ReportPortal integration",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
# Run locally without ReportPortal
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Run with ReportPortal integration
|
||||||
|
python main.py --enable-reportportal --rp-token YOUR_TOKEN
|
||||||
|
|
||||||
|
# Run with custom Jan app path
|
||||||
|
python main.py --jan-app-path "C:/Custom/Path/Jan.exe"
|
||||||
|
|
||||||
|
# Run with different model
|
||||||
|
python main.py --model-name "gpt-4" --model-base-url "https://api.openai.com/v1"
|
||||||
|
|
||||||
|
# Using environment variables
|
||||||
|
ENABLE_REPORTPORTAL=true RP_TOKEN=xxx MODEL_NAME=gpt-4 python main.py
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get default Jan path
|
||||||
|
default_jan_path = get_default_jan_path()
|
||||||
|
|
||||||
|
# Computer server arguments
|
||||||
|
server_group = parser.add_argument_group('Computer Server Configuration')
|
||||||
|
server_group.add_argument(
|
||||||
|
'--skip-server-start',
|
||||||
|
action='store_true',
|
||||||
|
default=os.getenv('SKIP_SERVER_START', 'false').lower() == 'true',
|
||||||
|
help='Skip automatic computer server startup (env: SKIP_SERVER_START, default: false)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ReportPortal arguments
|
||||||
|
rp_group = parser.add_argument_group('ReportPortal Configuration')
|
||||||
|
rp_group.add_argument(
|
||||||
|
'--enable-reportportal',
|
||||||
|
action='store_true',
|
||||||
|
default=os.getenv('ENABLE_REPORTPORTAL', 'false').lower() == 'true',
|
||||||
|
help='Enable ReportPortal integration (env: ENABLE_REPORTPORTAL, default: false)'
|
||||||
|
)
|
||||||
|
rp_group.add_argument(
|
||||||
|
'--rp-endpoint',
|
||||||
|
default=os.getenv('RP_ENDPOINT', 'https://reportportal.menlo.ai'),
|
||||||
|
help='ReportPortal endpoint URL (env: RP_ENDPOINT, default: %(default)s)'
|
||||||
|
)
|
||||||
|
rp_group.add_argument(
|
||||||
|
'--rp-project',
|
||||||
|
default=os.getenv('RP_PROJECT', 'default_personal'),
|
||||||
|
help='ReportPortal project name (env: RP_PROJECT, default: %(default)s)'
|
||||||
|
)
|
||||||
|
rp_group.add_argument(
|
||||||
|
'--rp-token',
|
||||||
|
default=os.getenv('RP_TOKEN'),
|
||||||
|
help='ReportPortal API token (env: RP_TOKEN, required when --enable-reportportal is used)'
|
||||||
|
)
|
||||||
|
rp_group.add_argument(
|
||||||
|
'--launch-name',
|
||||||
|
default=os.getenv('LAUNCH_NAME'),
|
||||||
|
help='Custom launch name for ReportPortal (env: LAUNCH_NAME, default: auto-generated with timestamp)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Jan app arguments
|
||||||
|
jan_group = parser.add_argument_group('Jan Application Configuration')
|
||||||
|
jan_group.add_argument(
|
||||||
|
'--jan-app-path',
|
||||||
|
default=os.getenv('JAN_APP_PATH', default_jan_path),
|
||||||
|
help=f'Path to Jan application executable (env: JAN_APP_PATH, default: auto-detected or {default_jan_path})'
|
||||||
|
)
|
||||||
|
jan_group.add_argument(
|
||||||
|
'--jan-process-name',
|
||||||
|
default=os.getenv('JAN_PROCESS_NAME', 'Jan.exe' if IS_WINDOWS else ('Jan' if IS_MACOS else 'Jan-nightly')),
|
||||||
|
help='Jan process name for monitoring (env: JAN_PROCESS_NAME, default: platform-specific)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Model/Agent arguments
|
||||||
|
model_group = parser.add_argument_group('Model Configuration')
|
||||||
|
model_group.add_argument(
|
||||||
|
'--model-loop',
|
||||||
|
default=os.getenv('MODEL_LOOP', 'uitars'),
|
||||||
|
help='Agent loop type (env: MODEL_LOOP, default: %(default)s)'
|
||||||
|
)
|
||||||
|
model_group.add_argument(
|
||||||
|
'--model-provider',
|
||||||
|
default=os.getenv('MODEL_PROVIDER', 'oaicompat'),
|
||||||
|
help='Model provider (env: MODEL_PROVIDER, default: %(default)s)'
|
||||||
|
)
|
||||||
|
model_group.add_argument(
|
||||||
|
'--model-name',
|
||||||
|
default=os.getenv('MODEL_NAME', 'ByteDance-Seed/UI-TARS-1.5-7B'),
|
||||||
|
help='Model name (env: MODEL_NAME, default: %(default)s)'
|
||||||
|
)
|
||||||
|
model_group.add_argument(
|
||||||
|
'--model-base-url',
|
||||||
|
default=os.getenv('MODEL_BASE_URL', 'http://10.200.108.58:1234/v1'),
|
||||||
|
help='Model base URL (env: MODEL_BASE_URL, default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test execution arguments
|
||||||
|
test_group = parser.add_argument_group('Test Execution Configuration')
|
||||||
|
test_group.add_argument(
|
||||||
|
'--max-turns',
|
||||||
|
type=int,
|
||||||
|
default=int(os.getenv('MAX_TURNS', '30')),
|
||||||
|
help='Maximum number of turns per test (env: MAX_TURNS, default: %(default)s)'
|
||||||
|
)
|
||||||
|
test_group.add_argument(
|
||||||
|
'--tests-dir',
|
||||||
|
default=os.getenv('TESTS_DIR', 'tests'),
|
||||||
|
help='Directory containing test files (env: TESTS_DIR, default: %(default)s)'
|
||||||
|
)
|
||||||
|
test_group.add_argument(
|
||||||
|
'--delay-between-tests',
|
||||||
|
type=int,
|
||||||
|
default=int(os.getenv('DELAY_BETWEEN_TESTS', '3')),
|
||||||
|
help='Delay in seconds between tests (env: DELAY_BETWEEN_TESTS, default: %(default)s)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Validate ReportPortal token if ReportPortal is enabled
|
||||||
|
if args.enable_reportportal and not args.rp_token:
|
||||||
|
parser.error("--rp-token (or RP_TOKEN env var) is required when --enable-reportportal is used")
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""
|
||||||
|
Main function to scan and run all test files with optional ReportPortal integration
|
||||||
|
"""
|
||||||
|
# Parse command line arguments
|
||||||
|
args = parse_arguments()
|
||||||
|
|
||||||
|
# Initialize final exit code
|
||||||
|
final_exit_code = 0
|
||||||
|
|
||||||
|
# Start computer server if not skipped
|
||||||
|
server_thread = None
|
||||||
|
if not args.skip_server_start:
|
||||||
|
server_thread = start_computer_server()
|
||||||
|
if server_thread is None:
|
||||||
|
logger.error("Failed to start computer server. Exiting...")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.info("Skipping computer server startup (assuming it's already running)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Build agent config from arguments
|
||||||
|
agent_config = {
|
||||||
|
"loop": args.model_loop,
|
||||||
|
"model_provider": args.model_provider,
|
||||||
|
"model_name": args.model_name,
|
||||||
|
"model_base_url": args.model_base_url
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log configuration
|
||||||
|
logger.info("=== Configuration ===")
|
||||||
|
logger.info(f"Computer server: {'STARTED' if server_thread else 'EXTERNAL'}")
|
||||||
|
logger.info(f"Tests directory: {args.tests_dir}")
|
||||||
|
logger.info(f"Max turns per test: {args.max_turns}")
|
||||||
|
logger.info(f"Delay between tests: {args.delay_between_tests}s")
|
||||||
|
logger.info(f"Jan app path: {args.jan_app_path}")
|
||||||
|
logger.info(f"Jan app exists: {os.path.exists(args.jan_app_path)}")
|
||||||
|
logger.info(f"Jan process name: {args.jan_process_name}")
|
||||||
|
logger.info(f"Model: {args.model_name}")
|
||||||
|
logger.info(f"Model URL: {args.model_base_url}")
|
||||||
|
logger.info(f"Model provider: {args.model_provider}")
|
||||||
|
logger.info(f"ReportPortal integration: {'ENABLED' if args.enable_reportportal else 'DISABLED'}")
|
||||||
|
if args.enable_reportportal:
|
||||||
|
logger.info(f"ReportPortal endpoint: {args.rp_endpoint}")
|
||||||
|
logger.info(f"ReportPortal project: {args.rp_project}")
|
||||||
|
logger.info(f"ReportPortal token: {'SET' if args.rp_token else 'NOT SET'}")
|
||||||
|
logger.info(f"Launch name: {args.launch_name if args.launch_name else 'AUTO-GENERATED'}")
|
||||||
|
logger.info("======================")
|
||||||
|
|
||||||
|
# Scan all test files
|
||||||
|
test_files = scan_test_files(args.tests_dir)
|
||||||
|
|
||||||
|
if not test_files:
|
||||||
|
logger.warning(f"No test files found in directory: {args.tests_dir}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Found {len(test_files)} test files")
|
||||||
|
|
||||||
|
# Track test results for final exit code
|
||||||
|
test_results = {"passed": 0, "failed": 0, "total": len(test_files)}
|
||||||
|
|
||||||
|
# Initialize ReportPortal client only if enabled
|
||||||
|
rp_client = None
|
||||||
|
launch_id = None
|
||||||
|
|
||||||
|
if args.enable_reportportal:
|
||||||
|
try:
|
||||||
|
rp_client = RPClient(
|
||||||
|
endpoint=args.rp_endpoint,
|
||||||
|
project=args.rp_project,
|
||||||
|
api_key=args.rp_token
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start ReportPortal launch
|
||||||
|
current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
|
# Use custom launch name if provided, otherwise generate default
|
||||||
|
if args.launch_name:
|
||||||
|
launch_name = args.launch_name
|
||||||
|
logger.info(f"Using custom launch name: {launch_name}")
|
||||||
|
else:
|
||||||
|
launch_name = f"E2E Test Run - {current_time}"
|
||||||
|
logger.info(f"Using auto-generated launch name: {launch_name}")
|
||||||
|
|
||||||
|
launch_id = rp_client.start_launch(
|
||||||
|
name=launch_name,
|
||||||
|
start_time=timestamp(),
|
||||||
|
description=f"Automated E2E test run with {len(test_files)} test cases\n"
|
||||||
|
f"Model: {args.model_name}\n"
|
||||||
|
f"Max turns: {args.max_turns}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Started ReportPortal launch: {launch_name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to initialize ReportPortal: {e}")
|
||||||
|
logger.warning("Continuing without ReportPortal integration...")
|
||||||
|
rp_client = None
|
||||||
|
launch_id = None
|
||||||
|
else:
|
||||||
|
logger.info("Running in local development mode - results will not be uploaded to ReportPortal")
|
||||||
|
|
||||||
|
# Start computer environment
|
||||||
|
logger.info("Initializing computer environment...")
|
||||||
|
|
||||||
|
# Get platform-specific computer configuration
|
||||||
|
computer_config = get_computer_config()
|
||||||
|
logger.info(f"Using computer config: {computer_config}")
|
||||||
|
|
||||||
|
computer = Computer(
|
||||||
|
os_type=computer_config["os_type"],
|
||||||
|
use_host_computer_server=True
|
||||||
|
)
|
||||||
|
await computer.run()
|
||||||
|
logger.info("Computer environment ready")
|
||||||
|
|
||||||
|
# Run each test sequentially with turn monitoring
|
||||||
|
for i, test_data in enumerate(test_files, 1):
|
||||||
|
logger.info(f"Running test {i}/{len(test_files)}: {test_data['path']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Pass all configs to test runner
|
||||||
|
test_result = await run_single_test_with_timeout(
|
||||||
|
computer=computer,
|
||||||
|
test_data=test_data,
|
||||||
|
rp_client=rp_client, # Can be None
|
||||||
|
launch_id=launch_id, # Can be None
|
||||||
|
max_turns=args.max_turns,
|
||||||
|
jan_app_path=args.jan_app_path,
|
||||||
|
jan_process_name=args.jan_process_name,
|
||||||
|
agent_config=agent_config,
|
||||||
|
enable_reportportal=args.enable_reportportal
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track test result - properly handle different return formats
|
||||||
|
test_passed = False
|
||||||
|
|
||||||
|
if test_result:
|
||||||
|
# Check different possible return formats
|
||||||
|
if isinstance(test_result, dict):
|
||||||
|
# Dictionary format: check 'success' key
|
||||||
|
test_passed = test_result.get('success', False)
|
||||||
|
elif isinstance(test_result, bool):
|
||||||
|
# Boolean format: direct boolean value
|
||||||
|
test_passed = test_result
|
||||||
|
elif hasattr(test_result, 'success'):
|
||||||
|
# Object format: check success attribute
|
||||||
|
test_passed = getattr(test_result, 'success', False)
|
||||||
|
else:
|
||||||
|
# Any truthy value is considered success
|
||||||
|
test_passed = bool(test_result)
|
||||||
|
else:
|
||||||
|
test_passed = False
|
||||||
|
|
||||||
|
# Update counters and log result
|
||||||
|
if test_passed:
|
||||||
|
test_results["passed"] += 1
|
||||||
|
logger.info(f"✅ Test {i} PASSED: {test_data['path']}")
|
||||||
|
else:
|
||||||
|
test_results["failed"] += 1
|
||||||
|
logger.error(f"❌ Test {i} FAILED: {test_data['path']}")
|
||||||
|
|
||||||
|
# Debug log for troubleshooting
|
||||||
|
logger.info(f"🔍 Debug - Test result: type={type(test_result)}, value={test_result}, success_field={test_result.get('success', 'N/A') if isinstance(test_result, dict) else 'N/A'}, final_passed={test_passed}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
test_results["failed"] += 1
|
||||||
|
logger.error(f"❌ Test {i} FAILED with exception: {test_data['path']} - {e}")
|
||||||
|
|
||||||
|
# Add delay between tests
|
||||||
|
if i < len(test_files):
|
||||||
|
logger.info(f"Waiting {args.delay_between_tests} seconds before next test...")
|
||||||
|
await asyncio.sleep(args.delay_between_tests)
|
||||||
|
|
||||||
|
# Log final test results summary
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("TEST EXECUTION SUMMARY")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info(f"Total tests: {test_results['total']}")
|
||||||
|
logger.info(f"Passed: {test_results['passed']}")
|
||||||
|
logger.info(f"Failed: {test_results['failed']}")
|
||||||
|
logger.info(f"Success rate: {(test_results['passed']/test_results['total']*100):.1f}%")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
if test_results["failed"] > 0:
|
||||||
|
logger.error(f"❌ Test execution completed with {test_results['failed']} failures!")
|
||||||
|
final_exit_code = 1
|
||||||
|
else:
|
||||||
|
logger.info("✅ All tests completed successfully!")
|
||||||
|
final_exit_code = 0
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Test execution interrupted by user")
|
||||||
|
final_exit_code = 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in main execution: {e}")
|
||||||
|
final_exit_code = 1
|
||||||
|
finally:
|
||||||
|
# Finish ReportPortal launch only if it was started
|
||||||
|
if args.enable_reportportal and rp_client and launch_id:
|
||||||
|
try:
|
||||||
|
rp_client.finish_launch(
|
||||||
|
launch_id=launch_id,
|
||||||
|
end_time=timestamp()
|
||||||
|
)
|
||||||
|
rp_client.session.close()
|
||||||
|
logger.info("ReportPortal launch finished and session closed")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error finishing ReportPortal launch: {e}")
|
||||||
|
|
||||||
|
# Note: daemon thread will automatically terminate when main program ends
|
||||||
|
if server_thread:
|
||||||
|
logger.info("Computer server will stop when main program exits (daemon thread)")
|
||||||
|
|
||||||
|
# Exit with appropriate code based on test results
|
||||||
|
logger.info(f"Exiting with code: {final_exit_code}")
|
||||||
|
exit(final_exit_code)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
307
autoqa/reportportal_handler.py
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from reportportal_client.helpers import timestamp
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def upload_turn_folder(client, test_item_id, turn_path, turn_name, force_fail=False):
|
||||||
|
"""
|
||||||
|
Upload turn folder content to ReportPortal
|
||||||
|
"""
|
||||||
|
step_item_id = client.start_test_item(
|
||||||
|
parent_item_id=test_item_id,
|
||||||
|
name=turn_name,
|
||||||
|
start_time=timestamp(),
|
||||||
|
item_type="STEP"
|
||||||
|
)
|
||||||
|
|
||||||
|
uploaded = False
|
||||||
|
step_has_errors = False # Track if this step has any errors
|
||||||
|
|
||||||
|
for fname in sorted(os.listdir(turn_path)):
|
||||||
|
fpath = os.path.join(turn_path, fname)
|
||||||
|
|
||||||
|
if fname.endswith(".json"):
|
||||||
|
try:
|
||||||
|
with open(fpath, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="INFO",
|
||||||
|
message=f"[{fname}]\n{json.dumps(data, indent=2)}",
|
||||||
|
item_id=step_item_id
|
||||||
|
)
|
||||||
|
uploaded = True
|
||||||
|
except Exception as e:
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="ERROR",
|
||||||
|
message=f"[ERROR parsing {fname}] {str(e)}",
|
||||||
|
item_id=step_item_id
|
||||||
|
)
|
||||||
|
step_has_errors = True
|
||||||
|
|
||||||
|
elif fname.endswith(".png"):
|
||||||
|
try:
|
||||||
|
with open(fpath, "rb") as img_file:
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="INFO",
|
||||||
|
message=f"Screenshot: {fname}",
|
||||||
|
item_id=step_item_id,
|
||||||
|
attachment={
|
||||||
|
"name": fname,
|
||||||
|
"data": img_file.read(),
|
||||||
|
"mime": mimetypes.guess_type(fname)[0] or "image/png"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
uploaded = True
|
||||||
|
except Exception as e:
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="ERROR",
|
||||||
|
message=f"[ERROR attaching {fname}] {str(e)}",
|
||||||
|
item_id=step_item_id
|
||||||
|
)
|
||||||
|
step_has_errors = True
|
||||||
|
|
||||||
|
if not uploaded:
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="WARNING",
|
||||||
|
message="No data found in this turn.",
|
||||||
|
item_id=step_item_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine step status based on test case result
|
||||||
|
if force_fail:
|
||||||
|
step_status = "FAILED"
|
||||||
|
else:
|
||||||
|
step_status = "FAILED" if step_has_errors else "PASSED"
|
||||||
|
|
||||||
|
client.finish_test_item(
|
||||||
|
item_id=step_item_id,
|
||||||
|
end_time=timestamp(),
|
||||||
|
status=step_status
|
||||||
|
)
|
||||||
|
|
||||||
|
def extract_test_result_from_trajectory(trajectory_dir):
|
||||||
|
"""
|
||||||
|
Extract test result from the last turn's API response
|
||||||
|
Returns True only if found {"result": True}, False for all other cases including {"result": False}
|
||||||
|
"""
|
||||||
|
if not trajectory_dir or not os.path.exists(trajectory_dir):
|
||||||
|
logger.warning(f"Trajectory directory not found: {trajectory_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get all turn folders and find the last one
|
||||||
|
turn_folders = [f for f in os.listdir(trajectory_dir)
|
||||||
|
if os.path.isdir(os.path.join(trajectory_dir, f)) and f.startswith("turn_")]
|
||||||
|
|
||||||
|
if not turn_folders:
|
||||||
|
logger.warning("No turn folders found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Sort to get the last turn
|
||||||
|
last_turn = sorted(turn_folders)[-1]
|
||||||
|
last_turn_path = os.path.join(trajectory_dir, last_turn)
|
||||||
|
|
||||||
|
logger.info(f"Checking result in last turn: {last_turn}")
|
||||||
|
|
||||||
|
# Look for API call response files
|
||||||
|
response_files = [f for f in os.listdir(last_turn_path)
|
||||||
|
if f.startswith("api_call_") and f.endswith("_response.json")]
|
||||||
|
|
||||||
|
if not response_files:
|
||||||
|
logger.warning("No API response files found in last turn")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check the last response file
|
||||||
|
last_response_file = sorted(response_files)[-1]
|
||||||
|
response_file_path = os.path.join(last_turn_path, last_response_file)
|
||||||
|
|
||||||
|
logger.info(f"Checking response file: {last_response_file}")
|
||||||
|
|
||||||
|
with open(response_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# Extract content from response
|
||||||
|
if 'response' in data and 'choices' in data['response'] and data['response']['choices']:
|
||||||
|
last_choice = data['response']['choices'][-1]
|
||||||
|
if 'message' in last_choice and 'content' in last_choice['message']:
|
||||||
|
content = last_choice['message']['content']
|
||||||
|
logger.info(f"Last response content: {content}")
|
||||||
|
|
||||||
|
# Look for result patterns - need to check both True and False
|
||||||
|
true_pattern = r'\{\s*"result"\s*:\s*True\s*\}'
|
||||||
|
false_pattern = r'\{\s*"result"\s*:\s*False\s*\}'
|
||||||
|
|
||||||
|
true_match = re.search(true_pattern, content)
|
||||||
|
false_match = re.search(false_pattern, content)
|
||||||
|
|
||||||
|
if true_match:
|
||||||
|
logger.info(f"Found test result: True - PASSED")
|
||||||
|
return True
|
||||||
|
elif false_match:
|
||||||
|
logger.info(f"Found test result: False - FAILED")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.warning("No valid result pattern found in response content - marking as FAILED")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.warning("Could not extract content from response structure")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error extracting test result: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def upload_test_results_to_rp(client, launch_id, test_path, trajectory_dir, force_stopped=False, video_path=None):
|
||||||
|
"""
|
||||||
|
Upload test results to ReportPortal with proper status based on test result
|
||||||
|
"""
|
||||||
|
if not trajectory_dir or not os.path.exists(trajectory_dir):
|
||||||
|
logger.warning(f"Trajectory directory not found: {trajectory_dir}")
|
||||||
|
formatted_test_path = test_path.replace('\\', '/').replace('.txt', '').replace('/', '__')
|
||||||
|
test_item_id = client.start_test_item(
|
||||||
|
launch_id=launch_id,
|
||||||
|
name=formatted_test_path,
|
||||||
|
start_time=timestamp(),
|
||||||
|
item_type="TEST",
|
||||||
|
description=f"Test case from: {test_path}"
|
||||||
|
)
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="ERROR",
|
||||||
|
message="❌ TEST FAILED ❌\nNo trajectory directory found",
|
||||||
|
item_id=test_item_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upload video if available
|
||||||
|
if video_path and os.path.exists(video_path):
|
||||||
|
try:
|
||||||
|
with open(video_path, "rb") as video_file:
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="INFO",
|
||||||
|
message="Screen recording of test execution",
|
||||||
|
item_id=test_item_id,
|
||||||
|
attachment={
|
||||||
|
"name": f"test_recording_{formatted_test_path}.mp4",
|
||||||
|
"data": video_file.read(),
|
||||||
|
"mime": "video/x-msvideo"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(f"Uploaded video for failed test: {video_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error uploading video: {e}")
|
||||||
|
|
||||||
|
client.finish_test_item(
|
||||||
|
item_id=test_item_id,
|
||||||
|
end_time=timestamp(),
|
||||||
|
status="FAILED"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
formatted_test_path = test_path.replace('\\', '/').replace('.txt', '').replace('/', '__')
|
||||||
|
|
||||||
|
# Determine final status
|
||||||
|
if force_stopped:
|
||||||
|
final_status = "FAILED"
|
||||||
|
status_message = "exceeded maximum turn limit (30 turns)"
|
||||||
|
else:
|
||||||
|
test_result = extract_test_result_from_trajectory(trajectory_dir)
|
||||||
|
if test_result is True:
|
||||||
|
final_status = "PASSED"
|
||||||
|
status_message = "completed successfully with positive result"
|
||||||
|
else:
|
||||||
|
final_status = "FAILED"
|
||||||
|
status_message = "no valid success result found"
|
||||||
|
|
||||||
|
# Create test item
|
||||||
|
test_item_id = client.start_test_item(
|
||||||
|
launch_id=launch_id,
|
||||||
|
name=formatted_test_path,
|
||||||
|
start_time=timestamp(),
|
||||||
|
item_type="TEST",
|
||||||
|
description=f"Test case from: {test_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
turn_folders = [f for f in os.listdir(trajectory_dir)
|
||||||
|
if os.path.isdir(os.path.join(trajectory_dir, f)) and f.startswith("turn_")]
|
||||||
|
|
||||||
|
# Add clear status log
|
||||||
|
status_emoji = "✅" if final_status == "PASSED" else "❌"
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="INFO" if final_status == "PASSED" else "ERROR",
|
||||||
|
message=f"{status_emoji} TEST {final_status} {status_emoji}\nReason: {status_message}\nTotal turns: {len(turn_folders)}",
|
||||||
|
item_id=test_item_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upload screen recording video first
|
||||||
|
if video_path and os.path.exists(video_path):
|
||||||
|
logger.info(f"Attempting to upload video: {video_path}")
|
||||||
|
logger.info(f"Video file size: {os.path.getsize(video_path)} bytes")
|
||||||
|
try:
|
||||||
|
with open(video_path, "rb") as video_file:
|
||||||
|
video_data = video_file.read()
|
||||||
|
logger.info(f"Read video data: {len(video_data)} bytes")
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="INFO",
|
||||||
|
message="🎥 Screen recording of test execution",
|
||||||
|
item_id=test_item_id,
|
||||||
|
attachment={
|
||||||
|
"name": f"test_recording_{formatted_test_path}.mp4",
|
||||||
|
"data": video_data,
|
||||||
|
"mime": "video/x-msvideo"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
logger.info(f"Successfully uploaded screen recording: {video_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error uploading screen recording: {e}")
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="WARNING",
|
||||||
|
message=f"Failed to upload screen recording: {str(e)}",
|
||||||
|
item_id=test_item_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Video upload skipped - video_path: {video_path}, exists: {os.path.exists(video_path) if video_path else 'N/A'}")
|
||||||
|
client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="WARNING",
|
||||||
|
message="No screen recording available for this test",
|
||||||
|
item_id=test_item_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upload all turn data with appropriate status
|
||||||
|
# If test failed, mark all turns as failed
|
||||||
|
force_fail_turns = (final_status == "FAILED")
|
||||||
|
|
||||||
|
for turn_folder in sorted(turn_folders):
|
||||||
|
turn_path = os.path.join(trajectory_dir, turn_folder)
|
||||||
|
upload_turn_folder(client, test_item_id, turn_path, turn_folder, force_fail=force_fail_turns)
|
||||||
|
|
||||||
|
# Finish with correct status
|
||||||
|
client.finish_test_item(
|
||||||
|
item_id=test_item_id,
|
||||||
|
end_time=timestamp(),
|
||||||
|
status=final_status
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Uploaded test results for {formatted_test_path}: {final_status}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error uploading test results: {e}")
|
||||||
|
client.finish_test_item(
|
||||||
|
item_id=test_item_id,
|
||||||
|
end_time=timestamp(),
|
||||||
|
status="FAILED"
|
||||||
|
)
|
||||||
18
autoqa/requirements.txt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Core dependencies
|
||||||
|
cua-computer[all]>=0.3.5
|
||||||
|
cua-agent[all]>=0.3.0
|
||||||
|
cua-agent @ git+https://github.com/menloresearch/cua.git@compute-agent-0.3.0-patch#subdirectory=libs/python/agent
|
||||||
|
|
||||||
|
# ReportPortal integration
|
||||||
|
reportportal-client>=5.6.5
|
||||||
|
|
||||||
|
# Screen recording and automation
|
||||||
|
opencv-python>=4.12.0
|
||||||
|
numpy>=2.2.6
|
||||||
|
PyAutoGUI>=0.9.54
|
||||||
|
|
||||||
|
# System utilities
|
||||||
|
psutil>=7.0.0
|
||||||
|
|
||||||
|
# Server component
|
||||||
|
cua-computer-server>=0.1.19
|
||||||
84
autoqa/screen_recorder.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import pyautogui
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ScreenRecorder:
|
||||||
|
def __init__(self, output_path, fps=10):
|
||||||
|
self.output_path = output_path
|
||||||
|
self.fps = fps
|
||||||
|
self.recording = False
|
||||||
|
self.writer = None
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def start_recording(self):
|
||||||
|
"""Start screen recording"""
|
||||||
|
if self.recording:
|
||||||
|
logger.warning("Recording already in progress")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.recording = True
|
||||||
|
self.thread = threading.Thread(target=self._record_screen, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
logger.info(f"Started screen recording: {self.output_path}")
|
||||||
|
|
||||||
|
def stop_recording(self):
|
||||||
|
"""Stop screen recording"""
|
||||||
|
if not self.recording:
|
||||||
|
logger.warning("No recording in progress")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.recording = False
|
||||||
|
if self.thread:
|
||||||
|
self.thread.join(timeout=5)
|
||||||
|
if self.writer:
|
||||||
|
self.writer.release()
|
||||||
|
logger.info(f"Stopped screen recording: {self.output_path}")
|
||||||
|
|
||||||
|
def _record_screen(self):
|
||||||
|
"""Internal method to record screen"""
|
||||||
|
try:
|
||||||
|
# Get screen dimensions
|
||||||
|
screen_size = pyautogui.size()
|
||||||
|
|
||||||
|
# Try MP4 with H264 codec for better compatibility
|
||||||
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # or 'H264'
|
||||||
|
output_path_mp4 = self.output_path
|
||||||
|
|
||||||
|
self.writer = cv2.VideoWriter(
|
||||||
|
output_path_mp4,
|
||||||
|
fourcc,
|
||||||
|
self.fps,
|
||||||
|
screen_size
|
||||||
|
)
|
||||||
|
|
||||||
|
while self.recording:
|
||||||
|
try:
|
||||||
|
# Capture screen
|
||||||
|
screenshot = pyautogui.screenshot()
|
||||||
|
|
||||||
|
# Convert PIL image to numpy array
|
||||||
|
frame = np.array(screenshot)
|
||||||
|
|
||||||
|
# Convert RGB to BGR (OpenCV uses BGR)
|
||||||
|
frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
|
||||||
|
|
||||||
|
# Write frame
|
||||||
|
self.writer.write(frame)
|
||||||
|
|
||||||
|
# Control FPS
|
||||||
|
time.sleep(1.0 / self.fps)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error capturing frame: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in screen recording: {e}")
|
||||||
|
finally:
|
||||||
|
if self.writer:
|
||||||
|
self.writer.release()
|
||||||
116
autoqa/scripts/README.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# AutoQA Scripts
|
||||||
|
|
||||||
|
This directory contains platform-specific scripts used by the AutoQA GitHub Actions workflow. These scripts help maintain a cleaner and more maintainable workflow file by extracting complex inline scripts into separate files.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
autoqa/scripts/
|
||||||
|
├── setup_permissions.sh # Setup executable permissions for all scripts
|
||||||
|
├── windows_cleanup.ps1 # Windows: Clean existing Jan installations
|
||||||
|
├── windows_download.ps1 # Windows: Download Jan app installer
|
||||||
|
├── windows_install.ps1 # Windows: Install Jan app
|
||||||
|
├── windows_post_cleanup.ps1 # Windows: Post-test cleanup
|
||||||
|
├── run_tests.ps1 # Windows: Run AutoQA tests
|
||||||
|
├── ubuntu_cleanup.sh # Ubuntu: Clean existing Jan installations
|
||||||
|
├── ubuntu_download.sh # Ubuntu: Download Jan app (.deb)
|
||||||
|
├── ubuntu_install.sh # Ubuntu: Install Jan app
|
||||||
|
├── ubuntu_post_cleanup.sh # Ubuntu: Post-test cleanup
|
||||||
|
├── macos_cleanup.sh # macOS: Clean existing Jan installations
|
||||||
|
├── macos_download.sh # macOS: Download Jan app (.dmg)
|
||||||
|
├── macos_install.sh # macOS: Install Jan app
|
||||||
|
├── macos_post_cleanup.sh # macOS: Post-test cleanup
|
||||||
|
├── run_tests.sh # Unix: Run AutoQA tests (Ubuntu/macOS)
|
||||||
|
├── README.md # This file
|
||||||
|
└── PERMISSIONS.md # Permission setup documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Functions
|
||||||
|
|
||||||
|
### Windows Scripts (.ps1)
|
||||||
|
|
||||||
|
- **windows_cleanup.ps1**: Removes existing Jan installations and kills running processes
|
||||||
|
- **windows_download.ps1**: Downloads Jan installer with priority-based URL selection
|
||||||
|
- **windows_install.ps1**: Installs Jan app and sets environment variables
|
||||||
|
- **windows_post_cleanup.ps1**: Comprehensive cleanup after tests including uninstallation
|
||||||
|
- **run_tests.ps1**: Runs the AutoQA Python tests with proper arguments
|
||||||
|
|
||||||
|
### Ubuntu Scripts (.sh)
|
||||||
|
|
||||||
|
- **ubuntu_cleanup.sh**: Removes existing Jan installations and kills running processes
|
||||||
|
- **ubuntu_download.sh**: Downloads Jan .deb package with priority-based URL selection
|
||||||
|
- **ubuntu_install.sh**: Installs Jan .deb package and sets environment variables
|
||||||
|
- **ubuntu_post_cleanup.sh**: Comprehensive cleanup after tests including package removal
|
||||||
|
|
||||||
|
### macOS Scripts (.sh)
|
||||||
|
|
||||||
|
- **macos_cleanup.sh**: Removes existing Jan installations and kills running processes
|
||||||
|
- **macos_download.sh**: Downloads Jan .dmg package with priority-based URL selection
|
||||||
|
- **macos_install.sh**: Mounts DMG, extracts .app, and installs to Applications
|
||||||
|
- **macos_post_cleanup.sh**: Comprehensive cleanup after tests
|
||||||
|
|
||||||
|
### Common Scripts
|
||||||
|
|
||||||
|
- **setup_permissions.sh**: Automatically sets executable permissions for all shell scripts
|
||||||
|
- **run_tests.sh**: Platform-agnostic test runner for Unix-based systems (Ubuntu/macOS)
|
||||||
|
|
||||||
|
## Usage in GitHub Actions
|
||||||
|
|
||||||
|
These scripts are called from the `.github/workflows/autoqa.yml` workflow file:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Setup permissions first (Ubuntu/macOS)
|
||||||
|
- name: Setup script permissions
|
||||||
|
run: |
|
||||||
|
chmod +x autoqa/scripts/setup_permissions.sh
|
||||||
|
./autoqa/scripts/setup_permissions.sh
|
||||||
|
|
||||||
|
# Then use scripts without chmod
|
||||||
|
- name: Clean existing Jan installations
|
||||||
|
run: |
|
||||||
|
./autoqa/scripts/ubuntu_cleanup.sh
|
||||||
|
|
||||||
|
# Windows example (no chmod needed)
|
||||||
|
- name: Clean existing Jan installations
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
.\autoqa\scripts\windows_cleanup.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
1. **Maintainability**: Complex scripts are in separate files, easier to read and modify
|
||||||
|
2. **Reusability**: Scripts can be reused across different workflows or locally
|
||||||
|
3. **Testing**: Scripts can be tested independently
|
||||||
|
4. **Version Control**: Better diff tracking for script changes
|
||||||
|
5. **Platform Consistency**: Similar functionality across platforms in separate files
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
When modifying these scripts:
|
||||||
|
|
||||||
|
1. Test them locally on the respective platforms
|
||||||
|
2. Ensure proper error handling and exit codes
|
||||||
|
3. Follow platform-specific best practices
|
||||||
|
4. Update this README if new scripts are added
|
||||||
|
|
||||||
|
## Script Parameters
|
||||||
|
|
||||||
|
### Windows Scripts
|
||||||
|
|
||||||
|
- Most scripts accept `-IsNightly` parameter to handle nightly vs stable builds
|
||||||
|
- Download script accepts multiple URL sources with priority ordering
|
||||||
|
|
||||||
|
### Unix Scripts
|
||||||
|
|
||||||
|
- Most scripts accept positional parameters for nightly flag and URLs
|
||||||
|
- Scripts use `$1`, `$2`, etc. for parameter access
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Scripts set these environment variables for subsequent workflow steps:
|
||||||
|
|
||||||
|
- `JAN_APP_URL`: The selected Jan app download URL
|
||||||
|
- `IS_NIGHTLY`: Boolean flag indicating if it's a nightly build
|
||||||
|
- `JAN_APP_PATH`: Path to the installed Jan executable
|
||||||
|
- `JAN_PROCESS_NAME`: Name of the Jan process for monitoring
|
||||||
34
autoqa/scripts/macos_cleanup.sh
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# macOS cleanup script for Jan app
|
||||||
|
|
||||||
|
echo "Cleaning existing Jan installations..."
|
||||||
|
|
||||||
|
# Kill any running Jan processes (both regular and nightly)
|
||||||
|
pkill -f "Jan" || true
|
||||||
|
pkill -f "jan" || true
|
||||||
|
pkill -f "Jan-nightly" || true
|
||||||
|
pkill -f "jan-nightly" || true
|
||||||
|
|
||||||
|
# Remove Jan app directories
|
||||||
|
rm -rf /Applications/Jan.app
|
||||||
|
rm -rf /Applications/Jan-nightly.app
|
||||||
|
rm -rf ~/Applications/Jan.app
|
||||||
|
rm -rf ~/Applications/Jan-nightly.app
|
||||||
|
|
||||||
|
# Remove Jan data folders (both regular and nightly)
|
||||||
|
rm -rf ~/Library/Application\ Support/Jan
|
||||||
|
rm -rf ~/Library/Application\ Support/Jan-nightly
|
||||||
|
rm -rf ~/Library/Application\ Support/jan.ai.app
|
||||||
|
rm -rf ~/Library/Application\ Support/jan-nightly.ai.app
|
||||||
|
rm -rf ~/Library/Preferences/jan.*
|
||||||
|
rm -rf ~/Library/Preferences/jan-nightly.*
|
||||||
|
rm -rf ~/Library/Caches/jan.*
|
||||||
|
rm -rf ~/Library/Caches/jan-nightly.*
|
||||||
|
rm -rf ~/Library/Caches/jan.ai.app
|
||||||
|
rm -rf ~/Library/Caches/jan-nightly.ai.app
|
||||||
|
rm -rf ~/Library/WebKit/jan.ai.app
|
||||||
|
rm -rf ~/Library/WebKit/jan-nightly.ai.app
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/jan.ai.app
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/jan-nightly.ai.app
|
||||||
|
|
||||||
|
echo "Jan cleanup completed"
|
||||||
49
autoqa/scripts/macos_download.sh
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# macOS download script for Jan app
|
||||||
|
|
||||||
|
WORKFLOW_INPUT_URL="$1"
|
||||||
|
WORKFLOW_INPUT_IS_NIGHTLY="$2"
|
||||||
|
REPO_VARIABLE_URL="$3"
|
||||||
|
REPO_VARIABLE_IS_NIGHTLY="$4"
|
||||||
|
DEFAULT_URL="$5"
|
||||||
|
DEFAULT_IS_NIGHTLY="$6"
|
||||||
|
|
||||||
|
# Determine Jan app URL and nightly flag from multiple sources (priority order):
|
||||||
|
# 1. Workflow dispatch input (manual trigger)
|
||||||
|
# 2. Repository variable JAN_APP_URL
|
||||||
|
# 3. Default URL from env
|
||||||
|
|
||||||
|
JAN_APP_URL=""
|
||||||
|
IS_NIGHTLY="false"
|
||||||
|
|
||||||
|
if [ -n "$WORKFLOW_INPUT_URL" ]; then
|
||||||
|
JAN_APP_URL="$WORKFLOW_INPUT_URL"
|
||||||
|
IS_NIGHTLY="$WORKFLOW_INPUT_IS_NIGHTLY"
|
||||||
|
echo "Using Jan app URL from workflow input: $JAN_APP_URL"
|
||||||
|
echo "Is nightly build: $IS_NIGHTLY"
|
||||||
|
elif [ -n "$REPO_VARIABLE_URL" ]; then
|
||||||
|
JAN_APP_URL="$REPO_VARIABLE_URL"
|
||||||
|
IS_NIGHTLY="$REPO_VARIABLE_IS_NIGHTLY"
|
||||||
|
echo "Using Jan app URL from repository variable: $JAN_APP_URL"
|
||||||
|
echo "Is nightly build: $IS_NIGHTLY"
|
||||||
|
else
|
||||||
|
JAN_APP_URL="$DEFAULT_URL"
|
||||||
|
IS_NIGHTLY="$DEFAULT_IS_NIGHTLY"
|
||||||
|
echo "Using default Jan app URL: $JAN_APP_URL"
|
||||||
|
echo "Is nightly build: $IS_NIGHTLY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export for later steps
|
||||||
|
echo "JAN_APP_URL=$JAN_APP_URL" >> $GITHUB_ENV
|
||||||
|
echo "IS_NIGHTLY=$IS_NIGHTLY" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "Downloading Jan app from: $JAN_APP_URL"
|
||||||
|
curl -L -o "/tmp/jan-installer.dmg" "$JAN_APP_URL"
|
||||||
|
|
||||||
|
if [ ! -f "/tmp/jan-installer.dmg" ]; then
|
||||||
|
echo "❌ Failed to download Jan app"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Successfully downloaded Jan app"
|
||||||
|
ls -la "/tmp/jan-installer.dmg"
|
||||||
86
autoqa/scripts/macos_install.sh
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# macOS install script for Jan app
|
||||||
|
|
||||||
|
echo "Installing Jan app from DMG..."
|
||||||
|
|
||||||
|
# Mount the DMG
|
||||||
|
hdiutil attach "/tmp/jan-installer.dmg" -mountpoint "/tmp/jan-mount"
|
||||||
|
|
||||||
|
# Find the .app file in the mounted DMG
|
||||||
|
APP_FILE=$(find "/tmp/jan-mount" -name "*.app" -type d | head -1)
|
||||||
|
|
||||||
|
if [ -z "$APP_FILE" ]; then
|
||||||
|
echo "❌ No .app file found in DMG"
|
||||||
|
hdiutil detach "/tmp/jan-mount" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found app file: $APP_FILE"
|
||||||
|
|
||||||
|
# Copy to Applications directory
|
||||||
|
cp -R "$APP_FILE" /Applications/
|
||||||
|
|
||||||
|
# Unmount the DMG
|
||||||
|
hdiutil detach "/tmp/jan-mount"
|
||||||
|
|
||||||
|
# Determine app name and executable path
|
||||||
|
APP_NAME=$(basename "$APP_FILE")
|
||||||
|
|
||||||
|
echo "App name: $APP_NAME"
|
||||||
|
|
||||||
|
# First, check what's actually in the MacOS folder
|
||||||
|
echo "Contents of MacOS folder:"
|
||||||
|
ls -la "/Applications/$APP_NAME/Contents/MacOS/"
|
||||||
|
|
||||||
|
# Find all executable files in MacOS folder
|
||||||
|
echo "Looking for executable files..."
|
||||||
|
find "/Applications/$APP_NAME/Contents/MacOS/" -type f -perm +111 -ls
|
||||||
|
|
||||||
|
# Try to find the main executable - it's usually the one with the same name as the app (without .app)
|
||||||
|
APP_BASE_NAME=$(basename "$APP_NAME" .app)
|
||||||
|
POTENTIAL_EXECUTABLES=(
|
||||||
|
"/Applications/$APP_NAME/Contents/MacOS/$APP_BASE_NAME"
|
||||||
|
"/Applications/$APP_NAME/Contents/MacOS/Jan"
|
||||||
|
"/Applications/$APP_NAME/Contents/MacOS/Jan-nightly"
|
||||||
|
)
|
||||||
|
|
||||||
|
APP_PATH=""
|
||||||
|
for potential_exec in "${POTENTIAL_EXECUTABLES[@]}"; do
|
||||||
|
echo "Checking: $potential_exec"
|
||||||
|
if [ -f "$potential_exec" ] && [ -x "$potential_exec" ]; then
|
||||||
|
APP_PATH="$potential_exec"
|
||||||
|
echo "Found executable: $APP_PATH"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# If still not found, get any executable file
|
||||||
|
if [ -z "$APP_PATH" ]; then
|
||||||
|
echo "No predefined executable found, searching for any executable..."
|
||||||
|
APP_PATH=$(find "/Applications/$APP_NAME/Contents/MacOS/" -type f -perm +111 | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$APP_PATH" ]; then
|
||||||
|
echo "❌ No executable found in MacOS folder"
|
||||||
|
ls -la "/Applications/$APP_NAME/Contents/MacOS/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROCESS_NAME=$(basename "$APP_PATH")
|
||||||
|
|
||||||
|
echo "App installed at: /Applications/$APP_NAME"
|
||||||
|
echo "Executable path: $APP_PATH"
|
||||||
|
echo "Process name: $PROCESS_NAME"
|
||||||
|
|
||||||
|
# Export for test step
|
||||||
|
echo "JAN_APP_PATH=$APP_PATH" >> $GITHUB_ENV
|
||||||
|
echo "PROCESS_NAME=$PROCESS_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# Verify installation
|
||||||
|
if [ -f "$APP_PATH" ]; then
|
||||||
|
echo "✅ Jan app installed successfully"
|
||||||
|
ls -la "/Applications/$APP_NAME"
|
||||||
|
else
|
||||||
|
echo "❌ Jan app installation failed - executable not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
38
autoqa/scripts/macos_post_cleanup.sh
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# macOS post-test cleanup script
|
||||||
|
|
||||||
|
echo "Cleaning up after tests..."
|
||||||
|
|
||||||
|
# Kill any running Jan processes (both regular and nightly)
|
||||||
|
pkill -f "Jan" || true
|
||||||
|
pkill -f "jan" || true
|
||||||
|
pkill -f "Jan-nightly" || true
|
||||||
|
pkill -f "jan-nightly" || true
|
||||||
|
|
||||||
|
# Remove Jan app directories
|
||||||
|
rm -rf /Applications/Jan.app
|
||||||
|
rm -rf /Applications/Jan-nightly.app
|
||||||
|
rm -rf ~/Applications/Jan.app
|
||||||
|
rm -rf ~/Applications/Jan-nightly.app
|
||||||
|
|
||||||
|
# Remove Jan data folders (both regular and nightly)
|
||||||
|
rm -rf ~/Library/Application\ Support/Jan
|
||||||
|
rm -rf ~/Library/Application\ Support/Jan-nightly
|
||||||
|
rm -rf ~/Library/Application\ Support/jan.ai.app
|
||||||
|
rm -rf ~/Library/Application\ Support/jan-nightly.ai.app
|
||||||
|
rm -rf ~/Library/Preferences/jan.*
|
||||||
|
rm -rf ~/Library/Preferences/jan-nightly.*
|
||||||
|
rm -rf ~/Library/Caches/jan.*
|
||||||
|
rm -rf ~/Library/Caches/jan-nightly.*
|
||||||
|
rm -rf ~/Library/Caches/jan.ai.app
|
||||||
|
rm -rf ~/Library/Caches/jan-nightly.ai.app
|
||||||
|
rm -rf ~/Library/WebKit/jan.ai.app
|
||||||
|
rm -rf ~/Library/WebKit/jan-nightly.ai.app
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/jan.ai.app
|
||||||
|
rm -rf ~/Library/Saved\ Application\ State/jan-nightly.ai.app
|
||||||
|
|
||||||
|
# Clean up downloaded installer
|
||||||
|
rm -f "/tmp/jan-installer.dmg"
|
||||||
|
rm -rf "/tmp/jan-mount"
|
||||||
|
|
||||||
|
echo "Cleanup completed"
|
||||||
31
autoqa/scripts/run_tests.ps1
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# Windows test runner script
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$JanAppPath,
|
||||||
|
[string]$ProcessName,
|
||||||
|
[string]$RpToken
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "Starting Auto QA Tests..."
|
||||||
|
|
||||||
|
Write-Host "Jan app path: $JanAppPath"
|
||||||
|
Write-Host "Process name: $ProcessName"
|
||||||
|
Write-Host "Current working directory: $(Get-Location)"
|
||||||
|
Write-Host "Contents of current directory:"
|
||||||
|
Get-ChildItem
|
||||||
|
Write-Host "Contents of trajectories directory (if exists):"
|
||||||
|
if (Test-Path "trajectories") {
|
||||||
|
Get-ChildItem "trajectories"
|
||||||
|
} else {
|
||||||
|
Write-Host "trajectories directory not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the main test with proper arguments
|
||||||
|
if ($JanAppPath -and $ProcessName) {
|
||||||
|
python main.py --enable-reportportal --rp-token "$RpToken" --jan-app-path "$JanAppPath" --jan-process-name "$ProcessName"
|
||||||
|
} elseif ($JanAppPath) {
|
||||||
|
python main.py --enable-reportportal --rp-token "$RpToken" --jan-app-path "$JanAppPath"
|
||||||
|
} else {
|
||||||
|
python main.py --enable-reportportal --rp-token "$RpToken"
|
||||||
|
}
|
||||||
69
autoqa/scripts/run_tests.sh
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Common test runner script
|
||||||
|
|
||||||
|
JAN_APP_PATH="$1"
|
||||||
|
PROCESS_NAME="$2"
|
||||||
|
RP_TOKEN="$3"
|
||||||
|
PLATFORM="$4"
|
||||||
|
|
||||||
|
echo "Starting Auto QA Tests..."
|
||||||
|
echo "Platform: $PLATFORM"
|
||||||
|
echo "Jan app path: $JAN_APP_PATH"
|
||||||
|
echo "Process name: $PROCESS_NAME"
|
||||||
|
|
||||||
|
# Platform-specific setup
|
||||||
|
if [ "$PLATFORM" = "ubuntu" ]; then
|
||||||
|
# Get the current display session
|
||||||
|
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
||||||
|
echo "Display ID: $DISPLAY"
|
||||||
|
|
||||||
|
# Verify display is working
|
||||||
|
if [ -z "$DISPLAY" ]; then
|
||||||
|
echo "No display session found, falling back to :0"
|
||||||
|
export DISPLAY=:0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using display: $DISPLAY"
|
||||||
|
|
||||||
|
# Test display connection
|
||||||
|
xdpyinfo -display $DISPLAY >/dev/null 2>&1 || {
|
||||||
|
echo "Display $DISPLAY is not available"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make Jan executable if needed
|
||||||
|
if [ -f "/usr/bin/Jan-nightly" ]; then
|
||||||
|
sudo chmod +x /usr/bin/Jan-nightly
|
||||||
|
fi
|
||||||
|
if [ -f "/usr/bin/Jan" ]; then
|
||||||
|
sudo chmod +x /usr/bin/Jan
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# macOS specific setup
|
||||||
|
if [ "$PLATFORM" = "macos" ]; then
|
||||||
|
# Verify Jan app path
|
||||||
|
if [ ! -f "$JAN_APP_PATH" ]; then
|
||||||
|
echo "❌ Jan app not found at: $JAN_APP_PATH"
|
||||||
|
echo "Available files in /Applications:"
|
||||||
|
ls -la /Applications/ | grep -i jan || echo "No Jan apps found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Change to autoqa directory to ensure correct working directory
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
echo "Current working directory: $(pwd)"
|
||||||
|
echo "Contents of current directory:"
|
||||||
|
ls -la
|
||||||
|
echo "Contents of trajectories directory (if exists):"
|
||||||
|
ls -la trajectories/ 2>/dev/null || echo "trajectories directory not found"
|
||||||
|
|
||||||
|
# Run the main test with proper arguments
|
||||||
|
if [ -n "$JAN_APP_PATH" ] && [ -n "$PROCESS_NAME" ]; then
|
||||||
|
python main.py --enable-reportportal --rp-token "$RP_TOKEN" --jan-app-path "$JAN_APP_PATH" --jan-process-name "$PROCESS_NAME"
|
||||||
|
elif [ -n "$JAN_APP_PATH" ]; then
|
||||||
|
python main.py --enable-reportportal --rp-token "$RP_TOKEN" --jan-app-path "$JAN_APP_PATH"
|
||||||
|
else
|
||||||
|
python main.py --enable-reportportal --rp-token "$RP_TOKEN"
|
||||||
|
fi
|
||||||
15
autoqa/scripts/setup_permissions.sh
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Setup script permissions for AutoQA scripts
|
||||||
|
|
||||||
|
echo "Setting up permissions for AutoQA scripts..."
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Make all shell scripts executable
|
||||||
|
chmod +x "$SCRIPT_DIR"/*.sh
|
||||||
|
|
||||||
|
echo "✅ All shell scripts are now executable:"
|
||||||
|
ls -la "$SCRIPT_DIR"/*.sh
|
||||||
|
|
||||||
|
echo "✅ Permission setup completed"
|
||||||
22
autoqa/scripts/ubuntu_cleanup.sh
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Ubuntu cleanup script for Jan app
|
||||||
|
|
||||||
|
echo "Cleaning existing Jan installations..."
|
||||||
|
|
||||||
|
# Remove Jan data folders (both regular and nightly)
|
||||||
|
rm -rf ~/.config/Jan
|
||||||
|
rm -rf ~/.config/Jan-nightly
|
||||||
|
rm -rf ~/.local/share/Jan
|
||||||
|
rm -rf ~/.local/share/Jan-nightly
|
||||||
|
rm -rf ~/.cache/jan
|
||||||
|
rm -rf ~/.cache/jan-nightly
|
||||||
|
rm -rf ~/.local/share/jan-nightly.ai.app
|
||||||
|
rm -rf ~/.local/share/jan.ai.app
|
||||||
|
|
||||||
|
# Kill any running Jan processes (both regular and nightly)
|
||||||
|
pkill -f "Jan" || true
|
||||||
|
pkill -f "jan" || true
|
||||||
|
pkill -f "Jan-nightly" || true
|
||||||
|
pkill -f "jan-nightly" || true
|
||||||
|
|
||||||
|
echo "Jan cleanup completed"
|
||||||
57
autoqa/scripts/ubuntu_download.sh
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Ubuntu download script for Jan app
|
||||||
|
|
||||||
|
WORKFLOW_INPUT_URL="$1"
|
||||||
|
WORKFLOW_INPUT_IS_NIGHTLY="$2"
|
||||||
|
REPO_VARIABLE_URL="$3"
|
||||||
|
REPO_VARIABLE_IS_NIGHTLY="$4"
|
||||||
|
DEFAULT_URL="$5"
|
||||||
|
DEFAULT_IS_NIGHTLY="$6"
|
||||||
|
|
||||||
|
# Determine Jan app URL and nightly flag from multiple sources (priority order):
|
||||||
|
# 1. Workflow dispatch input (manual trigger)
|
||||||
|
# 2. Repository variable JAN_APP_URL_LINUX
|
||||||
|
# 3. Default URL from env
|
||||||
|
|
||||||
|
JAN_APP_URL=""
|
||||||
|
IS_NIGHTLY=false
|
||||||
|
|
||||||
|
if [ -n "$WORKFLOW_INPUT_URL" ]; then
|
||||||
|
JAN_APP_URL="$WORKFLOW_INPUT_URL"
|
||||||
|
IS_NIGHTLY="$WORKFLOW_INPUT_IS_NIGHTLY"
|
||||||
|
echo "Using Jan app URL from workflow input: $JAN_APP_URL"
|
||||||
|
echo "Is nightly build: $IS_NIGHTLY"
|
||||||
|
elif [ -n "$REPO_VARIABLE_URL" ]; then
|
||||||
|
JAN_APP_URL="$REPO_VARIABLE_URL"
|
||||||
|
IS_NIGHTLY="$REPO_VARIABLE_IS_NIGHTLY"
|
||||||
|
echo "Using Jan app URL from repository variable: $JAN_APP_URL"
|
||||||
|
echo "Is nightly build: $IS_NIGHTLY"
|
||||||
|
else
|
||||||
|
JAN_APP_URL="$DEFAULT_URL"
|
||||||
|
IS_NIGHTLY="$DEFAULT_IS_NIGHTLY"
|
||||||
|
echo "Using default Jan app URL: $JAN_APP_URL"
|
||||||
|
echo "Is nightly build: $IS_NIGHTLY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set environment variables for later steps
|
||||||
|
echo "JAN_APP_URL=$JAN_APP_URL" >> $GITHUB_ENV
|
||||||
|
echo "IS_NIGHTLY=$IS_NIGHTLY" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "Downloading Jan app from: $JAN_APP_URL"
|
||||||
|
|
||||||
|
DOWNLOAD_PATH="/tmp/jan-installer.deb"
|
||||||
|
|
||||||
|
# Download the package
|
||||||
|
if ! wget "$JAN_APP_URL" -O "$DOWNLOAD_PATH"; then
|
||||||
|
echo "Failed to download Jan app"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$DOWNLOAD_PATH" ]; then
|
||||||
|
FILE_SIZE=$(stat -c%s "$DOWNLOAD_PATH")
|
||||||
|
echo "Downloaded Jan app successfully. Size: $FILE_SIZE bytes"
|
||||||
|
echo "File saved to: $DOWNLOAD_PATH"
|
||||||
|
else
|
||||||
|
echo "Downloaded file not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
34
autoqa/scripts/ubuntu_install.sh
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Ubuntu install script for Jan app
|
||||||
|
|
||||||
|
IS_NIGHTLY="$1"
|
||||||
|
|
||||||
|
INSTALLER_PATH="/tmp/jan-installer.deb"
|
||||||
|
|
||||||
|
echo "Installing Jan app..."
|
||||||
|
echo "Is nightly build: $IS_NIGHTLY"
|
||||||
|
|
||||||
|
# Install the .deb package
|
||||||
|
sudo apt install "$INSTALLER_PATH" -y
|
||||||
|
sudo apt-get install -f -y
|
||||||
|
|
||||||
|
# Wait for installation to complete
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Verify installation based on nightly flag
|
||||||
|
if [ "$IS_NIGHTLY" = "true" ]; then
|
||||||
|
DEFAULT_JAN_PATH="/usr/bin/Jan-nightly"
|
||||||
|
PROCESS_NAME="Jan-nightly"
|
||||||
|
else
|
||||||
|
DEFAULT_JAN_PATH="/usr/bin/Jan"
|
||||||
|
PROCESS_NAME="Jan"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$DEFAULT_JAN_PATH" ]; then
|
||||||
|
echo "Jan app installed successfully at: $DEFAULT_JAN_PATH"
|
||||||
|
echo "JAN_APP_PATH=$DEFAULT_JAN_PATH" >> $GITHUB_ENV
|
||||||
|
echo "JAN_PROCESS_NAME=$PROCESS_NAME" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "Jan app not found at expected location: $DEFAULT_JAN_PATH"
|
||||||
|
echo "Will auto-detect during test run"
|
||||||
|
fi
|
||||||
44
autoqa/scripts/ubuntu_post_cleanup.sh
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Ubuntu post-test cleanup script
|
||||||
|
|
||||||
|
IS_NIGHTLY="$1"
|
||||||
|
|
||||||
|
echo "Cleaning up after tests..."
|
||||||
|
|
||||||
|
# Kill any running Jan processes (both regular and nightly)
|
||||||
|
pkill -f "Jan" || true
|
||||||
|
pkill -f "jan" || true
|
||||||
|
pkill -f "Jan-nightly" || true
|
||||||
|
pkill -f "jan-nightly" || true
|
||||||
|
|
||||||
|
# Remove Jan data folders (both regular and nightly)
|
||||||
|
rm -rf ~/.config/Jan
|
||||||
|
rm -rf ~/.config/Jan-nightly
|
||||||
|
rm -rf ~/.local/share/Jan
|
||||||
|
rm -rf ~/.local/share/Jan-nightly
|
||||||
|
rm -rf ~/.cache/jan
|
||||||
|
rm -rf ~/.cache/jan-nightly
|
||||||
|
rm -rf ~/.local/share/jan-nightly.ai.app
|
||||||
|
rm -rf ~/.local/share/jan.ai.app
|
||||||
|
|
||||||
|
# Try to uninstall Jan app
|
||||||
|
if [ "$IS_NIGHTLY" = "true" ]; then
|
||||||
|
PACKAGE_NAME="jan-nightly"
|
||||||
|
else
|
||||||
|
PACKAGE_NAME="jan"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Attempting to uninstall package: $PACKAGE_NAME"
|
||||||
|
|
||||||
|
if dpkg -l | grep -q "$PACKAGE_NAME"; then
|
||||||
|
echo "Found package $PACKAGE_NAME, uninstalling..."
|
||||||
|
sudo dpkg -r "$PACKAGE_NAME" || true
|
||||||
|
sudo apt-get autoremove -y || true
|
||||||
|
else
|
||||||
|
echo "Package $PACKAGE_NAME not found in dpkg list"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up downloaded installer
|
||||||
|
rm -f "/tmp/jan-installer.deb"
|
||||||
|
|
||||||
|
echo "Cleanup completed"
|
||||||
50
autoqa/scripts/windows_cleanup.ps1
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# Windows cleanup script for Jan app
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$IsNightly = "false"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "Cleaning existing Jan installations..."
|
||||||
|
|
||||||
|
# Remove Jan data folders (both regular and nightly)
|
||||||
|
$janAppData = "$env:APPDATA\Jan"
|
||||||
|
$janNightlyAppData = "$env:APPDATA\Jan-nightly"
|
||||||
|
$janLocalAppData = "$env:LOCALAPPDATA\jan.ai.app"
|
||||||
|
$janNightlyLocalAppData = "$env:LOCALAPPDATA\jan-nightly.ai.app"
|
||||||
|
|
||||||
|
if (Test-Path $janAppData) {
|
||||||
|
Write-Host "Removing $janAppData"
|
||||||
|
Remove-Item -Path $janAppData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $janNightlyAppData) {
|
||||||
|
Write-Host "Removing $janNightlyAppData"
|
||||||
|
Remove-Item -Path $janNightlyAppData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $janLocalAppData) {
|
||||||
|
Write-Host "Removing $janLocalAppData"
|
||||||
|
Remove-Item -Path $janLocalAppData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $janNightlyLocalAppData) {
|
||||||
|
Write-Host "Removing $janNightlyLocalAppData"
|
||||||
|
Remove-Item -Path $janNightlyLocalAppData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Kill any running Jan processes (both regular and nightly)
|
||||||
|
Get-Process -Name "Jan" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-Process -Name "jan" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-Process -Name "Jan-nightly" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-Process -Name "jan-nightly" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# Remove Jan extensions folder
|
||||||
|
$janExtensionsPath = "$env:USERPROFILE\jan\extensions"
|
||||||
|
if (Test-Path $janExtensionsPath) {
|
||||||
|
Write-Host "Removing $janExtensionsPath"
|
||||||
|
Remove-Item -Path $janExtensionsPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Jan cleanup completed"
|
||||||
63
autoqa/scripts/windows_download.ps1
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# Windows download script for Jan app
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$WorkflowInputUrl = "",
|
||||||
|
[string]$WorkflowInputIsNightly = "",
|
||||||
|
[string]$RepoVariableUrl = "",
|
||||||
|
[string]$RepoVariableIsNightly = "",
|
||||||
|
[string]$DefaultUrl = "",
|
||||||
|
[string]$DefaultIsNightly = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Determine Jan app URL and nightly flag from multiple sources (priority order):
|
||||||
|
# 1. Workflow dispatch input (manual trigger)
|
||||||
|
# 2. Repository variable JAN_APP_URL
|
||||||
|
# 3. Default URL from env
|
||||||
|
|
||||||
|
$janAppUrl = ""
|
||||||
|
$isNightly = $false
|
||||||
|
|
||||||
|
if ($WorkflowInputUrl -ne "") {
|
||||||
|
$janAppUrl = $WorkflowInputUrl
|
||||||
|
$isNightly = [System.Convert]::ToBoolean($WorkflowInputIsNightly)
|
||||||
|
Write-Host "Using Jan app URL from workflow input: $janAppUrl"
|
||||||
|
Write-Host "Is nightly build: $isNightly"
|
||||||
|
}
|
||||||
|
elseif ($RepoVariableUrl -ne "") {
|
||||||
|
$janAppUrl = $RepoVariableUrl
|
||||||
|
$isNightly = [System.Convert]::ToBoolean($RepoVariableIsNightly)
|
||||||
|
Write-Host "Using Jan app URL from repository variable: $janAppUrl"
|
||||||
|
Write-Host "Is nightly build: $isNightly"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$janAppUrl = $DefaultUrl
|
||||||
|
$isNightly = [System.Convert]::ToBoolean($DefaultIsNightly)
|
||||||
|
Write-Host "Using default Jan app URL: $janAppUrl"
|
||||||
|
Write-Host "Is nightly build: $isNightly"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set environment variables for later steps
|
||||||
|
Write-Output "JAN_APP_URL=$janAppUrl" >> $env:GITHUB_ENV
|
||||||
|
Write-Output "IS_NIGHTLY=$isNightly" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
Write-Host "Downloading Jan app from: $janAppUrl"
|
||||||
|
|
||||||
|
$downloadPath = "$env:TEMP\jan-installer.exe"
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Use wget for better performance
|
||||||
|
wget.exe "$janAppUrl" -O "$downloadPath"
|
||||||
|
|
||||||
|
if (Test-Path $downloadPath) {
|
||||||
|
$fileSize = (Get-Item $downloadPath).Length
|
||||||
|
Write-Host "Downloaded Jan app successfully. Size: $fileSize bytes"
|
||||||
|
Write-Host "File saved to: $downloadPath"
|
||||||
|
} else {
|
||||||
|
throw "Downloaded file not found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "Failed to download Jan app: $_"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
43
autoqa/scripts/windows_install.ps1
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# Windows install script for Jan app
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$IsNightly = "false"
|
||||||
|
)
|
||||||
|
|
||||||
|
$installerPath = "$env:TEMP\jan-installer.exe"
|
||||||
|
$isNightly = [System.Convert]::ToBoolean($IsNightly)
|
||||||
|
|
||||||
|
Write-Host "Installing Jan app..."
|
||||||
|
Write-Host "Is nightly build: $isNightly"
|
||||||
|
|
||||||
|
# Try silent installation first
|
||||||
|
try {
|
||||||
|
Start-Process -FilePath $installerPath -ArgumentList "/S" -Wait -NoNewWindow
|
||||||
|
Write-Host "Jan app installed silently"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "Silent installation failed, trying normal installation..."
|
||||||
|
Start-Process -FilePath $installerPath -Wait -NoNewWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait a bit for installation to complete
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
|
||||||
|
# Verify installation based on nightly flag
|
||||||
|
if ($isNightly) {
|
||||||
|
$defaultJanPath = "$env:LOCALAPPDATA\Programs\jan-nightly\Jan-nightly.exe"
|
||||||
|
$processName = "Jan-nightly.exe"
|
||||||
|
} else {
|
||||||
|
$defaultJanPath = "$env:LOCALAPPDATA\Programs\jan\Jan.exe"
|
||||||
|
$processName = "Jan.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $defaultJanPath) {
|
||||||
|
Write-Host "Jan app installed successfully at: $defaultJanPath"
|
||||||
|
Write-Output "JAN_APP_PATH=$defaultJanPath" >> $env:GITHUB_ENV
|
||||||
|
Write-Output "JAN_PROCESS_NAME=$processName" >> $env:GITHUB_ENV
|
||||||
|
} else {
|
||||||
|
Write-Warning "Jan app not found at expected location: $defaultJanPath"
|
||||||
|
Write-Host "Will auto-detect during test run"
|
||||||
|
}
|
||||||
102
autoqa/scripts/windows_post_cleanup.ps1
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# Windows post-test cleanup script
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$IsNightly = "false"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "Cleaning up after tests..."
|
||||||
|
|
||||||
|
# Kill any running Jan processes (both regular and nightly)
|
||||||
|
Get-Process -Name "Jan" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-Process -Name "jan" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-Process -Name "Jan-nightly" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Get-Process -Name "jan-nightly" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# Remove Jan data folders (both regular and nightly)
|
||||||
|
$janAppData = "$env:APPDATA\Jan"
|
||||||
|
$janNightlyAppData = "$env:APPDATA\Jan-nightly"
|
||||||
|
$janLocalAppData = "$env:LOCALAPPDATA\jan.ai.app"
|
||||||
|
$janNightlyLocalAppData = "$env:LOCALAPPDATA\jan-nightly.ai.app"
|
||||||
|
$janProgramsPath = "$env:LOCALAPPDATA\Programs\Jan"
|
||||||
|
$janNightlyProgramsPath = "$env:LOCALAPPDATA\Programs\Jan-nightly"
|
||||||
|
|
||||||
|
if (Test-Path $janAppData) {
|
||||||
|
Write-Host "Removing $janAppData"
|
||||||
|
Remove-Item -Path $janAppData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $janNightlyAppData) {
|
||||||
|
Write-Host "Removing $janNightlyAppData"
|
||||||
|
Remove-Item -Path $janNightlyAppData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $janLocalAppData) {
|
||||||
|
Write-Host "Removing $janLocalAppData"
|
||||||
|
Remove-Item -Path $janLocalAppData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $janNightlyLocalAppData) {
|
||||||
|
Write-Host "Removing $janNightlyLocalAppData"
|
||||||
|
Remove-Item -Path $janNightlyLocalAppData -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $janProgramsPath) {
|
||||||
|
Write-Host "Removing $janProgramsPath"
|
||||||
|
Remove-Item -Path $janProgramsPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $janNightlyProgramsPath) {
|
||||||
|
Write-Host "Removing $janNightlyProgramsPath"
|
||||||
|
Remove-Item -Path $janNightlyProgramsPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove Jan extensions folder
|
||||||
|
$janExtensionsPath = "$env:USERPROFILE\jan\extensions"
|
||||||
|
if (Test-Path $janExtensionsPath) {
|
||||||
|
Write-Host "Removing $janExtensionsPath"
|
||||||
|
Remove-Item -Path $janExtensionsPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to uninstall Jan app silently
|
||||||
|
try {
|
||||||
|
$isNightly = [System.Convert]::ToBoolean($IsNightly)
|
||||||
|
|
||||||
|
# Determine uninstaller path based on nightly flag
|
||||||
|
if ($isNightly) {
|
||||||
|
$uninstallerPath = "$env:LOCALAPPDATA\Programs\jan-nightly\uninstall.exe"
|
||||||
|
$installPath = "$env:LOCALAPPDATA\Programs\jan-nightly"
|
||||||
|
} else {
|
||||||
|
$uninstallerPath = "$env:LOCALAPPDATA\Programs\jan\uninstall.exe"
|
||||||
|
$installPath = "$env:LOCALAPPDATA\Programs\jan"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Looking for uninstaller at: $uninstallerPath"
|
||||||
|
|
||||||
|
if (Test-Path $uninstallerPath) {
|
||||||
|
Write-Host "Found uninstaller, attempting silent uninstall..."
|
||||||
|
Start-Process -FilePath $uninstallerPath -ArgumentList "/S" -Wait -NoNewWindow -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Uninstall completed"
|
||||||
|
} else {
|
||||||
|
Write-Host "No uninstaller found, attempting manual cleanup..."
|
||||||
|
|
||||||
|
if (Test-Path $installPath) {
|
||||||
|
Write-Host "Removing installation directory: $installPath"
|
||||||
|
Remove-Item -Path $installPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Jan app cleanup completed"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Failed to uninstall Jan app cleanly: $_"
|
||||||
|
Write-Host "Manual cleanup may be required"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up downloaded installer
|
||||||
|
$installerPath = "$env:TEMP\jan-installer.exe"
|
||||||
|
if (Test-Path $installerPath) {
|
||||||
|
Remove-Item -Path $installerPath -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Cleanup completed"
|
||||||
319
autoqa/test_runner.py
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
# from computer import Computer
|
||||||
|
from agent import ComputerAgent, LLM
|
||||||
|
|
||||||
|
from utils import is_jan_running, force_close_jan, start_jan_app, get_latest_trajectory_folder
|
||||||
|
from screen_recorder import ScreenRecorder
|
||||||
|
from reportportal_handler import upload_test_results_to_rp
|
||||||
|
from reportportal_client.helpers import timestamp
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def run_single_test_with_timeout(computer, test_data, rp_client, launch_id, max_turns=30,
|
||||||
|
jan_app_path=None, jan_process_name="Jan.exe", agent_config=None,
|
||||||
|
enable_reportportal=False):
|
||||||
|
"""
|
||||||
|
Run a single test case with turn count monitoring, forced stop, and screen recording
|
||||||
|
Returns dict with test result: {"success": bool, "status": str, "message": str}
|
||||||
|
"""
|
||||||
|
path = test_data['path']
|
||||||
|
prompt = test_data['prompt']
|
||||||
|
|
||||||
|
# Default agent config if not provided
|
||||||
|
if agent_config is None:
|
||||||
|
agent_config = {
|
||||||
|
"loop": "uitars",
|
||||||
|
"model_provider": "oaicompat",
|
||||||
|
"model_name": "ByteDance-Seed/UI-TARS-1.5-7B",
|
||||||
|
"model_base_url": "http://10.200.108.58:1234/v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create trajectory_dir from path (remove .txt extension)
|
||||||
|
trajectory_name = str(Path(path).with_suffix(''))
|
||||||
|
trajectory_base_dir = os.path.abspath(f"trajectories/{trajectory_name.replace(os.sep, '/')}")
|
||||||
|
|
||||||
|
# Ensure trajectories directory exists
|
||||||
|
os.makedirs(os.path.dirname(trajectory_base_dir), exist_ok=True)
|
||||||
|
|
||||||
|
# Create recordings directory
|
||||||
|
recordings_dir = "recordings"
|
||||||
|
os.makedirs(recordings_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Create video filename
|
||||||
|
current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
safe_test_name = trajectory_name.replace('/', '_').replace('\\', '_')
|
||||||
|
video_filename = f"{safe_test_name}_{current_time}.mp4"
|
||||||
|
video_path = os.path.abspath(os.path.join(recordings_dir, video_filename))
|
||||||
|
|
||||||
|
# Initialize result tracking
|
||||||
|
test_result_data = {
|
||||||
|
"success": False,
|
||||||
|
"status": "UNKNOWN",
|
||||||
|
"message": "Test execution incomplete",
|
||||||
|
"trajectory_dir": None,
|
||||||
|
"video_path": video_path
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Starting test: {path}")
|
||||||
|
logger.info(f"Current working directory: {os.getcwd()}")
|
||||||
|
logger.info(f"Trajectory base directory: {trajectory_base_dir}")
|
||||||
|
logger.info(f"Screen recording will be saved to: {video_path}")
|
||||||
|
logger.info(f"Using model: {agent_config['model_name']} from {agent_config['model_base_url']}")
|
||||||
|
logger.info(f"ReportPortal upload: {'ENABLED' if enable_reportportal else 'DISABLED'}")
|
||||||
|
|
||||||
|
trajectory_dir = None
|
||||||
|
agent_task = None
|
||||||
|
monitor_stop_event = threading.Event()
|
||||||
|
force_stopped_due_to_turns = False # Track if test was force stopped
|
||||||
|
|
||||||
|
# Initialize screen recorder
|
||||||
|
recorder = ScreenRecorder(video_path, fps=10)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Step 1: Check and force close Jan app if running
|
||||||
|
if is_jan_running(jan_process_name):
|
||||||
|
logger.info("Jan application is running, force closing...")
|
||||||
|
force_close_jan(jan_process_name)
|
||||||
|
|
||||||
|
# Step 2: Start Jan app in maximized mode
|
||||||
|
if jan_app_path:
|
||||||
|
start_jan_app(jan_app_path)
|
||||||
|
else:
|
||||||
|
start_jan_app() # Use default path
|
||||||
|
|
||||||
|
# Step 3: Start screen recording
|
||||||
|
recorder.start_recording()
|
||||||
|
|
||||||
|
# Step 4: Create agent for this test using config
|
||||||
|
agent = ComputerAgent(
|
||||||
|
computer=computer,
|
||||||
|
loop=agent_config["loop"],
|
||||||
|
model=LLM(
|
||||||
|
provider=agent_config["model_provider"],
|
||||||
|
name=agent_config["model_name"],
|
||||||
|
provider_base_url=agent_config["model_base_url"]
|
||||||
|
),
|
||||||
|
trajectory_dir=trajectory_base_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 5: Start monitoring thread
|
||||||
|
def monitor_thread():
|
||||||
|
nonlocal force_stopped_due_to_turns
|
||||||
|
while not monitor_stop_event.is_set():
|
||||||
|
try:
|
||||||
|
if os.path.exists(trajectory_base_dir):
|
||||||
|
folders = [f for f in os.listdir(trajectory_base_dir)
|
||||||
|
if os.path.isdir(os.path.join(trajectory_base_dir, f))]
|
||||||
|
|
||||||
|
if folders:
|
||||||
|
latest_folder = sorted(folders)[-1]
|
||||||
|
latest_folder_path = os.path.join(trajectory_base_dir, latest_folder)
|
||||||
|
|
||||||
|
if os.path.exists(latest_folder_path):
|
||||||
|
turn_folders = [f for f in os.listdir(latest_folder_path)
|
||||||
|
if os.path.isdir(os.path.join(latest_folder_path, f)) and f.startswith("turn_")]
|
||||||
|
|
||||||
|
turn_count = len(turn_folders)
|
||||||
|
logger.info(f"Current turn count: {turn_count}")
|
||||||
|
|
||||||
|
if turn_count >= max_turns:
|
||||||
|
logger.warning(f"Turn count exceeded {max_turns} for test {path}, forcing stop")
|
||||||
|
force_stopped_due_to_turns = True # Mark as force stopped
|
||||||
|
# Cancel the agent task
|
||||||
|
if agent_task and not agent_task.done():
|
||||||
|
agent_task.cancel()
|
||||||
|
monitor_stop_event.set()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check every 5 seconds
|
||||||
|
if not monitor_stop_event.wait(5):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in monitor thread: {e}")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Start monitoring in background thread
|
||||||
|
monitor_thread_obj = threading.Thread(target=monitor_thread, daemon=True)
|
||||||
|
monitor_thread_obj.start()
|
||||||
|
|
||||||
|
# Step 6: Run the test with prompt
|
||||||
|
logger.info(f"Running test case: {path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create the agent task
|
||||||
|
async def run_agent():
|
||||||
|
async for result in agent.run(prompt):
|
||||||
|
if monitor_stop_event.is_set():
|
||||||
|
logger.warning(f"Test {path} stopped due to turn limit")
|
||||||
|
break
|
||||||
|
logger.info(f"Test result for {path}: {result}")
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
agent_task = asyncio.create_task(run_agent())
|
||||||
|
|
||||||
|
# Wait for agent task to complete or timeout
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(agent_task, timeout=600) # 10 minute timeout as backup
|
||||||
|
if not monitor_stop_event.is_set():
|
||||||
|
logger.info(f"Successfully completed test execution: {path}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Test {path} was stopped due to turn limit")
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning(f"Test {path} timed out after 10 minutes")
|
||||||
|
agent_task.cancel()
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.warning(f"Test {path} was cancelled due to turn limit")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Stop monitoring
|
||||||
|
monitor_stop_event.set()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error running test {path}: {e}")
|
||||||
|
monitor_stop_event.set()
|
||||||
|
# Update result data for exception case
|
||||||
|
test_result_data.update({
|
||||||
|
"success": False,
|
||||||
|
"status": "ERROR",
|
||||||
|
"message": f"Test execution failed with exception: {str(e)}",
|
||||||
|
"trajectory_dir": None
|
||||||
|
})
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Step 7: Stop screen recording
|
||||||
|
try:
|
||||||
|
recorder.stop_recording()
|
||||||
|
logger.info(f"Screen recording saved to: {video_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error stopping screen recording: {e}")
|
||||||
|
|
||||||
|
# Step 8: Upload results to ReportPortal only if enabled
|
||||||
|
if enable_reportportal and rp_client and launch_id:
|
||||||
|
# Get trajectory folder first
|
||||||
|
trajectory_dir = get_latest_trajectory_folder(trajectory_base_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if trajectory_dir:
|
||||||
|
logger.info(f"Uploading results to ReportPortal for: {path}")
|
||||||
|
logger.info(f"Video path for upload: {video_path}")
|
||||||
|
logger.info(f"Video exists: {os.path.exists(video_path)}")
|
||||||
|
if os.path.exists(video_path):
|
||||||
|
logger.info(f"Video file size: {os.path.getsize(video_path)} bytes")
|
||||||
|
upload_test_results_to_rp(rp_client, launch_id, path, trajectory_dir, force_stopped_due_to_turns, video_path)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Test completed but no trajectory found for: {path}")
|
||||||
|
# Handle case where test completed but no trajectory found
|
||||||
|
formatted_test_path = path.replace('\\', '/').replace('.txt', '').replace('/', '__')
|
||||||
|
test_item_id = rp_client.start_test_item(
|
||||||
|
launch_id=launch_id,
|
||||||
|
name=formatted_test_path,
|
||||||
|
start_time=timestamp(),
|
||||||
|
item_type="TEST"
|
||||||
|
)
|
||||||
|
rp_client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="ERROR",
|
||||||
|
message="Test execution completed but no trajectory data found",
|
||||||
|
item_id=test_item_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Still upload video for failed test
|
||||||
|
if video_path and os.path.exists(video_path):
|
||||||
|
try:
|
||||||
|
with open(video_path, "rb") as video_file:
|
||||||
|
rp_client.log(
|
||||||
|
time=timestamp(),
|
||||||
|
level="INFO",
|
||||||
|
message="🎥 Screen recording of failed test",
|
||||||
|
item_id=test_item_id,
|
||||||
|
attachment={
|
||||||
|
"name": f"failed_test_recording_{formatted_test_path}.mp4",
|
||||||
|
"data": video_file.read(),
|
||||||
|
"mime": "video/x-msvideo"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error uploading video for failed test: {e}")
|
||||||
|
|
||||||
|
rp_client.finish_test_item(
|
||||||
|
item_id=test_item_id,
|
||||||
|
end_time=timestamp(),
|
||||||
|
status="FAILED"
|
||||||
|
)
|
||||||
|
except Exception as upload_error:
|
||||||
|
logger.error(f"Error uploading results for {path}: {upload_error}")
|
||||||
|
else:
|
||||||
|
# For non-ReportPortal mode, still get trajectory for final results
|
||||||
|
trajectory_dir = get_latest_trajectory_folder(trajectory_base_dir)
|
||||||
|
|
||||||
|
# Always process results for consistency (both RP and local mode)
|
||||||
|
# trajectory_dir is already set above, no need to call get_latest_trajectory_folder again
|
||||||
|
if trajectory_dir:
|
||||||
|
# Extract test result for processing
|
||||||
|
from reportportal_handler import extract_test_result_from_trajectory
|
||||||
|
|
||||||
|
if force_stopped_due_to_turns:
|
||||||
|
final_status = "FAILED"
|
||||||
|
status_message = "exceeded maximum turn limit ({} turns)".format(max_turns)
|
||||||
|
test_result_data.update({
|
||||||
|
"success": False,
|
||||||
|
"status": final_status,
|
||||||
|
"message": status_message,
|
||||||
|
"trajectory_dir": trajectory_dir
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
test_result = extract_test_result_from_trajectory(trajectory_dir)
|
||||||
|
if test_result is True:
|
||||||
|
final_status = "PASSED"
|
||||||
|
status_message = "completed successfully with positive result"
|
||||||
|
test_result_data.update({
|
||||||
|
"success": True,
|
||||||
|
"status": final_status,
|
||||||
|
"message": status_message,
|
||||||
|
"trajectory_dir": trajectory_dir
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
final_status = "FAILED"
|
||||||
|
status_message = "no valid success result found"
|
||||||
|
test_result_data.update({
|
||||||
|
"success": False,
|
||||||
|
"status": final_status,
|
||||||
|
"message": status_message,
|
||||||
|
"trajectory_dir": trajectory_dir
|
||||||
|
})
|
||||||
|
|
||||||
|
if not enable_reportportal:
|
||||||
|
# Local development mode - log results
|
||||||
|
logger.info(f"🏠 LOCAL RESULT: {path} - {final_status} ({status_message})")
|
||||||
|
logger.info(f"📹 Video saved: {video_path}")
|
||||||
|
logger.info(f"📁 Trajectory: {trajectory_dir}")
|
||||||
|
else:
|
||||||
|
final_status = "FAILED"
|
||||||
|
status_message = "no trajectory found"
|
||||||
|
test_result_data.update({
|
||||||
|
"success": False,
|
||||||
|
"status": final_status,
|
||||||
|
"message": status_message,
|
||||||
|
"trajectory_dir": None
|
||||||
|
})
|
||||||
|
|
||||||
|
if not enable_reportportal:
|
||||||
|
logger.warning(f"🏠 LOCAL RESULT: {path} - {final_status} ({status_message})")
|
||||||
|
|
||||||
|
# Step 9: Always force close Jan app after test completion
|
||||||
|
logger.info(f"Cleaning up after test: {path}")
|
||||||
|
force_close_jan(jan_process_name)
|
||||||
|
|
||||||
|
# Return test result
|
||||||
|
return test_result_data
|
||||||
15
autoqa/tests/new-user/1-user-start-chatting.txt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
prompt = """
|
||||||
|
You are going to test the Jan application by downloading and chatting with a model (qwen2.5).
|
||||||
|
|
||||||
|
Step-by-step instructions:
|
||||||
|
1. Given the Jan application is already opened.
|
||||||
|
2. In the **bottom-left corner**, click the **“Hub”** menu item.
|
||||||
|
3. Scroll through the model list or use the search bar to find **qwen2.5**.
|
||||||
|
4. Click **“Use”** on the qwen2.5 model.
|
||||||
|
5. Wait for the model to finish downloading and become ready.
|
||||||
|
6. Once redirected to the chat screen, type any message into the input box (e.g. `Hello qwen2.5`).
|
||||||
|
7. Press **Enter** to send the message.
|
||||||
|
8. Wait for the model’s response.
|
||||||
|
|
||||||
|
If the model responds correctly, return: {"result": True}, otherwise return: {"result": False}.
|
||||||
|
"""
|
||||||
343
autoqa/utils.py
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import psutil
|
||||||
|
import time
|
||||||
|
import pyautogui
|
||||||
|
import platform
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Cross-platform window management
|
||||||
|
IS_LINUX = platform.system() == "Linux"
|
||||||
|
IS_WINDOWS = platform.system() == "Windows"
|
||||||
|
IS_MACOS = platform.system() == "Darwin"
|
||||||
|
|
||||||
|
if IS_WINDOWS:
|
||||||
|
try:
|
||||||
|
import pygetwindow as gw
|
||||||
|
except ImportError:
|
||||||
|
gw = None
|
||||||
|
logger.warning("pygetwindow not available on this system")
|
||||||
|
|
||||||
|
def is_jan_running(jan_process_name="Jan.exe"):
|
||||||
|
"""
|
||||||
|
Check if Jan application is currently running
|
||||||
|
"""
|
||||||
|
for proc in psutil.process_iter(['pid', 'name']):
|
||||||
|
try:
|
||||||
|
if proc.info['name'] and jan_process_name.lower() in proc.info['name'].lower():
|
||||||
|
return True
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def force_close_jan(jan_process_name="Jan.exe"):
|
||||||
|
"""
|
||||||
|
Force close Jan application if it's running
|
||||||
|
"""
|
||||||
|
logger.info("Checking for running Jan processes...")
|
||||||
|
closed_any = False
|
||||||
|
|
||||||
|
for proc in psutil.process_iter(['pid', 'name']):
|
||||||
|
try:
|
||||||
|
if proc.info['name'] and jan_process_name.lower() in proc.info['name'].lower():
|
||||||
|
logger.info(f"Force closing Jan process (PID: {proc.info['pid']})")
|
||||||
|
proc.kill()
|
||||||
|
closed_any = True
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if closed_any:
|
||||||
|
logger.info("Waiting for Jan processes to terminate...")
|
||||||
|
time.sleep(3) # Wait for processes to fully terminate
|
||||||
|
else:
|
||||||
|
logger.info("No Jan processes found running")
|
||||||
|
|
||||||
|
def find_jan_window_linux():
|
||||||
|
"""
|
||||||
|
Find Jan window on Linux using wmctrl
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['wmctrl', '-l'], capture_output=True, text=True, timeout=10)
|
||||||
|
if result.returncode == 0:
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if 'jan' in line.lower() or 'Jan' in line:
|
||||||
|
# Extract window ID (first column)
|
||||||
|
window_id = line.split()[0]
|
||||||
|
logger.info(f"Found Jan window with ID: {window_id}")
|
||||||
|
return window_id
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError) as e:
|
||||||
|
logger.warning(f"wmctrl command failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def maximize_jan_window_linux():
|
||||||
|
"""
|
||||||
|
Maximize Jan window on Linux using wmctrl
|
||||||
|
"""
|
||||||
|
window_id = find_jan_window_linux()
|
||||||
|
if window_id:
|
||||||
|
try:
|
||||||
|
# Maximize window using wmctrl
|
||||||
|
subprocess.run(['wmctrl', '-i', '-r', window_id, '-b', 'add,maximized_vert,maximized_horz'],
|
||||||
|
timeout=5)
|
||||||
|
logger.info("Jan window maximized using wmctrl")
|
||||||
|
return True
|
||||||
|
except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e:
|
||||||
|
logger.warning(f"Failed to maximize with wmctrl: {e}")
|
||||||
|
|
||||||
|
# Fallback: Try xdotool
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['xdotool', 'search', '--name', 'Jan'],
|
||||||
|
capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
window_id = result.stdout.strip().split('\n')[0]
|
||||||
|
subprocess.run(['xdotool', 'windowactivate', window_id], timeout=5)
|
||||||
|
subprocess.run(['xdotool', 'key', 'alt+F10'], timeout=5) # Maximize shortcut
|
||||||
|
logger.info("Jan window maximized using xdotool")
|
||||||
|
return True
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError) as e:
|
||||||
|
logger.warning(f"xdotool command failed: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def find_jan_window_macos():
|
||||||
|
"""
|
||||||
|
Find Jan window on macOS using AppleScript
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# AppleScript to find Jan window
|
||||||
|
script = '''
|
||||||
|
tell application "System Events"
|
||||||
|
set janApps to (every process whose name contains "Jan")
|
||||||
|
if length of janApps > 0 then
|
||||||
|
return name of first item of janApps
|
||||||
|
else
|
||||||
|
return ""
|
||||||
|
end if
|
||||||
|
end tell
|
||||||
|
'''
|
||||||
|
result = subprocess.run(['osascript', '-e', script],
|
||||||
|
capture_output=True, text=True, timeout=10)
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
app_name = result.stdout.strip()
|
||||||
|
logger.info(f"Found Jan app: {app_name}")
|
||||||
|
return app_name
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError) as e:
|
||||||
|
logger.warning(f"AppleScript command failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def maximize_jan_window_macos():
|
||||||
|
"""
|
||||||
|
Maximize Jan window on macOS using AppleScript
|
||||||
|
"""
|
||||||
|
app_name = find_jan_window_macos()
|
||||||
|
if app_name:
|
||||||
|
try:
|
||||||
|
# AppleScript to maximize window
|
||||||
|
script = f'''
|
||||||
|
tell application "System Events"
|
||||||
|
tell process "{app_name}"
|
||||||
|
set frontmost to true
|
||||||
|
tell window 1
|
||||||
|
set value of attribute "AXFullScreen" to true
|
||||||
|
end tell
|
||||||
|
end tell
|
||||||
|
end tell
|
||||||
|
'''
|
||||||
|
result = subprocess.run(['osascript', '-e', script], timeout=10)
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info("Jan window maximized using AppleScript")
|
||||||
|
return True
|
||||||
|
except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e:
|
||||||
|
logger.warning(f"Failed to maximize with AppleScript: {e}")
|
||||||
|
|
||||||
|
# Fallback: Try Command+M (fullscreen hotkey on macOS)
|
||||||
|
try:
|
||||||
|
logger.info("Trying Cmd+Ctrl+F hotkey to maximize")
|
||||||
|
pyautogui.hotkey('cmd', 'ctrl', 'f')
|
||||||
|
time.sleep(1)
|
||||||
|
logger.info("Attempted to maximize using Cmd+Ctrl+F")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Hotkey maximize failed: {e}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def maximize_jan_window():
|
||||||
|
"""
|
||||||
|
Find and maximize Jan window (cross-platform)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Wait a bit for window to appear
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
if IS_LINUX:
|
||||||
|
return maximize_jan_window_linux()
|
||||||
|
|
||||||
|
elif IS_MACOS:
|
||||||
|
return maximize_jan_window_macos()
|
||||||
|
|
||||||
|
elif IS_WINDOWS and gw:
|
||||||
|
# Method 1: Try to find window by title containing "Jan"
|
||||||
|
windows = gw.getWindowsWithTitle("Jan")
|
||||||
|
if windows:
|
||||||
|
jan_window = windows[0]
|
||||||
|
logger.info(f"Found Jan window: {jan_window.title}")
|
||||||
|
jan_window.maximize()
|
||||||
|
logger.info("Jan window maximized using pygetwindow")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Fallback methods for both platforms
|
||||||
|
# Method 2: Try Alt+Space then X (maximize hotkey) - works on both platforms
|
||||||
|
logger.info("Trying Alt+Space+X hotkey to maximize")
|
||||||
|
pyautogui.hotkey('alt', 'space')
|
||||||
|
time.sleep(0.5)
|
||||||
|
pyautogui.press('x')
|
||||||
|
logger.info("Attempted to maximize using Alt+Space+X")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not maximize Jan window: {e}")
|
||||||
|
|
||||||
|
# Method 3: Platform-specific fallback
|
||||||
|
try:
|
||||||
|
if IS_WINDOWS:
|
||||||
|
logger.info("Trying Windows+Up arrow to maximize")
|
||||||
|
pyautogui.hotkey('win', 'up')
|
||||||
|
elif IS_LINUX:
|
||||||
|
logger.info("Trying Alt+F10 to maximize")
|
||||||
|
pyautogui.hotkey('alt', 'F10')
|
||||||
|
elif IS_MACOS:
|
||||||
|
logger.info("Trying macOS specific maximize")
|
||||||
|
pyautogui.hotkey('cmd', 'tab') # Switch to Jan if it's running
|
||||||
|
time.sleep(0.5)
|
||||||
|
return True
|
||||||
|
except Exception as e2:
|
||||||
|
logger.warning(f"All maximize methods failed: {e2}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_jan_app(jan_app_path=None):
|
||||||
|
"""
|
||||||
|
Start Jan application in maximized window (cross-platform)
|
||||||
|
"""
|
||||||
|
# Set default path based on platform
|
||||||
|
if jan_app_path is None:
|
||||||
|
if IS_WINDOWS:
|
||||||
|
jan_app_path = os.path.expanduser(r"~\AppData\Local\Programs\jan\Jan.exe")
|
||||||
|
elif IS_LINUX:
|
||||||
|
jan_app_path = "/usr/bin/Jan" # or "/usr/bin/Jan" for regular
|
||||||
|
elif IS_MACOS:
|
||||||
|
jan_app_path = "/Applications/Jan.app/Contents/MacOS/Jan" # Default macOS path
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"Platform {platform.system()} not supported")
|
||||||
|
|
||||||
|
logger.info(f"Starting Jan application from: {jan_app_path}")
|
||||||
|
|
||||||
|
if not os.path.exists(jan_app_path):
|
||||||
|
logger.error(f"Jan executable not found at: {jan_app_path}")
|
||||||
|
raise FileNotFoundError(f"Jan app not found at {jan_app_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Start the Jan application
|
||||||
|
if IS_WINDOWS:
|
||||||
|
subprocess.Popen([jan_app_path], shell=True)
|
||||||
|
elif IS_LINUX:
|
||||||
|
# On Linux, start with DISPLAY environment variable
|
||||||
|
env = os.environ.copy()
|
||||||
|
subprocess.Popen([jan_app_path], env=env)
|
||||||
|
elif IS_MACOS:
|
||||||
|
# On macOS, use 'open' command to launch .app bundle properly
|
||||||
|
if jan_app_path.endswith('.app/Contents/MacOS/Jan'):
|
||||||
|
# Use the .app bundle path instead
|
||||||
|
app_bundle = jan_app_path.replace('/Contents/MacOS/Jan', '')
|
||||||
|
subprocess.Popen(['open', app_bundle])
|
||||||
|
elif jan_app_path.endswith('.app'):
|
||||||
|
# Direct .app bundle
|
||||||
|
subprocess.Popen(['open', jan_app_path])
|
||||||
|
elif '/Contents/MacOS/' in jan_app_path:
|
||||||
|
# Extract app bundle from full executable path
|
||||||
|
app_bundle = jan_app_path.split('/Contents/MacOS/')[0]
|
||||||
|
subprocess.Popen(['open', app_bundle])
|
||||||
|
else:
|
||||||
|
# Fallback: try to execute directly
|
||||||
|
subprocess.Popen([jan_app_path])
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(f"Platform {platform.system()} not supported")
|
||||||
|
logger.info("Jan application started")
|
||||||
|
|
||||||
|
# Wait for app to fully load
|
||||||
|
logger.info("Waiting for Jan application to initialize...")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Try to maximize the window
|
||||||
|
if maximize_jan_window():
|
||||||
|
logger.info("Jan application maximized successfully")
|
||||||
|
else:
|
||||||
|
logger.warning("Could not maximize Jan application window")
|
||||||
|
|
||||||
|
# Wait a bit more after maximizing
|
||||||
|
time.sleep(10)
|
||||||
|
logger.info("Jan application should be ready")
|
||||||
|
time.sleep(10) # Additional wait to ensure everything is ready
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting Jan application: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def scan_test_files(tests_dir="tests"):
|
||||||
|
"""
|
||||||
|
Scan tests folder and find all .txt files
|
||||||
|
Returns list with format [{'path': 'relative_path', 'prompt': 'file_content'}]
|
||||||
|
"""
|
||||||
|
test_files = []
|
||||||
|
tests_path = Path(tests_dir)
|
||||||
|
|
||||||
|
if not tests_path.exists():
|
||||||
|
logger.error(f"Tests directory {tests_dir} does not exist!")
|
||||||
|
return test_files
|
||||||
|
|
||||||
|
# Scan all .txt files in folder and subfolders
|
||||||
|
for txt_file in tests_path.rglob("*.txt"):
|
||||||
|
try:
|
||||||
|
# Read file content
|
||||||
|
with open(txt_file, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read().strip()
|
||||||
|
|
||||||
|
# Get relative path
|
||||||
|
relative_path = txt_file.relative_to(tests_path)
|
||||||
|
|
||||||
|
test_files.append({
|
||||||
|
'path': str(relative_path),
|
||||||
|
'prompt': content
|
||||||
|
})
|
||||||
|
logger.info(f"Found test file: {relative_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading file {txt_file}: {e}")
|
||||||
|
|
||||||
|
return test_files
|
||||||
|
|
||||||
|
def get_latest_trajectory_folder(trajectory_base_path):
|
||||||
|
"""
|
||||||
|
Get the latest created folder in trajectory base path
|
||||||
|
"""
|
||||||
|
if not os.path.exists(trajectory_base_path):
|
||||||
|
logger.warning(f"Trajectory base path not found: {trajectory_base_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get all folders and sort by creation time (latest first)
|
||||||
|
folders = [f for f in os.listdir(trajectory_base_path)
|
||||||
|
if os.path.isdir(os.path.join(trajectory_base_path, f))]
|
||||||
|
|
||||||
|
if not folders:
|
||||||
|
logger.warning(f"No trajectory folders found in: {trajectory_base_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sort by folder name (assuming timestamp format like 20250715_100443)
|
||||||
|
folders.sort(reverse=True)
|
||||||
|
latest_folder = folders[0]
|
||||||
|
|
||||||
|
full_path = os.path.join(trajectory_base_path, latest_folder)
|
||||||
|
logger.info(f"Found latest trajectory folder: {full_path}")
|
||||||
|
return full_path
|
||||||
@ -20,7 +20,7 @@
|
|||||||
"embla-carousel-react": "^8.0.0",
|
"embla-carousel-react": "^8.0.0",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"lucide-react": "^0.372.0",
|
"lucide-react": "^0.522.0",
|
||||||
"next": "^14.1.4",
|
"next": "^14.1.4",
|
||||||
"next-seo": "^6.5.0",
|
"next-seo": "^6.5.0",
|
||||||
"next-sitemap": "^4.2.3",
|
"next-sitemap": "^4.2.3",
|
||||||
@ -1503,7 +1503,7 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.372.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-0cKdqmilHXWUwWAWnf6CrrjHD8YaqPMtLrmEHXolZusNTr9epULCsiJwIOHk2q1yFxdEwd96D4zShlAj67UJdA=="],
|
"lucide-react": ["lucide-react@0.522.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-jnJbw974yZ7rQHHEFKJOlWAefG3ATSCZHANZxIdx8Rk/16siuwjgA4fBULpXEAWx/RlTs3FzmKW/udWUuO0aRw=="],
|
||||||
|
|
||||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||||
|
|
||||||
|
|||||||
BIN
docs/public/assets/images/changelog/release_v0_6_5.gif
Normal file
|
After Width: | Height: | Size: 14 MiB |
@ -11,6 +11,11 @@
|
|||||||
"type": "page",
|
"type": "page",
|
||||||
"title": "Documentation"
|
"title": "Documentation"
|
||||||
},
|
},
|
||||||
|
"local-server": {
|
||||||
|
"type": "page",
|
||||||
|
"title": "Jan Local Server",
|
||||||
|
"display": "hidden"
|
||||||
|
},
|
||||||
"cortex": {
|
"cortex": {
|
||||||
"type": "page",
|
"type": "page",
|
||||||
"title": "Cortex",
|
"title": "Cortex",
|
||||||
|
|||||||
26
docs/src/pages/changelog/2025-07-17-responsive-ui.mdx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "Jan v0.6.5 brings responsive UI and MCP examples!"
|
||||||
|
version: 0.6.5
|
||||||
|
description: "New MCP examples, updated pages, and bug fixes!"
|
||||||
|
date: 2025-07-17
|
||||||
|
ogImage: "/assets/images/changelog/release_v0_6_5.gif"
|
||||||
|
---
|
||||||
|
|
||||||
|
import ChangelogHeader from "@/components/Changelog/ChangelogHeader"
|
||||||
|
|
||||||
|
<ChangelogHeader title="Jan v0.6.5 brings responsive UI and MCP examples!" date="2025-07-17" ogImage="/assets/images/changelog/release_v0_6_5.gif" />
|
||||||
|
|
||||||
|
## Highlights 🎉
|
||||||
|
|
||||||
|
Jan v0.6.5 brings responsive UI improvements, enhanced model provider management, and better Linux compatibility alongside
|
||||||
|
new MCP examples.
|
||||||
|
|
||||||
|
- Support responsive UI on Jan
|
||||||
|
- Rework of Model Providers UI
|
||||||
|
- Bump version of llama.cpp
|
||||||
|
- Fix the bug where fetching models from custom provider can cause app to crash
|
||||||
|
- AppImage can now render on wayland + mesa
|
||||||
|
|
||||||
|
Update your Jan or [download the latest](https://jan.ai/).
|
||||||
|
|
||||||
|
For more details, see the [GitHub release notes](https://github.com/menloresearch/jan/releases/tag/v0.6.5).
|
||||||
BIN
docs/src/pages/docs/_assets/e2b-key.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
docs/src/pages/docs/_assets/e2b-key1.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
docs/src/pages/docs/_assets/e2b-key2.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
docs/src/pages/docs/_assets/e2b-key3.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
docs/src/pages/docs/_assets/e2b-key4.png
Normal file
|
After Width: | Height: | Size: 500 KiB |
BIN
docs/src/pages/docs/_assets/e2b-key5.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
docs/src/pages/docs/_assets/e2b-key6.png
Normal file
|
After Width: | Height: | Size: 558 KiB |
BIN
docs/src/pages/docs/_assets/e2b-key7.png
Normal file
|
After Width: | Height: | Size: 649 KiB |
BIN
docs/src/pages/docs/_assets/exa.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/src/pages/docs/_assets/exa1.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
docs/src/pages/docs/_assets/exa2.png
Normal file
|
After Width: | Height: | Size: 507 KiB |
BIN
docs/src/pages/docs/_assets/exa3.png
Normal file
|
After Width: | Height: | Size: 468 KiB |
BIN
docs/src/pages/docs/_assets/exa4.png
Normal file
|
After Width: | Height: | Size: 662 KiB |
BIN
docs/src/pages/docs/_assets/mcp-server.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
@ -17,17 +17,15 @@
|
|||||||
"title": "TUTORIALS",
|
"title": "TUTORIALS",
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
},
|
},
|
||||||
"quickstart": "Quickstart",
|
|
||||||
"remote-models": "Connect to Remote Models",
|
"remote-models": "Connect to Remote Models",
|
||||||
"server-examples": "Integrations",
|
|
||||||
|
|
||||||
"explanation-separator": {
|
"explanation-separator": {
|
||||||
"title": "EXPLANATION",
|
"title": "EXPLANATION",
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
},
|
},
|
||||||
"llama-cpp": "Local AI Engine",
|
"llama-cpp": "Local AI Engine",
|
||||||
"api-server": "Server Overview",
|
"model-parameters": "Model Parameters",
|
||||||
"data-folder": "Jan Data Folder",
|
|
||||||
"privacy-policy": {
|
"privacy-policy": {
|
||||||
"type": "page",
|
"type": "page",
|
||||||
"display": "hidden",
|
"display": "hidden",
|
||||||
@ -40,13 +38,14 @@
|
|||||||
},
|
},
|
||||||
"manage-models": "Manage Models",
|
"manage-models": "Manage Models",
|
||||||
"mcp": "Model Context Protocol",
|
"mcp": "Model Context Protocol",
|
||||||
|
"mcp-examples": "MCP Examples",
|
||||||
|
|
||||||
"reference-separator": {
|
"reference-separator": {
|
||||||
"title": "REFERENCE",
|
"title": "REFERENCE",
|
||||||
"type": "separator"
|
"type": "separator"
|
||||||
},
|
},
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
"data-folder": "Jan Data Folder",
|
||||||
"troubleshooting": "Troubleshooting",
|
"troubleshooting": "Troubleshooting",
|
||||||
"model-parameters": "Model Parameters",
|
|
||||||
"privacy": "Privacy"
|
"privacy": "Privacy"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,9 +10,5 @@
|
|||||||
"linux": {
|
"linux": {
|
||||||
"title": "Linux",
|
"title": "Linux",
|
||||||
"href": "/docs/desktop/linux"
|
"href": "/docs/desktop/linux"
|
||||||
},
|
|
||||||
"beta": {
|
|
||||||
"title": "BETA apps",
|
|
||||||
"href": "/docs/desktop/beta"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,81 +0,0 @@
|
|||||||
---
|
|
||||||
title: Beta Builds
|
|
||||||
description: Try out the latest features of Jan through our beta builds. Get early access to new features and help shape the future of Jan.
|
|
||||||
keywords:
|
|
||||||
[
|
|
||||||
Jan,
|
|
||||||
beta,
|
|
||||||
preview,
|
|
||||||
early-access,
|
|
||||||
testing,
|
|
||||||
development,
|
|
||||||
experimental-features,
|
|
||||||
feedback,
|
|
||||||
support
|
|
||||||
]
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Tabs, Callout } from 'nextra/components'
|
|
||||||
|
|
||||||
# Jan Beta Builds
|
|
||||||
|
|
||||||
Welcome to Jan's beta program! Beta builds give you early access to new features and improvements before they reach the stable release. By participating in the beta program, you can help shape the future of Jan by providing feedback and reporting issues.
|
|
||||||
|
|
||||||
<Callout type="warning">
|
|
||||||
Beta builds may contain bugs and unstable features. We recommend using the stable release for production environments.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
## Download Beta Builds
|
|
||||||
|
|
||||||
Get the latest beta builds for your platform:
|
|
||||||
|
|
||||||
<Tabs items={['macOS', 'Windows', 'Linux']}>
|
|
||||||
<Tabs.Tab>
|
|
||||||
- [MacOS Universal](https://app.jan.ai/download/beta/mac-universal)
|
|
||||||
</Tabs.Tab>
|
|
||||||
|
|
||||||
<Tabs.Tab>
|
|
||||||
- [Windows Installer](https://app.jan.ai/download/beta/win-x64)
|
|
||||||
</Tabs.Tab>
|
|
||||||
|
|
||||||
<Tabs.Tab>
|
|
||||||
- [jan.deb](https://app.jan.ai/download/beta/linux-amd64-deb)
|
|
||||||
- [jan.AppImage](https://app.jan.ai/download/beta/linux-amd64-appimage)
|
|
||||||
</Tabs.Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Getting Help & Support
|
|
||||||
|
|
||||||
### Discord Community
|
|
||||||
Join our Discord community to:
|
|
||||||
- Get help with beta builds
|
|
||||||
- Discuss new features
|
|
||||||
- Connect with other beta testers
|
|
||||||
- Share feedback directly with the team
|
|
||||||
|
|
||||||
[Join our Discord Server](https://discord.com/channels/1107178041848909847/1149558035971321886) or report directly to [#🆘|jan-help](https://discord.com/channels/1107178041848909847/1192090449725358130)
|
|
||||||
|
|
||||||
### GitHub Issues
|
|
||||||
Found a bug or have a feature request? Let us know on GitHub:
|
|
||||||
|
|
||||||
1. Check if the issue has already been reported
|
|
||||||
2. Create a new issue with:
|
|
||||||
- Clear description of the problem
|
|
||||||
- Steps to reproduce
|
|
||||||
- Expected vs actual behavior
|
|
||||||
- System information
|
|
||||||
- Screenshots if applicable
|
|
||||||
|
|
||||||
[File an Issue on GitHub](https://github.com/menloresearch/jan/issues)
|
|
||||||
|
|
||||||
## Beta Program Benefits
|
|
||||||
|
|
||||||
- Early access to new features
|
|
||||||
- Direct influence on product development
|
|
||||||
- Opportunity to shape Jan's future
|
|
||||||
- Access to beta-specific support channels
|
|
||||||
- Regular updates with the latest improvements
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Your feedback is invaluable in helping us improve Jan. Thank you for being part of our beta testing community!
|
|
||||||
</Callout>
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: llama.cpp
|
title: llama.cpp Engine
|
||||||
description: A step-by-step guide on how to customize the llama.cpp engine.
|
description: Understand and configure Jan's local AI engine for running models on your hardware.
|
||||||
keywords:
|
keywords:
|
||||||
[
|
[
|
||||||
Jan,
|
Jan,
|
||||||
@ -27,139 +27,181 @@ import { Tabs } from 'nextra/components'
|
|||||||
import { Callout, Steps } from 'nextra/components'
|
import { Callout, Steps } from 'nextra/components'
|
||||||
import { Settings, EllipsisVertical, Plus, FolderOpen, Pencil } from 'lucide-react'
|
import { Settings, EllipsisVertical, Plus, FolderOpen, Pencil } from 'lucide-react'
|
||||||
|
|
||||||
# Local Model Management
|
# Local AI Engine (llama.cpp)
|
||||||
|
|
||||||
## Overview
|
## What is llama.cpp?
|
||||||
Jan uses **llama.cpp** for running local AI models. You can find its settings in **Settings** (<Settings width={16} height={16} style={{display:"inline"}}/>) > **Local Engine** > **llama.cpp**:
|
|
||||||
|
llama.cpp is the engine that runs AI models locally on your computer. Think of it as the software that takes an AI model file and makes it actually work on your hardware - whether that's your CPU, graphics card, or Apple's M-series chips.
|
||||||
|
|
||||||
|
Originally created by Georgi Gerganov, llama.cpp is designed to run large language models efficiently on consumer hardware without requiring specialized AI accelerators or cloud connections.
|
||||||
|
|
||||||
|
## Why This Matters
|
||||||
|
|
||||||
|
**Privacy**: Your conversations never leave your computer
|
||||||
|
**Cost**: No monthly subscription fees or API costs
|
||||||
|
**Speed**: No internet required once models are downloaded
|
||||||
|
**Control**: Choose exactly which models to run and how they behave
|
||||||
|
|
||||||
|
## Accessing Engine Settings
|
||||||
|
|
||||||
|
Find llama.cpp settings at **Settings** (<Settings width={16} height={16} style={{display:"inline"}}/>) > **Local Engine** > **llama.cpp**:
|
||||||
|
|
||||||
<br/>
|
|
||||||

|

|
||||||
<br/>
|
|
||||||
|
|
||||||
These settings are for advanced users, you would want to check these settings when:
|
<Callout type="info">
|
||||||
- Your AI models are running slowly or not working
|
These are advanced settings. You typically only need to adjust them if models aren't working properly or you want to optimize performance for your specific hardware.
|
||||||
- You've installed new hardware (like a graphics card)
|
</Callout>
|
||||||
- You want to tinker & test performance with different [backends](/docs/local-engines/llama-cpp#available-backends)
|
|
||||||
|
|
||||||
## Engine Version and Updates
|
## Engine Management
|
||||||
- **Engine Version**: View current version of llama.cpp engine
|
|
||||||
- **Check Updates**: Verify if a newer version is available & install available updates when it's available
|
|
||||||
|
|
||||||
|
| Feature | What It Does | When You Need It |
|
||||||
|
|---------|-------------|------------------|
|
||||||
|
| **Engine Version** | Shows which version of llama.cpp you're running | Check compatibility with newer models |
|
||||||
|
| **Check Updates** | Downloads newer engine versions | When new models require updated engine |
|
||||||
|
| **Backend Selection** | Choose the version optimized for your hardware | After installing new graphics cards or when performance is poor |
|
||||||
|
|
||||||
## Available Backends
|
## Hardware Backends
|
||||||
|
|
||||||
Jan offers different backend variants for **llama.cpp** based on your operating system, you can:
|
Jan offers different backend versions optimized for your specific hardware. Think of these as different "drivers" - each one is tuned for particular processors or graphics cards.
|
||||||
- Download different backends as needed
|
|
||||||
- Switch between backends for different hardware configurations
|
|
||||||
- View currently installed backends in the list
|
|
||||||
|
|
||||||
<Callout type="warning">
|
<Callout type="warning">
|
||||||
Choose the backend that matches your hardware. Using the wrong variant may cause performance issues or prevent models from loading.
|
Using the wrong backend can make models run slowly or fail to load. Pick the one that matches your hardware.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
<Tabs items={['Windows', 'Linux', 'macOS']}>
|
<Tabs items={['Windows', 'Linux', 'macOS']}>
|
||||||
|
|
||||||
<Tabs.Tab>
|
<Tabs.Tab>
|
||||||
### CUDA Support (NVIDIA GPUs)
|
|
||||||
- `llama.cpp-avx-cuda-11-7`
|
|
||||||
- `llama.cpp-avx-cuda-12-0`
|
|
||||||
- `llama.cpp-avx2-cuda-11-7`
|
|
||||||
- `llama.cpp-avx2-cuda-12-0`
|
|
||||||
- `llama.cpp-avx512-cuda-11-7`
|
|
||||||
- `llama.cpp-avx512-cuda-12-0`
|
|
||||||
- `llama.cpp-noavx-cuda-11-7`
|
|
||||||
- `llama.cpp-noavx-cuda-12-0`
|
|
||||||
|
|
||||||
### CPU Only
|
### NVIDIA Graphics Cards (Recommended for Speed)
|
||||||
- `llama.cpp-avx`
|
Choose based on your CUDA version (check NVIDIA Control Panel):
|
||||||
- `llama.cpp-avx2`
|
|
||||||
- `llama.cpp-avx512`
|
|
||||||
- `llama.cpp-noavx`
|
|
||||||
|
|
||||||
### Other Accelerators
|
**For CUDA 12.0:**
|
||||||
- `llama.cpp-vulkan`
|
- `llama.cpp-avx2-cuda-12-0` (most common)
|
||||||
|
- `llama.cpp-avx512-cuda-12-0` (newer Intel/AMD CPUs)
|
||||||
|
|
||||||
|
**For CUDA 11.7:**
|
||||||
|
- `llama.cpp-avx2-cuda-11-7` (most common)
|
||||||
|
- `llama.cpp-avx512-cuda-11-7` (newer Intel/AMD CPUs)
|
||||||
|
|
||||||
|
### CPU Only (No Graphics Card Acceleration)
|
||||||
|
- `llama.cpp-avx2` (most modern CPUs)
|
||||||
|
- `llama.cpp-avx512` (newer Intel/AMD CPUs)
|
||||||
|
- `llama.cpp-avx` (older CPUs)
|
||||||
|
- `llama.cpp-noavx` (very old CPUs)
|
||||||
|
|
||||||
|
### Other Graphics Cards
|
||||||
|
- `llama.cpp-vulkan` (AMD, Intel Arc, some others)
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
- For detailed hardware compatibility, please visit our guide for [Windows](/docs/desktop/windows#compatibility).
|
**Quick Test**: Start with `avx2-cuda-12-0` if you have an NVIDIA card, or `avx2` for CPU-only. If it doesn't work, try the `avx` variant.
|
||||||
- AVX, AVX2, and AVX-512 are CPU instruction sets. For best performance, use the most advanced instruction set your CPU supports.
|
|
||||||
- CUDA versions should match your installed NVIDIA drivers.
|
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
||||||
<Tabs.Tab>
|
<Tabs.Tab>
|
||||||
### CUDA Support (NVIDIA GPUs)
|
|
||||||
- `llama.cpp-avx-cuda-11-7`
|
### NVIDIA Graphics Cards
|
||||||
- `llama.cpp-avx-cuda-12-0`
|
Same CUDA options as Windows:
|
||||||
- `llama.cpp-avx2-cuda-11-7`
|
- `llama.cpp-avx2-cuda-12-0` (most common)
|
||||||
- `llama.cpp-avx2-cuda-12-0`
|
- `llama.cpp-avx2-cuda-11-7` (older drivers)
|
||||||
- `llama.cpp-avx512-cuda-11-7`
|
|
||||||
- `llama.cpp-avx512-cuda-12-0`
|
|
||||||
- `llama.cpp-noavx-cuda-11-7`
|
|
||||||
- `llama.cpp-noavx-cuda-12-0`
|
|
||||||
|
|
||||||
### CPU Only
|
### CPU Only
|
||||||
- `llama.cpp-avx`
|
- `llama.cpp-avx2` (most modern CPUs)
|
||||||
- `llama.cpp-avx2`
|
- `llama.cpp-avx512` (newer Intel/AMD CPUs)
|
||||||
- `llama.cpp-avx512`
|
- `llama.cpp-arm64` (ARM processors like Raspberry Pi)
|
||||||
- `llama.cpp-noavx`
|
|
||||||
|
|
||||||
### Other Accelerators
|
### Other Graphics Cards
|
||||||
- `llama.cpp-vulkan`
|
- `llama.cpp-vulkan` (AMD, Intel graphics)
|
||||||
- `llama.cpp-arm64`
|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
- For detailed hardware compatibility, please visit our guide for [Linux](docs/desktop/linux).
|
|
||||||
- AVX, AVX2, and AVX-512 are CPU instruction sets. For best performance, use the most advanced instruction set your CPU supports.
|
|
||||||
- CUDA versions should match your installed NVIDIA drivers.
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
||||||
<Tabs.Tab>
|
<Tabs.Tab>
|
||||||
### Apple Silicon
|
|
||||||
- `llama.cpp-mac-arm64`: For M1/M2/M3 Macs
|
|
||||||
|
|
||||||
### Intel
|
### Apple Silicon (M1/M2/M3/M4)
|
||||||
- `llama.cpp-mac-amd64`: For Intel-based Macs
|
- `llama.cpp-mac-arm64` (recommended)
|
||||||
|
|
||||||
|
### Intel Macs
|
||||||
|
- `llama.cpp-mac-amd64`
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
For detailed hardware compatibility, please visit our guide for [Mac](/docs/desktop/mac#compatibility).
|
Apple Silicon Macs automatically use the GPU through Metal - no additional setup needed.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
## Performance Settings
|
## Performance Settings
|
||||||
|
|
||||||
|
These control how efficiently models run:
|
||||||
|
|
||||||
| Setting | Description | Default |
|
| Setting | What It Does | Recommended Value | Impact |
|
||||||
|---------|-------------|---------|
|
|---------|-------------|------------------|---------|
|
||||||
| **Continuous Batching** | - Enables processing multiple requests simultaneously<br></br>- Improves throughput for concurrent operations | Enabled |
|
| **Continuous Batching** | Process multiple requests at once | Enabled | Faster when using multiple tools or having multiple conversations |
|
||||||
| **Parallel Operations** | - Number of prompts to run in parallel<br></br>- Affects model inference speed | 4 |
|
| **Parallel Operations** | How many requests to handle simultaneously | 4 | Higher = more multitasking, but uses more memory |
|
||||||
| **CPU Threads** | - Number of CPU cores to use when running without GPU<br></br>- Higher thread counts may improve performance but increase CPU usage | Auto-detected based on your system's capabilities |
|
| **CPU Threads** | How many processor cores to use | Auto-detected | More threads can speed up CPU processing |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Memory Settings
|
## Memory Settings
|
||||||
|
|
||||||
| Setting | Description | Default |
|
These control how models use your computer's memory:
|
||||||
|---------|-------------|---------|
|
|
||||||
| **Flash Attention** | - Optimizes attention computation<br></br>- Reduces memory usage<br></br>- Recommended for most cases | Enabled |
|
|
||||||
| **Caching** | - Enable to store recent prompts and responses<br></br>- Improves response time for repeated prompts | Enabled |
|
|
||||||
| **KV Cache Type** | - KV cache implementation type; controls memory usage and precision trade-off<br></br>- Options:<br></br>• f16 (most stable)<br></br>• q8_0 (balanced)<br></br>• q4_0 (lowest memory) | f16 |
|
|
||||||
| **mmap** | - Enables memory-mapped model loading<br></br>- Reduces memory usage<br></br>- Recommended for large models | Enabled |
|
|
||||||
| **Context Shift** | - Automatically shifts the context window when the model is unable to process the entire prompt<br/> - Ensures that the most relevant information is always included <br/> - Recommended for long conversations and multiple tool calls | Disabled |
|
|
||||||
|
|
||||||
|
| Setting | What It Does | Recommended Value | When to Change |
|
||||||
|
|---------|-------------|------------------|----------------|
|
||||||
|
| **Flash Attention** | More efficient memory usage | Enabled | Leave enabled unless you have problems |
|
||||||
|
| **Caching** | Remember recent conversations | Enabled | Speeds up follow-up questions |
|
||||||
|
| **KV Cache Type** | Memory precision trade-off | f16 | Change to q8_0 or q4_0 if running out of memory |
|
||||||
|
| **mmap** | Load models more efficiently | Enabled | Helps with large models |
|
||||||
|
| **Context Shift** | Handle very long conversations | Disabled | Enable for very long chats or multiple tool calls |
|
||||||
|
|
||||||
## Best Practices
|
### KV Cache Types Explained
|
||||||
- Start with default settings
|
- **f16**: Most stable, uses more memory
|
||||||
- Adjust based on your hardware capabilities
|
- **q8_0**: Balanced memory usage and quality
|
||||||
- Monitor system performance
|
- **q4_0**: Uses least memory, slight quality loss
|
||||||
- Test changes with your specific use case
|
|
||||||
|
## Troubleshooting Common Issues
|
||||||
|
|
||||||
|
**Models won't load:**
|
||||||
|
- Try a different backend (switch from CUDA to CPU or vice versa)
|
||||||
|
- Check if you have enough RAM/VRAM
|
||||||
|
- Update to latest engine version
|
||||||
|
|
||||||
|
**Very slow performance:**
|
||||||
|
- Make sure you're using GPU acceleration (CUDA/Metal/Vulkan backend)
|
||||||
|
- Increase GPU Layers in model settings
|
||||||
|
- Close other memory-intensive programs
|
||||||
|
|
||||||
|
**Out of memory errors:**
|
||||||
|
- Reduce Context Size in model settings
|
||||||
|
- Switch KV Cache Type to q8_0 or q4_0
|
||||||
|
- Try a smaller model variant
|
||||||
|
|
||||||
|
**Random crashes:**
|
||||||
|
- Switch to a more stable backend (try avx instead of avx2)
|
||||||
|
- Disable overclocking if you have it enabled
|
||||||
|
- Update graphics drivers
|
||||||
|
|
||||||
|
## Quick Setup Guide
|
||||||
|
|
||||||
|
**For most users:**
|
||||||
|
1. Use the default backend that Jan installs
|
||||||
|
2. Leave all performance settings at defaults
|
||||||
|
3. Only adjust if you experience problems
|
||||||
|
|
||||||
|
**If you have an NVIDIA graphics card:**
|
||||||
|
1. Download the appropriate CUDA backend
|
||||||
|
2. Make sure GPU Layers is set high in model settings
|
||||||
|
3. Enable Flash Attention
|
||||||
|
|
||||||
|
**If models are too slow:**
|
||||||
|
1. Check you're using GPU acceleration
|
||||||
|
2. Try enabling Continuous Batching
|
||||||
|
3. Close other applications using memory
|
||||||
|
|
||||||
|
**If running out of memory:**
|
||||||
|
1. Change KV Cache Type to q8_0
|
||||||
|
2. Reduce Context Size in model settings
|
||||||
|
3. Try a smaller model
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
Performance impact varies by hardware, model size, and usage patterns.
|
Most users can run Jan successfully without changing any of these settings. The defaults are chosen to work well on typical hardware.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|||||||
290
docs/src/pages/docs/mcp-examples/data-analysis/e2b.mdx
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
---
|
||||||
|
title: E2B Code Sandbox
|
||||||
|
description: Execute Python code securely in isolated sandbox environments with E2B.
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan,
|
||||||
|
MCP,
|
||||||
|
Model Context Protocol,
|
||||||
|
E2B,
|
||||||
|
code execution,
|
||||||
|
sandbox,
|
||||||
|
data analysis,
|
||||||
|
Python,
|
||||||
|
secure computing,
|
||||||
|
tool calling,
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout, Steps } from 'nextra/components'
|
||||||
|
|
||||||
|
# E2B Code Sandbox MCP
|
||||||
|
|
||||||
|
E2B MCP provides isolated Python execution environments. Your AI can run actual code instead of just describing what code might do.
|
||||||
|
|
||||||
|
The real value emerges when you combine secure remote execution with Jan's flexible model selection. You can use
|
||||||
|
local models for conversation and reasoning while offloading actual computation to E2B's sandboxes. This means you
|
||||||
|
get the privacy and control of local models plus the computational power of cloud infrastructure, without the
|
||||||
|
complexity of managing Python environments or dependencies locally.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Jan with MCP enabled
|
||||||
|
- E2B API key from [e2b.dev](https://e2b.dev/)
|
||||||
|
- Node.js installed
|
||||||
|
- Model with tool calling support
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
1. **Enable MCP**: Go to **Settings** > **MCP Servers**, toggle **Allow All MCP Tool Permission** ON
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
Don't forget that MCP gets enabled once you turn on Experimental Features in Jan's General settings.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
2. **Get API Key**: Sign up at [e2b.dev](https://e2b.dev/), generate an API key
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Add a meaningful name to your key.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. **Add MCP Server**: Click `+` in MCP Servers section
|
||||||
|
|
||||||
|
Configure:
|
||||||
|
- **Server Name**: `e2b-server`
|
||||||
|
- **Command**: `npx`
|
||||||
|
- **Arguments**: `@e2b/mcp-server`
|
||||||
|
- **Environment Variables**:
|
||||||
|
- Key: `E2B_API_KEY`
|
||||||
|
- Value: `your-api-key`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. **Verify**: Check server shows as active
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Pre-installed Libraries
|
||||||
|
|
||||||
|
The sandbox includes these packages by default:
|
||||||
|
|
||||||
|
**Data Analysis & Science:**
|
||||||
|
- `pandas` (1.5.3) - Data manipulation
|
||||||
|
- `numpy` (1.26.4) - Numerical computing
|
||||||
|
- `scipy` (1.12.0) - Scientific computing
|
||||||
|
- `scikit-learn` (1.4.1) - Machine learning
|
||||||
|
- `sympy` (1.12) - Symbolic mathematics
|
||||||
|
|
||||||
|
**Visualization:**
|
||||||
|
- `matplotlib` (3.8.3) - Static plots
|
||||||
|
- `seaborn` (0.13.2) - Statistical visualization
|
||||||
|
- `plotly` (5.19.0) - Interactive charts
|
||||||
|
- `bokeh` (3.3.4) - Web-ready visualizations
|
||||||
|
|
||||||
|
**Data Processing:**
|
||||||
|
- `requests` (2.26.0) - HTTP requests
|
||||||
|
- `beautifulsoup4` (4.12.3) - HTML/XML parsing
|
||||||
|
- `openpyxl` (3.1.2) - Excel files
|
||||||
|
- `python-docx` (1.1.0) - Word documents
|
||||||
|
|
||||||
|
**Text & NLP:**
|
||||||
|
- `nltk` (3.8.1) - Natural language processing
|
||||||
|
- `spacy` (3.7.4) - Advanced NLP
|
||||||
|
- `textblob` (0.18.0) - Text processing
|
||||||
|
- `gensim` (4.3.2) - Topic modeling
|
||||||
|
|
||||||
|
**Image & Audio:**
|
||||||
|
- `opencv-python` (4.9.0) - Computer vision
|
||||||
|
- `scikit-image` (0.22.0) - Image processing
|
||||||
|
- `imageio` (2.34.0) - Image I/O
|
||||||
|
- `librosa` (0.10.1) - Audio analysis
|
||||||
|
|
||||||
|
Additional packages can be installed as needed.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
|
||||||
|
For the following examples, we'll use Claude 4 Sonnet but you can use any local or remote
|
||||||
|
model with tool calling capabilities you'd like.
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
Make sure you activate Tools on the model you're using.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Basic Data Analysis
|
||||||
|
|
||||||
|
Start small. Open a new chat, confirm that the model has tools enabled and ask it to create a small dataset of 100 students with grades and study hours.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
Create a small dataset of 100 students with grades and study hours.
|
||||||
|
Calculate the correlation and create a scatter plot.
|
||||||
|
```
|
||||||
|
|
||||||
|
The model will:
|
||||||
|
1. Generate data with pandas (100 rows)
|
||||||
|
2. Calculate correlation coefficient
|
||||||
|
3. Create a matplotlib scatter plot
|
||||||
|
4. Add trend line
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
<Callout type="warning">
|
||||||
|
As of `v0.6.5`, Jan won't be able to display visualizations in the chat but we're to get this fixed for the next release.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
|
||||||
|
### Statistical Computing
|
||||||
|
|
||||||
|
```
|
||||||
|
Run a Monte Carlo simulation with 10,000 iterations to estimate π.
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
- Numerical computation with numpy
|
||||||
|
- Convergence plot showing estimate improvement
|
||||||
|
- Final π estimate
|
||||||
|
|
||||||
|
|
||||||
|
For more intensive simulations, increase iterations gradually and monitor performance.
|
||||||
|
|
||||||
|
### Machine Learning
|
||||||
|
|
||||||
|
```
|
||||||
|
Create a simple 2-class dataset with 200 samples. Train a logistic regression
|
||||||
|
model and visualize the decision boundary.
|
||||||
|
```
|
||||||
|
|
||||||
|
The model will:
|
||||||
|
- Generate synthetic 2D classification data
|
||||||
|
- Train a single scikit-learn model
|
||||||
|
- Plot data points and decision boundary
|
||||||
|
|
||||||
|
|
||||||
|
### Time Series Analysis
|
||||||
|
|
||||||
|
```
|
||||||
|
Generate daily temperature data for one year. Calculate moving averages
|
||||||
|
and identify seasonal patterns.
|
||||||
|
```
|
||||||
|
|
||||||
|
Output includes:
|
||||||
|
- Line plot of temperature data
|
||||||
|
- Moving average overlay
|
||||||
|
- Simple seasonal decomposition
|
||||||
|
|
||||||
|
|
||||||
|
### Scaling Up
|
||||||
|
|
||||||
|
Once basic examples work, you can increase complexity:
|
||||||
|
- Larger datasets (1000+ samples)
|
||||||
|
- Multiple models for comparison
|
||||||
|
- Complex visualizations with subplots
|
||||||
|
- Advanced statistical tests
|
||||||
|
|
||||||
|
The sandbox handles moderate computational loads well. For very large datasets or intensive ML training, consider breaking work into smaller chunks.
|
||||||
|
|
||||||
|
## Chart Generation
|
||||||
|
|
||||||
|
E2B automatically detects and extracts charts from matplotlib code. Charts are returned as base64-encoded images and downloadable files.
|
||||||
|
|
||||||
|
### Static Charts
|
||||||
|
|
||||||
|
```python
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
x = np.linspace(0, 10, 100)
|
||||||
|
y = np.sin(x)
|
||||||
|
|
||||||
|
plt.figure(figsize=(10, 6))
|
||||||
|
plt.plot(x, y)
|
||||||
|
plt.title('Sine Wave')
|
||||||
|
plt.xlabel('x')
|
||||||
|
plt.ylabel('sin(x)')
|
||||||
|
plt.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
E2B captures the plot and makes it available for download.
|
||||||
|
|
||||||
|
### Interactive Charts
|
||||||
|
|
||||||
|
The system extracts chart data for frontend visualization:
|
||||||
|
|
||||||
|
```python
|
||||||
|
plt.bar(['A', 'B', 'C'], [10, 20, 15])
|
||||||
|
plt.title('Sample Bar Chart')
|
||||||
|
plt.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns structured data:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "bar",
|
||||||
|
"title": "Sample Bar Chart",
|
||||||
|
"elements": [
|
||||||
|
{"label": "A", "value": 10},
|
||||||
|
{"label": "B", "value": 20},
|
||||||
|
{"label": "C", "value": 15}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported chart types: line, bar, scatter, pie, box plots.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
- **run_code**: Execute Python code
|
||||||
|
- **install_package**: Add Python packages
|
||||||
|
- **create_file**: Save files to sandbox
|
||||||
|
- **read_file**: Access sandbox files
|
||||||
|
- **list_files**: Browse sandbox contents
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Connection Issues:**
|
||||||
|
- Verify API key is correct
|
||||||
|
- Check Node.js installation
|
||||||
|
- Restart Jan if server won't start
|
||||||
|
|
||||||
|
**Execution Problems:**
|
||||||
|
- Free sandboxes have 2 cores and 1GB RAM - start with small datasets
|
||||||
|
- Large computations may time out or run out of memory
|
||||||
|
- Scale up complexity gradually after testing basic examples
|
||||||
|
- Some packages may require explicit installation
|
||||||
|
|
||||||
|
**Package Installation:**
|
||||||
|
- Most data science packages install successfully
|
||||||
|
- System dependencies may cause failures for some packages
|
||||||
|
- Try alternative packages if installation fails
|
||||||
|
|
||||||
|
<Callout type="warning">
|
||||||
|
E2B has computational and memory limits. Break large operations into smaller chunks if needed.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
E2B is useful for:
|
||||||
|
|
||||||
|
- **Academic Research**: Statistical analysis, data visualization, hypothesis testing
|
||||||
|
- **Data Science**: Exploratory data analysis, model prototyping, result validation
|
||||||
|
- **Financial Analysis**: Portfolio optimization, risk calculations, market simulations
|
||||||
|
- **Scientific Computing**: Numerical simulations, mathematical modeling, algorithm testing
|
||||||
|
- **Prototyping**: Quick algorithm validation, proof-of-concept development
|
||||||
|
|
||||||
|
The sandbox provides isolated execution without local environment setup or dependency management.
|
||||||
216
docs/src/pages/docs/mcp-examples/search/exa.mdx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
---
|
||||||
|
title: Exa Search MCP
|
||||||
|
description: Connect Jan to real-time web search with Exa's AI-powered search engine.
|
||||||
|
keywords:
|
||||||
|
[
|
||||||
|
Jan,
|
||||||
|
MCP,
|
||||||
|
Model Context Protocol,
|
||||||
|
Exa,
|
||||||
|
web search,
|
||||||
|
real-time search,
|
||||||
|
research,
|
||||||
|
AI search,
|
||||||
|
tool calling,
|
||||||
|
]
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Callout, Steps } from 'nextra/components'
|
||||||
|
|
||||||
|
# Exa Search MCP
|
||||||
|
|
||||||
|
[Exa MCP](https://docs.exa.ai/examples/exa-mcp) provides real-time web search capabilities for AI models. Instead of relying on training data,
|
||||||
|
models can access current web content through Exa's search API.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
Exa MCP includes eight search functions:
|
||||||
|
|
||||||
|
- `web_search_exa`: General web search with content extraction
|
||||||
|
- `research_paper_search`: Academic papers and research content
|
||||||
|
- `company_research`: Company analysis and business intelligence
|
||||||
|
- `crawling`: Extract content from specific URLs
|
||||||
|
- `competitor_finder`: Find business competitors
|
||||||
|
- `linkedin_search`: Search LinkedIn profiles and companies
|
||||||
|
- `wikipedia_search_exa`: Wikipedia content retrieval
|
||||||
|
- `github_search`: Repository and code search
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Jan with MCP enabled
|
||||||
|
- Exa API key from [dashboard.exa.ai](https://dashboard.exa.ai/api-keys)
|
||||||
|
- Model with tool calling support
|
||||||
|
- Node.js installed
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
Tool calling support varies by model. Jan Nano 32k and 128k, Claude, Gemini, GPT-4o and above models work reliably. For both local and remote models,
|
||||||
|
verify tool calling is enabled in model parameters.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Enable MCP
|
||||||
|
|
||||||
|
1. Go to **Settings** > **MCP Servers**
|
||||||
|
2. Toggle **Allow All MCP Tool Permission** ON
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Get API Key
|
||||||
|
|
||||||
|
1. Visit [dashboard.exa.ai/api-keys](https://dashboard.exa.ai/api-keys)
|
||||||
|
2. Create account or sign in
|
||||||
|
3. Generate API key
|
||||||
|
4. Save the key
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Configure MCP Server
|
||||||
|
|
||||||
|
Click `+` in MCP Servers section:
|
||||||
|
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- **Server Name**: `exa`
|
||||||
|
- **Command**: `npx`
|
||||||
|
- **Arguments**: `-y exa-mcp-server`
|
||||||
|
- **Environment Variables**:
|
||||||
|
- Key: `EXA_API_KEY`
|
||||||
|
- Value: `your-api-key`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Verify Setup
|
||||||
|
|
||||||
|
Check server status in the MCP Servers list.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Model Configuration
|
||||||
|
|
||||||
|
Use a compatible model provider:
|
||||||
|
|
||||||
|
- **Jan Nano 32k**
|
||||||
|
- **Anthropic**
|
||||||
|
- **OpenAI**
|
||||||
|
- **OpenRouter**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Start a new chat with a tool-enabled model. Exa tools will appear in the available tools list.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Example Queries
|
||||||
|
|
||||||
|
**Current Events & Activities:**
|
||||||
|
```
|
||||||
|
What is happening this week, mid July 2025, in Sydney, Australia?
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Investment Research:**
|
||||||
|
```
|
||||||
|
Find recent research papers about quantum computing startups that received Series A funding in 2024-2025
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tech Discovery:**
|
||||||
|
```
|
||||||
|
Find GitHub repositories for WebAssembly runtime engines written in Rust with active development
|
||||||
|
```
|
||||||
|
|
||||||
|
**Career Intelligence:**
|
||||||
|
```
|
||||||
|
Search LinkedIn for AI safety researchers at major tech companies who published papers in the last 6 months
|
||||||
|
```
|
||||||
|
|
||||||
|
**Competitive Analysis:**
|
||||||
|
```
|
||||||
|
Research emerging competitors to OpenAI in the large language model space, focusing on companies founded after 2023
|
||||||
|
```
|
||||||
|
|
||||||
|
**Travel & Local Research:**
|
||||||
|
```
|
||||||
|
Find authentic local food experiences in Tokyo that aren't in typical tourist guides, mentioned in recent travel blogs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Academic Research:**
|
||||||
|
```
|
||||||
|
Find recent papers about carbon capture technology breakthroughs published in Nature or Science during 2025
|
||||||
|
```
|
||||||
|
|
||||||
|
**Creator Economy:**
|
||||||
|
```
|
||||||
|
Research successful creators who transitioned from TikTok to longer-form content platforms in 2024-2025
|
||||||
|
```
|
||||||
|
|
||||||
|
**Emerging Tech Trends:**
|
||||||
|
```
|
||||||
|
Find startups working on brain-computer interfaces that have raised funding in the past 12 months
|
||||||
|
```
|
||||||
|
|
||||||
|
**Health & Wellness:**
|
||||||
|
```
|
||||||
|
Extract information about the latest longevity research findings from Peter Attia's recent podcast episodes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Regulatory Intelligence:**
|
||||||
|
```
|
||||||
|
Find recent AI regulation developments in the EU that could impact US companies, focusing on July 2025 updates
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supply Chain Research:**
|
||||||
|
```
|
||||||
|
Research companies developing sustainable packaging alternatives that have partnerships with major retailers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### Academic Research
|
||||||
|
Literature reviews, finding recent papers, tracking research trends.
|
||||||
|
|
||||||
|
### Business Intelligence
|
||||||
|
Competitor analysis, market research, company information gathering.
|
||||||
|
|
||||||
|
### Technical Research
|
||||||
|
Finding libraries, tools, and code repositories. Documentation research.
|
||||||
|
|
||||||
|
### Content Analysis
|
||||||
|
Extracting and analyzing content from specific URLs for research.
|
||||||
|
|
||||||
|
### Professional Search
|
||||||
|
LinkedIn searches for industry connections and expertise.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Connection Issues:**
|
||||||
|
- Verify API key accuracy
|
||||||
|
- Check Node.js installation
|
||||||
|
- Restart Jan
|
||||||
|
- Make sure you have enough credits in your Exa account
|
||||||
|
|
||||||
|
**Tool Calling Problems:**
|
||||||
|
- Confirm tool calling is enabled for your model
|
||||||
|
- Try Jan Nano 32k, Claude, Gemini, GPT-4o and above models
|
||||||
|
- Check MCP server status
|
||||||
|
|
||||||
|
**Search Quality:**
|
||||||
|
- Use specific, descriptive queries
|
||||||
|
- Prefer natural language over keywords
|
||||||
|
|
||||||
|
**API Errors:**
|
||||||
|
- Verify API key at [dashboard.exa.ai](https://dashboard.exa.ai)
|
||||||
|
- Check rate limits on your plan
|
||||||
|
- Regenerate API key if needed
|
||||||
|
|
||||||
|
<Callout type="warning">
|
||||||
|
Exa has API rate limits. Check your plan limits to avoid interruptions.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Exa MCP enables real-time web search within Jan's privacy-focused environment. Models can access current information while maintaining
|
||||||
|
local conversation processing.
|
||||||
@ -124,7 +124,7 @@ you will need to wait for a response). Efficient management of tools and their o
|
|||||||
|
|
||||||
To illustrate how MCPs can be used within Jan, we will walk through an example using the [Browser MCP](https://browsermcp.io/).
|
To illustrate how MCPs can be used within Jan, we will walk through an example using the [Browser MCP](https://browsermcp.io/).
|
||||||
|
|
||||||
Before we begin, you will need to enable Jan's MCP functionality at `Settings` > `MCP Servers`, and toggle
|
Before we begin, you will need to enable experimental features at `General` > `Advanced`. Next, go to `Settings` > `MCP Servers`, and toggle
|
||||||
the `Allow All MCP Tool Permission` switch ON.
|
the `Allow All MCP Tool Permission` switch ON.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Model Parameters
|
title: Model Parameters
|
||||||
description: Manage your interaction with AI locally.
|
description: Customize how your AI models behave and perform.
|
||||||
keywords:
|
keywords:
|
||||||
[
|
[
|
||||||
Jan,
|
Jan,
|
||||||
@ -12,64 +12,99 @@ keywords:
|
|||||||
conversational AI,
|
conversational AI,
|
||||||
no-subscription fee,
|
no-subscription fee,
|
||||||
large language models,
|
large language models,
|
||||||
threads,
|
model settings,
|
||||||
chat history,
|
parameters,
|
||||||
thread history,
|
|
||||||
]
|
]
|
||||||
---
|
---
|
||||||
import { Callout, Steps } from 'nextra/components'
|
import { Callout, Steps } from 'nextra/components'
|
||||||
|
|
||||||
# Model Parameters
|
# Model Parameters
|
||||||
To customize model settings for a conversation or a model:
|
|
||||||
- In **Threads**, click the **Gear icon** next to selected model
|
|
||||||
- Or, in **Settings > Model Providers > Llama.cpp**, click the **gear icon** next to a model for advanced settings
|
|
||||||
- Click the **edit button** next to a model to configure capabilities
|
|
||||||
|
|
||||||
## Inference & Engine Parameters (Gear Icon)
|
Model parameters control how your AI thinks and responds. Think of them as the AI's personality settings and performance controls.
|
||||||
These settings are available in the model settings modal:
|
|
||||||
| Parameter | Description |
|
## How to Access Settings
|
||||||
|---------------------|-------------|
|
|
||||||
| **Context Size** | Maximum prompt context length (how much text the model can consider at once). |
|
**For individual conversations:**
|
||||||
| **GPU Layers** | Number of model layers to offload to GPU. More layers = faster, but uses more VRAM. |
|
- In **Threads**, click the **gear icon** next to your selected model
|
||||||
| **Temperature** | Controls response randomness. Lower = more focused, higher = more creative. |
|
|
||||||
| **Top K** | Top-K sampling. Limits next token selection to the K most likely. |
|
**For permanent model settings:**
|
||||||
| **Top P** | Top-P (nucleus) sampling. Limits next token selection to a cumulative probability. |
|
- Go to **Settings > Model Providers > Llama.cpp**, click the **gear icon** next to a model
|
||||||
| **Min P** | Minimum probability for token selection. |
|
|
||||||
| **Repeat Last N** | Number of tokens to consider for repeat penalty. |
|
**For model capabilities:**
|
||||||
| **Repeat Penalty** | Penalize repeating token sequences. |
|
- Click the **edit button** next to a model to enable features like vision or tools
|
||||||
| **Presence Penalty**| Penalize alpha presence (encourages new topics). |
|
|
||||||
| **Frequency Penalty** | Reduces word repetition. |
|
## Performance Settings (Gear Icon)
|
||||||
|
|
||||||
|
These settings control how the model thinks and performs:
|
||||||
|
|
||||||
|
| Setting | What It Does | Simple Explanation |
|
||||||
|
|---------|-------------|-------------------|
|
||||||
|
| **Context Size** | How much text the model remembers | Like the model's working memory. Larger = remembers more of your conversation, but uses more computer memory. |
|
||||||
|
| **GPU Layers** | How much work your graphics card does | More layers on GPU = faster responses, but needs more graphics memory. Start high and reduce if you get errors. |
|
||||||
|
| **Temperature** | How creative vs. predictable responses are | Low (0.1-0.3) = focused, consistent answers. High (0.7-1.0) = creative, varied responses. Try 0.7 for general use. |
|
||||||
|
| **Top K** | How many word choices the model considers | Smaller numbers (20-40) = more focused. Larger numbers (80-100) = more variety. Most people don't need to change this. |
|
||||||
|
| **Top P** | Another way to control word variety | Works with Top K. Values like 0.9 work well. Lower = more focused, higher = more creative. |
|
||||||
|
| **Min P** | Minimum chance a word needs to be chosen | Prevents very unlikely words. Usually fine at default settings. |
|
||||||
|
| **Repeat Last N** | How far back to check for repetition | Helps prevent the model from repeating itself. Default values usually work well. |
|
||||||
|
| **Repeat Penalty** | How much to avoid repeating words | Higher values (1.1-1.3) reduce repetition. Too high makes responses awkward. |
|
||||||
|
| **Presence Penalty** | Encourages talking about new topics | Higher values make the model explore new subjects instead of staying on one topic. |
|
||||||
|
| **Frequency Penalty** | Reduces word repetition | Similar to repeat penalty but focuses on how often words are used. |
|
||||||
|
|
||||||
<br/>
|
|
||||||

|

|
||||||
<br/>
|
|
||||||
|
|
||||||
## Model Capabilities (Edit Button)
|
## Model Capabilities (Edit Button)
|
||||||
These toggles are available when you click the edit button next to a model:
|
|
||||||
- **Vision**: Enable image input/output
|
|
||||||
- **Tools**: Enable advanced tools (web search, file ops, code)
|
|
||||||
- **Embeddings**: Enable embedding generation
|
|
||||||
- **Web Search**: Allow model to search the web
|
|
||||||
- **Reasoning**: Enable advanced reasoning features
|
|
||||||
|
|
||||||
<br/>
|
These toggle switches enable special features:
|
||||||
|
|
||||||
|
- **Vision**: Let the model see and analyze images you share
|
||||||
|
- **Tools**: Enable advanced features like web search, file operations, and code execution
|
||||||
|
- **Embeddings**: Allow the model to create numerical representations of text (for advanced users)
|
||||||
|
- **Web Search**: Let the model search the internet for current information
|
||||||
|
- **Reasoning**: Enable step-by-step thinking for complex problems
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
<br/>
|
|
||||||
|
|
||||||
### Model Parameters
|
## Hardware Settings
|
||||||
This setting defines and configures the model's behavior.
|
|
||||||
| Parameter | Description |
|
|
||||||
|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| **Prompt Template** | A structured format that guides how the model should respond. Contains **placeholders** and **instructions** that help shape the model's output in a consistent way.|
|
|
||||||
|
|
||||||
### Engine Parameters
|
These control how efficiently the model runs on your computer:
|
||||||
These settings parameters control how the model runs on your hardware.
|
|
||||||
| Parameter | Description |
|
### GPU Layers
|
||||||
|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
Think of your model as a stack of layers, like a cake. Each layer can run on either your main processor (CPU) or graphics card (GPU). Your graphics card is usually much faster.
|
||||||
| **Number of GPU Layers (ngl)** | - Controls how many layers of the model run on your GPU.<br></br>- More layers on GPU generally means faster processing, but requires more GPU memory.|
|
|
||||||
| **Context Length** | - Controls how much text the model can consider at once.<br></br>- Longer context allows the model to handle more input but uses more memory and runs slower.<br></br>- The maximum context length varies with the model used.<br></br>|
|
- **More GPU layers** = Faster responses, but uses more graphics memory
|
||||||
|
- **Fewer GPU layers** = Slower responses, but uses less graphics memory
|
||||||
|
|
||||||
|
Start with the maximum number and reduce if you get out-of-memory errors.
|
||||||
|
|
||||||
|
### Context Length
|
||||||
|
This is like the model's short-term memory - how much of your conversation it can remember at once.
|
||||||
|
|
||||||
|
- **Longer context** = Remembers more of your conversation, better for long discussions
|
||||||
|
- **Shorter context** = Uses less memory, runs faster, but might "forget" earlier parts of long conversations
|
||||||
|
|
||||||
<Callout type="info">
|
<Callout type="info">
|
||||||
By default, Jan defaults to the minimum between **8192** and the model's maximum context length, you can adjust this based on your needs.
|
Jan defaults to 8192 tokens (roughly 6000 words) or your model's maximum, whichever is smaller. This handles most conversations well.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
## Quick Setup Guide
|
||||||
|
|
||||||
|
**For most users:**
|
||||||
|
1. Enable **Tools** if you want web search and code execution
|
||||||
|
2. Set **Temperature** to 0.7 for balanced creativity
|
||||||
|
3. Max out **GPU Layers** (reduce only if you get memory errors)
|
||||||
|
4. Leave other settings at defaults
|
||||||
|
|
||||||
|
**For creative writing:**
|
||||||
|
- Increase **Temperature** to 0.8-1.0
|
||||||
|
- Increase **Top P** to 0.95
|
||||||
|
|
||||||
|
**For factual/technical work:**
|
||||||
|
- Decrease **Temperature** to 0.1-0.3
|
||||||
|
- Enable **Tools** for web search and calculations
|
||||||
|
|
||||||
|
**Troubleshooting:**
|
||||||
|
- **Responses too repetitive?** Increase Temperature or Repeat Penalty
|
||||||
|
- **Out of memory errors?** Reduce GPU Layers or Context Size
|
||||||
|
- **Responses too random?** Decrease Temperature
|
||||||
|
- **Model running slowly?** Increase GPU Layers (if you have VRAM) or reduce Context Size
|
||||||
|
|||||||
@ -1,143 +0,0 @@
|
|||||||
---
|
|
||||||
title: Installation
|
|
||||||
description: Jan is an open-source ChatGPT-alternative and self-hosted AI platform - build and run AI on your own desktop or server.
|
|
||||||
sidebar_position: 2
|
|
||||||
keywords:
|
|
||||||
[
|
|
||||||
Jan,
|
|
||||||
Customizable Intelligence, LLM,
|
|
||||||
local AI,
|
|
||||||
privacy focus,
|
|
||||||
free and open source,
|
|
||||||
private and offline,
|
|
||||||
conversational AI,
|
|
||||||
no-subscription fee,
|
|
||||||
large language models,
|
|
||||||
quickstart,
|
|
||||||
getting started,
|
|
||||||
using AI model,
|
|
||||||
installation,
|
|
||||||
]
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Callout, Steps } from 'nextra/components'
|
|
||||||
import { Settings } from 'lucide-react'
|
|
||||||
|
|
||||||
|
|
||||||
# Quickstart
|
|
||||||
|
|
||||||
<Steps>
|
|
||||||
|
|
||||||
### Step 1: Install Jan
|
|
||||||
|
|
||||||
1. [Download Jan](/download)
|
|
||||||
2. Install the app on your system ([Mac](/docs/desktop/mac), [Windows](/docs/desktop/windows), [Linux](/docs/desktop/linux))
|
|
||||||
3. Launch Jan
|
|
||||||
|
|
||||||
Once installed, you'll see Jan's interface with no pre-installed models. You can:
|
|
||||||
- Download and run local AI models
|
|
||||||
- Connect to cloud-based AI model providers if desired
|
|
||||||
<br/>
|
|
||||||
|
|
||||||

|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Step 2: Download a Model
|
|
||||||
|
|
||||||
Jan offers various local AI models, from nimble lightweights to hefty powerhouses:
|
|
||||||
1. Go to the **Hub Tab** or to [HuggingFace](https://huggingface.co/models) where you will be able
|
|
||||||
to find even more models alongside their details. (TIP: You can copy the URL or name of the model on the Hub Tab
|
|
||||||
and download it there.)
|
|
||||||
2. Browse models and tap any for details (Models need to be in GGUF format)
|
|
||||||
3. Select one that matches your needs & hardware specs
|
|
||||||
4. Hit **Download** to begin (a progress bar will appear for the duration of the download)
|
|
||||||
|
|
||||||
<br/>
|
|
||||||

|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<Callout type="warning">
|
|
||||||
Local models consume your computer's memory and processing power. Choose carefully based on your hardware
|
|
||||||
specifications ([Mac](/docs/desktop/mac#minimum-requirements), [Windows](/docs/desktop/windows#compatibility),
|
|
||||||
[Linux](/docs/desktop/linux#compatibility)).
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
**Note:** Some Hugging Face models require an access token. Enter yours
|
|
||||||
in **Settings > Model Providers > Llama.cpp > Hugging Face Access Token** before importing.
|
|
||||||
|
|
||||||
<br/>
|
|
||||||

|
|
||||||
<br/>
|
|
||||||
|
|
||||||
|
|
||||||
For alternative installation methods, see the [Model Management](/manage-models) section.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Step 3: Turn on GPU Acceleration (Optional)
|
|
||||||
|
|
||||||
While your model downloads, let's supercharge your setup. On **Windows** or **Linux** with
|
|
||||||
a compatible graphics card, you can dramatically boost performance with GPU acceleration.
|
|
||||||
1. Head to **(<Settings width={16} height={16} style={{display:"inline"}}/>) Settings** > **Hardware**
|
|
||||||
2. Under **GPUs**, toggle the setting to ON if not already enabled.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<Callout type="info">
|
|
||||||
Install all required dependencies and drivers before enabling GPU acceleration. Check the **GPU Setup Guide** for [Windows](/docs/desktop/windows#gpu-acceleration) & [Linux](/docs/desktop/linux#gpu-acceleration).
|
|
||||||
</Callout>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Step 4: Customize Assistant Instructions
|
|
||||||
|
|
||||||
With your model ready to roll, you can tailor how it responds by tweaking instructions or model configurations
|
|
||||||
through the [Assistants feature](/docs/assistants).
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
You can also go to the assistant tab to manage all of your personalized instructions. The cool thing about
|
|
||||||
these is that you can use them no matter which model you choose.
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
### Step 5: Start Chatting and Update the Settings
|
|
||||||
|
|
||||||
Model downloaded? Instructions set? Time to chat. Type your message in the **input field** at
|
|
||||||
the bottom to kickstart the conversation.
|
|
||||||
|
|
||||||
Fine-tune your experience by:
|
|
||||||
- Tweaking [model parameters](/docs/model-parameters) via the **Gear icon** next to your model or in **Assistant Settings**
|
|
||||||
- Switching models for different tasks through the **model selector** in **Model** tab or **input field**
|
|
||||||
- [Creating new threads](/docs/threads#creating-new-thread) with custom instructions and configurations
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
|
|
||||||
### Step 6: Connect to cloud models (Optional)
|
|
||||||
|
|
||||||
Jan plays nice with both open source and cloud models. Connect to OpenAI (GPT-4o, o1), Anthropic (Claude), Groq, Mistral, and others:
|
|
||||||
1. Open any **Thread**
|
|
||||||
2. Select a model from the **model selector** dropdown
|
|
||||||
3. Pick your provider, click the **Gear icon** beside it
|
|
||||||
4. Grab a valid API key from your chosen provider (make sure it has sufficient credits)
|
|
||||||
5. Paste your **API Key** into Jan
|
|
||||||
|
|
||||||
For detailed setup, check [Remote APIs](/docs/remote-models/openai).
|
|
||||||
|
|
||||||
<br/>
|
|
||||||

|
|
||||||
<br/>
|
|
||||||
|
|
||||||
</Steps>
|
|
||||||
|
Before Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 618 KiB |
@ -1,86 +1,145 @@
|
|||||||
---
|
---
|
||||||
title: Start Chatting
|
title: Start Chatting
|
||||||
description: Manage your interaction with AI models locally.
|
description: Download models and manage your conversations with AI models locally.
|
||||||
keywords:
|
keywords:
|
||||||
[
|
[
|
||||||
Jan,
|
Jan,
|
||||||
Customizable Intelligence, LLM,
|
|
||||||
local AI,
|
local AI,
|
||||||
privacy focus,
|
LLM,
|
||||||
free and open source,
|
chat,
|
||||||
private and offline,
|
|
||||||
conversational AI,
|
|
||||||
no-subscription fee,
|
|
||||||
large language models,
|
|
||||||
threads,
|
threads,
|
||||||
chat history,
|
models,
|
||||||
thread history,
|
download,
|
||||||
|
installation,
|
||||||
|
conversations,
|
||||||
]
|
]
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Callout } from 'nextra/components'
|
import { Callout, Steps } from 'nextra/components'
|
||||||
import { SquarePen, Pencil, Ellipsis, Paintbrush, Trash2 } from 'lucide-react'
|
import { SquarePen, Pencil, Ellipsis, Paintbrush, Trash2, Settings } from 'lucide-react'
|
||||||
|
|
||||||
|
# Start Chatting
|
||||||
|
|
||||||
# Chat with a Model
|
<Steps>
|
||||||
|
|
||||||
Jan organizes your conversations with a model into chats or threads, making it easy to track and revisit
|
### Step 1: Install Jan
|
||||||
your interactions. This guide will help you effectively manage your chat history.
|
|
||||||
|
|
||||||
## Creating New Conversation/Thread
|
1. [Download Jan](/download)
|
||||||
1. Click **New Chat** (<SquarePen width={16} height={16} style={{display:"inline"}}/>) icon on the
|
2. Install the app ([Mac](/docs/desktop/mac), [Windows](/docs/desktop/windows), [Linux](/docs/desktop/linux))
|
||||||
bottom left of Jan.
|
3. Launch Jan
|
||||||
2. Select your preferred model in **Model Selector** in input field & start chatting.
|
|
||||||
|
|
||||||
<br/>
|
### Step 2: Download a Model
|
||||||

|
|
||||||
|
|
||||||
## View Your Chat History
|
Jan requires a model to chat. Download one from the Hub:
|
||||||
|
|
||||||
1. Once you open Jan, the default screen is **Chat**
|
1. Go to the **Hub Tab**
|
||||||
2. On the **left sidebar**, you can:
|
2. Browse available models (must be GGUF format)
|
||||||
- View your **Conversations** and scroll through your history
|
3. Select one matching your hardware specs
|
||||||
- Click any chat to open the full conversation
|
4. Click **Download**
|
||||||
|
|
||||||
## Favorites and Recents
|

|
||||||
|
|
||||||
Jan helps you quickly access important and recent conversations with **Favorites** and **Recents**
|
|
||||||
in the left sidebar:
|
|
||||||
- **Favorites**: Pin threads you use often for instant access. Click the three dots icon on the right of the
|
|
||||||
thread and a context menu will pop up with the favorite option for you to click on.
|
|
||||||
- **Recents**: See your most recently accessed threads for quick navigation.
|
|
||||||
|
|
||||||
<br/>
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## Edit a Chat Title
|
|
||||||
1. Navigate to the **Conversation** that you want to edit title on the sidebar to your left
|
|
||||||
2. Hover on the conversation and click on **three dots** (<Ellipsis width={16} height={16} style={{display:"inline"}}/>) icon
|
|
||||||
3. Click <Pencil width={16} height={16} style={{display:"inline"}}/> **Rename**
|
|
||||||
4. Add new title & save
|
|
||||||
|
|
||||||
<br/>
|
|
||||||

|
|
||||||
|
|
||||||
## Delete Thread
|
|
||||||
|
|
||||||
<Callout type="warning">
|
<Callout type="warning">
|
||||||
There's no undo for chat deletion, so make sure you REALLY want to remove it permanently.
|
Models consume memory and processing power. Choose based on your hardware specs.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
When you want to completely remove a thread:
|
**HuggingFace models:** Some require an access token. Add yours in **Settings > Model Providers > Llama.cpp > Hugging Face Access Token**.
|
||||||
|
|
||||||
1. Navigate to the **Thread** that you want to delete in left sidebar
|

|
||||||
2. Hover on the thread and click on **three dots** (<Ellipsis width={16} height={16} style={{display:"inline"}}/>) icon
|
|
||||||
|
### Step 3: Enable GPU Acceleration (Optional)
|
||||||
|
|
||||||
|
For Windows/Linux with compatible graphics cards:
|
||||||
|
|
||||||
|
1. Go to **(<Settings width={16} height={16} style={{display:"inline"}}/>) Settings** > **Hardware**
|
||||||
|
2. Toggle **GPUs** to ON
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
Install required drivers before enabling GPU acceleration. See setup guides for [Windows](/docs/desktop/windows#gpu-acceleration) & [Linux](/docs/desktop/linux#gpu-acceleration).
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
### Step 4: Start Chatting
|
||||||
|
|
||||||
|
1. Click **New Chat** (<SquarePen width={16} height={16} style={{display:"inline"}}/>) icon
|
||||||
|
2. Select your model in the input field dropdown
|
||||||
|
3. Type your message and start chatting
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Managing Conversations
|
||||||
|
|
||||||
|
Jan organizes conversations into threads for easy tracking and revisiting.
|
||||||
|
|
||||||
|
### View Chat History
|
||||||
|
|
||||||
|
- **Left sidebar** shows all conversations
|
||||||
|
- Click any chat to open the full conversation
|
||||||
|
- **Favorites**: Pin important threads for quick access
|
||||||
|
- **Recents**: Access recently used threads
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Edit Chat Titles
|
||||||
|
|
||||||
|
1. Hover over a conversation in the sidebar
|
||||||
|
2. Click **three dots** (<Ellipsis width={16} height={16} style={{display:"inline"}}/>) icon
|
||||||
|
3. Click <Pencil width={16} height={16} style={{display:"inline"}}/> **Rename**
|
||||||
|
4. Enter new title and save
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Delete Threads
|
||||||
|
|
||||||
|
<Callout type="warning">
|
||||||
|
Thread deletion is permanent. No undo available.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
**Single thread:**
|
||||||
|
1. Hover over thread in sidebar
|
||||||
|
2. Click **three dots** (<Ellipsis width={16} height={16} style={{display:"inline"}}/>) icon
|
||||||
3. Click <Trash2 width={16} height={16} style={{display:"inline"}}/> **Delete**
|
3. Click <Trash2 width={16} height={16} style={{display:"inline"}}/> **Delete**
|
||||||
|
|
||||||
|
**All threads:**
|
||||||
|
1. Hover over `Recents` category
|
||||||
|
2. Click **three dots** (<Ellipsis width={16} height={16} style={{display:"inline"}}/>) icon
|
||||||
|
3. Select <Trash2 width={16} height={16} style={{display:"inline"}}/> **Delete All**
|
||||||
|
|
||||||
<br/>
|
## Advanced Features
|
||||||

|
|
||||||
|
|
||||||
### Delete all threads at once
|
### Custom Assistant Instructions
|
||||||
|
|
||||||
In case you need to remove all conversations at once:
|
Customize how models respond:
|
||||||
1. Hover on the `Recents` category and click on **three dots** (<Ellipsis width={16} height={16} style={{display:"inline"}}/>) icon
|
|
||||||
2. Select <Trash2 width={16} height={16} style={{display:"inline"}}/> **Delete All**
|
1. Use the assistant dropdown in the input field
|
||||||
|
2. Or go to the **Assistant tab** to create custom instructions
|
||||||
|
3. Instructions work across all models
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Model Parameters
|
||||||
|
|
||||||
|
Fine-tune model behavior:
|
||||||
|
- Click the **Gear icon** next to your model
|
||||||
|
- Adjust parameters in **Assistant Settings**
|
||||||
|
- Switch models via the **model selector**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Connect Cloud Models (Optional)
|
||||||
|
|
||||||
|
Connect to OpenAI, Anthropic, Groq, Mistral, and others:
|
||||||
|
|
||||||
|
1. Open any thread
|
||||||
|
2. Select a cloud model from the dropdown
|
||||||
|
3. Click the **Gear icon** beside the provider
|
||||||
|
4. Add your API key (ensure sufficient credits)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For detailed setup, see [Remote APIs](/docs/remote-models/openai).
|
||||||
|
|||||||
BIN
docs/src/pages/local-server/_assets/add_assistant.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
docs/src/pages/local-server/_assets/anthropic.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
docs/src/pages/local-server/_assets/api-server-logs.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/src/pages/local-server/_assets/api-server.png
Normal file
|
After Width: | Height: | Size: 598 KiB |
BIN
docs/src/pages/local-server/_assets/api-server2.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
docs/src/pages/local-server/_assets/assistant-add-dialog.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 120 KiB |
BIN
docs/src/pages/local-server/_assets/assistant-dropdown.png
Normal file
|
After Width: | Height: | Size: 450 KiB |
BIN
docs/src/pages/local-server/_assets/assistant-edit-dialog.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
docs/src/pages/local-server/_assets/assistants-ui-overview.png
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
docs/src/pages/local-server/_assets/cohere.png
Normal file
|
After Width: | Height: | Size: 524 KiB |
BIN
docs/src/pages/local-server/_assets/deepseek.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-01.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-02.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-03.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-04.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-05.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-06.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-07.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-08.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-09.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
docs/src/pages/local-server/_assets/extensions-10.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/src/pages/local-server/_assets/google.png
Normal file
|
After Width: | Height: | Size: 541 KiB |
BIN
docs/src/pages/local-server/_assets/gpu_accl.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
docs/src/pages/local-server/_assets/groq.png
Normal file
|
After Width: | Height: | Size: 537 KiB |
BIN
docs/src/pages/local-server/_assets/hardware.png
Normal file
|
After Width: | Height: | Size: 576 KiB |
BIN
docs/src/pages/local-server/_assets/hf-unsloth.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/src/pages/local-server/_assets/hf_and_jan.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/src/pages/local-server/_assets/hf_token.png
Normal file
|
After Width: | Height: | Size: 487 KiB |
BIN
docs/src/pages/local-server/_assets/install-engines-01.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
docs/src/pages/local-server/_assets/install-engines-02.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
docs/src/pages/local-server/_assets/install-engines-03.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
docs/src/pages/local-server/_assets/jan-app-new.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
docs/src/pages/local-server/_assets/jan-app.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/src/pages/local-server/_assets/jan-nano-bench.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
docs/src/pages/local-server/_assets/jan-nano-demo.gif
Normal file
|
After Width: | Height: | Size: 22 MiB |