chore: import upstream snapshot with attribution
Auto Bump and Release / auto-bump-and-release (push) Waiting to run
style-check / pre-commit (push) Waiting to run
unit-test / unit testing with python (push) Waiting to run
unit-test / unit testing with python 3.10 (push) Waiting to run
unit-test / unit testing with python 3.11 (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:49 +08:00
commit da6a09ff09
415 changed files with 70274 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
{
"extends": ["@commitlint/config-conventional"],
"defaultIgnores": true,
"rules": {
"body-leading-blank": [1, "always"],
"body-max-line-length": [2, "always", 100],
"footer-leading-blank": [1, "always"],
"footer-max-line-length": [2, "always", 10000],
"header-max-length": [2, "always", 200],
"subject-case": [
2,
"never",
[]
],
"subject-empty": [2, "never"],
"subject-full-stop": [2, "never", "."],
"type-case": [2, "always", "lower-case"],
"type-empty": [2, "never"],
"type-enum": [
2,
"always",
[
"build",
"chore",
"ci",
"docs",
"feat",
"fix",
"perf",
"refactor",
"revert",
"style",
"test"
]
]
}
}
+15
View File
@@ -0,0 +1,15 @@
.github/
.git/
.mypy_cache/
__pycache__/
ktem_app_data/
env/
.pre-commit-config.yaml
.commitlintrc
.gitignore
.gitattributes
README.md
*.zip
*.sh
!/launch.sh
+64
View File
@@ -0,0 +1,64 @@
# this is an example .env file, use it to create your own .env file and place it in the root of the project
# settings for OpenAI
OPENAI_API_BASE=https://api.openai.com/v1
OPENAI_API_KEY=<YOUR_OPENAI_KEY>
OPENAI_CHAT_MODEL=gpt-4o-mini
OPENAI_EMBEDDINGS_MODEL=text-embedding-3-large
# settings for Azure OpenAI
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_API_KEY=
OPENAI_API_VERSION=2024-02-15-preview
AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-35-turbo
AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=text-embedding-ada-002
# settings for Cohere
COHERE_API_KEY=<COHERE_API_KEY>
# settings for Mistral
# MISTRAL_API_KEY=placeholder
# settings for VoyageAI
VOYAGE_API_KEY=<VOYAGE_API_KEY>
# settings for local models
LOCAL_MODEL=qwen2.5:7b
LOCAL_MODEL_EMBEDDINGS=nomic-embed-text
# settings for GraphRAG
GRAPHRAG_API_KEY=<YOUR_OPENAI_KEY>
GRAPHRAG_LLM_MODEL=gpt-4o-mini
GRAPHRAG_EMBEDDING_MODEL=text-embedding-3-small
# set to true if you want to use customized GraphRAG config file
USE_CUSTOMIZED_GRAPHRAG_SETTING=false
# settings for Azure DI
AZURE_DI_ENDPOINT=
AZURE_DI_CREDENTIAL=
# settings for Adobe API
# get free credential at https://acrobatservices.adobe.com/dc-integration-creation-app-cdn/main.html?api=pdf-extract-api
# also install pip install "pdfservices-sdk@git+https://github.com/niallcm/pdfservices-python-sdk.git@bump-and-unfreeze-requirements"
PDF_SERVICES_CLIENT_ID=
PDF_SERVICES_CLIENT_SECRET=
# settings for PDF.js
PDFJS_VERSION_DIST="pdfjs-4.0.379-dist"
# settings for PaddleOCR
PADDLE_DEVICE=gpu
# variable for authentication method selection
# for authentication with google leave empty
# for authentication with keycloak :
# AUTHENTICATION_METHOD="KEYCLOAK"
AUTHENTICATION_METHOD=
# settings for keycloak
KEYCLOAK_SERVER_URL=
KEYCLOAK_CLIENT_ID=
KEYCLOAK_REALM=
KEYCLOAK_CLIENT_SECRET=
+1
View File
@@ -0,0 +1 @@
*.bat text eol=crlf
+85
View File
@@ -0,0 +1,85 @@
name: "Bug Report"
description: Report something that is not working as expected
title: "[BUG] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
*Please fill this form with as much information as possible.*
- type: textarea
id: description
attributes:
label: "Description"
description: Please enter an explicit description of your issue
placeholder: Short and explicit description of your incident...
validations:
required: true
- type: textarea
id: reprod
attributes:
label: "Reproduction steps"
description: Please enter an explicit description of your issue
value: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
render: bash
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: "Screenshots"
description: If applicable, add screenshots to help explain your problem.
value: |
![DESCRIPTION](LINK.png)
render: bash
validations:
required: false
- type: textarea
id: logs
attributes:
label: "Logs"
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: bash
validations:
required: false
- type: dropdown
id: browsers
attributes:
label: "Browsers"
description: What browsers are you seeing the problem on ?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- Opera
- Brave
- Other
validations:
required: false
- type: dropdown
id: os
attributes:
label: "OS"
description: What is the impacted environment ?
multiple: true
options:
- Windows
- MacOS
- Linux
- Other
validations:
required: false
- type: textarea
id: additional_information
attributes:
label: "Additional information"
description: Add any relevant information or context.
placeholder:
validations:
required: false
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
@@ -0,0 +1,49 @@
name: "Feature Request"
description: Brainstorm and propose new features for the project
title: "[REQUEST] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
*Please fill this form with as much information as possible.*
- type: textarea
id: reference_issues
attributes:
label: "Reference Issues"
description: Common issues
placeholder: "#Issues IDs"
validations:
required: false
- type: textarea
id: summary
attributes:
label: "Summary"
description: Provide a brief explanation of the feature
placeholder: Describe in a few lines your feature request
validations:
required: true
- type: textarea
id: basic_example
attributes:
label: "Basic Example"
description: Indicate here some basic examples of your feature.
placeholder: A few specific words about your feature request.
validations:
required: true
- type: textarea
id: drawbacks
attributes:
label: "Drawbacks"
description: What are the drawbacks/impacts of your feature request ?
placeholder: Identify the drawbacks and impacts while being neutral on your feature request
validations:
required: true
- type: textarea
id: additional_information
attributes:
label: "Additional information"
description: Add any additional information that you think is important for your feature request
placeholder:
validations:
required: false
+18
View File
@@ -0,0 +1,18 @@
## Description
- Please include a summary of the changes and the related issue.
- Fixes # (issue)
## Type of change
- [ ] New features (non-breaking change).
- [ ] Bug fix (non-breaking change).
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected).
## Checklist
- [ ] I have performed a self-review of my code.
- [ ] I have added thorough tests if it is a core feature.
- [ ] There is a reference to the original bug report and related work.
- [ ] I have commented on my code, particularly in hard-to-understand areas.
- [ ] The feature is well documented.
@@ -0,0 +1,62 @@
name: Auto Bump and Release
on:
push:
branches:
- main
jobs:
auto-bump-and-release:
runs-on: ubuntu-latest
steps:
- name: Clone the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Update Application Version
id: update-version
uses: anothrNick/github-tag-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_V: true
DEFAULT_BUMP: patch
MAJOR_STRING_TOKEN: "bump:major"
MINOR_STRING_TOKEN: "bump:minor"
PATCH_STRING_TOKEN: "bump:patch"
- name: Create release for ${{ steps.update-version.outputs.new_tag }}
# need to repeat this if statement because Github Action doesn't support early
# stopping for steps
if: ${{ steps.update-version.outputs.new_tag != steps.update-version.outputs.old_tag }}
run: |
echo Create release folder
mkdir kotaemon-app
echo ${{ steps.update-version.outputs.new_tag }} > kotaemon-app/VERSION
cp LICENSE.txt kotaemon-app/
cp flowsettings.py kotaemon-app/
cp app.py kotaemon-app/
cp .env.example kotaemon-app/.env
cp -r scripts kotaemon-app/
mkdir -p kotaemon-app/libs/ktem/ktem/
cp -r libs/ktem/ktem/assets kotaemon-app/libs/ktem/ktem/
tree kotaemon-app
zip -r kotaemon-app.zip kotaemon-app
- name: Release ${{ steps.update-version.outputs.new_tag }}
if: ${{ steps.update-version.outputs.new_tag != steps.update-version.outputs.old_tag }}
uses: softprops/action-gh-release@v2
with:
files: kotaemon-app.zip
fail_on_unmatched_files: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
tag_name: ${{ steps.update-version.outputs.new_tag }}
make_latest: true
- name: Setup latest branch locally without switching current branch
if: ${{ steps.update-version.outputs.new_tag != steps.update-version.outputs.old_tag }}
run: git fetch origin latest:latest
- name: Update latest branch
if: ${{ steps.update-version.outputs.new_tag != steps.update-version.outputs.old_tag }}
run: |
git branch -f latest tags/${{ steps.update-version.outputs.new_tag }}
git checkout latest
git push -f -u origin latest
+106
View File
@@ -0,0 +1,106 @@
name: Build and Push Docker Image
on:
release:
types:
- created
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
workflow_dispatch:
env:
REGISTRY: ghcr.io
jobs:
build:
name: Build and push container
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
strategy:
matrix:
target:
- lite
- full
- ollama
# The maximum number of jobs that can run simultaneously
max-parallel: 1
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Set repository and image name
run: |
echo "FULL_IMAGE_NAME=${{ env.REGISTRY }}/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: "${{ github.repository }}"
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:latest
platforms: arm64,arm
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Set up Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
# branch
type=ref,event=branch,suffix=-${{ matrix.target }}
# semver with suffix for lite/full targets
type=semver,pattern={{version}},suffix=-${{ matrix.target }}
# latest tag with suffix for lite/full targets
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref, 'pre') }},suffix=-${{ matrix.target }}
flavor: |
# This is disabled here so we can use the raw form above
latest=false
# Suffix is not used here since there's no way to disable it above
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build docker image
uses: docker/build-push-action@v6
with:
file: Dockerfile
context: .
push: true
platforms: linux/amd64, linux/arm64
tags: |
${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
target: ${{ matrix.target }}
cache-from: type=gha
cache-to: type=gha,mode=max
+77
View File
@@ -0,0 +1,77 @@
name: "Lint PR"
on:
pull_request:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: write
jobs:
pr-title:
name: Validate PR title
runs-on: ubuntu-latest
permissions: write-all
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Hey there and thank you for opening this pull request! 👋🏼
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
```
${{ steps.lint_pr_title.outputs.error_message }}
```
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true
commitlint:
if: false # Disable this job for now
name: Validate commit messages
runs-on: ubuntu-latest
permissions: write-all
steps:
- uses: actions/checkout@v4
- uses: wagoid/commitlint-github-action@v6
id: commitlint
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
configFile: ./.commitlintrc
- uses: buildingcash/json-to-markdown-table-action@v1
if: always() && (steps.commitlint.outcome != 'success')
id: table
with:
json: ${{ steps.commitlint.outputs.results }}
- uses: marocchino/sticky-pull-request-comment@v2
if: always() && (steps.commitlint.outcome != 'success')
with:
header: commitlint-error
message: |
**All commits** in this PR need to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and [.commitlintrc](${{ github.server_url }}/${{ github.repository }}/blob/${{ github.head_ref || github.ref_name }}/.commitlintrc).
Details:
${{ steps.table.outputs.table }}
- if: ${{ steps.commitlint.outcome == 'success' }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: commitlint-error
delete: true
+20
View File
@@ -0,0 +1,20 @@
name: style-check
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- name: Clone the repo
uses: actions/checkout@v4
- name: Setup python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: run pre-commit
uses: pre-commit/action@v3.0.0
+114
View File
@@ -0,0 +1,114 @@
name: unit-test
on:
pull_request:
branches: [main]
push:
branches: [main]
env:
THEFLOW_TEMP_PATH: ./tmp
jobs:
unit-test:
# if: false # temporary disable this job due to legacy interface
#TODO: enable this job after the new interface is ready
if: ${{ !cancelled() }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
defaults:
run:
shell: ${{ matrix.shell }}
strategy:
matrix:
python-version: ["3.10", "3.11"]
include:
- os: ubuntu-latest
shell: bash
ACTIVATE_ENV: ". env/bin/activate"
GITHUB_OUTPUT: "$GITHUB_OUTPUT"
# - os: windows-latest
# shell: pwsh
# ACTIVATE_ENV: env/Scripts/activate.ps1
# GITHUB_OUTPUT: "$env:GITHUB_OUTPUT"
name: unit testing with python ${{ matrix.python-version }}
steps:
- name: Clone the repo
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Get Head Commit Message
id: get-head-commit-message
run: echo "message=$(git show -s --format=%s)" | tee -a ${{ matrix.GITHUB_OUTPUT }}
- name: Check ignore caching
id: check-ignore-cache
run: |
ignore_cache=${{ contains(steps.get-head-commit-message.outputs.message, '[ignore cache]') }}
echo "check=$ignore_cache" | tee -a ${{ matrix.GITHUB_OUTPUT }}
- name: Set up Python ${{ matrix.python-version }} on ${{ runner.os }}
uses: actions/setup-python@v4
id: setup_python
with:
python-version: ${{ matrix.python-version }}
architecture: x64
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
- name: Get cache key
id: get-cache-key
run: |
pip install "setuptools-git-versioning>=2.0,<3"
package_version=$(setuptools-git-versioning)
cache_key="${{ runner.os }}-py${{ matrix.python-version }}-v${package_version}"
echo "key=$cache_key" | tee -a ${{ matrix.GITHUB_OUTPUT }}
- name: Try to restore dependencies from ${{ steps.get-cache-key.outputs.key }}
id: restore-dependencies
if: steps.check-ignore-cache.outputs.check != 'true'
uses: actions/cache/restore@v3
with:
path: ${{ env.pythonLocation }}
key: ${{ steps.get-cache-key.outputs.key }}
# could using cache of previous ver to reuse unchanged packages
restore-keys: ${{ runner.os }}-py${{ matrix.python-version }}
- name: Check cache hit
id: check-cache-hit
run: |
echo "cache-hit=${{ steps.restore-dependencies.outputs.cache-hit }}"
echo "cache-matched-key=${{ steps.restore-dependencies.outputs.cache-matched-key }}"
cache_hit=${{ steps.restore-dependencies.outputs.cache-primary-key == steps.restore-dependencies.outputs.cache-matched-key }}
echo "check=$cache_hit" | tee -a ${{ matrix.GITHUB_OUTPUT }}
- name: Install additional dependencies (if any)
run: |
uv sync --frozen --no-cache
- name: New dependencies cache for key ${{ steps.restore-dependencies.outputs.cache-primary-key }}
if: |
steps.check-ignore-cache.outputs.check != 'true' &&
steps.check-cache-hit.outputs.check != 'true'
uses: actions/cache/save@v3
with:
path: ${{ env.pythonLocation }}
key: ${{ steps.restore-dependencies.outputs.cache-primary-key }}
- name: Install OS-based packages
run: |
sudo apt update -qqy
sudo apt install -y poppler-utils libpoppler-dev tesseract-ocr
- name: Test kotaemon with pytest
run: |
source .venv/bin/activate
uv pip show pytest
cd libs/kotaemon
pytest
+479
View File
@@ -0,0 +1,479 @@
# Created by https://www.toptal.com/developers/gitignore/api/python,linux,macos,windows,vim,emacs,visualstudiocode,pycharm
# Edit at https://www.toptal.com/developers/gitignore?templates=python,linux,macos,windows,vim,emacs,visualstudiocode,pycharm
activate*
activate/*
kotaemon-env*
.env
### Emacs ###
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/
dist/
# Flycheck
flycheck_*.el
# server auth directory
/server/
# projectiles files
.projectile
# directory configuration
.dir-locals.el
# network security
/network-security.data
### Linux ###
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### PyCharm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### PyCharm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# Azure Toolkit for IntelliJ plugin
# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
.idea/**/azureSettings.xml
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# PDF files
*.pdf
!libs/kotaemon/tests/resources/*.pdf
.theflow/
# End of https://www.toptal.com/developers/gitignore/api/python,linux,macos,windows,vim,emacs,visualstudiocode,pycharm
*.py[coid]
logs/
.gitsecret/keys/random_seed
!*.secret
.envrc
.env
S.gpg-agent*
.vscode/settings.json
examples/example1/assets
storage/*
# Conda and env storages
*install_dir/
doc_env/
# application data
ktem_app_data/
gradio_tmp/
+71
View File
@@ -0,0 +1,71 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-yaml
args: ["--unsafe"]
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: mixed-line-ending
- id: detect-aws-credentials
args: ["--allow-missing-credentials"]
- id: detect-private-key
- id: check-added-large-files
args: ["--maxkb=750"]
- id: debug-statements
- repo: https://github.com/ambv/black
rev: 22.3.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black"]
language_version: python3.10
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
args: ["--max-line-length", "88", "--extend-ignore", "E203"]
- repo: https://github.com/myint/autoflake
rev: v1.4
hooks:
- id: autoflake
additional_dependencies: [setuptools]
args:
[
"--in-place",
"--remove-unused-variables",
"--remove-all-unused-imports",
"--ignore-init-module-imports",
"--exclude=tests/*",
]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
types_or: [markdown, yaml]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.7.1"
hooks:
- id: mypy
additional_dependencies:
[
types-PyYAML==6.0.12.11,
"types-requests",
"sqlmodel",
"types-Markdown",
"types-cachetools",
types-tzlocal,
]
args: ["--check-untyped-defs", "--ignore-missing-imports"]
exclude: "^templates/"
- repo: https://github.com/codespell-project/codespell
rev: v2.2.4
hooks:
- id: codespell
additional_dependencies:
- tomli
+1
View File
@@ -0,0 +1 @@
3.10
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+115
View File
@@ -0,0 +1,115 @@
# Contributing to Kotaemon
Welcome 👋 to the Kotaemon project! We're thrilled that you're interested in contributing. Whether you're fixing bugs, adding new features, or improving documentation, your efforts are highly appreciated. This guide aims to help you get started with contributing to Kotaemon.
<a href="https://github.com/Cinnamon/kotaemon/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Cinnamon/kotaemon" />
</a>
### Table of Contents
1. [📖 Code of Conduct](#code-of-conduct)
2. [🔁 Contributing via Pull Requests](#contributing-via-pull-requests)
3. [📥 Opening an Issue](#-opening-an-issue)
4. [📝 Commit Messages](#-commit-messages)
5. [🧾 License](#-license)
## 📖 Code of Conduct
Please review our [code of conduct](./CODE_OF_CONDUCT.md), which is in effect at all times. We expect everyone who contributes to this project to honor it.
## 🔁 Contributing via Pull Requests
1. [**Fork the repository**](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo): Click on the [Fork](https://github.com/Cinnamon/kotaemon/fork) button on the repository's page to create a copy of Kotaemon under your GitHub account.
2. [**Clone your code**](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository): Clone your forked repository to your local machine.
3. [**Create new branch**](https://docs.github.com/en/desktop/making-changes-in-a-branch/managing-branches-in-github-desktop): Create a new branch in your forked repo with a descriptive name that reflects your changes.
```sh
git checkout -b descriptive-name-for-your-changes
```
4. **Setup the development environment**: If you are working on the code, make sure to install the necessary dependencies for development
```sh
pip install -e "libs/kotaemon[dev]"
```
5. **Make your changes**: Ensure your code follows the project's coding style and passes all test cases.
- Check the coding style
```sh
pre-commit run --all-files
```
- Run the tests
```sh
pytest libs/kotaemon/tests/
```
6. [**Commit your changes**](https://docs.github.com/en/desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project-in-github-desktop): Once you are done with your changes, add and commit them with clear messages.
```sh
git add your_changes.py
git commit -m "clear message described your changes."
git push -u origin descriptive-name-for-your-changes
```
7. [**Create a pull request**](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request): When you are satisfied with your changes, [submit a pull request](https://github.com/Cinnamon/kotaemon/compare) from your forked repository to Kotaemon repository. In the pull request, provide a clear description of your changes and any related issues. For the title of the pull request, please refer to our [commit messages convention](#-commit-messages).
8. **Wait for reviews**: Wait for the maintainers to review your pull request. If everything is okay, your changes will be merged into the Kotaemon project.
### GitHub Actions CI Tests
All pull requests must pass the [GitHub Actions Continuous Integration (CI)](https://docs.github.com/en/actions/about-github-actions/about-continuous-integration-with-github-actions) tests before they can be merged. These tests include coding-style checks, PR title validation, unit tests, etc. to ensure that your changes meet the project's quality standards. Please review and fix any CI failures that arise.
## 📥 Opening an Issue
Before [creating an issues](https://github.com/Cinnamon/kotaemon/issues/new/choose), search through existing issues to ensure you are not opening a duplicate. If you are reporting a bug or issue, please provide a reproducible example to help us quickly identify the problem.
## 📝 Commit Messages
### Overview
We use [Angular convention](https://www.conventionalcommits.org/en/) for commit messages to maintain consistency and clarity in our project history. Please take a moment to familiarize yourself with this convention before making your first commit.
_For the sake of simplicity, we use [squashing merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits) with pull requests. Therefore, if you contribute via a pull request, just make sure your PR's title, instead of the whole commits, follows this convention._
Commit format:
```sh
<gitmoji> <type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
Examples:
```sh
docs(api): update api doc
```
### Commit types
| Types | Description |
| :--------- | :------------------------------------------------------------ |
| `feat` | New features |
| `fix` | Bug fix |
| `docs` | Documentation only changes |
| `build` | Changes that affect the build system or external dependencies |
| `chore` | Something that doesnt fit the other types |
| `ci` | Changes to our CI configuration files and scripts |
| `perf` | Improve performance |
| `refactor` | Refactor code |
| `revert` | Revert a previous commit |
| `style` | Improve structure/format of the code |
| `test` | Add, update or pass tests |
## 🧾 License
All contributions will be licensed under the project's license: [Apache License 2.0](https://github.com/Cinnamon/kotaemon/blob/main/LICENSE.txt).
+124
View File
@@ -0,0 +1,124 @@
# Lite version
FROM python:3.11-slim AS lite
# Common dependencies
RUN apt-get update -qqy && \
apt-get install -y --no-install-recommends \
ssh \
git \
gcc \
g++ \
poppler-utils \
libpoppler-dev \
unzip \
curl \
cargo \
&& \
apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/*
# Setup args
ARG TARGETPLATFORM
ARG TARGETARCH
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONIOENCODING=UTF-8
ENV TARGETARCH=${TARGETARCH}
# Create working directory
WORKDIR /app
# Download pdfjs
COPY scripts/download_pdfjs.sh /app/scripts/download_pdfjs.sh
RUN chmod +x /app/scripts/download_pdfjs.sh
ENV PDFJS_PREBUILT_DIR="/app/libs/ktem/ktem/assets/prebuilt/pdfjs-dist"
RUN bash scripts/download_pdfjs.sh $PDFJS_PREBUILT_DIR
# Install uv dependencies
RUN pip install --no-cache-dir "uv"
# Copy contents
COPY . /app
COPY launch.sh /app/launch.sh
COPY .env.example /app/.env
# Install pip packages
RUN --mount=type=ssh \
--mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-cache \
&& uv pip install --python .venv "pdfservices-sdk@git+https://github.com/niallcm/pdfservices-python-sdk.git@bump-and-unfreeze-requirements"
RUN --mount=type=ssh \
--mount=type=cache,target=/root/.cache/uv \
if [ "$TARGETARCH" = "amd64" ]; then uv pip install --python .venv "graphrag<=0.3.6" future; fi
ENTRYPOINT ["sh", "/app/launch.sh"]
# Full version
FROM lite AS full
# Additional dependencies for full version
RUN apt-get update -qqy && \
apt-get install -y --no-install-recommends \
tesseract-ocr \
tesseract-ocr-jpn \
libsm6 \
libxext6 \
libreoffice \
ffmpeg \
libmagic-dev \
&& \
apt-get autoremove && apt-get clean && rm -rf /var/lib/apt/lists/*
# Install torch and torchvision for unstructured
RUN --mount=type=ssh \
--mount=type=cache,target=/root/.cache/uv \
uv pip install --python .venv torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
# Install additional pip packages (adv + unstructured)
RUN --mount=type=ssh \
--mount=type=cache,target=/root/.cache/uv \
uv pip install --python .venv "libs/kotaemon[adv]" \
&& uv pip install --python .venv unstructured[all-docs]
# Download NLTK data from LlamaIndex
RUN /app/.venv/bin/python -c "from llama_index.core.readers.base import BaseReader"
# Optional reader: docling
RUN --mount=type=ssh \
--mount=type=cache,target=/root/.cache/uv \
uv pip install --python .venv "libs/kotaemon[docling]"
# Optional RAG: lightRAG
ENV USE_LIGHTRAG=true
RUN --mount=type=ssh \
--mount=type=cache,target=/root/.cache/uv \
uv pip install --python .venv "libs/kotaemon[lightrag]"
ENTRYPOINT ["sh", "/app/launch.sh"]
# PaddleOCR version (GPU-only)
FROM full AS paddle
ARG CUDA_VERSION=130
# Install paddlepaddle and paddleocr
RUN --mount=type=ssh \
--mount=type=cache,target=/root/.cache/uv \
uv pip install --python .venv paddlepaddle-gpu==3.3.0 \
-i "https://www.paddlepaddle.org.cn/packages/stable/cu${CUDA_VERSION}/" \
&& uv pip install --python .venv "libs/kotaemon[paddleocr]"
ENTRYPOINT ["sh", "/app/launch.sh"]
# Ollama-bundled version
FROM full AS ollama
# Install ollama
RUN curl -fsSL https://ollama.com/install.sh | sh
# RUN nohup bash -c "ollama serve &" && sleep 4 && ollama pull qwen2.5:7b
RUN nohup bash -c "ollama serve &" && sleep 4 && ollama pull nomic-embed-text
ENTRYPOINT ["sh", "/app/launch.sh"]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+411
View File
@@ -0,0 +1,411 @@
<div align="center">
# kotaemon
An open-source clean & customizable RAG UI for chatting with your documents. Built with both end users and
developers in mind.
![Preview](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/preview-graph.png)
<a href="https://trendshift.io/repositories/11607" target="_blank"><img src="https://trendshift.io/api/badge/repositories/11607" alt="Cinnamon%2Fkotaemon | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[Live Demo #1](https://huggingface.co/spaces/cin-model/kotaemon) |
[Live Demo #2](https://huggingface.co/spaces/cin-model/kotaemon-demo) |
[Online Install](https://cinnamon.github.io/kotaemon/online_install/) |
[Colab Notebook (Local RAG)](https://colab.research.google.com/drive/1eTfieec_UOowNizTJA1NjawBJH9y_1nn)
[User Guide](https://cinnamon.github.io/kotaemon/) |
[Developer Guide](https://cinnamon.github.io/kotaemon/development/) |
[Feedback](https://github.com/Cinnamon/kotaemon/issues) |
[Contact](mailto:kotaemon.support@cinnamon.is)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/release/python-31013/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
<a href="https://github.com/Cinnamon/kotaemon/pkgs/container/kotaemon" target="_blank">
<img src="https://img.shields.io/badge/docker_pull-kotaemon:latest-brightgreen" alt="docker pull ghcr.io/cinnamon/kotaemon:latest"></a>
![download](https://img.shields.io/github/downloads/Cinnamon/kotaemon/total.svg?label=downloads&color=blue)
<a href='https://huggingface.co/spaces/cin-model/kotaemon-demo'><img src='https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue'></a>
<a href="https://hellogithub.com/en/repository/d3141471a0244d5798bc654982b263eb" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=d3141471a0244d5798bc654982b263eb&claim_uid=RLiD9UZ1rEHNaMf&theme=small" alt="FeaturedHelloGitHub" /></a>
</div>
<!-- start-intro -->
## Introduction
This project serves as a functional RAG UI for both end users who want to do QA on their
documents and developers who want to build their own RAG pipeline.
<br>
```yml
+----------------------------------------------------------------------------+
| End users: Those who use apps built with `kotaemon`. |
| (You use an app like the one in the demo above) |
| +----------------------------------------------------------------+ |
| | Developers: Those who built with `kotaemon`. | |
| | (You have `import kotaemon` somewhere in your project) | |
| | +----------------------------------------------------+ | |
| | | Contributors: Those who make `kotaemon` better. | | |
| | | (You make PR to this repo) | | |
| | +----------------------------------------------------+ | |
| +----------------------------------------------------------------+ |
+----------------------------------------------------------------------------+
```
### For end users
- **Clean & Minimalistic UI**: A user-friendly interface for RAG-based QA.
- **Support for Various LLMs**: Compatible with LLM API providers (OpenAI, AzureOpenAI, Cohere, etc.) and local LLMs (via `ollama` and `llama-cpp-python`).
- **Easy Installation**: Simple scripts to get you started quickly.
### For developers
- **Framework for RAG Pipelines**: Tools to build your own RAG-based document QA pipeline.
- **Customizable UI**: See your RAG pipeline in action with the provided UI, built with <a href='https://github.com/gradio-app/gradio'>Gradio <img src='https://img.shields.io/github/stars/gradio-app/gradio'></a>.
- **Gradio Theme**: If you use Gradio for development, check out our theme here: [kotaemon-gradio-theme](https://github.com/lone17/kotaemon-gradio-theme).
## Key Features
- **Host your own document QA (RAG) web-UI**: Support multi-user login, organize your files in private/public collections, collaborate and share your favorite chat with others.
- **Organize your LLM & Embedding models**: Support both local LLMs & popular API providers (OpenAI, Azure, Ollama, Groq).
- **Hybrid RAG pipeline**: Sane default RAG pipeline with hybrid (full-text & vector) retriever and re-ranking to ensure best retrieval quality.
- **Multi-modal QA support**: Perform Question Answering on multiple documents with figures and tables support. Support multi-modal document parsing (selectable options on UI).
- **Advanced citations with document preview**: By default the system will provide detailed citations to ensure the correctness of LLM answers. View your citations (incl. relevant score) directly in the _in-browser PDF viewer_ with highlights. Warning when retrieval pipeline return low relevant articles.
- **Support complex reasoning methods**: Use question decomposition to answer your complex/multi-hop question. Support agent-based reasoning with `ReAct`, `ReWOO` and other agents.
- **Configurable settings UI**: You can adjust most important aspects of retrieval & generation process on the UI (incl. prompts).
- **Extensible**: Being built on Gradio, you are free to customize or add any UI elements as you like. Also, we aim to support multiple strategies for document indexing & retrieval. `GraphRAG` indexing pipeline is provided as an example.
![Preview](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/preview.png)
## Installation
> If you are not a developer and just want to use the app, please check out our easy-to-follow [User Guide](https://cinnamon.github.io/kotaemon/). Download the `.zip` file from the [latest release](https://github.com/Cinnamon/kotaemon/releases/latest) to get all the newest features and bug fixes.
### System requirements
1. [Python](https://www.python.org/downloads/) >= 3.10
2. [Docker](https://www.docker.com/): optional, if you [install with Docker](#with-docker-recommended)
3. [Unstructured](https://docs.unstructured.io/open-source/installation/full-installation#full-installation) if you want to process files other than `.pdf`, `.html`, `.mhtml`, and `.xlsx` documents. Installation steps differ depending on your operating system. Please visit the link and follow the specific instructions provided there.
### With Docker (recommended)
1. We support both `lite` & `full` version of Docker images. With `full` version, the extra packages of `unstructured` will be installed, which can support additional file types (`.doc`, `.docx`, ...) but the cost is larger docker image size. For most users, the `lite` image should work well in most cases.
- To use the `full` version.
```shell
docker run \
-e GRADIO_SERVER_NAME=0.0.0.0 \
-e GRADIO_SERVER_PORT=7860 \
-v ./ktem_app_data:/app/ktem_app_data \
-p 7860:7860 -it --rm \
ghcr.io/cinnamon/kotaemon:main-full
```
- To use the `full` version with bundled **Ollama** for _local / private RAG_.
```shell
# change image name to
docker run <...> ghcr.io/cinnamon/kotaemon:main-ollama
```
- To use the `lite` version.
```shell
# change image name to
docker run <...> ghcr.io/cinnamon/kotaemon:main-lite
```
2. We currently support and test two platforms: `linux/amd64` and `linux/arm64` (for newer Mac). You can specify the platform by passing `--platform` in the `docker run` command. For example:
```shell
# To run docker with platform linux/arm64
docker run \
-e GRADIO_SERVER_NAME=0.0.0.0 \
-e GRADIO_SERVER_PORT=7860 \
-v ./ktem_app_data:/app/ktem_app_data \
-p 7860:7860 -it --rm \
--platform linux/arm64 \
ghcr.io/cinnamon/kotaemon:main-lite
```
3. Once everything is set up correctly, you can go to `http://localhost:7860/` to access the WebUI.
4. We use [GHCR](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) to store docker images, all images can be found [here.](https://github.com/Cinnamon/kotaemon/pkgs/container/kotaemon)
### Without Docker
1. Clone the repository:
```shell
git clone https://github.com/Cinnamon/kotaemon
cd kotaemon
```
2. Setup the environment:
- **Option 1: Using [uv](https://docs.astral.sh/uv/getting-started/installation/) (recommended)**
```shell
uv sync --python 3.10
source .venv/bin/activate
```
- **Option 2: Using conda**
```shell
conda create -n kotaemon python=3.10
conda activate kotaemon
pip install -e "libs/kotaemon[all]"
pip install -e "libs/ktem"
```
3. Create a `.env` file in the root of this project. Use `.env.example` as a template.
The `.env` file is there to serve use cases where users want to pre-config the models before starting up the app (e.g. deploy the app on HF hub). The file will only be used to populate the db once upon the first run, it will no longer be used in consequent runs.
4. (Optional) To enable in-browser `PDF_JS` viewer, download [PDF_JS_DIST](https://github.com/mozilla/pdf.js/releases/download/v4.0.379/pdfjs-4.0.379-dist.zip) then extract it to `libs/ktem/ktem/assets/prebuilt`.
<img src="https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/pdf-viewer-setup.png" alt="pdf-setup" width="300">
5. Start the web server:
```shell
python app.py
```
- The app will be automatically launched in your browser.
- Default username and password are both `admin`. You can set up additional users directly through the UI.
![Chat tab](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/chat-tab.png)
6. Check the `Resources` tab and `LLMs and Embeddings` and ensure that your `api_key` value is set correctly from your `.env` file. If it is not set, you can set it there.
### Setup GraphRAG
> [!NOTE]
> Official MS GraphRAG indexing only works with OpenAI or Ollama API.
> We recommend most users to use NanoGraphRAG implementation for straightforward integration with Kotaemon.
<details>
<summary>Setup Nano GRAPHRAG</summary>
- Install nano-GraphRAG: `pip install nano-graphrag`
- `nano-graphrag` install might introduce version conflicts, see [this issue](https://github.com/Cinnamon/kotaemon/issues/440)
- To quickly fix: `pip uninstall hnswlib chroma-hnswlib && pip install chroma-hnswlib`
- Launch Kotaemon with `USE_NANO_GRAPHRAG=true` environment variable.
- Set your default LLM & Embedding models in Resources setting and it will be recognized automatically from NanoGraphRAG.
</details>
<details>
<summary>Setup LIGHTRAG</summary>
- Install LightRAG: `pip install git+https://github.com/HKUDS/LightRAG.git`
- `LightRAG` install might introduce version conflicts, see [this issue](https://github.com/Cinnamon/kotaemon/issues/440)
- To quickly fix: `pip uninstall hnswlib chroma-hnswlib && pip install chroma-hnswlib`
- Launch Kotaemon with `USE_LIGHTRAG=true` environment variable.
- Set your default LLM & Embedding models in Resources setting and it will be recognized automatically from LightRAG.
</details>
<details>
<summary>Setup MS GRAPHRAG</summary>
- **Non-Docker Installation**: If you are not using Docker, install GraphRAG with the following command:
```shell
pip install "graphrag<=0.3.6" future
```
- **Setting Up API KEY**: To use the GraphRAG retriever feature, ensure you set the `GRAPHRAG_API_KEY` environment variable. You can do this directly in your environment or by adding it to a `.env` file.
- **Using Local Models and Custom Settings**: If you want to use GraphRAG with local models (like `Ollama`) or customize the default LLM and other configurations, set the `USE_CUSTOMIZED_GRAPHRAG_SETTING` environment variable to true. Then, adjust your settings in the `settings.yaml.example` file.
</details>
### Setup Local Models (for local/private RAG)
See [Local model setup](docs/local_model.md).
### Setup multimodal document parsing (OCR, table parsing, figure extraction)
These options are available:
- [Azure Document Intelligence (API)](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence)
- [Adobe PDF Extract (API)](https://developer.adobe.com/document-services/docs/overview/pdf-extract-api/)
- [Docling (local, open-source)](https://github.com/DS4SD/docling) see [integrations/docling.md](./docs/integrations/docling.md) for Kotaemon-specific setup.
- [PaddleOCR (local, open-source)](https://github.com/PADDLEPADDLE/PADDLEOCR) see [integrations/paddle_ocr.md](./docs/integrations/paddle_ocr.md) for Kotaemon-specific setup.
Select corresponding loaders in `Settings -> Retrieval Settings -> File loader`
### Customize your application
- By default, all application data is stored in the `./ktem_app_data` folder. You can back up or copy this folder to transfer your installation to a new machine.
- For advanced users or specific use cases, you can customize these files:
- `flowsettings.py`
- `.env`
#### `flowsettings.py`
This file contains the configuration of your application. You can use the example
[here](flowsettings.py) as the starting point.
<details>
<summary>Notable settings</summary>
```python
# setup your preferred document store (with full-text search capabilities)
KH_DOCSTORE=(Elasticsearch | LanceDB | SimpleFileDocumentStore)
# setup your preferred vectorstore (for vector-based search)
KH_VECTORSTORE=(ChromaDB | LanceDB | InMemory | Milvus | Qdrant)
# Enable / disable multimodal QA
KH_REASONINGS_USE_MULTIMODAL=True
# Setup your new reasoning pipeline or modify existing one.
KH_REASONINGS = [
"ktem.reasoning.simple.FullQAPipeline",
"ktem.reasoning.simple.FullDecomposeQAPipeline",
"ktem.reasoning.react.ReactAgentPipeline",
"ktem.reasoning.rewoo.RewooAgentPipeline",
]
```
</details>
#### `.env`
This file provides another way to configure your models and credentials.
<details>
<summary>Configure model via the .env file</summary>
- Alternatively, you can configure the models via the `.env` file with the information needed to connect to the LLMs. This file is located in the folder of the application. If you don't see it, you can create one.
- Currently, the following providers are supported:
- **OpenAI**
In the `.env` file, set the `OPENAI_API_KEY` variable with your OpenAI API key in order
to enable access to OpenAI's models. There are other variables that can be modified,
please feel free to edit them to fit your case. Otherwise, the default parameter should
work for most people.
```shell
OPENAI_API_BASE=https://api.openai.com/v1
OPENAI_API_KEY=<your OpenAI API key here>
OPENAI_CHAT_MODEL=gpt-3.5-turbo
OPENAI_EMBEDDINGS_MODEL=text-embedding-ada-002
```
- **Azure OpenAI**
For OpenAI models via Azure platform, you need to provide your Azure endpoint and API
key. You might also need to provide your developments' name for the chat model and the
embedding model depending on how you set up Azure development.
```shell
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_API_KEY=
OPENAI_API_VERSION=2024-02-15-preview
AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-35-turbo
AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=text-embedding-ada-002
```
- **Local Models**
- Using `ollama` OpenAI compatible server:
- Install [ollama](https://github.com/ollama/ollama) and start the application.
- Pull your model, for example:
```shell
ollama pull llama3.1:8b
ollama pull nomic-embed-text
```
- Set the model names on web UI and make it as default:
![Models](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/models.png)
- Using `GGUF` with `llama-cpp-python`
You can search and download a LLM to be ran locally from the [Hugging Face Hub](https://huggingface.co/models). Currently, these model formats are supported:
- GGUF
You should choose a model whose size is less than your device's memory and should leave
about 2 GB. For example, if you have 16 GB of RAM in total, of which 12 GB is available,
then you should choose a model that takes up at most 10 GB of RAM. Bigger models tend to
give better generation but also take more processing time.
Here are some recommendations and their size in memory:
- [Qwen1.5-1.8B-Chat-GGUF](https://huggingface.co/Qwen/Qwen1.5-1.8B-Chat-GGUF/resolve/main/qwen1_5-1_8b-chat-q8_0.gguf?download=true): around 2 GB
Add a new LlamaCpp model with the provided model name on the web UI.
</details>
### Adding your own RAG pipeline
#### Custom Reasoning Pipeline
1. Check the default pipeline implementation in [here](libs/ktem/ktem/reasoning/simple.py). You can make quick adjustment to how the default QA pipeline work.
2. Add new `.py` implementation in `libs/ktem/ktem/reasoning/` and later include it in `flowssettings` to enable it on the UI.
#### Custom Indexing Pipeline
- Check sample implementation in `libs/ktem/ktem/index/file/graph`
> (more instruction WIP).
<!-- end-intro -->
## Citation
Please cite this project as
```BibTeX
@misc{kotaemon2024,
title = {Kotaemon - An open-source RAG-based tool for chatting with any content.},
author = {The Kotaemon Team},
year = {2024},
howpublished = {\url{https://github.com/Cinnamon/kotaemon}},
}
```
## Star History
<a href="https://star-history.com/#Cinnamon/kotaemon&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=Cinnamon/kotaemon&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=Cinnamon/kotaemon&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=Cinnamon/kotaemon&type=Date" />
</picture>
</a>
## Contribution
Since our project is actively being developed, we greatly value your feedback and contributions. Please see our [Contributing Guide](https://github.com/Cinnamon/kotaemon/blob/main/CONTRIBUTING.md) to get started. Thank you to all our contributors!
<a href="https://github.com/Cinnamon/kotaemon/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Cinnamon/kotaemon" />
</a>
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`Cinnamon/kotaemon`
- 原始仓库:https://github.com/Cinnamon/kotaemon
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+26
View File
@@ -0,0 +1,26 @@
import os
from theflow.settings import settings as flowsettings
KH_APP_DATA_DIR = getattr(flowsettings, "KH_APP_DATA_DIR", ".")
KH_GRADIO_SHARE = getattr(flowsettings, "KH_GRADIO_SHARE", False)
GRADIO_TEMP_DIR = os.getenv("GRADIO_TEMP_DIR", None)
# override GRADIO_TEMP_DIR if it's not set
if GRADIO_TEMP_DIR is None:
GRADIO_TEMP_DIR = os.path.join(KH_APP_DATA_DIR, "gradio_tmp")
os.environ["GRADIO_TEMP_DIR"] = GRADIO_TEMP_DIR
from ktem.main import App # noqa
app = App()
demo = app.make()
demo.queue().launch(
favicon_path=app._favicon,
inbrowser=True,
allowed_paths=[
"libs/ktem/ktem/assets",
GRADIO_TEMP_DIR,
],
share=KH_GRADIO_SHARE,
)
+9
View File
@@ -0,0 +1,9 @@
mkdocs
mkdocstrings[python]
mkdocs-material
mkdocs-gen-files
mkdocs-literate-nav
mkdocs-git-revision-date-localized-plugin
mkdocs-section-index
mkdocs-include-markdown-plugin[cache]
mdx_truly_sane_lists
+11
View File
@@ -0,0 +1,11 @@
# About Kotaemon
An open-source tool for chatting with your documents. Built with both end users and
developers in mind.
[Source Code](https://github.com/Cinnamon/kotaemon) |
[HF Space](https://huggingface.co/spaces/cin-model/kotaemon-demo)
[Installation Guide](https://cinnamon.github.io/kotaemon/) |
[Developer Guide](https://cinnamon.github.io/kotaemon/development/) |
[Feedback](https://github.com/Cinnamon/kotaemon/issues)
+116
View File
@@ -0,0 +1,116 @@
# Contributing
## Setting up
- Clone the repo
```shell
git clone git@github.com:Cinnamon/kotaemon.git
cd kotaemon
```
- Install the environment
- Create a conda environment (python >= 3.10 is recommended)
```shell
conda create -n kotaemon python=3.10
conda activate kotaemon
# install dependencies
cd libs/kotaemon
pip install -e ".[all]"
```
- Or run the installer (one of the `scripts/run_*` scripts depends on your OS), then
you will have all the dependencies installed as a conda environment at
`install_dir/env`.
```shell
conda activate install_dir/env
```
- Pre-commit
```shell
pre-commit install
```
- Test
```shell
pytest tests
```
## Package overview
`kotaemon` library focuses on the AI building blocks to implement a RAG-based QA application. It consists of base interfaces, core components and a list of utilities:
- Base interfaces: `kotaemon` defines the base interface of a component in a pipeline. A pipeline is also a component. By clearly define this interface, a pipeline of steps can be easily constructed and orchestrated.
- Core components: `kotaemon` implements (or wraps 3rd-party libraries
like Langchain, llama-index,... when possible) commonly used components in
kotaemon use cases. Some of these components are: LLM, vector store,
document store, retriever... For a detailed list and description of these
components, please refer to the [API Reference](../reference/Summary.md) section.
- List of utilities: `kotaemon` provides utilities and tools that are
usually needed in client project. For example, it provides a prompt
engineering UI for AI developers in a project to quickly create a prompt
engineering tool for DMs and QALs. It also provides a command to quickly spin
up a project code base. For a full list and description of these utilities,
please refer to the [Utilities](utilities.md) section.
```mermaid
mindmap
root((kotaemon))
Base Interfaces
Document
LLMInterface
RetrievedDocument
BaseEmbeddings
BaseChat
BaseCompletion
...
Core Components
LLMs
AzureOpenAI
OpenAI
Embeddings
AzureOpenAI
OpenAI
HuggingFaceEmbedding
VectorStore
InMemoryVectorstore
ChromaVectorstore
Agent
Tool
DocumentStore
...
Utilities
Scaffold project
PromptUI
Documentation Support
```
## Common conventions
- PR title: One-line description (example: Feat: Declare BaseComponent and decide LLM call interface).
- [Encouraged] Provide a quick description in the PR, so that:
- Reviewers can quickly understand the direction of the PR.
- It will be included in the commit message when the PR is merged.
## Environment caching on PR
- To speed up CI, environments are cached based on the version specified in `__init__.py`.
- Since dependencies versions in `setup.py` are not pinned, you need to pump the version in order to use a new environment. That environment will then be cached and used by your subsequence commits within the PR, until you pump the version again
- The new environment created during your PR is cached and will be available to others once the PR is merged.
- If you are experimenting with new dependencies and want a fresh environment every time, add `[ignore cache]` in your commit message. The CI will create a fresh environment to run your commit and then discard it.
- If your PR include updated dependencies, the recommended workflow would be:
- Doing development as usual.
- When you want to run the CI, push a commit with the message containing `[ignore cache]`.
- Once the PR is final, pump the version in `__init__.py` and push a final commit not containing `[ignore cache]`.
## Merge PR guideline
- Use squash and merge option
- 1st line message is the PR title.
- The text area is the PR description.
+71
View File
@@ -0,0 +1,71 @@
# Creating a component
A fundamental concept in kotaemon is "component".
Anything that isn't data or data structure is a "component". A component can be
thought of as a step within a pipeline. It takes in some input, processes it,
and returns an output, just the same as a Python function! The output will then
become an input for the next component in a pipeline. In fact, a pipeline is just
a component. More appropriately, a nested component: a component that makes use of one or more other components in
the processing step. So in reality, there isn't a difference between a pipeline
and a component! Because of that, in kotaemon, we will consider them the
same as "component".
To define a component, you will:
1. Create a class that subclasses from `kotaemon.base.BaseComponent`
2. Declare init params with type annotation
3. Declare nodes (nodes are just other components!) with type annotation
4. Implement the processing logic in `run`.
The syntax of a component is as follow:
```python
from kotaemon.base import BaseComponent
from kotaemon.llms import LCAzureChatOpenAI
from kotaemon.parsers import RegexExtractor
class FancyPipeline(BaseComponent):
param1: str = "This is param1"
param2: int = 10
param3: float
node1: BaseComponent # this is a node because of BaseComponent type annotation
node2: LCAzureChatOpenAI # this is also a node because LCAzureChatOpenAI subclasses BaseComponent
node3: RegexExtractor # this is also a node bceause RegexExtractor subclasses BaseComponent
def run(self, some_text: str):
prompt = (self.param1 + some_text) * int(self.param2 + self.param3)
llm_pred = self.node2(prompt).text
matches = self.node3(llm_pred)
return matches
```
Then this component can be used as follow:
```python
llm = LCAzureChatOpenAI(endpoint="some-endpont")
extractor = RegexExtractor(pattern=["yes", "Yes"])
component = FancyPipeline(
param1="Hello"
param3=1.5
node1=llm,
node2=llm,
node3=extractor
)
component("goodbye")
```
This way, we can define each operation as a reusable component, and use them to
compose larger reusable components!
## Benefits of component
By defining a component as above, we formally encapsulate all the necessary
information inside a single class. This introduces several benefits:
1. Allow tools like promptui to inspect the inner working of a component in
order to automatically generate the promptui.
2. Allow visualizing a pipeline for debugging purpose.
+34
View File
@@ -0,0 +1,34 @@
# Data & Data Structure Components
The data & data structure components include:
- The `Document` class.
- The document store.
- The vector store.
## Data Loader
- PdfLoader
- Layout-aware with table parsing PdfLoader
- MathPixLoader: To use this loader, you need MathPix API key, refer to [mathpix docs](https://docs.mathpix.com/#introduction) for more information
- OCRLoader: This loader uses lib-table and Flax pipeline to perform OCR and read table structure from PDF file (TODO: add more info about deployment of this module).
- Output:
- Document: text + metadata to identify whether it is table or not
```
- "source": source file name
- "type": "table" or "text"
- "table_origin": original table in markdown format (to be feed to LLM or visualize using external tools)
- "page_label": page number in the original PDF document
```
## Document Store
- InMemoryDocumentStore
## Vector Store
- ChromaVectorStore
- InMemoryVectorStore
+5
View File
@@ -0,0 +1,5 @@
{%
include-markdown "../../README.md"
start="<!-- start-intro -->"
end="<!-- end-intro -->"
%}
+169
View File
@@ -0,0 +1,169 @@
# Utilities
## Prompt engineering UI
![chat-ui](images/271332562-ac8f9aac-d853-4571-a48b-d866a99eaf3e.png)
**_Important:_** despite the name prompt engineering UI, this tool allows testers to test any kind of parameters that are exposed by developers. Prompt is one kind of param. There can be other type of params that testers can tweak (e.g. top_k, temperature...).
In the development process, developers typically build the pipeline. However, for use
cases requiring expertise in prompt creation, non-technical members (testers, domain experts) can be more
effective. To facilitate this, `kotaemon` offers a user-friendly prompt engineering UI
that developers integrate into their pipelines. This enables non-technical members to
adjust prompts and parameters, run experiments, and export results for optimization.
As of Sept 2023, there are 2 kinds of prompt engineering UI:
- Simple pipeline: run one-way from start to finish.
- Chat pipeline: interactive back-and-forth.
### Simple pipeline
For simple pipeline, the supported client project workflow looks as follow:
1. [tech] Build pipeline
2. [tech] Export pipeline to config: `$ kotaemon promptui export <module.path.piplineclass> --output <path/to/config/file.yml>`
3. [tech] Customize the config
4. [tech] Spin up prompt engineering UI: `$ kotaemon promptui run <path/to/config/file.yml>`
5. [non-tech] Change params, run inference
6. [non-tech] Export to Excel
7. [non-tech] Select the set of params that achieve the best output
The prompt engineering UI prominently involves from step 2 to step 7 (step 1 is normally
done by the developers, while step 7 happens exclusively in Excel file).
#### Step 2 - Export pipeline to config
Command:
```shell
$ kotaemon promptui export <module.path.piplineclass> --output <path/to/config/file.yml>
```
where:
- `<module.path.pipelineclass>` is a dot-separated path to the pipeline. For example, if your pipeline can be accessed with `from projectA.pipelines import AnsweringPipeline`, then this value is `projectA.pipelines.AnswerPipeline`.
- `<path/to/config/file.yml>` is the target file path that the config will be exported to. If the config file already exists, and contains information of other pipelines, the config of current pipeline will additionally be added. If it contains information of the current pipeline (in the past), the old information will be replaced.
By default, all params in a pipeline (including nested params) will be export to the configuration file. For params that you do not wish to expose to the UI, you can directly remove them from the config YAML file. You can also annotate those param with `ignore_ui=True`, and they will be ignored in the config generation process. Example:
```python
class Pipeline(BaseComponent):
param1: str = Param(default="hello")
param2: str = Param(default="goodbye", ignore_ui=True)
```
Declared as above, and `param1` will show up in the config YAML file, while `param2` will not.
#### Step 3 - Customize the config
developers can further edit the config file in this step to get the most suitable UI (step 4) with their tasks. The exported config will have this overall schema:
```yml
<module.path.pipelineclass1>:
params: ... (Detail param information to initiate a pipeline. This corresponds to the pipeline init parameters.)
inputs: ... (Detail the input of the pipeline e.g. a text prompt. This corresponds to the params of `run(...)` method.)
outputs: ... (Detail the output of the pipeline e.g. prediction, accuracy... This is the output information we wish to see in the UI.)
logs: ... (Detail what information should show up in the log.)
```
##### Input and params
The inputs section have the overall schema as follow:
```yml
inputs:
<input-variable-name-1>:
component: <supported-UI-component>
params: # this section is optional)
value: <default-value>
<input-variable-name-2>: ... # similar to above
params:
<param-variable-name-1>: ... # similar to those in the inputs
```
The list of supported prompt UI and their corresponding gradio UI components:
```python
COMPONENTS_CLASS = {
"text": gr.components.Textbox,
"checkbox": gr.components.CheckboxGroup,
"dropdown": gr.components.Dropdown,
"file": gr.components.File,
"image": gr.components.Image,
"number": gr.components.Number,
"radio": gr.components.Radio,
"slider": gr.components.Slider,
}
```
##### Outputs
The outputs are a list of variables that we wish to show in the UI. Since in Python, the function output doesn't have variable name, so output declaration is a little bit different than input and param declaration:
```yml
outputs:
- component: <supported-UI-component>
step: <name-of-pipeline-step>
item: <jsonpath way to retrieve the info>
- ... # similar to above
```
where:
- component: the same text string and corresponding Gradio UI as in inputs & params
- step: the pipeline step that we wish to look fetch and show output on the UI
- item: the jsonpath mechanism to get the targeted variable from the step above
##### Logs
The logs show a list of sheetname and how to retrieve the desired information.
```yml
logs:
<logname>:
inputs:
- name: <column name>
step: <the pipeline step that we would wish to see the input>
variable: <the variable in the step>
- ...
outputs:
- name: <column name>
step: <the pipeline step that we would wish to see the output>
item: <how to retrieve the output of that step>
```
#### Step 4 + 5 - Spin up prompt engineering UI + Perform prompt engineering
Command:
```shell
$ kotaemon promptui run <path/to/config/file.yml>
```
This will generate an UI as follow:
![Screenshot from 2023-09-20 12-20-31](images/269170198-9ac1b95a-b667-42e7-b318-98a1b805d6df.png)
where:
- The tabs at the top of the UI corresponds to the pipeline to do prompt engineering.
- The inputs and params tabs allow users to edit (these corresponds to the inputs and params in the config file).
- The outputs panel holds the UI elements to show the outputs defined in config file.
- The Run button: will execute pipeline with the supplied inputs and params, and render result in the outputs panel.
- The Export button: will export the logs of all the run to an Excel files users to inspect for best set of params.
#### Step 6 - Export to Excel
Upon clicking export, the users can download Excel file.
### Chat pipeline
Chat pipeline workflow is different from simple pipeline workflow. In simple pipeline, each Run creates a set of output, input and params for users to compare. In chat pipeline, each Run is not a one-off run, but a long interactive session. Hence, the workflow is as follow:
1. Set the desired parameters.
2. Click "New chat" to start a chat session with the supplied parameters. This set of parameters will persist until the end of the chat session. During an ongoing chat session, changing the parameters will not take any effect.
3. Chat and interact with the chat bot on the right panel. You can add any additional input (if any), and they will be supplied to the chatbot.
4. During chat, the log of the chat will show up in the "Output" tabs. This is empty by default, so if you want to show the log here, tell the AI developers to configure the UI settings.
5. When finishing chat, select your preference in the radio box. Click "End chat". This will save the chat log and the preference to disk.
6. To compare the result of different run, click "Export" to get an Excel spreadsheet summary of different run.
+5
View File
@@ -0,0 +1,5 @@
.language-pycon .gp,
.language-pycon .go {
/* Generic.Prompt, Generic.Output */
user-select: none;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

+48
View File
@@ -0,0 +1,48 @@
# Getting Started with Kotaemon
![type:video](https://github.com/Cinnamon/kotaemon/assets/25688648/815ecf68-3a02-4914-a0dd-3f8ec7e75cd9)
This page is intended for **end users** who want to use the `kotaemon` tool for Question
Answering on local documents. If you are a **developer** who wants contribute to the project, please visit the [development](development/index.md) page.
## Installation (Online HuggingFace Space) - easy (10 mins)
Visit this [guide](online_install.md).
## Installation (Offline) - intermediate (20 mins)
### Download
Download the `kotaemon-app.zip` file from the [latest release](https://github.com/Cinnamon/kotaemon/releases/latest/).
### Run setup script
0. Unzip the downloaded file.
1. Navigate to the `scripts` folder and start an installer that matches your OS:
- Windows: `run_windows.bat`. Just double click the file.
- macOS: `run_macos.sh`
1. Right click on your file and select Open with and Other.
2. Enable All Applications and choose Terminal.
3. NOTE: If you always want to open that file with Terminal, then check Always Open With.
4. From now on, double click on your file and it should work.
- Linux: `run_linux.sh`. Please run the script using `bash run_linux.sh` in your terminal.
2. After the installation, the installer will ask to launch the ktem's UI, answer to continue.
3. If launched, the application will be open automatically in your browser.
4. Default login information is: `username: admin / password: admin`. You should change this credential right after the first login on the UI.
## Launch
To launch the app after initial setup or any change, simply run the `run_*` script again.
A browser window will be opened and greets you with this screen:
![Chat tab](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/chat-tab.png)
## Usage
For how to use the application, see [Usage](usage.md). This page will also be available to
you within the application.
## Feedback
Feel free to create a bug report or a feature request on our [repo](https://github.com/Cinnamon/kotaemon/issues).
+29
View File
@@ -0,0 +1,29 @@
# Docling
Kotaemon provides a [Docling](https://github.com/DS4SD/docling) reader to enable local document ingestion with structure-aware parsing, including text, tables, and figures.
The reader is located under `kotaemon/loaders/docling_loader`.
## Prerequisites
- Install Docling:
```bash
uv pip install -e "libs/kotaemon[docling]"
```
- Configure optional figure captioning:
Docling can generate figure captions when a VLM endpoint is available. Set `KH_VLM_ENDPOINT` in your `.env` file or application settings to enable captioning.
```bash
KH_VLM_ENDPOINT=http://your-vlm-endpoint
```
If `KH_VLM_ENDPOINT` is not set, Docling will still extract text, tables, and figure metadata, but it will skip generated figure captions.
## Configure the loader
1. Run Kotaemon and open the app UI.
2. Navigate to Settings → Retrieval Settings → File loader.
3. Select `Docling (figure+table extraction)`.
4. Save the settings, then upload or ingest a document. Kotaemon will use Docling during indexing and convert extracted content into `Document` objects.
+43
View File
@@ -0,0 +1,43 @@
# PaddleOCR
Kotaemon provides two [PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR) readers to enable document ingestion with full layout understanding, including multilingual text, tables, figures, formulas, and seals.
- `PaddleOCRVLReader`: Wraps the PaddleOCR-VL 1.5 visual-language model for robust layout and VQA-based parsing.
- `PPStructureV3Reader`: Uses the PPStructureV3 pipeline for structured layout analysis, including table and chart detection.
Both readers are located under `kotaemon/loaders/paddleocr_loader`.
## Prerequisites
- Install PaddlePaddle:
- Ensure that the installed PaddlePaddle version matches your system configuration and hardware (CPU/GPU). For additional wheel options, refer to the [PaddlePaddle official website](https://www.paddlepaddle.org.cn/install/quick?docurl=undefined).
- Check device support for [PaddleOCR-VL](https://www.paddleocr.ai/main/en/version3.x/pipeline_usage/PaddleOCR-VL.html#inference-device-support-for-paddleocr-vl).
```bash
# CPU
uv pip install paddlepaddle==3.3.0 -i https://www.paddlepaddle.org.cn/packages/stable/cpu/
# gpurequires GPU driver version ≥550.54.14 (Linux) or ≥550.54.14 (Windows)
uv pip install paddlepaddle-gpu==3.3.0 -i https://www.paddlepaddle.org.cn/packages/stable/cu130/
```
- Install the PaddleOCR doc parser extras:
```bash
uv pip install -e "libs/kotaemon[paddleocr]"
```
- Configure the device: You can set the `PADDLE_DEVICE` environment variable in your .env file to control the execution device.
```bash
PADDLE_DEVICE=gpu # cpu, gpu:0
```
## Configure the loader
1. Run Kotaemon and open the app UI.
2. Navigate to Settings → File Loader (under indexing or ingestion settings).
3. Select one of the PaddleOCR loaders:
- `PaddleOCR PPStructureV3 (table+figure extraction)`
- `PaddleOCR-VL (VLM document parsing)`
4. Save the settings, then upload or ingest a document. Kotaemon will automatically use the selected PaddleOCR loader during indexing and convert extracted content into Document objects.
+90
View File
@@ -0,0 +1,90 @@
# Setup local LLMs & Embedding models
## Prepare local models
#### NOTE
In the case of using Docker image, please replace `http://localhost` with `http://host.docker.internal` to correctly communicate with service on the host machine. See [more detail](https://stackoverflow.com/questions/31324981/how-to-access-host-port-from-docker-container).
### Ollama OpenAI compatible server (recommended)
Install [ollama](https://github.com/ollama/ollama) and start the application.
Pull your model (e.g):
```
ollama pull llama3.1:8b
ollama pull nomic-embed-text
```
Setup LLM and Embedding model on Resources tab with type OpenAI. Set these model parameters to connect to Ollama:
```
api_key: ollama
base_url: http://localhost:11434/v1/
model: gemma2:2b (for llm) | nomic-embed-text (for embedding)
```
![Models](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/models.png)
### oobabooga/text-generation-webui OpenAI compatible server
Install [oobabooga/text-generation-webui](https://github.com/oobabooga/text-generation-webui/).
Follow the setup guide to download your models (GGUF, HF).
Also take a look at [OpenAI compatible server](https://github.com/oobabooga/text-generation-webui/wiki/12-%E2%80%90-OpenAI-API) for detail instructions.
Here is a short version
```
# install sentence-transformer for embeddings creation
pip install sentence_transformers
# change to text-generation-webui src dir
python server.py --api
```
Use the `Models` tab to download new model and press Load.
Setup LLM and Embedding model on Resources tab with type OpenAI. Set these model parameters to connect to `text-generation-webui`:
```
api_key: dummy
base_url: http://localhost:5000/v1/
model: any
```
### llama-cpp-python server (LLM only)
See [llama-cpp-python OpenAI server](https://llama-cpp-python.readthedocs.io/en/latest/server/).
Download any GGUF model weight on HuggingFace or other source. Place it somewhere on your local machine.
Run
```
LOCAL_MODEL=<path/to/GGUF> python scripts/serve_local.py
```
Setup LLM model on Resources tab with type OpenAI. Set these model parameters to connect to `llama-cpp-python`:
```
api_key: dummy
base_url: http://localhost:8000/v1/
model: model_name
```
## Use local models for RAG
- Set default LLM and Embedding model to a local variant.
![Models](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/llm-default.png)
- Set embedding model for the File Collection to a local model (e.g: `ollama`)
![Index](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/index-embedding.png)
- Go to Retrieval settings and choose LLM relevant scoring model as a local model (e.g: `ollama`). Or, you can choose to disable this feature if your machine cannot handle a lot of parallel LLM requests at the same time.
![Settings](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/retrieval-setting.png)
You are set! Start a new conversation to test your local RAG pipeline.
+13
View File
@@ -0,0 +1,13 @@
## Installation (Online HuggingFace Space)
1. Go to [HF kotaemon_template](https://huggingface.co/spaces/cin-model/kotaemon_template).
2. Use Duplicate function to create your own space. Or use this [direct link](https://huggingface.co/spaces/cin-model/kotaemon_template?duplicate=true).
![Duplicate space](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/duplicate_space.png)
![Change space params](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/change_space_params.png)
3. Wait for the build to complete and start up (apprx 10 mins).
![Wait space build](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/space_build.png)
![Close space build](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/close_logs_space.png)
4. Follow the first setup instructions (and register for Cohere API key if needed).
![Cohere API](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/cohere_api_key.png)
5. Complete the setup and use your own private space!
![App Startup](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/initial_startup.png)
+232
View File
@@ -0,0 +1,232 @@
# Add new indexing and reasoning pipeline to the application
@trducng
At high level, to add new indexing and reasoning pipeline:
1. You define your indexing or reasoning pipeline as a class from
`BaseComponent`.
2. You declare that class in the setting files `flowsettings.py`.
Then when `python app.py`, the application will dynamically load those
pipelines.
The below sections talk in more detail about how the pipelines should be
constructed.
## Define a pipeline as a class
In essence, a pipeline will subclass from `kotaemon.base.BaseComponent`.
Each pipeline has 2 main parts:
- All declared arguments and sub-pipelines.
- The logic inside the pipeline.
An example pipeline:
```python
from kotaemon.base import BaseComponent
class SoSimple(BaseComponent):
arg1: int
arg2: str
def run(self, arg3: str):
return self.arg1 * self.arg2 + arg3
```
This pipeline is simple for demonstration purpose, but we can imagine pipelines
with much more arguments, that can take other pipelines as arguments, and have
more complicated logic in the `run` method.
**_An indexing or reasoning pipeline is just a class subclass from
`BaseComponent` like above._**
For more detail on this topic, please refer to [Creating a
Component](/create-a-component/)
## Run signatures
**Note**: this section is tentative at the moment. We will finalize `def run`
function signature by latest early April.
The indexing pipeline:
```python
def run(
self,
file_paths: str | Path | list[str | Path],
reindex: bool = False,
**kwargs,
):
"""Index files to intermediate representation (e.g. vector, database...)
Args:
file_paths: the list of paths to files
reindex: if True, files in `file_paths` that already exists in database
should be reindex.
"""
```
The reasoning pipeline:
```python
def run(self, question: str, history: list, **kwargs) -> Document:
"""Answer the question
Args:
question: the user input
history: the chat history [(user_msg1, bot_msg1), (user_msg2, bot_msg2)...]
Returns:
kotaemon.base.Document: the final answer
"""
```
## Register your pipeline to ktem
To register your pipelines to ktem, you declare it in the `flowsettings.py`
file. This file locates at the current working directory where you start the
ktem. In most use cases, it is this
[one](https://github.com/Cinnamon/kotaemon/blob/main/flowsettings.py).
```python
KH_REASONING = ["<python.module.path.to.the.reasoning.class>"]
KH_INDEX = "<python.module.path.to.the.indexing.class>"
```
You can register multiple reasoning pipelines to ktem by populating the
`KH_REASONING` list. The user can select which reasoning pipeline to use
in their Settings page.
For now, there's only one supported index option for `KH_INDEX`.
Make sure that your class is discoverable by Python.
## Allow users to customize your pipeline in the app settings
To allow the users to configure your pipeline, you need to declare what you
allow the users to configure as a dictionary. `ktem` will include them into the
application settings.
In your pipeline class, add a classmethod `get_user_settings` that returns a
setting dictionary, add a classmethod `get_info` that returns an info
dictionary. Example:
```python
class SoSimple(BaseComponent):
... # as above
@classmethod
def get_user_settings(cls) -> dict:
"""The settings to the user"""
return {
"setting_1": {
"name": "Human-friendly name",
"value": "Default value",
"choices": [("Human-friendly Choice 1", "choice1-id"), ("HFC 2", "choice2-id")], # optional
"component": "Which Gradio UI component to render, can be: text, number, checkbox, dropdown, radio, checkboxgroup"
},
"setting_2": {
# follow the same rule as above
}
}
@classmethod
def get_info(cls) -> dict:
"""Pipeline information for bookkeeping purpose"""
return {
"id": "a unique id to differentiate this pipeline from other pipeline",
"name": "Human-friendly name of the pipeline",
"description": "Can be a short description of this pipeline"
}
```
Once adding these methods to your pipeline class, `ktem` will automatically
extract and add them to the settings.
## Construct to pipeline object
Once `ktem` runs your pipeline, it will call your classmethod `get_pipeline`
with the full user settings and expect to obtain the pipeline object. Within
this `get_pipeline` method, you implement all the necessary logics to initiate
the pipeline object. Example:
```python
class SoSimple(BaseComponent):
... # as above
@classmethod
def get_pipeline(self, setting):
obj = cls(arg1=setting["reasoning.id.setting1"])
return obj
```
## Reasoning: Stream output to UI
For fast user experience, you can stream the output directly to UI. This way,
user can start observing the output as soon as the LLM model generates the 1st
token, rather than having to wait the pipeline finishes to read the whole message.
To stream the output, you need to;
1. Turn the `run` function to async.
2. Pass in the output to a special queue with `self.report_output`.
```python
async def run(self, question: str, history: list, **kwargs) -> Document:
for char in "This is a long messages":
self.report_output({"output": text.text})
```
The argument to `self.report_output` is a dictionary, that contains either or
all of these 2 keys: "output", "evidence". The "output" string will be streamed
to the chat message, and the "evidence" string will be streamed to the
information panel.
## Access application LLMs, Embeddings
You can access users' collections of LLMs and embedding models with:
```python
from ktem.embeddings.manager import embeddings
from ktem.llms.manager import llms
llm = llms.get_default()
embedding_model = embeddings.get_default()
```
You can also allow the users to specifically select which llms or embedding
models they want to use through the settings.
```python
@classmethod
def get_user_settings(cls) -> dict:
from ktem.llms.manager import llms
return {
"citation_llm": {
"name": "LLM for citation",
"value": llms.get_default(),
"component: "dropdown",
"choices": list(llms.options().keys()),
},
...
}
```
## Optional: Access application data
You can access the user's application database, vector store as follow:
```python
# get the database that contains the source files
from ktem.db.models import Source, Index, Conversation, User
# get the vector store
```
+14
View File
@@ -0,0 +1,14 @@
`ktem` provides user management as an extension. To enable user management, in
your `flowsettings.py`, set the following variables:
- `KH_FEATURE_USER_MANAGEMENT`: True to enable.
- `KH_FEATURE_USER_MANAGEMENT_ADMIN`: the admin username. This user will be
created when the app 1st start.
- `KH_FEATURE_USER_MANAGEMENT_PASSWORD`: the admin password. This value
accompanies the admin username.
Once enabled, you have access to the following features:
- User login/logout (located in Settings Tab)
- User changing password (located in Settings Tab)
- Create / List / Edit / Delete user (located in Resources > Users Tab)
+8
View File
@@ -0,0 +1,8 @@
## Chat
The kotaemon focuses on question and answering over a corpus of data. Below
is the gentle introduction about the chat functionality.
- Users can upload corpus of files.
- Users can converse to the chatbot to ask questions about the corpus of files.
- Users can view the reference in the files.
+366
View File
@@ -0,0 +1,366 @@
## User group / tenant management
### Create new user group
(6 man-days)
**Description**: each client has a dedicated user group. Each user group has an
admin user who can do administrative tasks (e.g. creating user account in that
user group...). The workflow for creating new user group is as follow:
1. Cinnamon accesses the user group management UI.
2. On "Create user group" panel, we supply:
a. Client name: e.g. Apple.
b. Sub-domain name: e.g. apple.
c. Admin email, username & password.
3. The system will:
a. An Aurora Platform deployment with the specified sub-domain.
b. Send an email to the admin, with the username & password.
**Expectation**:
- The admin can go to the deployed Aurora Platform.
- The admin can login with the specified username & password.
**Condition**:
- When sub-domain name already exists, raise error.
- If error sending email to the client, raise the error, and delete the
newly-created user-group.
- Password rule:
- Have at least 8 characters.
- Must contain uppercase, lowercase, number and symbols.
---
### Delete user group
(2 man-days)
**Description**: in the tenant management page, we can delete the selected user
group. The user flow is as follow:
1. Cinnamon accesses the user group management UI,
2. View list of user groups.
3. Next to target user group, click delete.
4. Confirm whether to delete.
5. If Yes, delete the user group. If No, cancel the operation.
**Expectation**: when a user group is deleted, we expect to delete everything
related to the user groups: domain, files, databases, caches, deployments.
## User management
---
### Create user account (for admin user)
(1 man-day)
**Description**: the admin user in the client's account can create user account
for that user group. To create the new user, the client admin do:
1. Navigate to "Admin" > "Users"
2. In the "Create user" panel, supply:
- Username
- Password
- Confirm password
3. Click "Create"
**Expectation**:
- The user can create the account.
- The username:
- Is case-insensitive (e.g. Moon and moon will be the same)
- Can only contains these characters: a-z A-Z 0-9 \_ + - .
- Has maximum length of 32 characters
- The password is subjected to the following rule:
- 8-character minimum length
- Contains at least 1 number
- Contains at least 1 lowercase letter
- Contains at least 1 uppercase letter
- Contains at least 1 special character from the following set, or a
non-leading, non-trailing space character: `^ $ * . [ ] { } ( ) ? - " ! @ # % & / \ , > < ' : ; | _ ~ ` + =
---
### Delete user account (for admin user)
**Description**: the admin user in the client's account can delete user account.
Once an user account is deleted, he/she cannot login to Aurora Platform.
1. The admin user navigates to "Admin" > "Users".
2. In the user list panel, next to the username, the admin click on the "Delete"
button. The Confirmation dialog appears.
3. If "Delete", the user account is deleted. If "Cancel", do nothing. The
Confirmation dialog disappears.
**Expectation**:
- Once the user is deleted, the following information relating to the user will
be deleted:
- His/her personal setting.
- His/her conversations.
- The following information relating to the user will still be retained:
- His/her uploaded files.
---
### Edit user account (for admin user)
**Description**: the admin user can change any information about the user
account, including password. To change user information:
1. The admin user navigates to "Admin" > "Users".
2. In the user list panel, next to the username, the admin click on the "Edit"
button.
3. The user list disappears, the user detail appears, with the following
information show up:
- Username: (prefilled the username)
- Password: (blank)
- Confirm password: (blank)
4. The admin can edit any of the information, and click "Save" or "Cancel".
- If "Save": the information will be updated to the database, or show
error per Expectation below.
- If "Cancel": skip.
5. If Save success or Cancel, transfer back to the user list UI, where the user
information is updated accordingly.
**Expectation**:
- If the "Password" & "Confirm password" are different from each other, show
error: "Password mismatch".
- If both "Password" & \*"Confirm password" are blank, don't change the user
password.
- If changing password, the password rule is subjected to the same rule when
creating user.
- It's possible to change username. If changing username, the target user has to
use the new username.
---
### Sign-in
(3 man-days)
**Description**: the users can sign-in to Aurora Platform as follow:
1. User navigates to the URL.
2. If the user is not logged in, the UI just shows the login screen.
3. User types username & password.
4. If correct, the user will proceed to normal working UI.
5. If incorrect, the login screen shows text error.
---
### Sign-out
(1 man-day)
**Description**: the user can sign-out of Aurora Platform as follow:
1. User navigates to the Settings > User page.
2. User click on logout.
3. The user is signed out to the UI login screen.
**Expectation**: the user is completely signed out. Next time he/she uses the
Aurora Platform, he/she has to login again.
---
### Change password
**Description**: the user can change their password as follow:
1. User navigates to the Settings > User page.
2. In the change password section, the user provides these info and click
Change:
- Current password
- New password
- Confirm new password
3. If changing successfully, then the password is changed. Otherwise, show the
error on the UI.
**Expectation**:
- If changing password succeeds, next time they logout/login to the system, they
can use the new password.
- Password rule (Same as normal password rule when creating user)
- Errors:
- Password does not match.
- Violated password rules.
---
## Chat
### Chat to the bot
**Description**: the Aurora Platform focuses on question and answering over the
uploaded data. Each chat has the following components:
- Chat message: show the exchange between bots and humans.
- Text input + send button: for the user to input the message.
- Data source panel: for selecting the files that will scope the context for the
bot.
- Information panel: showing evidence as the bot answers user's questions.
The chat workflow looks as follow:
1. [Optional] User select files that they want to scope the context for the bot.
If the user doesn't select any files, then all files on Aurora Platform will
be the context for the bot.
- The user can type multi-line messages, using "Shift + Enter" for
line-break.
2. User sends the message (either clicking the Send button or hitting the Enter
key).
3. The bot in the chat conversation will return "Thinking..." while it
processes.
4. The information panel on the right begin to show data related to the user
message.
5. The bot begins to generate answer. The "Thinking..." placeholder disappears..
**Expecatation**:
- Messages:
- User can send multi-line messages, using "Shift + Enter" for line-break.
- User can thumbs up, thumbs down the AI response. This information is
recorded in the database.
- User can click on a copy button on the chat message to copy the content to
clipboard.
- Information panel:
- The information panel shows the latest evidence.
- The user can click on the message, and the reference for that message will
show up on the "Reference panel" (feature in-planning).
- The user can click on the title to show/hide the content.
- The whole information panel can be collapsed.
- Chatbot quality:
- The user can converse with the bot. The bot answer the user's requests in a
natural manner.
- The bot message should be streamed to the UI. The bot don't wait to gather
alll the text response, then dump all of them at once.
### Conversation - switch
**Description**: users can jump around between different conversations. They can
see the list of all conversations, can select an old converation, and continue
the chat under the context of the old conversation. The switching workflow is
like this:
1. Users click on the conversation dropdown. It will show a list of
conversations.
2. Within that dropdown, the user selects one conversation.
3. The chat messages, information panel, and selected data will show the content
in that old chat.
4. The user can continue chatting as normal under the context of this old chat.
**Expectation**:
- In the conversation drop down list, the conversations are ordered in created
date order.
- When there is no conversation, the conversation list is empty.
- When there is no conversation, the user can still converse with the chat bot.
When doing so, it automatically create new conversation.
### Conversation - create
**Description**: the user can explicitly start a new conversation with the
chatbot:
1. User click on the "New" button.
2. The new conversation is automatically created.
**Expectation**:
- The default conversation name is the current datetime.
- It become selected.
- It is added to the conversation list.
### Conversation - rename
**Description**: user can rename the chatbot by typing the name, and click on
the Rename button next to it.
- If rename succeeds: the name shown in the 1st dropdown will change accordingly
- If rename doesn't succeed: show error message in red color below the rename section
**Condition**:
- Name constraint:
- Min characters: 1
- Max characters: 40
- Could not having the same name with an existing conversation of the same
user.
### Conversation - delete
**Description**: user can delete the existing conversation as follow:
1. Click on Delete button.
2. The UI show confirmation with 2 buttons:
- Delete
- Cancel.
3. If Delete, delete the conversation, switch to the next oldest conversation,
close the confirmation panel.
4. If cancel, just close the confirmation panel.
## File management
The file management allows users to upload, list and delete files that they
upload to the Aurora Platform
### Upload file
**Description**: the user can upload files to the Aurora Platform. The uploaded
files will be served as context for our chatbot to refer to when it converses
with the user. To upload file, the user:
1. Navigate to the File tab.
2. Within the File tab, there is an Upload section.
3. User can add files to the Upload section through drag & drop, and or by click
on the file browser.
4. User can select some options relating to uploading and indexing. Depending on
the project, these options can be different. Nevertheless, they will discuss
below.
5. User click on "Upload and Index" button.
6. The app show notifications when indexing starts and finishes, and when errors
happen on the top right corner.
**Options**:
- Force re-index file. When user tries to upload files that already exists on
the system:
- If this option is True: will re-index those files.
- If this option is False: will skip indexing those files.
**Condition**:
- Max number of files: 100 files.
- Max number of pages per file: 500 pages
- Max file size: 10 MB
### List all files
**Description**: the user can know which files are on the system by:
1. Navigate to the File tab.
2. By default, it will show all the uploaded files, each with the following
information: file name, file size, number of pages, uploaded date
3. The UI also shows total number of pages, and total number of sizes in MB.
### Delete file
**Description**: users can delete files from this UI to free up the space, or to
remove outdated information. To remove the files:
1. User navigate to the File tab.
2. In the list of file, next to each file, there is a Delete button.
3. The user clicks on the Delete button. Confirmation dialog appear.
4. If Delete, delete the file. If Cancel, close the confirmation dialog.
**Expectation**: once the file is deleted:
- The database entry of that file is deleted.
- The file is removed from "Chat - Data source".
- The total number of pages and MB sizes are reduced accordingly.
- The reference to the file in the information panel is still retained.
+115
View File
@@ -0,0 +1,115 @@
The file index stores files in a local folder and index them for retrieval.
This file index provides the following infrastructure to support the indexing:
- SQL table Source: store the list of files that are indexed by the system
- Vector store: contain the embedding of segments of the files
- Document store: contain the text of segments of the files. Each text stored
in this document store is associated with a vector in the vector store.
- SQL table Index: store the relationship between (1) the source and the
docstore, and (2) the source and the vector store.
The indexing and retrieval pipelines are encouraged to use the above software
infrastructure.
## Indexing pipeline
The ktem has default indexing pipeline: `ktem.index.file.pipelines.IndexDocumentPipeline`.
This default pipeline works as follow:
- **Input**: list of file paths
- **Output**: list of nodes that are indexed into database
- **Process**:
- Read files into texts. Different file types has different ways to read texts.
- Split text files into smaller segments
- Run each segments into embeddings.
- Store the embeddings into vector store. Store the texts of each segment
into docstore. Store the list of files in Source. Store the linking
between Sources and docstore + vectorstore in Index table.
You can customize this default pipeline if your indexing process is close to the
default pipeline. You can create your own indexing pipeline if there are too
much different logic.
### Customize the default pipeline
The default pipeline provides the contact points in `flowsettings.py`.
1. `FILE_INDEX_PIPELINE_FILE_EXTRACTORS`. Supply overriding file extractor,
based on file extension. Example: `{".pdf": "path.to.PDFReader", ".xlsx": "path.to.ExcelReader"}`
2. `FILE_INDEX_PIPELINE_SPLITTER_CHUNK_SIZE`. The expected number of characters
of each text segment. Example: 1024.
3. `FILE_INDEX_PIPELINE_SPLITTER_CHUNK_OVERLAP`. The expected number of
characters that consecutive text segments should overlap with each other.
Example: 256.
### Create your own indexing pipeline
Your indexing pipeline will subclass `BaseFileIndexIndexing`.
You should define the following methods:
- `run(self, file_paths)`: run the indexing given the pipeline
- `get_pipeline(cls, user_settings, index_settings)`: return the
fully-initialized pipeline, ready to be used by ktem.
- `user_settings`: is a dictionary contains user settings (e.g. `{"pdf_mode": True, "num_retrieval": 5}`). You can declare these settings in the `get_user_settings` classmethod. ktem will collect these settings into the app Settings page, and will supply these user settings to your `get_pipeline` method.
- `index_settings`: is a dictionary. Currently it's empty for File Index.
- `get_user_settings`: to declare user settings, return a dictionary.
By subclassing `BaseFileIndexIndexing`, You will have access to the following resources:
- `self._Source`: the source table
- `self._Index`: the index table
- `self._VS`: the vector store
- `self._DS`: the docstore
Once you have prepared your pipeline, register it in `flowsettings.py`: `FILE_INDEX_PIPELINE = "<python.path.to.your.pipeline>"`.
## Retrieval pipeline
The ktem has default retrieval pipeline:
`ktem.index.file.pipelines.DocumentRetrievalPipeline`. This pipeline works as
follow:
- Input: user text query & optionally a list of source file ids
- Output: the output segments that match the user text query
- Process:
- If a list of source file ids is given, get the list of vector ids that
associate with those file ids.
- Embed the user text query.
- Query the vector store. Provide a list of vector ids to limit query scope
if the user restrict.
- Return the matched text segments
### Create your own retrieval pipeline
Your retrieval pipeline will subclass `BaseFileIndexRetriever`. The retriever
has the same database, vectorstore and docstore accesses like the indexing
pipeline.
You should define the following methods:
- `run(self, query, file_ids)`: retrieve relevant documents relating to the
query. If `file_ids` is given, you should restrict your search within these
`file_ids`.
- `get_pipeline(cls, user_settings, index_settings, selected)`: return the
fully-initialized pipeline, ready to be used by ktem.
- `user_settings`: is a dictionary contains user settings (e.g. `{"pdf_mode": True, "num_retrieval": 5}`). You can declare these settings in the `get_user_settings` classmethod. ktem will collect these settings into the app Settings page, and will supply these user settings to your `get_pipeline` method.
- `index_settings`: is a dictionary. Currently it's empty for File Index.
- `selected`: a list of file ids selected by user. If user doesn't select
anything, this variable will be None.
- `get_user_settings`: to declare user settings, return a dictionary.
Once you build the retrieval pipeline class, you can register it in
`flowsettings.py`: `FILE_INDEXING_RETRIEVER_PIPELIENS = ["path.to.retrieval.pipelie"]`. Because there can be
multiple parallel pipelines within an index, this variable takes a list of
string rather than a string.
## Software infrastructure
| Infra | Access | Schema | Ref |
| ---------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| SQL table Source | self.\_Source | - id (int): id of the source (auto)<br>- name (str): the name of the file<br>- path (str): the path of the file<br>- size (int): the file size in bytes<br>- note (dict): allow extra optional information about the file<br>- date_created (datetime): the time the file is created (auto) | This is SQLALchemy ORM class. Can consult |
| SQL table Index | self.\_Index | - id (int): id of the index entry (auto)<br>- source_id (int): the id of a file in the Source table<br>- target_id: the id of the segment in docstore or vector store<br>- relation_type (str): if the link is "document" or "vector" | This is SQLAlchemy ORM class |
| Vector store | self.\_VS | - self.\_VS.add: add the list of embeddings to the vector store (optionally associate metadata and ids)<br>- self.\_VS.delete: delete vector entries based on ids<br>- self.\_VS.query: get embeddings based on embeddings. | kotaemon > storages > vectorstores > BaseVectorStore |
| Doc store | self.\_DS | - self.\_DS.add: add the segments to document stores<br>- self.\_DS.get: get the segments based on id<br>- self.\_DS.get_all: get all segments<br>- self.\_DS.delete: delete segments based on id | kotaemon > storages > docstores > base > BaseDocumentStore |
+8
View File
@@ -0,0 +1,8 @@
# Overview
There are 3 kinds of settings in `ktem`, geared towards different stakeholders
for different use cases:
- Developer settings. These settings are meant for very basic app customization, such as database URL, cloud config, logging config, which features to enable... You will be interested in the developer settings if you deploy `ktem` to your customers, or if you build extension for `ktem` for developers. These settings are declared inside `flowsettings.py`.
- Admin settings. These settings show up in the Admin page, and are meant to allow admin-level user to customize low level features, such as which credentials to connect to data sources, which keys to use for LLM...
- [User settings](/pages/app/settings/user-settings/). These settings are meant for run-time users to tweak ktem to their personal needs, such as which output languages the chatbot should generate, which reasoning type to use...
+52
View File
@@ -0,0 +1,52 @@
# User settings
`ktem` allows developers to extend the index and the reasoning pipeline. In
many cases, these components can have settings that should be modified by
users at run-time, (e.g. `topk`, `chunksize`...). These are the user settings.
`ktem` allows developers to declare such user settings in their code. Once
declared, `ktem` will render them in a Settings page.
There are 2 places that `ktem` looks for declared user settings. You can
refer to the respective pages.
- In the index.
- In the reasoning pipeline.
## Syntax of a settings
A collection of settings is a dictionary of type `dict[str, dict]`, where the
key is a setting id, and the value is the description of the setting.
```python
settings = {
"topk": {
"name": "Top-k chunks",
"value": 10,
"component": "number",
},
"lang": {
"name": "Languages",
"value": "en",
"component": "dropdown",
"choices": [("en", "English"), ("cn", "Chinese")],
}
}
```
Each setting description must have:
- name: the human-understandable name of the settings.
- value: the default value of the settings.
- component: the UI component to render such setting on the UI. Available:
- "text": single-value
- "number": single-value
- "checkbox": single-value
- "dropdown": choices
- "radio": choices
- "checkboxgroup": choices
- choices: the list of choices, if the component type allows.
## Settings page structure
+65
View File
@@ -0,0 +1,65 @@
# import shutil
from pathlib import Path
from typing import Any, Iterable
import mkdocs_gen_files
# get the root source code directory
doc_dir_name = "docs"
doc_dir = Path(__file__)
while doc_dir.name != doc_dir_name and doc_dir != doc_dir.parent:
doc_dir = doc_dir.parent
if doc_dir == doc_dir.parent:
raise ValueError(f"root_name ({doc_dir_name}) not in path ({str(Path(__file__))}).")
def generate_docs_for_examples_readme(
examples_dir: Path, target_doc_folder: str, ignored_modules: Iterable[Any] = []
):
if not examples_dir.is_dir():
raise ModuleNotFoundError(str(examples_dir))
nav = mkdocs_gen_files.Nav()
for path in sorted(examples_dir.rglob("*README.md")):
# ignore modules with name starts with underscore (i.e. __init__)
if path.name.startswith("_") or path.name.startswith("test"):
continue
module_path = path.parent.relative_to(examples_dir).with_suffix("")
doc_path = path.parent.relative_to(examples_dir).with_suffix(".md")
full_doc_path = Path(target_doc_folder, doc_path)
parts = list(module_path.parts)
identifier = ".".join(parts)
if "tests" in parts:
continue
ignore = False
for each_module in ignored_modules:
if identifier.startswith(each_module):
ignore = True
break
if ignore:
continue
nav_titles = [name.replace("_", " ").title() for name in parts]
nav[nav_titles] = doc_path.as_posix()
with mkdocs_gen_files.open(full_doc_path, "w") as f:
f.write(f'--8<-- "{path.relative_to(examples_dir.parent)}"')
mkdocs_gen_files.set_edit_path(
full_doc_path, Path("..") / path.relative_to(examples_dir.parent)
)
with mkdocs_gen_files.open(f"{target_doc_folder}/NAV.md", "w") as nav_file:
nav_file.writelines(nav.build_literate_nav())
generate_docs_for_examples_readme(
examples_dir=doc_dir.parent / "examples",
target_doc_folder="examples",
)
+79
View File
@@ -0,0 +1,79 @@
# import shutil
from pathlib import Path
from typing import Any, Iterable
import mkdocs_gen_files
# get the root source code directory
doc_dir_name = "docs"
doc_dir = Path(__file__)
while doc_dir.name != doc_dir_name and doc_dir != doc_dir.parent:
doc_dir = doc_dir.parent
if doc_dir == doc_dir.parent:
raise ValueError(f"root_name ({doc_dir_name}) not in path ({str(Path(__file__))}).")
nav_title_map = {"cli": "CLI", "llms": "LLMs"}
def generate_docs_for_src_code(
code_dir: Path, target_doc_folder: str, ignored_modules: Iterable[Any] = []
):
if not code_dir.is_dir():
raise ModuleNotFoundError(str(code_dir))
nav = mkdocs_gen_files.Nav()
for path in sorted(code_dir.rglob("*.py")):
# ignore modules with name starts with underscore (i.e. __init__)
# if path.name.startswith("_") or path.name.startswith("test"):
# continue
module_path = path.relative_to(code_dir).with_suffix("")
doc_path = path.relative_to(code_dir).with_suffix(".md")
full_doc_path = Path(target_doc_folder, doc_path)
parts = list(module_path.parts)
if parts[-1] == "__init__":
doc_path = doc_path.with_name("index.md")
full_doc_path = full_doc_path.with_name("index.md")
parts.pop()
if not parts:
continue
if "tests" in parts:
continue
identifier = ".".join(parts)
ignore = False
for each_module in ignored_modules:
if identifier.startswith(each_module):
ignore = True
break
if ignore:
continue
nav_titles = [
nav_title_map.get(name, name.replace("_", " ").title()) for name in parts
]
nav[nav_titles] = doc_path.as_posix()
with mkdocs_gen_files.open(full_doc_path, "w") as f:
f.write(f"::: {identifier}")
# this method works in docs folder
mkdocs_gen_files.set_edit_path(
full_doc_path, Path("..") / path.relative_to(code_dir.parent)
)
with mkdocs_gen_files.open(f"{target_doc_folder}/Summary.md", "w") as nav_file:
nav_file.writelines(nav.build_literate_nav())
generate_docs_for_src_code(
code_dir=doc_dir.parent / "libs" / "kotaemon" / "kotaemon",
target_doc_folder="reference",
ignored_modules={"contribs"},
)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
!function(){"use strict";var e;e=function(e){"true"===localStorage.getItem("data-md-prefers-color-scheme")&&document.querySelector("body").setAttribute("data-md-color-scheme",e.matches?"dracula":"default")},new MutationObserver((function(t){t.forEach((function(t){if("childList"===t.type&&t.addedNodes.length)for(var a=0;a<t.addedNodes.length;a++){var r=t.addedNodes[a];if(1===r.nodeType&&"body"===r.tagName.toLowerCase()){d=r,o=void 0,c=void 0,l=void 0,o="not all"!==window.matchMedia("(prefers-color-scheme)").media,c=localStorage.getItem("data-md-color-scheme"),l=localStorage.getItem("data-md-prefers-color-scheme"),c||(c="dracula"),l||(l="false"),"true"===l&&o?c=window.matchMedia("(prefers-color-scheme: dark)").matches?"dracula":"default":l="false",d.setAttribute("data-md-prefers-color-scheme",l),d.setAttribute("data-md-color-scheme",c),o&&window.matchMedia("(prefers-color-scheme: dark)").addListener(e);break}}var d,o,c,l}))})).observe(document.querySelector("html"),{childList:!0}),window.toggleScheme=function(){var e=document.querySelector("body"),t="not all"!==window.matchMedia("(prefers-color-scheme)").media,a=e.getAttribute("data-md-color-scheme"),r=e.getAttribute("data-md-prefers-color-scheme");t&&"default"===a&&"true"!==r?(r="true",a=window.matchMedia("(prefers-color-scheme: dark)").matches?"dracula":"default"):t&&"true"===r?(r="false",a="dracula"):"dracula"===a?(r="false",a="default"):(r="false",a="dracula"),localStorage.setItem("data-md-prefers-color-scheme",r),e.setAttribute("data-md-prefers-color-scheme",r),e.setAttribute("data-md-color-scheme",a)}}();
//# sourceMappingURL=material-extra-theme-TVq-kNRT.js.map
@@ -0,0 +1 @@
{"version":3,"file":"material-extra-theme-TVq-kNRT.js","sources":["material-extra-theme.js"],"sourcesContent":["(() => {\n\n const preferToggle = e => {\n if (localStorage.getItem(\"data-md-prefers-color-scheme\") === \"true\") {\n document.querySelector(\"body\").setAttribute(\"data-md-color-scheme\", (e.matches) ? \"dracula\" : \"default\")\n }\n }\n\n const setupTheme = body => {\n const preferSupported = window.matchMedia(\"(prefers-color-scheme)\").media !== \"not all\"\n let scheme = localStorage.getItem(\"data-md-color-scheme\")\n let prefers = localStorage.getItem(\"data-md-prefers-color-scheme\")\n\n if (!scheme) {\n scheme = \"dracula\"\n }\n if (!prefers) {\n prefers = \"false\"\n }\n\n if (prefers === \"true\" && preferSupported) {\n scheme = (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) ? \"dracula\" : \"default\"\n } else {\n prefers = \"false\"\n }\n\n body.setAttribute(\"data-md-prefers-color-scheme\", prefers)\n body.setAttribute(\"data-md-color-scheme\", scheme)\n\n if (preferSupported) {\n const matchListener = window.matchMedia(\"(prefers-color-scheme: dark)\")\n matchListener.addListener(preferToggle)\n }\n }\n\n const observer = new MutationObserver(mutations => {\n mutations.forEach(mutation => {\n if (mutation.type === \"childList\") {\n if (mutation.addedNodes.length) {\n for (let i = 0; i < mutation.addedNodes.length; i++) {\n const el = mutation.addedNodes[i]\n\n if (el.nodeType === 1 && el.tagName.toLowerCase() === \"body\") {\n setupTheme(el)\n break\n }\n }\n }\n }\n })\n })\n\n observer.observe(document.querySelector(\"html\"), {childList: true})\n})()\n\nwindow.toggleScheme = () => {\n const body = document.querySelector(\"body\")\n const preferSupported = window.matchMedia(\"(prefers-color-scheme)\").media !== \"not all\"\n let scheme = body.getAttribute(\"data-md-color-scheme\")\n let prefer = body.getAttribute(\"data-md-prefers-color-scheme\")\n\n if (preferSupported && scheme === \"default\" && prefer !== \"true\") {\n prefer = \"true\"\n scheme = (window.matchMedia(\"(prefers-color-scheme: dark)\").matches) ? \"dracula\" : \"default\"\n } else if (preferSupported && prefer === \"true\") {\n prefer = \"false\"\n scheme = \"dracula\"\n } else if (scheme === \"dracula\") {\n prefer = \"false\"\n scheme = \"default\"\n } else {\n prefer = \"false\"\n scheme = \"dracula\"\n }\n localStorage.setItem(\"data-md-prefers-color-scheme\", prefer)\n body.setAttribute(\"data-md-prefers-color-scheme\", prefer)\n body.setAttribute(\"data-md-color-scheme\", scheme)\n}\n"],"names":["preferToggle","e","localStorage","getItem","document","querySelector","setAttribute","matches","MutationObserver","mutations","forEach","mutation","type","addedNodes","length","i","el","nodeType","tagName","toLowerCase","body","preferSupported","scheme","prefers","window","matchMedia","media","addListener","observe","childList","toggleScheme","getAttribute","prefer","setItem"],"mappings":"yBAAA,IAEQA,IAAe,SAAAC,GAC0C,SAAzDC,aAAaC,QAAQ,iCACvBC,SAASC,cAAc,QAAQC,aAAa,uBAAyBL,EAAEM,QAAW,UAAY,YA+BjF,IAAIC,kBAAiB,SAAAC,GACpCA,EAAUC,SAAQ,SAAAC,GAChB,GAAsB,cAAlBA,EAASC,MACPD,EAASE,WAAWC,OACtB,IAAK,IAAIC,EAAI,EAAGA,EAAIJ,EAASE,WAAWC,OAAQC,IAAK,CACnD,IAAMC,EAAKL,EAASE,WAAWE,GAE/B,GAAoB,IAAhBC,EAAGC,UAA+C,SAA7BD,EAAGE,QAAQC,cAA0B,CAlCrDC,EAmCIJ,EAlCfK,SACFC,SACAC,SAFEF,EAAwE,YAAtDG,OAAOC,WAAW,0BAA0BC,MAChEJ,EAASpB,aAAaC,QAAQ,wBAC9BoB,EAAUrB,aAAaC,QAAQ,gCAE9BmB,IACHA,EAAS,WAENC,IACHA,EAAU,SAGI,SAAZA,GAAsBF,EACxBC,EAAUE,OAAOC,WAAW,gCAAgClB,QAAW,UAAY,UAEnFgB,EAAU,QAGZH,EAAKd,aAAa,+BAAgCiB,GAClDH,EAAKd,aAAa,uBAAwBgB,GAEtCD,GACoBG,OAAOC,WAAW,gCAC1BE,YAAY3B,GAalB,KACF,CACF,CAtCW,IAAAoB,EACXC,EACFC,EACAC,CAsCJ,GACF,IAESK,QAAQxB,SAASC,cAAc,QAAS,CAACwB,WAAW,IAG/DL,OAAOM,aAAe,WACpB,IAAMV,EAAOhB,SAASC,cAAc,QAC9BgB,EAAwE,YAAtDG,OAAOC,WAAW,0BAA0BC,MAChEJ,EAASF,EAAKW,aAAa,wBAC3BC,EAASZ,EAAKW,aAAa,gCAE3BV,GAA8B,YAAXC,GAAmC,SAAXU,GAC7CA,EAAS,OACTV,EAAUE,OAAOC,WAAW,gCAAgClB,QAAW,UAAY,WAC1Ec,GAA8B,SAAXW,GAC5BA,EAAS,QACTV,EAAS,WACW,YAAXA,GACTU,EAAS,QACTV,EAAS,YAETU,EAAS,QACTV,EAAS,WAEXpB,aAAa+B,QAAQ,+BAAgCD,GACrDZ,EAAKd,aAAa,+BAAgC0B,GAClDZ,EAAKd,aAAa,uBAAwBgB,EAC5C"}
+6
View File
@@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block libs %}
{{ super() }}
{% include "partials/libs.html" ignore missing %}
{% endblock %}
+49
View File
@@ -0,0 +1,49 @@
{% import "partials/language.html" as lang with context %}
<footer class="md-footer">
{% if page.previous_page or page.next_page %}
<nav
class="md-footer__inner md-grid"
aria-label="{{ lang.t('footer.title') }}"
>
{% if page.previous_page %}
<a
href="{{ page.previous_page.url | url }}"
class="md-footer__link md-footer__link--prev"
rel="prev"
>
<div class="md-footer__button md-icon">
{% include ".icons/material/arrow-left.svg" %}
</div>
<div class="md-footer__title">
<div class="md-ellipsis">
<span class="md-footer__direction">
{{ lang.t("footer.previous") }}
</span>
{{ page.previous_page.title }}
</div>
</div>
</a>
{% endif %}
{% if page.next_page %}
<a
href="{{ page.next_page.url | url }}"
class="md-footer__link md-footer__link--next"
rel="next"
>
<div class="md-footer__title">
<div class="md-ellipsis">
<span class="md-footer__direction">
{{ lang.t("footer.next") }}
</span>
{{ page.next_page.title }}
</div>
</div>
<div class="md-footer__button md-icon">
{% include ".icons/material/arrow-right.svg" %}
</div>
</a>
{% endif %}
</nav>
{% endif %}
</footer>
+88
View File
@@ -0,0 +1,88 @@
{% set site_url = config.site_url | d(nav.homepage.url, true) | url %}
{% if not config.use_directory_urls and site_url[0] == site_url[-1] == "." %}
{% set site_url = site_url ~ "/index.html" %}
{% endif %}
<header class="md-header" data-md-component="header">
<nav
class="md-header__inner md-grid"
aria-label="{{ lang.t('header.title') }}"
>
<a
href="{{ site_url }}"
title="{{ config.site_name | e }}"
class="md-header__button md-logo"
aria-label="{{ config.site_name }}"
>
{% include "partials/logo.html" %}
</a>
<label class="md-header__button md-icon" for="__drawer">
{% include ".icons/material/menu" ~ ".svg" %}
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
{{ config.site_name }}
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
{% if page and page.meta and page.meta.title %}
{{ page.meta.title }}
{% else %}
{{ page.title }}
{% endif %}
</span>
</div>
</div>
</div>
<div class="md-header__options">
<div class="md-header-nav__scheme md-header-nav__button md-source__icon md-icon">
<a
href="javascript:toggleScheme();"
title="Light mode"
class="light-mode"
>
{% set icon = "material/weather-sunny" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</a>
<a
href="javascript:toggleScheme();"
title="Dark mode"
class="dark-mode"
>
{% set icon = "material/weather-night" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</a>
<a
href="javascript:toggleScheme();"
title="System preference"
class="system-mode"
>
{% set icon = "material/theme-light-dark" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</a>
<!-- <a
href="javascript:toggleScheme();"
title="Unknown scheme"
class="unknown-mode"
>
{% set icon = "material/help-circle" %}
{% include ".icons/" ~ icon ~ ".svg" %}
</a> -->
</div>
</div>
{% if "material/search" in config.plugins %}
<label class="md-header__button md-icon" for="__search">
{% include ".icons/material/magnify.svg" %}
</label>
{% include "partials/search.html" %}
{% endif %}
{% if config.repo_url %}
<div class="md-header__source">
{% include "partials/source.html" %}
</div>
{% endif %}
</nav>
</header>
+2
View File
@@ -0,0 +1,2 @@
<script src="{{ 'assets/pymdownx-extras/material-extra-theme-TVq-kNRT.js' | url }}" type="text/javascript"></script>
<script src="{{ 'assets/pymdownx-extras/material-extra-3rdparty-E-i8w1WA.js' | url }}" type="text/javascript"></script>
+157
View File
@@ -0,0 +1,157 @@
## 1. Add your AI models
![resources tab](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/resources-tab.png)
- The tool uses Large Language Model (LLMs) to perform various tasks in a QA pipeline.
So, you need to provide the application with access to the LLMs you want
to use.
- You only need to provide at least one. However, it is recommended that you include all the LLMs
that you have access to, you will be able to switch between them while using the
application.
To add a model:
1. Navigate to the `Resources` tab.
2. Select the `LLMs` sub-tab.
3. Select the `Add` sub-tab.
4. Config the model to add:
- Give it a name.
- Pick a vendor/provider (e.g. `ChatOpenAI`).
- Provide the specifications.
- (Optional) Set the model as default.
5. Click `Add` to add the model.
6. Select `Embedding Models` sub-tab and repeat the step 3 to 5 to add an embedding model.
<details markdown>
<summary>(Optional) Configure model via the .env file</summary>
Alternatively, you can configure the models via the `.env` file with the information needed to connect to the LLMs. This file is located in
the folder of the application. If you don't see it, you can create one.
Currently, the following providers are supported:
### OpenAI
In the `.env` file, set the `OPENAI_API_KEY` variable with your OpenAI API key in order
to enable access to OpenAI's models. There are other variables that can be modified,
please feel free to edit them to fit your case. Otherwise, the default parameter should
work for most people.
```shell
OPENAI_API_BASE=https://api.openai.com/v1
OPENAI_API_KEY=<your OpenAI API key here>
OPENAI_CHAT_MODEL=gpt-3.5-turbo
OPENAI_EMBEDDINGS_MODEL=text-embedding-ada-002
```
### Azure OpenAI
For OpenAI models via Azure platform, you need to provide your Azure endpoint and API
key. You might also need to provide your developments' name for the chat model and the
embedding model depending on how you set up Azure development.
```shell
AZURE_OPENAI_ENDPOINT=
AZURE_OPENAI_API_KEY=
OPENAI_API_VERSION=2024-02-15-preview # could be different for you
AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-35-turbo # change to your deployment name
AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=text-embedding-ada-002 # change to your deployment name
```
### Local models
Pros:
- Privacy. Your documents will be stored and process locally.
- Choices. There are a wide range of LLMs in terms of size, domain, language to choose
from.
- Cost. It's free.
Cons:
- Quality. Local models are much smaller and thus have lower generative quality than
paid APIs.
- Speed. Local models are deployed using your machine so the processing speed is
limited by your hardware.
#### Find and download a LLM
You can search and download a LLM to be ran locally from the [Hugging Face
Hub](https://huggingface.co/models). Currently, these model formats are supported:
- GGUF
You should choose a model whose size is less than your device's memory and should leave
about 2 GB. For example, if you have 16 GB of RAM in total, of which 12 GB is available,
then you should choose a model that take up at most 10 GB of RAM. Bigger models tend to
give better generation but also take more processing time.
Here are some recommendations and their size in memory:
- [Qwen1.5-1.8B-Chat-GGUF](https://huggingface.co/Qwen/Qwen1.5-1.8B-Chat-GGUF/resolve/main/qwen1_5-1_8b-chat-q8_0.gguf?download=true):
around 2 GB
#### Enable local models
To add a local model to the model pool, set the `LOCAL_MODEL` variable in the `.env`
file to the path of the model file.
```shell
LOCAL_MODEL=<full path to your model file>
```
Here is how to get the full path of your model file:
- On Windows 11: right click the file and select `Copy as Path`.
</details>
## 2. Upload your documents
![file index tab](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/file-index-tab.png)
In order to do QA on your documents, you need to upload them to the application first.
Navigate to the `File Index` tab and you will see 2 sections:
1. File upload:
- Drag and drop your file to the UI or select it from your file system.
Then click `Upload and Index`.
- The application will take some time to process the file and show a message once it is done.
2. File list:
- This section shows the list of files that have been uploaded to the application and allows users to delete them.
## 3. Chat with your documents
![chat tab](https://raw.githubusercontent.com/Cinnamon/kotaemon/main/docs/images/chat-tab.png)
Now navigate back to the `Chat` tab. The chat tab is divided into 3 regions:
1. Conversation Settings Panel
- Here you can select, create, rename, and delete conversations.
- By default, a new conversation is created automatically if no conversation is selected.
- Below that you have the file index, where you can choose whether to disable, select all files, or select which files to retrieve references from.
- If you choose "Disabled", no files will be considered as context during chat.
- If you choose "Search All", all files will be considered during chat.
- If you choose "Select", a dropdown will appear for you to select the
files to be considered during chat. If no files are selected, then no
files will be considered during chat.
2. Chat Panel
- This is where you can chat with the chatbot.
3. Information Panel
![information panel](https://raw.githubusercontent.com/Cinnamon/kotaemon/develop/docs/images/info-panel-scores.png)
- Supporting information such as the retrieved evidence and reference will be
displayed here.
- Direct citation for the answer produced by the LLM is highlighted.
- The confidence score of the answer and relevant scores of evidences are displayed to quickly assess the quality of the answer and retrieved content.
- Meaning of the score displayed:
- **Answer confidence**: answer confidence level from the LLM model.
- **Relevance score**: overall relevant score between evidence and user question.
- **Vectorstore score**: relevant score from vector embedding similarity calculation (show `full-text search` if retrieved from full-text search DB).
- **LLM relevant score**: relevant score from LLM model (which judge relevancy between question and evidence using specific prompt).
- **Reranking score**: relevant score from Cohere [reranking model](https://cohere.com/rerank).
Generally, the score quality is `LLM relevant score` > `Reranking score` > `Vectorscore`.
By default, overall relevance score is taken directly from LLM relevant score. Evidences are sorted based on their overall relevance score and whether they have citation or not.
+404
View File
@@ -0,0 +1,404 @@
import os
from importlib.metadata import version
from inspect import currentframe, getframeinfo
from pathlib import Path
from decouple import config
from ktem.utils.lang import SUPPORTED_LANGUAGE_MAP
from theflow.settings.default import * # noqa
cur_frame = currentframe()
if cur_frame is None:
raise ValueError("Cannot get the current frame.")
this_file = getframeinfo(cur_frame).filename
this_dir = Path(this_file).parent
# change this if your app use a different name
KH_PACKAGE_NAME = "kotaemon_app"
KH_APP_VERSION = config("KH_APP_VERSION", None)
if not KH_APP_VERSION:
try:
# Caution: This might produce the wrong version
# https://stackoverflow.com/a/59533071
KH_APP_VERSION = version(KH_PACKAGE_NAME)
except Exception:
KH_APP_VERSION = "local"
KH_GRADIO_SHARE = config("KH_GRADIO_SHARE", default=False, cast=bool)
KH_ENABLE_FIRST_SETUP = config("KH_ENABLE_FIRST_SETUP", default=True, cast=bool)
KH_DEMO_MODE = config("KH_DEMO_MODE", default=False, cast=bool)
KH_OLLAMA_URL = config("KH_OLLAMA_URL", default="http://localhost:11434/v1/")
# App can be ran from anywhere and it's not trivial to decide where to store app data.
# So let's use the same directory as the flowsetting.py file.
KH_APP_DATA_DIR = this_dir / "ktem_app_data"
KH_APP_DATA_EXISTS = KH_APP_DATA_DIR.exists()
KH_APP_DATA_DIR.mkdir(parents=True, exist_ok=True)
# User data directory
KH_USER_DATA_DIR = KH_APP_DATA_DIR / "user_data"
KH_USER_DATA_DIR.mkdir(parents=True, exist_ok=True)
# markdown output directory
KH_MARKDOWN_OUTPUT_DIR = KH_APP_DATA_DIR / "markdown_cache_dir"
KH_MARKDOWN_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# chunks output directory
KH_CHUNKS_OUTPUT_DIR = KH_APP_DATA_DIR / "chunks_cache_dir"
KH_CHUNKS_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# zip output directory
KH_ZIP_OUTPUT_DIR = KH_APP_DATA_DIR / "zip_cache_dir"
KH_ZIP_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# zip input directory
KH_ZIP_INPUT_DIR = KH_APP_DATA_DIR / "zip_cache_dir_in"
KH_ZIP_INPUT_DIR.mkdir(parents=True, exist_ok=True)
# HF models can be big, let's store them in the app data directory so that it's easier
# for users to manage their storage.
# ref: https://huggingface.co/docs/huggingface_hub/en/guides/manage-cache
os.environ["HF_HOME"] = str(KH_APP_DATA_DIR / "huggingface")
os.environ["HF_HUB_CACHE"] = str(KH_APP_DATA_DIR / "huggingface")
# doc directory
KH_DOC_DIR = this_dir / "docs"
KH_MODE = "dev"
KH_SSO_ENABLED = config("KH_SSO_ENABLED", default=False, cast=bool)
KH_FEATURE_CHAT_SUGGESTION = config(
"KH_FEATURE_CHAT_SUGGESTION", default=False, cast=bool
)
KH_FEATURE_USER_MANAGEMENT = config(
"KH_FEATURE_USER_MANAGEMENT", default=True, cast=bool
)
KH_USER_CAN_SEE_PUBLIC = None
KH_FEATURE_USER_MANAGEMENT_ADMIN = str(
config("KH_FEATURE_USER_MANAGEMENT_ADMIN", default="admin")
)
KH_FEATURE_USER_MANAGEMENT_PASSWORD = str(
config("KH_FEATURE_USER_MANAGEMENT_PASSWORD", default="admin")
)
KH_ENABLE_ALEMBIC = False
KH_DATABASE = f"sqlite:///{KH_USER_DATA_DIR / 'sql.db'}"
KH_FILESTORAGE_PATH = str(KH_USER_DATA_DIR / "files")
KH_WEB_SEARCH_BACKEND = (
"kotaemon.indices.retrievers.tavily_web_search.WebSearch"
# "kotaemon.indices.retrievers.jina_web_search.WebSearch"
)
KH_DOCSTORE = {
# "__type__": "kotaemon.storages.ElasticsearchDocumentStore",
# "__type__": "kotaemon.storages.SimpleFileDocumentStore",
"__type__": "kotaemon.storages.LanceDBDocumentStore",
"path": str(KH_USER_DATA_DIR / "docstore"),
}
KH_VECTORSTORE = {
# "__type__": "kotaemon.storages.LanceDBVectorStore",
"__type__": "kotaemon.storages.ChromaVectorStore",
# "__type__": "kotaemon.storages.MilvusVectorStore",
# "__type__": "kotaemon.storages.QdrantVectorStore",
"path": str(KH_USER_DATA_DIR / "vectorstore"),
}
KH_LLMS = {}
KH_EMBEDDINGS = {}
KH_RERANKINGS = {}
# populate options from config
if config("AZURE_OPENAI_API_KEY", default="") and config(
"AZURE_OPENAI_ENDPOINT", default=""
):
if config("AZURE_OPENAI_CHAT_DEPLOYMENT", default=""):
KH_LLMS["azure"] = {
"spec": {
"__type__": "kotaemon.llms.AzureChatOpenAI",
"temperature": 0,
"azure_endpoint": config("AZURE_OPENAI_ENDPOINT", default=""),
"api_key": config("AZURE_OPENAI_API_KEY", default=""),
"api_version": config("OPENAI_API_VERSION", default="")
or "2024-02-15-preview",
"azure_deployment": config("AZURE_OPENAI_CHAT_DEPLOYMENT", default=""),
"timeout": 20,
},
"default": False,
}
if config("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT", default=""):
KH_EMBEDDINGS["azure"] = {
"spec": {
"__type__": "kotaemon.embeddings.AzureOpenAIEmbeddings",
"azure_endpoint": config("AZURE_OPENAI_ENDPOINT", default=""),
"api_key": config("AZURE_OPENAI_API_KEY", default=""),
"api_version": config("OPENAI_API_VERSION", default="")
or "2024-02-15-preview",
"azure_deployment": config(
"AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT", default=""
),
"timeout": 10,
},
"default": False,
}
OPENAI_DEFAULT = "<YOUR_OPENAI_KEY>"
OPENAI_API_KEY = config("OPENAI_API_KEY", default=OPENAI_DEFAULT)
GOOGLE_API_KEY = config("GOOGLE_API_KEY", default="your-key")
IS_OPENAI_DEFAULT = len(OPENAI_API_KEY) > 0 and OPENAI_API_KEY != OPENAI_DEFAULT
if OPENAI_API_KEY:
KH_LLMS["openai"] = {
"spec": {
"__type__": "kotaemon.llms.ChatOpenAI",
"temperature": 0,
"base_url": config("OPENAI_API_BASE", default="")
or "https://api.openai.com/v1",
"api_key": OPENAI_API_KEY,
"model": config("OPENAI_CHAT_MODEL", default="gpt-4o-mini"),
"timeout": 20,
},
"default": IS_OPENAI_DEFAULT,
}
KH_EMBEDDINGS["openai"] = {
"spec": {
"__type__": "kotaemon.embeddings.OpenAIEmbeddings",
"base_url": config("OPENAI_API_BASE", default="https://api.openai.com/v1"),
"api_key": OPENAI_API_KEY,
"model": config(
"OPENAI_EMBEDDINGS_MODEL", default="text-embedding-3-large"
),
"timeout": 10,
"context_length": 8191,
},
"default": IS_OPENAI_DEFAULT,
}
VOYAGE_API_KEY = config("VOYAGE_API_KEY", default="")
if VOYAGE_API_KEY:
KH_EMBEDDINGS["voyageai"] = {
"spec": {
"__type__": "kotaemon.embeddings.VoyageAIEmbeddings",
"api_key": VOYAGE_API_KEY,
"model": config("VOYAGE_EMBEDDINGS_MODEL", default="voyage-3-large"),
},
"default": False,
}
KH_RERANKINGS["voyageai"] = {
"spec": {
"__type__": "kotaemon.rerankings.VoyageAIReranking",
"model_name": "rerank-2",
"api_key": VOYAGE_API_KEY,
},
"default": False,
}
if config("LOCAL_MODEL", default=""):
KH_LLMS["ollama"] = {
"spec": {
"__type__": "kotaemon.llms.ChatOpenAI",
"base_url": KH_OLLAMA_URL,
"model": config("LOCAL_MODEL", default="qwen2.5:7b"),
"api_key": "ollama",
},
"default": False,
}
KH_LLMS["ollama-long-context"] = {
"spec": {
"__type__": "kotaemon.llms.LCOllamaChat",
"base_url": KH_OLLAMA_URL.replace("v1/", ""),
"model": config("LOCAL_MODEL", default="qwen2.5:7b"),
"num_ctx": 8192,
},
"default": False,
}
KH_EMBEDDINGS["ollama"] = {
"spec": {
"__type__": "kotaemon.embeddings.OpenAIEmbeddings",
"base_url": KH_OLLAMA_URL,
"model": config("LOCAL_MODEL_EMBEDDINGS", default="nomic-embed-text"),
"api_key": "ollama",
},
"default": False,
}
KH_EMBEDDINGS["fast_embed"] = {
"spec": {
"__type__": "kotaemon.embeddings.FastEmbedEmbeddings",
"model_name": "BAAI/bge-base-en-v1.5",
},
"default": False,
}
# additional LLM configurations
KH_LLMS["claude"] = {
"spec": {
"__type__": "kotaemon.llms.chats.LCAnthropicChat",
"model_name": "claude-3-5-sonnet-20240620",
"api_key": "your-key",
},
"default": False,
}
KH_LLMS["google"] = {
"spec": {
"__type__": "kotaemon.llms.chats.LCGeminiChat",
"model_name": "gemini-1.5-flash",
"api_key": GOOGLE_API_KEY,
},
"default": not IS_OPENAI_DEFAULT,
}
KH_LLMS["groq"] = {
"spec": {
"__type__": "kotaemon.llms.ChatOpenAI",
"base_url": "https://api.groq.com/openai/v1",
"model": "llama-3.1-8b-instant",
"api_key": "your-key",
},
"default": False,
}
KH_LLMS["cohere"] = {
"spec": {
"__type__": "kotaemon.llms.chats.LCCohereChat",
"model_name": "command-r-plus-08-2024",
"api_key": config("COHERE_API_KEY", default="your-key"),
},
"default": False,
}
KH_LLMS["mistral"] = {
"spec": {
"__type__": "kotaemon.llms.ChatOpenAI",
"base_url": "https://api.mistral.ai/v1",
"model": "ministral-8b-latest",
"api_key": config("MISTRAL_API_KEY", default="your-key"),
},
"default": False,
}
# additional embeddings configurations
KH_EMBEDDINGS["cohere"] = {
"spec": {
"__type__": "kotaemon.embeddings.LCCohereEmbeddings",
"model": "embed-multilingual-v3.0",
"cohere_api_key": config("COHERE_API_KEY", default="your-key"),
"user_agent": "default",
},
"default": False,
}
KH_EMBEDDINGS["google"] = {
"spec": {
"__type__": "kotaemon.embeddings.LCGoogleEmbeddings",
"model": "models/text-embedding-004",
"google_api_key": GOOGLE_API_KEY,
},
"default": not IS_OPENAI_DEFAULT,
}
KH_EMBEDDINGS["mistral"] = {
"spec": {
"__type__": "kotaemon.embeddings.LCMistralEmbeddings",
"model": "mistral-embed",
"api_key": config("MISTRAL_API_KEY", default="your-key"),
},
"default": False,
}
# KH_EMBEDDINGS["huggingface"] = {
# "spec": {
# "__type__": "kotaemon.embeddings.LCHuggingFaceEmbeddings",
# "model_name": "sentence-transformers/all-mpnet-base-v2",
# },
# "default": False,
# }
# default reranking models
KH_RERANKINGS["cohere"] = {
"spec": {
"__type__": "kotaemon.rerankings.CohereReranking",
"model_name": "rerank-v4.0-fast",
"cohere_api_key": config("COHERE_API_KEY", default=""),
},
"default": True,
}
KH_REASONINGS = [
"ktem.reasoning.simple.FullQAPipeline",
"ktem.reasoning.simple.FullDecomposeQAPipeline",
"ktem.reasoning.react.ReactAgentPipeline",
"ktem.reasoning.rewoo.RewooAgentPipeline",
]
KH_REASONINGS_USE_MULTIMODAL = config("USE_MULTIMODAL", default=False, cast=bool)
KH_VLM_ENDPOINT = "{0}/openai/deployments/{1}/chat/completions?api-version={2}".format(
config("AZURE_OPENAI_ENDPOINT", default=""),
config("OPENAI_VISION_DEPLOYMENT_NAME", default="gpt-4o"),
config("OPENAI_API_VERSION", default=""),
)
SETTINGS_APP: dict[str, dict] = {}
SETTINGS_REASONING = {
"use": {
"name": "Reasoning options",
"value": None,
"choices": [],
"component": "radio",
},
"lang": {
"name": "Language",
"value": "en",
"choices": [(lang, code) for code, lang in SUPPORTED_LANGUAGE_MAP.items()],
"component": "dropdown",
},
"max_context_length": {
"name": "Max context length (LLM)",
"value": 32000,
"component": "number",
},
}
USE_GLOBAL_GRAPHRAG = config("USE_GLOBAL_GRAPHRAG", default=True, cast=bool)
USE_NANO_GRAPHRAG = config("USE_NANO_GRAPHRAG", default=False, cast=bool)
USE_LIGHTRAG = config("USE_LIGHTRAG", default=True, cast=bool)
USE_MS_GRAPHRAG = config("USE_MS_GRAPHRAG", default=True, cast=bool)
GRAPHRAG_INDEX_TYPES = []
if USE_MS_GRAPHRAG:
GRAPHRAG_INDEX_TYPES.append("ktem.index.file.graph.GraphRAGIndex")
if USE_NANO_GRAPHRAG:
GRAPHRAG_INDEX_TYPES.append("ktem.index.file.graph.NanoGraphRAGIndex")
if USE_LIGHTRAG:
GRAPHRAG_INDEX_TYPES.append("ktem.index.file.graph.LightRAGIndex")
KH_INDEX_TYPES = [
"ktem.index.file.FileIndex",
*GRAPHRAG_INDEX_TYPES,
]
GRAPHRAG_INDICES = [
{
"name": graph_type.split(".")[-1].replace("Index", "")
+ " Collection", # get last name
"config": {
"supported_file_types": (
".png, .jpeg, .jpg, .tiff, .tif, .pdf, .xls, .xlsx, .doc, .docx, "
".pptx, .csv, .html, .mhtml, .txt, .md, .zip"
),
"private": True,
},
"index_type": graph_type,
}
for graph_type in GRAPHRAG_INDEX_TYPES
]
KH_INDICES = [
{
"name": "File Collection",
"config": {
"supported_file_types": (
".png, .jpeg, .jpg, .tiff, .tif, .pdf, .xls, .xlsx, .doc, .docx, "
".pptx, .csv, .html, .mhtml, .txt, .md, .zip"
),
"private": True,
},
"index_type": "ktem.index.file.FileIndex",
},
*GRAPHRAG_INDICES,
]
+26
View File
@@ -0,0 +1,26 @@
# fly.toml app configuration file generated for kotaemon on 2024-12-24T20:56:32+07:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'kotaemon'
primary_region = 'sin'
[build]
[mounts]
destination = "/app/ktem_app_data"
source = "ktem_volume"
[http_service]
internal_port = 7860
force_https = true
auto_stop_machines = 'suspend'
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
memory = '4gb'
cpu_kind = 'shared'
cpus = 4
Executable
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
if [ -z "$GRADIO_SERVER_NAME" ]; then
export GRADIO_SERVER_NAME="0.0.0.0"
fi
if [ -z "$GRADIO_SERVER_PORT" ]; then
export GRADIO_SERVER_PORT="7860"
fi
# Check if environment variable KH_DEMO_MODE is set to true
if [ "$KH_DEMO_MODE" = "true" ]; then
echo "KH_DEMO_MODE is true. Launching in demo mode..."
# Command to launch in demo mode
GR_FILE_ROOT_PATH="/app" KH_FEATURE_USER_MANAGEMENT=false USE_LIGHTRAG=false .venv/bin/uvicorn sso_app_demo:app --host "$GRADIO_SERVER_NAME" --port "$GRADIO_SERVER_PORT"
else
if [ "$KH_SSO_ENABLED" = "true" ]; then
echo "KH_SSO_ENABLED is true. Launching in SSO mode..."
GR_FILE_ROOT_PATH="/app" KH_SSO_ENABLED=true .venv/bin/uvicorn sso_app:app --host "$GRADIO_SERVER_NAME" --port "$GRADIO_SERVER_PORT"
else
ollama serve &
.venv/bin/python app.py
fi
fi
+129
View File
@@ -0,0 +1,129 @@
# kotaemon
Quick and easy AI components to build Kotaemon
## Documentation
TBD
## Install
```shell
pip install kotaemon@git+ssh://git@github.com/Cinnamon/kotaemon.git
```
## Contribute
### Setup
- Create conda environment (suggest 3.10)
```shell
conda create -n kotaemon python=3.10
conda activate kotaemon
```
- Clone the repo
```shell
git clone git@github.com:Cinnamon/kotaemon.git
cd kotaemon
```
- Install all
```shell
pip install -e ".[dev]"
```
- Pre-commit
```shell
pre-commit install
```
- Test
```shell
pytest tests
```
### Credential sharing
This repo uses [git-secret](https://sobolevn.me/git-secret/) to share credentials, which
internally uses `gpg` to encrypt and decrypt secret files.
This repo uses `python-dotenv` to manage credentials stored as environment variable.
Please note that the use of `python-dotenv` and credentials are for development
purposes only. Thus, it should not be used in the main source code (i.e. `kotaemon/` and `tests/`), but can be used in `examples/`.
#### Install git-secret
Please follow the [official guide](https://sobolevn.me/git-secret/installation) to install git-secret.
For Windows users, see [For Windows users](#for-windows-users).
For users who don't have sudo privilege to install packages, follow the `Manual Installation` in the [official guide](https://sobolevn.me/git-secret/installation) and set `PREFIX` to a path that you have access to. And please don't forget to add `PREFIX` to your `PATH`.
#### Gaining access
In order to gain access to the secret files, you must provide your gpg public file to anyone who has access and ask them to ask your key to the keyring. For a quick tutorial on generating your gpg key pair, you can refer to the `Using gpg` section from the [git-secret main page](https://sobolevn.me/git-secret/).
#### Decrypt the secret file
The credentials are encrypted in the `.env.secret` file. To print the decrypted content to stdout, run
```shell
git-secret cat [filename]
```
Or to get the decrypted `.env` file, run
```shell
git-secret reveal [filename]
```
#### For Windows users
git-secret is currently not available for Windows, thus the easiest way is to use it in WSL (please use the latest version of WSL2). From there you have 2 options:
1. Using the gpg of WSL.
This is the most straight-forward option since you would use WSL just like any other unix environment. However, the downside is that you have to make WSL your main environment, which means WSL must have write permission on your repo. To achieve this, you must either:
- Clone and store your repo inside WSL's file system.
- Provide WSL with necessary permission on your Windows file system. This can be achieve by setting `automount` options for WSL. To do that, add these content to `/etc/wsl.conf` and then restart your sub-system.
```shell
[automount]
options = "metadata,umask=022,fmask=011"
```
This enables all permissions for user owner.
2. Using the gpg of Windows but with git-secret from WSL.
For those who use Windows as the main environment, having to switch back and forth between Windows and WSL will be inconvenient. You can instead stay within your Windows environment and apply some tricks to use `git-secret` from WSL.
- Install and setup `gpg` on Windows.
- Install `git-secret` on WSL. Now in Windows, you can invoke `git-secret` using `wsl git-secret`.
- Alternatively you can setup alias in CMD to shorten the syntax. Please refer to [this SO answer](https://stackoverflow.com/a/65823225) for the instruction. Some recommended aliases are:
```bat
@echo off
:: Commands
DOSKEY ls=dir /B $*
DOSKEY ll=dir /a $*
DOSKEY git-secret=wsl git-secret $*
DOSKEY gs=wsl git-secret $*
```
Now you can invoke `git-secret` in CMD using `git-secret` or `gs`.
- For Powershell users, similar behaviours can be achieved using `Set-Alias` and `profile.ps1`. Please refer this [SO thread](https://stackoverflow.com/questions/61081434/how-do-i-create-a-permanent-alias-file-in-powershell-core) as an example.
### Code base structure
- documents: define document
- loaders
+23
View File
@@ -0,0 +1,23 @@
# Disable telemetry with monkey patching
import logging
logger = logging.getLogger(__name__)
try:
import posthog
def capture(*args, **kwargs):
logger.info("posthog.capture called with args: %s, kwargs: %s", args, kwargs)
posthog.capture = capture
except ImportError:
pass
try:
import os
os.environ["HAYSTACK_TELEMETRY_ENABLED"] = "False"
import haystack.telemetry
haystack.telemetry.telemetry = None
except ImportError:
pass
+33
View File
@@ -0,0 +1,33 @@
from .base import BaseAgent
from .io import AgentFinish, AgentOutput, AgentType, BaseScratchPad
from .langchain_based import LangchainAgent
from .react.agent import ReactAgent
from .rewoo.agent import RewooAgent
from .tools import (
BaseTool,
ComponentTool,
GoogleSearchTool,
LLMTool,
MCPTool,
WikipediaTool,
)
__all__ = [
# agent
"BaseAgent",
"ReactAgent",
"RewooAgent",
"LangchainAgent",
# tool
"BaseTool",
"ComponentTool",
"GoogleSearchTool",
"WikipediaTool",
"LLMTool",
"MCPTool",
# io
"AgentType",
"AgentOutput",
"AgentFinish",
"BaseScratchPad",
]
+57
View File
@@ -0,0 +1,57 @@
from typing import Optional, Union
from kotaemon.base import BaseComponent, Node, Param
from kotaemon.llms import BaseLLM, PromptTemplate
from .io import AgentOutput, AgentType
from .tools import BaseTool
class BaseAgent(BaseComponent):
"""Define base agent interface"""
name: str = Param(help="Name of the agent.")
agent_type: AgentType = Param(help="Agent type, must be one of AgentType")
description: str = Param(
help=(
"Description used to tell the model how/when/why to use the agent. You can"
" provide few-shot examples as a part of the description. This will be"
" input to the prompt of LLM."
)
)
llm: Optional[BaseLLM] = Node(
help=(
"LLM to be used for the agent (optional). LLM must implement BaseLLM"
" interface."
)
)
prompt_template: Optional[Union[PromptTemplate, dict[str, PromptTemplate]]] = Param(
help="A prompt template or a dict to supply different prompt to the agent"
)
plugins: list[BaseTool] = Param(
default_callback=lambda _: [],
help="List of plugins / tools to be used in the agent",
)
@staticmethod
def safeguard_run(run_func, *args, **kwargs):
def wrapper(self, *args, **kwargs):
try:
return run_func(self, *args, **kwargs)
except Exception as e:
return AgentOutput(
text="",
agent_type=self.agent_type,
status="failed",
error=str(e),
)
return wrapper
def add_tools(self, tools: list[BaseTool]) -> None:
"""Helper method to add tools and update agent state if needed"""
self.plugins.extend(tools)
def run(self, *args, **kwargs) -> AgentOutput | list[AgentOutput]:
"""Run the component."""
raise NotImplementedError()
@@ -0,0 +1,3 @@
from .base import AgentAction, AgentFinish, AgentOutput, AgentType, BaseScratchPad
__all__ = ["AgentOutput", "AgentFinish", "BaseScratchPad", "AgentType", "AgentAction"]
+258
View File
@@ -0,0 +1,258 @@
import json
import logging
import os
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, Literal, NamedTuple, Optional, Union
from pydantic import ConfigDict
from kotaemon.base import LLMInterface
def check_log():
"""
Checks if logging has been enabled.
:return: True if logging has been enabled, False otherwise.
:rtype: bool
"""
return os.environ.get("LOG_PATH", None) is not None
class AgentType(Enum):
"""
Enumerated type for agent types.
"""
openai = "openai"
openai_multi = "openai_multi"
openai_tool = "openai_tool"
self_ask = "self_ask"
react = "react"
rewoo = "rewoo"
vanilla = "vanilla"
class BaseScratchPad:
"""
Base class for output handlers.
Attributes:
-----------
logger : logging.Logger
The logger object to log messages.
Methods:
--------
stop():
Stop the output.
update_status(output: str, **kwargs):
Update the status of the output.
thinking(name: str):
Log that a process is thinking.
done(_all=False):
Log that the process is done.
stream_print(item: str):
Not implemented.
json_print(item: Dict[str, Any]):
Log a JSON object.
panel_print(item: Any, title: str = "Output", stream: bool = False):
Log a panel output.
clear():
Not implemented.
print(content: str, **kwargs):
Log arbitrary content.
format_json(json_obj: str):
Format a JSON object.
debug(content: str, **kwargs):
Log a debug message.
info(content: str, **kwargs):
Log an informational message.
warning(content: str, **kwargs):
Log a warning message.
error(content: str, **kwargs):
Log an error message.
critical(content: str, **kwargs):
Log a critical message.
"""
def __init__(self):
"""
Initialize the BaseOutput object.
"""
self.logger = logging
self.log = []
def stop(self):
"""
Stop the output.
"""
def update_status(self, output: str, **kwargs):
"""
Update the status of the output.
"""
if check_log():
self.logger.info(output)
def thinking(self, name: str):
"""
Log that a process is thinking.
"""
if check_log():
self.logger.info(f"{name} is thinking...")
def done(self, _all=False):
"""
Log that the process is done.
"""
if check_log():
self.logger.info("Done")
def stream_print(self, item: str):
"""
Stream print.
"""
def json_print(self, item: Dict[str, Any]):
"""
Log a JSON object.
"""
if check_log():
self.logger.info(json.dumps(item, indent=2))
def panel_print(self, item: Any, title: str = "Output", stream: bool = False):
"""
Log a panel output.
Args:
item : Any
The item to log.
title : str, optional
The title of the panel, defaults to "Output".
stream : bool, optional
"""
if not stream:
self.log.append(item)
if check_log():
self.logger.info("-" * 20)
self.logger.info(item)
self.logger.info("-" * 20)
def clear(self):
"""
Not implemented.
"""
def print(self, content: str, **kwargs):
"""
Log arbitrary content.
"""
self.log.append(content)
if check_log():
self.logger.info(content)
def format_json(self, json_obj: str):
"""
Format a JSON object.
"""
formatted_json = json.dumps(json_obj, indent=2)
return formatted_json
def debug(self, content: str, **kwargs):
"""
Log a debug message.
"""
if check_log():
self.logger.debug(content, **kwargs)
def info(self, content: str, **kwargs):
"""
Log an informational message.
"""
if check_log():
self.logger.info(content, **kwargs)
def warning(self, content: str, **kwargs):
"""
Log a warning message.
"""
if check_log():
self.logger.warning(content, **kwargs)
def error(self, content: str, **kwargs):
"""
Log an error message.
"""
if check_log():
self.logger.error(content, **kwargs)
def critical(self, content: str, **kwargs):
"""
Log a critical message.
"""
if check_log():
self.logger.critical(content, **kwargs)
@dataclass
class AgentAction:
"""Agent's action to take.
Args:
tool: The tool to invoke.
tool_input: The input to the tool.
log: The log message.
"""
tool: str
tool_input: Union[str, dict]
log: str
class AgentFinish(NamedTuple):
"""Agent's return value when finishing execution.
Args:
return_values: The return values of the agent.
log: The log message.
"""
return_values: dict
log: str
class AgentOutput(LLMInterface):
"""Output from an agent.
Args:
text: The text output from the agent.
agent_type: The type of agent.
status: The status after executing the agent.
error: The error message if any.
"""
model_config = ConfigDict(extra="allow")
text: str
type: str = "agent"
agent_type: AgentType
status: Literal["thinking", "finished", "stopped", "failed"]
error: Optional[str] = None
intermediate_steps: Optional[list] = None
@@ -0,0 +1,79 @@
from typing import List, Optional
from langchain.agents import AgentType as LCAgentType
from langchain.agents import initialize_agent
from langchain.agents.agent import AgentExecutor as LCAgentExecutor
from kotaemon.llms import LLM, ChatLLM
from .base import BaseAgent
from .io import AgentOutput, AgentType
from .tools import BaseTool
class LangchainAgent(BaseAgent):
"""Wrapper for Langchain Agent"""
name: str = "LangchainAgent"
agent_type: AgentType
description: str = "LangchainAgent for answering multi-step reasoning questions"
AGENT_TYPE_MAP = {
AgentType.openai: LCAgentType.OPENAI_FUNCTIONS,
AgentType.openai_multi: LCAgentType.OPENAI_MULTI_FUNCTIONS,
AgentType.react: LCAgentType.ZERO_SHOT_REACT_DESCRIPTION,
AgentType.self_ask: LCAgentType.SELF_ASK_WITH_SEARCH,
}
agent: Optional[LCAgentExecutor] = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.agent_type not in self.AGENT_TYPE_MAP:
raise NotImplementedError(
f"AgentType {self.agent_type } not supported by Langchain wrapper"
)
self.update_agent_tools()
def update_agent_tools(self):
assert isinstance(self.llm, (ChatLLM, LLM))
langchain_plugins = [tool.to_langchain_format() for tool in self.plugins]
# a fix for search_doc tool name:
# use "Intermediate Answer" for self-ask agent
found_search_tool = False
if self.agent_type == AgentType.self_ask:
for plugin in langchain_plugins:
if plugin.name == "search_doc":
plugin.name = "Intermediate Answer"
langchain_plugins = [plugin]
found_search_tool = True
break
if self.agent_type != AgentType.self_ask or found_search_tool:
# reinit Langchain AgentExecutor
self.agent = initialize_agent(
langchain_plugins,
self.llm.to_langchain_format(),
agent=self.AGENT_TYPE_MAP[self.agent_type],
handle_parsing_errors=True,
verbose=True,
)
def add_tools(self, tools: List[BaseTool]) -> None:
super().add_tools(tools)
self.update_agent_tools()
return
def run(self, instruction: str) -> AgentOutput:
assert (
self.agent is not None
), "Lanchain AgentExecutor is not correctly initialized"
# Langchain AgentExecutor call
output = self.agent(instruction)["output"]
return AgentOutput(
text=output,
agent_type=self.agent_type,
status="finished",
)
@@ -0,0 +1,3 @@
from .agent import ReactAgent
__all__ = ["ReactAgent"]
@@ -0,0 +1,361 @@
import logging
import re
from functools import partial
from typing import Optional
import tiktoken
from kotaemon.agents.base import BaseAgent, BaseLLM
from kotaemon.agents.io import AgentAction, AgentFinish, AgentOutput, AgentType
from kotaemon.agents.tools import BaseTool
from kotaemon.base import Document, Param
from kotaemon.indices.splitters import TokenSplitter
from kotaemon.llms import PromptTemplate
FINAL_ANSWER_ACTION = "Final Answer:"
class ReactAgent(BaseAgent):
"""
Sequential ReactAgent class inherited from BaseAgent.
Implementing ReAct agent paradigm https://arxiv.org/pdf/2210.03629.pdf
"""
name: str = "ReactAgent"
agent_type: AgentType = AgentType.react
description: str = "ReactAgent for answering multi-step reasoning questions"
llm: BaseLLM
prompt_template: Optional[PromptTemplate] = None
output_lang: str = "English"
plugins: list[BaseTool] = Param(
default_callback=lambda _: [], help="List of tools to be used in the agent. "
)
examples: dict[str, str | list[str]] = Param(
default_callback=lambda _: {}, help="Examples to be used in the agent. "
)
intermediate_steps: list[tuple[AgentAction | AgentFinish, str]] = Param(
default_callback=lambda _: [],
help="List of AgentAction and observation (tool) output",
)
max_iterations: int = 5
strict_decode: bool = False
max_context_length: int = Param(
default=3000,
help="Max context length for each tool output.",
)
trim_func: TokenSplitter | None = None
def _compose_plugin_description(self) -> str:
"""
Compose the worker prompt from the workers.
Example:
toolname1[input]: tool1 description
toolname2[input]: tool2 description
"""
prompt = ""
try:
for plugin in self.plugins:
prompt += f"{plugin.name}[input]: {plugin.description}\n"
except Exception:
raise ValueError("Worker must have a name and description.")
return prompt
def _construct_scratchpad(
self, intermediate_steps: list[tuple[AgentAction | AgentFinish, str]] = []
) -> str:
"""Construct the scratchpad that lets the agent continue its thought process."""
thoughts = ""
for action, observation in intermediate_steps:
thoughts += action.log
thoughts += f"\nObservation: {observation}\nThought:"
return thoughts
def _parse_output(self, text: str) -> Optional[AgentAction | AgentFinish]:
"""
Parse text output from LLM for the next Action or Final Answer
Using Regex to parse "Action:\n Action Input:\n" for the next Action
Using FINAL_ANSWER_ACTION to parse Final Answer
Args:
text[str]: input text to parse
"""
includes_answer = FINAL_ANSWER_ACTION in text
regex = (
r"Action\s*\d*\s*:[\s]*(.*?)[\s]*Action\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
)
action_match = re.search(regex, text, re.DOTALL)
action_output: Optional[AgentAction | AgentFinish] = None
if action_match:
if includes_answer:
raise Exception(
"Parsing LLM output produced both a final answer "
f"and a parse-able action: {text}"
)
action = action_match.group(1).strip()
action_input = action_match.group(2)
tool_input = action_input.strip(" ")
# ensure if its a well formed SQL query we don't remove any trailing " chars
if tool_input.startswith("SELECT ") is False:
tool_input = tool_input.strip('"')
action_output = AgentAction(action, tool_input, text)
elif includes_answer:
action_output = AgentFinish(
{"output": text.split(FINAL_ANSWER_ACTION)[-1].strip()}, text
)
else:
if self.strict_decode:
raise Exception(f"Could not parse LLM output: `{text}`")
else:
action_output = AgentFinish({"output": text}, text)
return action_output
def _compose_prompt(self, instruction) -> str:
"""
Compose the prompt from template, worker description, examples and instruction.
"""
agent_scratchpad = self._construct_scratchpad(self.intermediate_steps)
tool_description = self._compose_plugin_description()
tool_names = ", ".join([plugin.name for plugin in self.plugins])
if self.prompt_template is None:
from .prompt import zero_shot_react_prompt
self.prompt_template = zero_shot_react_prompt
return self.prompt_template.populate(
instruction=instruction,
agent_scratchpad=agent_scratchpad,
tool_description=tool_description,
tool_names=tool_names,
lang=self.output_lang,
)
def _format_function_map(self) -> dict[str, BaseTool]:
"""Format the function map for the open AI function API.
Return:
Dict[str, Callable]: The function map.
"""
# Map the function name to the real function object.
function_map = {}
for plugin in self.plugins:
function_map[plugin.name] = plugin
return function_map
def _trim(self, text: str | Document) -> str:
"""
Trim the text to the maximum token length.
"""
evidence_trim_func = (
self.trim_func
if self.trim_func
else TokenSplitter(
chunk_size=self.max_context_length,
chunk_overlap=0,
separator=" ",
tokenizer=partial(
tiktoken.encoding_for_model("gpt-3.5-turbo").encode,
allowed_special=set(),
disallowed_special="all",
),
)
)
if isinstance(text, str):
texts = evidence_trim_func([Document(text=text)])
elif isinstance(text, Document):
texts = evidence_trim_func([text])
else:
raise ValueError("Invalid text type to trim")
trim_text = texts[0].text
logging.info(f"len (trimmed): {len(trim_text)}")
return trim_text
def clear(self):
"""
Clear and reset the agent.
"""
self.intermediate_steps = []
def run(self, instruction, max_iterations=None) -> AgentOutput:
"""
Run the agent with the given instruction.
Args:
instruction: Instruction to run the agent with.
max_iterations: Maximum number of iterations
of reasoning steps, defaults to 10.
Return:
AgentOutput object.
"""
if not max_iterations:
max_iterations = self.max_iterations
assert max_iterations > 0
self.clear()
logging.info(f"Running {self.name} with instruction: {instruction}")
total_cost = 0.0
total_token = 0
status = "failed"
response_text = None
for step_count in range(1, max_iterations + 1):
prompt = self._compose_prompt(instruction)
logging.info(f"Prompt: {prompt}")
response = self.llm(
prompt, stop=["Observation:"]
) # could cause bugs if llm doesn't have `stop` as a parameter
response_text = response.text
logging.info(f"Response: {response_text}")
action_step = self._parse_output(response_text)
if action_step is None:
raise ValueError("Invalid action")
is_finished_chain = isinstance(action_step, AgentFinish)
if is_finished_chain:
result = ""
else:
assert isinstance(action_step, AgentAction)
action_name = action_step.tool
tool_input = action_step.tool_input
logging.info(f"Action: {action_name}")
logging.info(f"Tool Input: {tool_input}")
function_map = self._format_function_map()
if action_name not in function_map:
available = ", ".join(function_map.keys())
result = (
f"Tool '{action_name}' not found. "
f"Available tools: {available}"
)
else:
result = function_map[action_name](tool_input)
# trim the worker output to 1000 tokens, as we are appending
# all workers' logs and it can exceed the token limit if we
# don't limit each. Fix this number regarding to the LLM capacity.
result = self._trim(result)
logging.info(f"Result: {result}")
self.intermediate_steps.append((action_step, result))
if is_finished_chain:
logging.info(f"Finished after {step_count} steps.")
status = "finished"
break
else:
status = "stopped"
return AgentOutput(
text=response_text,
agent_type=self.agent_type,
status=status,
total_tokens=total_token,
total_cost=total_cost,
intermediate_steps=self.intermediate_steps,
max_iterations=max_iterations,
)
def stream(self, instruction, max_iterations=None):
"""
Stream the agent with the given instruction.
Args:
instruction: Instruction to run the agent with.
max_iterations: Maximum number of iterations
of reasoning steps, defaults to 10.
Return:
AgentOutput object.
"""
if not max_iterations:
max_iterations = self.max_iterations
assert max_iterations > 0
self.clear()
logging.info(f"Running {self.name} with instruction: {instruction}")
print(f"Running {self.name} with instruction: {instruction}")
total_cost = 0.0
total_token = 0
status = "failed"
response_text = None
for step_count in range(1, max_iterations + 1):
prompt = self._compose_prompt(instruction)
logging.info(f"Prompt: {prompt}")
print(f"Prompt: {prompt}")
response = self.llm(
prompt, stop=["Observation:"]
) # TODO: could cause bugs if llm doesn't have `stop` as a parameter
response_text = response.text
logging.info(f"Response: {response_text}")
print(f"Response: {response_text}")
action_step = self._parse_output(response_text)
if action_step is None:
raise ValueError("Invalid action")
is_finished_chain = isinstance(action_step, AgentFinish)
if is_finished_chain:
result = response_text
if "Final Answer:" in response_text:
result = response_text.split("Final Answer:")[-1].strip()
else:
assert isinstance(action_step, AgentAction)
action_name = action_step.tool
tool_input = action_step.tool_input
logging.info(f"Action: {action_name}")
print(f"Action: {action_name}")
logging.info(f"Tool Input: {tool_input}")
print(f"Tool Input: {tool_input}")
function_map = self._format_function_map()
if action_name not in function_map:
available = ", ".join(function_map.keys())
result = (
f"Tool '{action_name}' not found. "
f"Available tools: {available}"
)
else:
result = function_map[action_name](tool_input)
# trim the worker output to 1000 tokens, as we are appending
# all workers' logs and it can exceed the token limit if we
# don't limit each. Fix this number regarding to the LLM capacity.
result = self._trim(result)
logging.info(f"Result: {result}")
print(f"Result: {result}")
self.intermediate_steps.append((action_step, result))
if is_finished_chain:
logging.info(f"Finished after {step_count} steps.")
status = "finished"
yield AgentOutput(
text=result,
agent_type=self.agent_type,
status=status,
intermediate_steps=self.intermediate_steps[-1],
)
break
else:
yield AgentOutput(
text="",
agent_type=self.agent_type,
status="thinking",
intermediate_steps=self.intermediate_steps[-1],
)
else:
status = "stopped"
yield AgentOutput(
text="",
agent_type=self.agent_type,
status=status,
intermediate_steps=self.intermediate_steps[-1],
)
return AgentOutput(
text=response_text,
agent_type=self.agent_type,
status=status,
total_tokens=total_token,
total_cost=total_cost,
intermediate_steps=self.intermediate_steps,
max_iterations=max_iterations,
)
@@ -0,0 +1,28 @@
# flake8: noqa
from kotaemon.llms import PromptTemplate
zero_shot_react_prompt = PromptTemplate(
template="""Answer the following questions as best you can. Give answer in {lang}. You have access to the following tools:
{tool_description}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action, should be different from the action input of the same action in previous steps.
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
#Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin! After each Action Input.
Question: {instruction}
Thought:{agent_scratchpad}
"""
)
@@ -0,0 +1,3 @@
from .agent import RewooAgent
__all__ = ["RewooAgent"]
@@ -0,0 +1,384 @@
import logging
import re
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from typing import Any
import tiktoken
from kotaemon.agents.base import BaseAgent
from kotaemon.agents.io import AgentOutput, AgentType, BaseScratchPad
from kotaemon.agents.tools import BaseTool
from kotaemon.agents.utils import get_plugin_response_content
from kotaemon.base import Document, Node, Param
from kotaemon.indices.qa.citation import CitationPipeline
from kotaemon.indices.splitters import TokenSplitter
from kotaemon.llms import BaseLLM, PromptTemplate
from .planner import Planner
from .solver import Solver
class RewooAgent(BaseAgent):
"""Distributive RewooAgent class inherited from BaseAgent.
Implementing ReWOO paradigm https://arxiv.org/pdf/2305.18323.pdf"""
name: str = "RewooAgent"
agent_type: AgentType = AgentType.rewoo
description: str = "RewooAgent for answering multi-step reasoning questions"
output_lang: str = "English"
planner_llm: BaseLLM
solver_llm: BaseLLM
prompt_template: dict[str, PromptTemplate] = Param(
default_callback=lambda _: {},
help="A dict to supply different prompt to the agent.",
)
plugins: list[BaseTool] = Param(
default_callback=lambda _: [], help="A list of plugins to be used in the model."
)
examples: dict[str, str | list[str]] = Param(
default_callback=lambda _: {}, help="Examples to be used in the agent."
)
max_context_length: int = Param(
default=3000,
help="Max context length for each tool output.",
)
trim_func: TokenSplitter | None = None
@Node.auto(depends_on=["planner_llm", "plugins", "prompt_template", "examples"])
def planner(self):
return Planner(
model=self.planner_llm,
plugins=self.plugins,
prompt_template=self.prompt_template.get("Planner", None),
examples=self.examples.get("Planner", None),
)
@Node.auto(depends_on=["solver_llm", "prompt_template", "examples"])
def solver(self):
return Solver(
model=self.solver_llm,
prompt_template=self.prompt_template.get("Solver", None),
examples=self.examples.get("Solver", None),
output_lang=self.output_lang,
)
def _parse_plan_map(
self, planner_response: str
) -> tuple[dict[str, list[str]], dict[str, str]]:
"""
Parse planner output. It should be an n-to-n mapping from Plans to #Es.
This is because sometimes LLM cannot follow the strict output format.
Example:
#Plan1
#E1
#E2
should result in: {"#Plan1": ["#E1", "#E2"]}
Or:
#Plan1
#Plan2
#E1
should result in: {"#Plan1": [], "#Plan2": ["#E1"]}
This function should also return a plan map.
Returns:
tuple[Dict[str, List[str]], Dict[str, str]]: A list of plan map
"""
valid_chunk = [
line
for line in planner_response.splitlines()
if line.startswith("#Plan") or line.startswith("#E")
]
plan_to_es: dict[str, list[str]] = dict()
plans: dict[str, str] = dict()
prev_key = ""
for line in valid_chunk:
key, description = line.split(":", 1)
key = key.strip()
if key.startswith("#Plan"):
plans[key] = description.strip()
plan_to_es[key] = []
prev_key = key
elif key.startswith("#E"):
plan_to_es[prev_key].append(key)
return plan_to_es, plans
def _parse_planner_evidences(
self, planner_response: str
) -> tuple[dict[str, str], list[list[str]]]:
"""
Parse planner output. This should return a mapping from #E to tool call.
It should also identify the level of each #E in dependency map.
Example:
{
"#E1": "Tool1", "#E2": "Tool2",
"#E3": "Tool3", "#E4": "Tool4"
}, [[#E1, #E2], [#E3, #E4]]
Returns:
tuple[dict[str, str], List[List[str]]]:
A mapping from #E to tool call and a list of levels.
"""
evidences: dict[str, str] = dict()
dependence: dict[str, list[str]] = dict()
for line in planner_response.splitlines():
if line.startswith("#E") and line[2].isdigit():
e, tool_call = line.split(":", 1)
e, tool_call = e.strip(), tool_call.strip()
if len(e) == 3:
dependence[e] = []
evidences[e] = tool_call
for var in re.findall(r"#E\d+", tool_call):
if var in evidences:
dependence[e].append(var)
else:
evidences[e] = "No evidence found"
level = []
while dependence:
select = [i for i in dependence if not dependence[i]]
if len(select) == 0:
raise ValueError("Circular dependency detected.")
level.append(select)
for item in select:
dependence.pop(item)
for item in dependence:
for i in select:
if i in dependence[item]:
dependence[item].remove(i)
return evidences, level
def _run_plugin(
self,
e: str,
planner_evidences: dict[str, str],
worker_evidences: dict[str, str],
output=BaseScratchPad(),
):
"""
Run a plugin for a given evidence.
This function should also cumulate the cost and tokens.
"""
result = dict(e=e, plugin_cost=0, plugin_token=0, evidence="")
tool_call = planner_evidences[e]
if "[" not in tool_call:
result["evidence"] = tool_call
else:
tool, tool_input = tool_call.split("[", 1)
tool_input = tool_input[:-1]
# find variables in input and replace with previous evidences
for var in re.findall(r"#E\d+", tool_input):
print("Tool input: ", tool_input)
print("Var: ", var)
print("Worker evidences: ", worker_evidences)
if var in worker_evidences:
tool_input = tool_input.replace(
var, worker_evidences.get(var, "") or ""
)
try:
selected_plugin = self._find_plugin(tool)
if selected_plugin is None:
raise ValueError("Invalid plugin detected")
tool_response = selected_plugin(tool_input)
result["evidence"] = get_plugin_response_content(tool_response)
except ValueError:
result["evidence"] = "No evidence found."
finally:
output.panel_print(
result["evidence"], f"[green] Function Response of [blue]{tool}: "
)
return result
def _get_worker_evidence(
self,
planner_evidences: dict[str, str],
evidences_level: list[list[str]],
output=BaseScratchPad(),
) -> Any:
"""
Parallel execution of plugins in DAG for speedup.
This is one of core benefits of ReWOO agents.
Args:
planner_evidences: A mapping from #E to tool call.
evidences_level: A list of levels of evidences.
Calculated from DAG of plugin calls.
output: Output object, defaults to BaseOutput().
Returns:
A mapping from #E to tool call.
"""
worker_evidences: dict[str, str] = dict()
plugin_cost, plugin_token = 0.0, 0.0
with ThreadPoolExecutor() as pool:
for level in evidences_level:
results = []
for e in level:
results.append(
pool.submit(
self._run_plugin,
e,
planner_evidences,
worker_evidences,
output,
)
)
if len(results) > 1:
output.update_status(f"Running tasks {level} in parallel.")
else:
output.update_status(f"Running task {level[0]}.")
for r in results:
resp = r.result()
plugin_cost += resp["plugin_cost"]
plugin_token += resp["plugin_token"]
worker_evidences[resp["e"]] = self._trim_evidence(resp["evidence"])
output.done()
return worker_evidences, plugin_cost, plugin_token
def _find_plugin(self, name: str):
for p in self.plugins:
if p.name == name:
return p
def _trim_evidence(self, evidence: str):
evidence_trim_func = (
self.trim_func
if self.trim_func
else TokenSplitter(
chunk_size=self.max_context_length,
chunk_overlap=0,
separator=" ",
tokenizer=partial(
tiktoken.encoding_for_model("gpt-3.5-turbo").encode,
allowed_special=set(),
disallowed_special="all",
),
)
)
if evidence:
texts = evidence_trim_func([Document(text=evidence)])
evidence = texts[0].text
logging.info(f"len (trimmed): {len(evidence)}")
return evidence
@BaseAgent.safeguard_run
def run(self, instruction: str, use_citation: bool = False) -> AgentOutput:
"""
Run the agent with a given instruction.
"""
logging.info(f"Running {self.name} with instruction: {instruction}")
total_cost = 0.0
total_token = 0
# Plan
planner_output = self.planner(instruction)
planner_text_output = planner_output.text
plan_to_es, plans = self._parse_plan_map(planner_text_output)
planner_evidences, evidence_level = self._parse_planner_evidences(
planner_text_output
)
# Work
worker_evidences, plugin_cost, plugin_token = self._get_worker_evidence(
planner_evidences, evidence_level
)
worker_log = ""
for plan in plan_to_es:
worker_log += f"{plan}: {plans[plan]}\n"
for e in plan_to_es[plan]:
worker_log += f"{e}: {worker_evidences[e]}\n"
# Solve
solver_output = self.solver(instruction, worker_log)
solver_output_text = solver_output.text
if use_citation:
citation_pipeline = CitationPipeline(llm=self.solver_llm)
citation = citation_pipeline(context=worker_log, question=instruction)
else:
citation = None
return AgentOutput(
text=solver_output_text,
agent_type=self.agent_type,
status="finished",
total_tokens=total_token,
total_cost=total_cost,
citation=citation,
metadata={"citation": citation, "worker_log": worker_log},
)
def stream(self, instruction: str, use_citation: bool = False):
"""
Stream the agent with a given instruction.
"""
logging.info(f"Streaming {self.name} with instruction: {instruction}")
total_cost = 0.0
total_token = 0
# Plan
planner_output = self.planner(instruction)
planner_text_output = planner_output.text
plan_to_es, plans = self._parse_plan_map(planner_text_output)
planner_evidences, evidence_level = self._parse_planner_evidences(
planner_text_output
)
print("Planner output:", planner_text_output)
# output planner to info panel
yield AgentOutput(
text="",
agent_type=self.agent_type,
status="thinking",
intermediate_steps=[{"planner_log": planner_text_output}],
)
# Work
worker_evidences, plugin_cost, plugin_token = self._get_worker_evidence(
planner_evidences, evidence_level
)
worker_log = ""
for plan in plan_to_es:
worker_log += f"{plan}: {plans[plan]}\n"
current_progress = f"{plan}: {plans[plan]}\n"
for e in plan_to_es[plan]:
worker_log += f"#Action: {planner_evidences.get(e, None)}\n"
worker_log += f"{e}: {worker_evidences[e]}\n"
current_progress += f"#Action: {planner_evidences.get(e, None)}\n"
current_progress += f"{e}: {worker_evidences[e]}\n"
yield AgentOutput(
text="",
agent_type=self.agent_type,
status="thinking",
intermediate_steps=[{"worker_log": current_progress}],
)
# Solve
solver_response = ""
for solver_output in self.solver.stream(instruction, worker_log):
solver_output_text = solver_output.text
solver_response += solver_output_text
yield AgentOutput(
text=solver_output_text,
agent_type=self.agent_type,
status="thinking",
)
if use_citation:
citation_pipeline = CitationPipeline(llm=self.solver_llm)
citation = citation_pipeline.invoke(
context=worker_log, question=instruction
)
else:
citation = None
return AgentOutput(
text="",
agent_type=self.agent_type,
status="finished",
total_tokens=total_token,
total_cost=total_cost,
citation=citation,
metadata={"citation": citation, "worker_log": worker_log},
)
@@ -0,0 +1,106 @@
from typing import Any, List, Optional, Union
from kotaemon.agents.base import BaseLLM, BaseTool
from kotaemon.agents.io import BaseScratchPad
from kotaemon.base import BaseComponent
from kotaemon.llms import PromptTemplate
from .prompt import few_shot_planner_prompt, zero_shot_planner_prompt
class Planner(BaseComponent):
model: BaseLLM
prompt_template: Optional[PromptTemplate] = None
examples: Optional[Union[str, List[str]]] = None
plugins: List[BaseTool]
def _compose_worker_description(self) -> str:
"""
Compose the worker prompt from the workers.
Example:
toolname1[input]: tool1 description
toolname2[input]: tool2 description
"""
prompt = ""
try:
for worker in self.plugins:
prompt += f"{worker.name}[input]: {worker.description}\n"
except Exception:
raise ValueError("Worker must have a name and description.")
return prompt
def _compose_fewshot_prompt(self) -> str:
if self.examples is None:
return ""
if isinstance(self.examples, str):
return self.examples
else:
return "\n\n".join([e.strip("\n") for e in self.examples])
def _compose_prompt(self, instruction) -> str:
"""
Compose the prompt from template, worker description, examples and instruction.
"""
worker_desctription = self._compose_worker_description()
fewshot = self._compose_fewshot_prompt()
if self.prompt_template is not None:
if "fewshot" in self.prompt_template.placeholders:
return self.prompt_template.populate(
tool_description=worker_desctription,
fewshot=fewshot,
task=instruction,
)
else:
return self.prompt_template.populate(
tool_description=worker_desctription, task=instruction
)
else:
if self.examples is not None:
return few_shot_planner_prompt.populate(
tool_description=worker_desctription,
fewshot=fewshot,
task=instruction,
)
else:
return zero_shot_planner_prompt.populate(
tool_description=worker_desctription, task=instruction
)
def run(self, instruction: str, output: BaseScratchPad = BaseScratchPad()) -> Any:
response = None
output.info("Running Planner")
prompt = self._compose_prompt(instruction)
output.debug(f"Prompt: {prompt}")
try:
response = self.model(prompt)
self.log_progress(".planner", response=response)
output.info("Planner run successful.")
except ValueError as e:
output.error("Planner failed to retrieve response from LLM")
raise ValueError("Planner failed to retrieve response from LLM") from e
return response
def stream(self, instruction: str, output: BaseScratchPad = BaseScratchPad()):
response = None
output.info("Running Planner")
prompt = self._compose_prompt(instruction)
output.debug(f"Prompt: {prompt}")
response = ""
try:
for text in self.model.stream(prompt):
response += text
yield text
self.log_progress(".planner", response=response)
output.info("Planner run successful.")
except NotImplementedError:
print("Streaming is not supported, falling back to normal run")
response = self.model(prompt)
yield response
except ValueError as e:
output.error("Planner failed to retrieve response from LLM")
raise ValueError("Planner failed to retrieve response from LLM") from e
return response
@@ -0,0 +1,119 @@
# flake8: noqa
from kotaemon.llms import PromptTemplate
zero_shot_planner_prompt = PromptTemplate(
template="""You are an AI agent who makes step-by-step plans to solve a problem under the help of external tools.
For each step, make one plan followed by one tool-call, which will be executed later to retrieve evidence for that step.
You should store each evidence into a distinct variable #E1, #E2, #E3 ... that can be referred to in later tool-call inputs.
##Available Tools##
{tool_description}
##Output Format (Replace '<...>')##
#Plan1: <describe your plan here>
#E1: <toolname>[<input here>] (eg. Search[What is Python])
#Plan2: <describe next plan>
#E2: <toolname>[<input here, you can use #E1 to represent its expected output>]
And so on...
##Your Task##
{task}
##Now Begin##
"""
)
one_shot_planner_prompt = PromptTemplate(
template="""You are an AI agent who makes step-by-step plans to solve a problem under the help of external tools.
For each step, make one plan followed by one tool-call, which will be executed later to retrieve evidence for that step.
You should store each evidence into a distinct variable #E1, #E2, #E3 ... that can be referred to in later tool-call inputs.
##Available Tools##
{tool_description}
##Output Format##
#Plan1: <describe your plan here>
#E1: <toolname>[<input here>]
#Plan2: <describe next plan>
#E2: <toolname>[<input here, you can use #E1 to represent its expected output>]
And so on...
##Example##
Task: What is the 4th root of 64 to the power of 3?
#Plan1: Find the 4th root of 64
#E1: Calculator[64^(1/4)]
#Plan2: Raise the result from #Plan1 to the power of 3
#E2: Calculator[#E1^3]
##Your Task##
{task}
##Now Begin##
"""
)
few_shot_planner_prompt = PromptTemplate(
template="""You are an AI agent who makes step-by-step plans to solve a problem under the help of external tools.
For each step, make one plan followed by one tool-call, which will be executed later to retrieve evidence for that step.
You should store each evidence into a distinct variable #E1, #E2, #E3 ... that can be referred to in later tool-call inputs.
##Available Tools##
{tool_description}
##Output Format (Replace '<...>')##
#Plan1: <describe your plan here>
#E1: <toolname>[<input>]
#Plan2: <describe next plan>
#E2: <toolname>[<input, you can use #E1 to represent its expected output>]
And so on...
##Examples##
{fewshot}
##Your Task##
{task}
##Now Begin##
"""
)
zero_shot_solver_prompt = PromptTemplate(
template="""You are an AI agent who solves a problem with my assistance. I will provide step-by-step plans(#Plan) and evidences(#E) that could be helpful.
Your task is to briefly summarize each step, then make a short final conclusion for your task. Give answer in {lang}.
##My Plans and Evidences##
{plan_evidence}
##Example Output##
First, I <did something> , and I think <...>; Second, I <...>, and I think <...>; ....
So, <your conclusion>.
##Your Task##
{task}
##Now Begin##
"""
)
few_shot_solver_prompt = PromptTemplate(
template="""You are an AI agent who solves a problem with my assistance. I will provide step-by-step plans and evidences that could be helpful.
Your task is to briefly summarize each step, then make a short final conclusion for your task. Give answer in {lang}.
##My Plans and Evidences##
{plan_evidence}
##Example Output##
First, I <did something> , and I think <...>; Second, I <...>, and I think <...>; ....
So, <your conclusion>.
##Example##
{fewshot}
##Your Task##
{task}
##Now Begin##
"""
)
@@ -0,0 +1,99 @@
from typing import Any, List, Optional, Union
from kotaemon.agents.io import BaseScratchPad
from kotaemon.base import BaseComponent
from kotaemon.llms import BaseLLM, PromptTemplate
from .prompt import few_shot_solver_prompt, zero_shot_solver_prompt
class Solver(BaseComponent):
model: BaseLLM
prompt_template: Optional[PromptTemplate] = None
examples: Optional[Union[str, List[str]]] = None
output_lang: str = "English"
def _compose_fewshot_prompt(self) -> str:
if self.examples is None:
return ""
if isinstance(self.examples, str):
return self.examples
else:
return "\n\n".join([e.strip("\n") for e in self.examples])
def _compose_prompt(self, instruction, plan_evidence, output_lang) -> str:
"""
Compose the prompt from template, plan&evidence, examples and instruction.
"""
fewshot = self._compose_fewshot_prompt()
if self.prompt_template is not None:
if "fewshot" in self.prompt_template.placeholders:
return self.prompt_template.populate(
plan_evidence=plan_evidence,
fewshot=fewshot,
task=instruction,
lang=output_lang,
)
else:
return self.prompt_template.populate(
plan_evidence=plan_evidence, task=instruction, lang=output_lang
)
else:
if self.examples is not None:
return few_shot_solver_prompt.populate(
plan_evidence=plan_evidence,
fewshot=fewshot,
task=instruction,
lang=output_lang,
)
else:
return zero_shot_solver_prompt.populate(
plan_evidence=plan_evidence,
task=instruction,
lang=output_lang,
)
def run(
self,
instruction: str,
plan_evidence: str,
output: BaseScratchPad = BaseScratchPad(),
) -> Any:
response = None
output.info("Running Solver")
output.debug(f"Instruction: {instruction}")
output.debug(f"Plan Evidence: {plan_evidence}")
prompt = self._compose_prompt(instruction, plan_evidence, self.output_lang)
output.debug(f"Prompt: {prompt}")
try:
response = self.model(prompt)
output.info("Solver run successful.")
except ValueError:
output.error("Solver failed to retrieve response from LLM")
return response
def stream(
self,
instruction: str,
plan_evidence: str,
output: BaseScratchPad = BaseScratchPad(),
) -> Any:
response = ""
output.info("Running Solver")
output.debug(f"Instruction: {instruction}")
output.debug(f"Plan Evidence: {plan_evidence}")
prompt = self._compose_prompt(instruction, plan_evidence, self.output_lang)
output.debug(f"Prompt: {prompt}")
try:
for text in self.model.stream(prompt):
response += text.text
yield text
output.info("Planner run successful.")
except NotImplementedError:
response = self.model(prompt).text
output.info("Solver run successful.")
except ValueError:
output.error("Solver failed to retrieve response from LLM")
return response

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