Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09da4a3002 | |||
| 93004306f2 | |||
| aed894246f | |||
| fa63a16591 | |||
| 40643663cb | |||
| d590e4423b | |||
| cdbf75e0ec | |||
| 8216e05784 | |||
| 2d4c51aa16 | |||
| cc43846d42 | |||
| 3bdec3b71a | |||
| a22a435690 | |||
| a681d47bce | |||
| 4bb06147c1 | |||
| 6b61b7e9c1 | |||
| 91033c7221 | |||
| ab9f005885 | |||
| 3da5724853 | |||
| c12362486f | |||
| d16eafae05 | |||
| bb1fbba161 | |||
| adb7206639 | |||
| 96143ac496 | |||
| 1df804e7df | |||
| 0ea278f633 | |||
| 7ed46260b3 | |||
| 9c58627372 | |||
| 07ba65d88d | |||
| cf9638e7b2 | |||
| a548863a09 | |||
| d5e40e1853 | |||
| a4708b3b86 | |||
| 81c8cc62a2 | |||
| e8b3d53faf | |||
| 2889799f10 | |||
| e24063caff | |||
| c595003481 | |||
| 05a4eef6ae | |||
| ac68986404 | |||
| 3f71050c47 | |||
| d12aeec1ff | |||
| addf1c0666 | |||
| d4b8542207 | |||
| fd97fb268a | |||
| 4f722621fd | |||
| 8ad6a5b5e7 | |||
| 2d6e860175 | |||
| 86e4146126 | |||
| cd0c7bc971 |
@@ -0,0 +1 @@
|
||||
OPENAI_API_KEY=
|
||||
@@ -0,0 +1,41 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us reproduce and fix the bug
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
#### Before submitting a bug, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/gventuri/pandas-ai/issues?q=is%3Aissue+sort%3Acreated-desc+).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 🐛 Describe the bug
|
||||
description: |
|
||||
Please provide a clear and concise description of what the bug is.
|
||||
|
||||
If relevant, add a minimal example so that we can reproduce the error by running the code. It is very important for the snippet to be as succinct (minimal) as possible, so please take time to trim down any irrelevant code to help us debug efficiently. We are going to copy-paste your code and we expect to get the same result as you did: avoid any external data, and include the relevant imports, etc. For example:
|
||||
|
||||
```python
|
||||
# All necessary imports at the beginning
|
||||
import embedchain as ec
|
||||
# Your code goes here
|
||||
|
||||
|
||||
```
|
||||
|
||||
Please also paste or describe the results you observe instead of the expected results. If you observe an error, please paste the error message including the **full** traceback of the exception. It may be relevant to wrap error messages in ```` ```triple quotes blocks``` ````.
|
||||
placeholder: |
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
```python
|
||||
Sample code to reproduce the problem
|
||||
```
|
||||
|
||||
```
|
||||
The error message you got, with the full traceback.
|
||||
````
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for contributing 🎉!
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
@@ -0,0 +1,32 @@
|
||||
name: 🚀 Feature request
|
||||
description: Submit a proposal/request for a new embedchain feature
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 🚀 The feature
|
||||
description: >
|
||||
A clear and concise description of the feature proposal
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation, pitch
|
||||
description: >
|
||||
Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., *"I'm working on X and would like Y to be possible"*. If this is related to another GitHub issue, please link here too.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives
|
||||
description: >
|
||||
A description of any alternative solutions or features you've considered, if any.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: >
|
||||
Add any other context or screenshots about the feature request.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for contributing 🎉!
|
||||
@@ -0,0 +1,41 @@
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Refactor (does not change functionality, e.g. code style improvements, linting)
|
||||
- [ ] Documentation update
|
||||
|
||||
## How Has This Been Tested?
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Unit Test
|
||||
- [ ] Test Script (please provide)
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||
- [ ] I have checked my code and corrected any misspellings
|
||||
|
||||
## Maintainer Checklist
|
||||
|
||||
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
|
||||
- [ ] Made sure Checks passed
|
||||
@@ -0,0 +1,24 @@
|
||||
name: cd
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish_to_pypi:
|
||||
name: publish to pypi on new release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: JRubics/poetry-publish@v1.16
|
||||
name: Build and publish to PyPI
|
||||
with:
|
||||
pypi_token: ${{ secrets.PYPI_TOKEN }}
|
||||
ignore_dev_requirements: "yes"
|
||||
repository_url: https://upload.pypi.org/legacy/
|
||||
repository_name: embedchain
|
||||
@@ -0,0 +1,28 @@
|
||||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install poetry
|
||||
run: pip install poetry==1.4.2
|
||||
- name: Install dependencies
|
||||
run: poetry install --all-extras
|
||||
- name: Lint with ruff
|
||||
run: make ci_lint
|
||||
- name: Test with pytest
|
||||
run: make ci_test
|
||||
+5
-1
@@ -166,4 +166,8 @@ cython_debug/
|
||||
# Database
|
||||
db
|
||||
|
||||
.vscode
|
||||
.vscode
|
||||
/poetry.lock
|
||||
.idea/
|
||||
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.220'
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: ruff
|
||||
# Respect `exclude` and `extend-exclude` settings.
|
||||
args: ["--force-exclude"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pytest-check
|
||||
name: pytest-check
|
||||
entry: poetry run pytest
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
@@ -0,0 +1,74 @@
|
||||
# Contributing to embedchain
|
||||
|
||||
Let us make contributing easy, collaborative and fun.
|
||||
|
||||
## Submit your Contribution through PR
|
||||
|
||||
To make a contribution, follow the following steps:
|
||||
|
||||
1. Fork and clone this repository
|
||||
2. Do the changes on your fork with dedicated feature branch `feature/f1`
|
||||
3. If you modified the code (new feature or bug-fix), please add tests for it
|
||||
4. Include proper documentation / docstring and examples to run the feature
|
||||
5. Check the linting
|
||||
6. Ensure that all tests pass
|
||||
7. Submit a pull request
|
||||
|
||||
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
|
||||
|
||||
|
||||
### 📦 Package manager
|
||||
|
||||
We use `poetry` as our package manager. You can install poetry by following the instructions [here](https://python-poetry.org/docs/#installation).
|
||||
|
||||
Please DO NOT use pip or conda to install the dependencies. Instead, use poetry:
|
||||
|
||||
```bash
|
||||
poetry install --all-extras
|
||||
or
|
||||
poetry install --with dev
|
||||
|
||||
#activate
|
||||
|
||||
poetry shell
|
||||
```
|
||||
|
||||
### 📌 Pre-commit
|
||||
|
||||
To ensure our standards, make sure to install pre-commit before star to contribute.
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
### 🧹 Linting
|
||||
|
||||
We use `ruff` to lint our code. You can run the linter by running the following command:
|
||||
|
||||
```bash
|
||||
make lint
|
||||
```
|
||||
|
||||
Make sure that the linter does not report any errors or warnings before submitting a pull request.
|
||||
|
||||
### Code Format with `black`
|
||||
|
||||
We use `black` to reformat the code by running the following command:
|
||||
|
||||
```bash
|
||||
make format
|
||||
```
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
We use `pytest` to test our code. You can run the tests by running the following command:
|
||||
|
||||
```bash
|
||||
poetry run pytest
|
||||
```
|
||||
|
||||
Make sure that all tests pass before submitting a pull request.
|
||||
|
||||
## 🚀 Release Process
|
||||
|
||||
At the moment, the release process is manual. We try to make frequent releases. Usually, we release a new version when we have a new feature or bugfix. A developer with admin rights to the repository will create a new release on GitHub, and then publish the new version to PyPI.
|
||||
@@ -4,7 +4,7 @@ PIP := $(PYTHON) -m pip
|
||||
PROJECT_NAME := embedchain
|
||||
|
||||
# Targets
|
||||
.PHONY: install format lint clean test
|
||||
.PHONY: install format lint clean test ci_lint ci_test
|
||||
|
||||
install:
|
||||
$(PIP) install --upgrade pip
|
||||
@@ -22,3 +22,9 @@ clean:
|
||||
|
||||
test:
|
||||
$(PYTHON) -m pytest
|
||||
|
||||
ci_lint:
|
||||
poetry run ruff .
|
||||
|
||||
ci_test:
|
||||
poetry run pytest
|
||||
|
||||
@@ -4,620 +4,79 @@
|
||||
[](https://discord.gg/6PzXDgEjG5)
|
||||
[](https://twitter.com/embedchain)
|
||||
[](https://embedchain.substack.com/)
|
||||
[](https://colab.research.google.com/drive/138lMWhENGeEu7Q1-6lNbNTHGLZXBBz_B?usp=sharing)
|
||||
|
||||
embedchain is a framework to easily create LLM powered bots over any dataset. If you want a javascript version, check out [embedchain-js](https://github.com/embedchain/embedchainjs)
|
||||
Embedchain is a framework to easily create LLM powered bots over any dataset. If you want a javascript version, check out [embedchain-js](https://github.com/embedchain/embedchainjs)
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Latest Updates](#latest-updates)
|
||||
- [What is embedchain?](#what-is-embedchain)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [App Types](#app-types)
|
||||
- [1. App (uses OpenAI models, paid)](#1-app-uses-openai-models-paid)
|
||||
- [2. OpenSourceApp (uses opensource models, free)](#2-opensourceapp-uses-opensource-models-free)
|
||||
- [3. PersonApp (uses OpenAI models, paid)](#3-personapp-uses-openai-models-paid)
|
||||
- [Add Dataset](#add-dataset)
|
||||
- [Interface Types](#interface-types)
|
||||
- [Query Interface](#query-interface)
|
||||
- [Chat Interface](#chat-interface)
|
||||
- [Format supported](#format-supported)
|
||||
- [Youtube Video](#youtube-video)
|
||||
- [PDF File](#pdf-file)
|
||||
- [Web Page](#web-page)
|
||||
- [Doc File](#doc-file)
|
||||
- [Text](#text)
|
||||
- [QnA Pair](#qna-pair)
|
||||
- [Reusing a Vector DB](#reusing-a-vector-db)
|
||||
- [More Formats coming soon](#more-formats-coming-soon)
|
||||
- [Testing](#testing)
|
||||
- [Advanced](#advanced)
|
||||
- [Configuration](#configuration)
|
||||
- [Example](#example)
|
||||
- [Configs](#configs)
|
||||
- [InitConfig](#initconfig)
|
||||
- [Add Config](#add-config)
|
||||
- [Query Config](#query-config)
|
||||
- [Chat Config](#chat-config)
|
||||
- [Other methods](#other-methods)
|
||||
- [Reset](#reset)
|
||||
- [Count](#count)
|
||||
- [How does it work?](#how-does-it-work)
|
||||
- [Contribution Guidelines](#contribution-guidelines)
|
||||
- [Tech Stack](#tech-stack)
|
||||
- [Team](#team)
|
||||
- [Author](#author)
|
||||
- [Maintainer](#maintainer)
|
||||
- [Citation](#citation)
|
||||
|
||||
# Latest Updates
|
||||
|
||||
- Introduce a new interface called `chat`. It remembers the history (last 5 messages) and can be used to powerful stateful bots. You can use it by calling `.chat` on any app instance. Works for both OpenAI and OpenSourceApp.
|
||||
|
||||
- Introduce a new app type called `OpenSourceApp`. It uses `gpt4all` as the LLM and `sentence transformers` all-MiniLM-L6-v2 as the embedding model. If you use this app, you dont have to pay for anything.
|
||||
|
||||
# What is embedchain?
|
||||
|
||||
Embedchain abstracts the entire process of loading a dataset, chunking it, creating embeddings and then storing in a vector database.
|
||||
|
||||
You can add a single or multiple dataset using `.add` and `.add_local` function and then use `.query` function to find an answer from the added datasets.
|
||||
|
||||
If you want to create a Naval Ravikant bot which has 1 youtube video, 1 book as pdf and 2 of his blog posts, as well as a question and answer pair you supply, all you need to do is add the links to the videos, pdf and blog posts and the QnA pair and embedchain will create a bot for you.
|
||||
|
||||
```python
|
||||
|
||||
from embedchain import App
|
||||
|
||||
naval_chat_bot = App()
|
||||
|
||||
# Embed Online Resources
|
||||
naval_chat_bot.add("youtube_video", "https://www.youtube.com/watch?v=3qHkcs3kG44")
|
||||
naval_chat_bot.add("pdf_file", "https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf")
|
||||
naval_chat_bot.add("web_page", "https://nav.al/feedback")
|
||||
naval_chat_bot.add("web_page", "https://nav.al/agi")
|
||||
|
||||
# Embed Local Resources
|
||||
naval_chat_bot.add_local("qna_pair", ("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."))
|
||||
|
||||
naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?")
|
||||
# answer: Naval argues that humans possess the unique capacity to understand explanations or concepts to the maximum extent possible in this physical reality.
|
||||
```
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Installation
|
||||
|
||||
First make sure that you have the package installed. If not, then install it using `pip`
|
||||
## 🔧 Quick install
|
||||
|
||||
```bash
|
||||
pip install embedchain
|
||||
```
|
||||
|
||||
## Usage
|
||||
## 🔥 Latest
|
||||
|
||||
Creating a chatbot involves 3 steps:
|
||||
- **[2023/07/19]** Released support for 🦙 `llama2` model. Start creating your `llama2` based bots like this:
|
||||
|
||||
- Import the App instance (App Types)
|
||||
- Add Dataset (Add Dataset)
|
||||
- Query or Chat on the dataset and get answers (Interface Types)
|
||||
```python
|
||||
import os
|
||||
|
||||
### App Types
|
||||
from embedchain import Llama2App
|
||||
|
||||
We have three types of App.
|
||||
os.environ['REPLICATE_API_TOKEN'] = "REPLICATE API TOKEN"
|
||||
|
||||
#### 1. App (uses OpenAI models, paid)
|
||||
zuck_bot = Llama2App()
|
||||
|
||||
```python
|
||||
from embedchain import App
|
||||
# Embed your data
|
||||
zuck_bot.add("youtube_video", "https://www.youtube.com/watch?v=Ff4fRgnuFgQ")
|
||||
zuck_bot.add("web_page", "https://en.wikipedia.org/wiki/Mark_Zuckerberg")
|
||||
|
||||
naval_chat_bot = App()
|
||||
```
|
||||
# Nice, your bot is ready now. Start asking questions to your bot.
|
||||
zuck_bot.query("Who is Mark Zuckerberg?")
|
||||
# Answer: Mark Zuckerberg is an American internet entrepreneur and business magnate. He is the co-founder and CEO of Facebook.
|
||||
```
|
||||
|
||||
- `App` uses OpenAI's model, so these are paid models. You will be charged for embedding model usage and LLM usage.
|
||||
|
||||
- `App` uses OpenAI's embedding model to create embeddings for chunks and ChatGPT API as LLM to get answer given the relevant docs. Make sure that you have an OpenAI account and an API key. If you have don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
## 🔍 Demo
|
||||
|
||||
- Once you have the API key, set it in an environment variable called `OPENAI_API_KEY`
|
||||
Try out embedchain in your browser:
|
||||
|
||||
```python
|
||||
import os
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xxxx"
|
||||
```
|
||||
[](https://colab.research.google.com/drive/138lMWhENGeEu7Q1-6lNbNTHGLZXBBz_B?usp=sharing)
|
||||
|
||||
#### 2. OpenSourceApp (uses opensource models, free)
|
||||
## 📖 Documentation
|
||||
|
||||
```python
|
||||
from embedchain import OpenSourceApp
|
||||
The documentation for embedchain can be found at [docs.embedchain.ai](https://docs.embedchain.ai).
|
||||
|
||||
naval_chat_bot = OpenSourceApp()
|
||||
```
|
||||
## 💻 Usage
|
||||
|
||||
- `OpenSourceApp` uses open source embedding and LLM model. It uses `all-MiniLM-L6-v2` from Sentence Transformers library as the embedding model and `gpt4all` as the LLM.
|
||||
Embedchain empowers you to create chatbot models similar to ChatGPT, using your own evolving dataset.
|
||||
|
||||
- Here there is no need to setup any api keys. You just need to install embedchain package and these will get automatically installed.
|
||||
### Queries
|
||||
|
||||
- Once you have imported and instantiated the app, every functionality from here onwards is the same for either type of app.
|
||||
|
||||
#### 3. PersonApp (uses OpenAI models, paid)
|
||||
|
||||
```python
|
||||
from embedchain import PersonApp
|
||||
|
||||
naval_chat_bot = PersonApp("name_of_person_or_character") #Like "Yoda"
|
||||
```
|
||||
|
||||
- `PersonApp` uses OpenAI's model, so these are paid models. You will be charged for embedding model usage and LLM usage.
|
||||
|
||||
- `PersonApp` uses OpenAI's embedding model to create embeddings for chunks and ChatGPT API as LLM to get answer given the relevant docs. Make sure that you have an OpenAI account and an API key. If you have don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
|
||||
- Once you have the API key, set it in an environment variable called `OPENAI_API_KEY`
|
||||
|
||||
```python
|
||||
import os
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xxxx"
|
||||
```
|
||||
|
||||
### Add Dataset
|
||||
|
||||
- This step assumes that you have already created an `app` instance by either using `App` or `OpenSourceApp`. We are calling our app instance as `naval_chat_bot`
|
||||
|
||||
- Now use `.add` function to add any dataset.
|
||||
|
||||
```python
|
||||
|
||||
# naval_chat_bot = App() or
|
||||
# naval_chat_bot = OpenSourceApp()
|
||||
|
||||
# Embed Online Resources
|
||||
naval_chat_bot.add("youtube_video", "https://www.youtube.com/watch?v=3qHkcs3kG44")
|
||||
naval_chat_bot.add("pdf_file", "https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf")
|
||||
naval_chat_bot.add("web_page", "https://nav.al/feedback")
|
||||
naval_chat_bot.add("web_page", "https://nav.al/agi")
|
||||
|
||||
# Embed Local Resources
|
||||
naval_chat_bot.add_local("qna_pair", ("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."))
|
||||
```
|
||||
|
||||
- If there is any other app instance in your script or app, you can change the import as
|
||||
|
||||
```python
|
||||
from embedchain import App as EmbedChainApp
|
||||
from embedchain import OpenSourceApp as EmbedChainOSApp
|
||||
from embedchain import PersonApp as EmbedChainPersonApp
|
||||
|
||||
# or
|
||||
|
||||
from embedchain import App as ECApp
|
||||
from embedchain import OpenSourceApp as ECOSApp
|
||||
from embedchain import PersonApp as ECPApp
|
||||
```
|
||||
|
||||
## Interface Types
|
||||
|
||||
### Query Interface
|
||||
|
||||
- This interface is like a question answering bot. It takes a question and gets the answer. It does not maintain context about the previous chats.
|
||||
|
||||
- To use this, call `.query` function to get the answer for any query.
|
||||
|
||||
```python
|
||||
print(naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?"))
|
||||
# answer: Naval argues that humans possess the unique capacity to understand explanations or concepts to the maximum extent possible in this physical reality.
|
||||
```
|
||||
|
||||
### Chat Interface
|
||||
|
||||
- This interface is chat interface where it remembers previous conversation. Right now it remembers 5 conversation by default.
|
||||
|
||||
- To use this, call `.chat` function to get the answer for any query.
|
||||
|
||||
```python
|
||||
print(naval_chat_bot.chat("How to be happy in life?"))
|
||||
# answer: The most important trick to being happy is to realize happiness is a skill you develop and a choice you make. You choose to be happy, and then you work at it. It's just like building muscles or succeeding at your job. It's about recognizing the abundance and gifts around you at all times.
|
||||
|
||||
print(naval_chat_bot.chat("who is naval ravikant?"))
|
||||
# answer: Naval Ravikant is an Indian-American entrepreneur and investor.
|
||||
|
||||
print(naval_chat_bot.chat("what did the author say about happiness?"))
|
||||
# answer: The author, Naval Ravikant, believes that happiness is a choice you make and a skill you develop. He compares the mind to the body, stating that just as the body can be molded and changed, so can the mind. He emphasizes the importance of being present in the moment and not getting caught up in regrets of the past or worries about the future. By being present and grateful for where you are, you can experience true happiness.
|
||||
```
|
||||
|
||||
### Stream Response
|
||||
|
||||
- You can add config to your query method to stream responses like ChatGPT does. You would require a downstream handler to render the chunk in your desirable format. Supports both OpenAI model and OpenSourceApp.
|
||||
|
||||
- To use this, instantiate a `QueryConfig` or `ChatConfig` object with `stream=True`. Then pass it to the `.chat()` or `.query()` method. The following example iterates through the chunks and prints them as they appear.
|
||||
|
||||
```python
|
||||
app = App()
|
||||
query_config = QueryConfig(stream = True)
|
||||
resp = app.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?", query_config)
|
||||
|
||||
for chunk in resp:
|
||||
print(chunk, end="", flush=True)
|
||||
# answer: Naval argues that humans possess the unique capacity to understand explanations or concepts to the maximum extent possible in this physical reality.
|
||||
```
|
||||
|
||||
## Format supported
|
||||
|
||||
We support the following formats:
|
||||
|
||||
### Youtube Video
|
||||
|
||||
To add any youtube video to your app, use the data_type (first argument to `.add`) as `youtube_video`. Eg:
|
||||
|
||||
```python
|
||||
app.add('youtube_video', 'a_valid_youtube_url_here')
|
||||
```
|
||||
|
||||
### PDF File
|
||||
|
||||
To add any pdf file, use the data_type as `pdf_file`. Eg:
|
||||
|
||||
```python
|
||||
app.add('pdf_file', 'a_valid_url_where_pdf_file_can_be_accessed')
|
||||
```
|
||||
|
||||
Note that we do not support password protected pdfs.
|
||||
|
||||
### Web Page
|
||||
|
||||
To add any web page, use the data_type as `web_page`. Eg:
|
||||
|
||||
```python
|
||||
app.add('web_page', 'a_valid_web_page_url')
|
||||
```
|
||||
|
||||
### Doc File
|
||||
|
||||
To add any doc/docx file, use the data_type as `docx`. Eg:
|
||||
|
||||
```python
|
||||
app.add('docx', 'a_local_docx_file_path')
|
||||
```
|
||||
|
||||
### Text
|
||||
|
||||
To supply your own text, use the data_type as `text` and enter a string. The text is not processed, this can be very versatile. Eg:
|
||||
|
||||
```python
|
||||
app.add_local('text', 'Seek wealth, not money or status. Wealth is having assets that earn while you sleep. Money is how we transfer time and wealth. Status is your place in the social hierarchy.')
|
||||
```
|
||||
|
||||
Note: This is not used in the examples because in most cases you will supply a whole paragraph or file, which did not fit.
|
||||
|
||||
### QnA Pair
|
||||
|
||||
To supply your own QnA pair, use the data_type as `qna_pair` and enter a tuple. Eg:
|
||||
|
||||
```python
|
||||
app.add_local('qna_pair', ("Question", "Answer"))
|
||||
```
|
||||
### Sitemap
|
||||
|
||||
To add a XML site map containing list of all urls, use the data_type as `sitemap` and enter the sitemap url. Eg:
|
||||
|
||||
```python
|
||||
app.add('sitemap', 'a_valid_sitemap_url/sitemap.xml')
|
||||
```
|
||||
|
||||
### Reusing a Vector DB
|
||||
|
||||
Default behavior is to create a persistent vector DB in the directory **./db**. You can split your application into two Python scripts: one to create a local vector DB and the other to reuse this local persistent vector DB. This is useful when you want to index hundreds of documents and separately implement a chat interface.
|
||||
|
||||
Create a local index:
|
||||
|
||||
```python
|
||||
|
||||
from embedchain import App
|
||||
|
||||
naval_chat_bot = App()
|
||||
naval_chat_bot.add("youtube_video", "https://www.youtube.com/watch?v=3qHkcs3kG44")
|
||||
naval_chat_bot.add("pdf_file", "https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf")
|
||||
```
|
||||
|
||||
You can reuse the local index with the same code, but without adding new documents:
|
||||
|
||||
```python
|
||||
|
||||
from embedchain import App
|
||||
|
||||
naval_chat_bot = App()
|
||||
print(naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?"))
|
||||
```
|
||||
|
||||
### More Formats coming soon
|
||||
|
||||
- If you want to add any other format, please create an [issue](https://github.com/embedchain/embedchain/issues) and we will add it to the list of supported formats.
|
||||
|
||||
## Testing
|
||||
|
||||
Before you consume valueable tokens, you should make sure that the embedding you have done works and that it's receiving the correct document from the database.
|
||||
|
||||
For this you can use the `dry_run` method.
|
||||
|
||||
Following the example above, add this to your script:
|
||||
|
||||
```python
|
||||
print(naval_chat_bot.dry_run('Can you tell me who Naval Ravikant is?'))
|
||||
|
||||
'''
|
||||
Use the following pieces of context to answer the query at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
Q: Who is Naval Ravikant?
|
||||
A: Naval Ravikant is an Indian-American entrepreneur and investor.
|
||||
Query: Can you tell me who Naval Ravikant is?
|
||||
Helpful Answer:
|
||||
'''
|
||||
```
|
||||
|
||||
_The embedding is confirmed to work as expected. It returns the right document, even if the question is asked slightly different. No prompt tokens have been consumed._
|
||||
|
||||
**The dry run will still consume tokens to embed your query, but it is only ~1/15 of the prompt.**
|
||||
|
||||
## Colab Notebook and Video Tutorials
|
||||
|
||||
Chinese Colab Tutorial:https://colab.research.google.com/drive/10_7Y0x4YXWVjuhhYwVraGQLpKAatTQTm?usp=sharing
|
||||
|
||||
Chinese Video Tutorial:https://www.bilibili.com/video/BV1YX4y1H7oN
|
||||
|
||||
# Advanced
|
||||
|
||||
## Configuration
|
||||
|
||||
Embedchain is made to work out of the box. However, for advanced users we're also offering configuration options. All of these configuration options are optional and have sane defaults.
|
||||
|
||||
### Example
|
||||
|
||||
Here's the readme example with configuration options.
|
||||
For example, you can use Embedchain to create an Elon Musk bot using the following code:
|
||||
|
||||
```python
|
||||
import os
|
||||
from embedchain import App
|
||||
from embedchain.config import InitConfig, AddConfig, QueryConfig
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
# Example: use your own embedding function
|
||||
config = InitConfig(ef=embedding_functions.OpenAIEmbeddingFunction(
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
organization_id=os.getenv("OPENAI_ORGANIZATION"),
|
||||
model_name="text-embedding-ada-002"
|
||||
))
|
||||
naval_chat_bot = App(config)
|
||||
# Create a bot instance
|
||||
os.environ["OPENAI_API_KEY"] = "YOUR API KEY"
|
||||
elon_bot = App()
|
||||
|
||||
# Example: define your own chunker config for `youtube_video`
|
||||
youtube_add_config = {
|
||||
"chunker": {
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 100,
|
||||
"length_function": len,
|
||||
}
|
||||
}
|
||||
naval_chat_bot.add("youtube_video", "https://www.youtube.com/watch?v=3qHkcs3kG44", AddConfig(**youtube_add_config))
|
||||
# Embed online resources
|
||||
elon_bot.add("web_page", "https://en.wikipedia.org/wiki/Elon_Musk")
|
||||
elon_bot.add("web_page", "https://tesla.com/elon-musk")
|
||||
elon_bot.add("youtube_video", "https://www.youtube.com/watch?v=MxZpaJK74Y4")
|
||||
|
||||
add_config = AddConfig()
|
||||
naval_chat_bot.add("pdf_file", "https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf", add_config)
|
||||
naval_chat_bot.add("web_page", "https://nav.al/feedback", add_config)
|
||||
naval_chat_bot.add("web_page", "https://nav.al/agi", add_config)
|
||||
|
||||
naval_chat_bot.add_local("qna_pair", ("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."), add_config)
|
||||
|
||||
query_config = QueryConfig() # Currently no options
|
||||
print(naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?", query_config))
|
||||
# Query the bot
|
||||
elon_bot.query("How many companies does Elon Musk run?")
|
||||
# Answer: Elon Musk runs four companies: Tesla, SpaceX, Neuralink, and The Boring Company
|
||||
```
|
||||
|
||||
Here's the example of using custom prompt template with `.query`
|
||||
## 🤝 Contributing
|
||||
|
||||
```python
|
||||
from embedchain.config import QueryConfig
|
||||
from embedchain.embedchain import App
|
||||
from string import Template
|
||||
import wikipedia
|
||||
|
||||
einstein_chat_bot = App()
|
||||
|
||||
# Embed Wikipedia page
|
||||
page = wikipedia.page("Albert Einstein")
|
||||
einstein_chat_bot.add("text", page.content)
|
||||
|
||||
# Example: use your own custom template with `$context` and `$query`
|
||||
einstein_chat_template = Template("""
|
||||
You are Albert Einstein, a German-born theoretical physicist,
|
||||
widely ranked among the greatest and most influential scientists of all time.
|
||||
|
||||
Use the following information about Albert Einstein to respond to
|
||||
the human's query acting as Albert Einstein.
|
||||
Context: $context
|
||||
|
||||
Keep the response brief. If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
|
||||
Human: $query
|
||||
Albert Einstein:""")
|
||||
query_config = QueryConfig(einstein_chat_template)
|
||||
queries = [
|
||||
"Where did you complete your studies?",
|
||||
"Why did you win nobel prize?",
|
||||
"Why did you divorce your first wife?",
|
||||
]
|
||||
for query in queries:
|
||||
response = einstein_chat_bot.query(query, query_config)
|
||||
print("Query: ", query)
|
||||
print("Response: ", response)
|
||||
|
||||
# Output
|
||||
# Query: Where did you complete your studies?
|
||||
# Response: I completed my secondary education at the Argovian cantonal school in Aarau, Switzerland.
|
||||
# Query: Why did you win nobel prize?
|
||||
# Response: I won the Nobel Prize in Physics in 1921 for my services to Theoretical Physics, particularly for my discovery of the law of the photoelectric effect.
|
||||
# Query: Why did you divorce your first wife?
|
||||
# Response: We divorced due to living apart for five years.
|
||||
```
|
||||
|
||||
**Client Mode**. By defining a (ChromaDB) server, you can run EmbedChain as a client only.
|
||||
|
||||
```python
|
||||
from embedchain import App
|
||||
config = InitConfig(host="localhost", port="8080")
|
||||
app = App(config)
|
||||
```
|
||||
This is useful for scalability. Say you have EmbedChain behind an API with multiple workers. If you separate clients and server, all clients can connect to the server, which only has to keep one instance of the database in memory. You also don't have to worry about replication.
|
||||
|
||||
To run a chroma db server, run `git clone https://github.com/chroma-core/chroma.git`, navigate to the directory (`cd chroma`) and then start the server with `docker-compose up -d --build`.
|
||||
|
||||
### Configs
|
||||
|
||||
This section describes all possible config options.
|
||||
|
||||
#### **InitConfig**
|
||||
|
||||
|option|description|type|default|
|
||||
|---|---|---|---|
|
||||
|log_level|log level|string|WARNING|
|
||||
|ef|embedding function|chromadb.utils.embedding_functions|{text-embedding-ada-002}|
|
||||
|db|vector database (experimental)|BaseVectorDB|ChromaDB|
|
||||
|host|hostname for (Chroma) DB server|string|None|
|
||||
|port|port number for (Chroma) DB server|string, int|None|
|
||||
|
||||
#### **Add Config**
|
||||
|
||||
|option|description|type|default|
|
||||
|---|---|---|---|
|
||||
|chunker|chunker config|ChunkerConfig|Default values for chunker depends on the `data_type`. Please refer [ChunkerConfig](#chunker-config)|
|
||||
|loader|loader config|LoaderConfig|None|
|
||||
|
||||
##### **Chunker Config**
|
||||
|
||||
|option|description|type|default|
|
||||
|---|---|---|---|
|
||||
|chunk_size|Maximum size of chunks to return|int|Default value for various `data_type` mentioned below|
|
||||
|chunk_overlap|Overlap in characters between chunks|int|Default value for various `data_type` mentioned below|
|
||||
|length_function|Function that measures the length of given chunks|typing.Callable|Default value for various `data_type` mentioned below|
|
||||
|
||||
Default values of chunker config parameters for different `data_type`:
|
||||
|
||||
|data_type|chunk_size|chunk_overlap|length_function|
|
||||
|---|---|---|---|
|
||||
|docx|1000|0|len|
|
||||
|text|300|0|len|
|
||||
|qna_pair|300|0|len|
|
||||
|web_page|500|0|len|
|
||||
|pdf_file|1000|0|len|
|
||||
|youtube_video|2000|0|len|
|
||||
|
||||
##### **Loader Config**
|
||||
|
||||
_coming soon_
|
||||
|
||||
#### **Query Config**
|
||||
|
||||
|option|description|type|default|
|
||||
|---|---|---|---|
|
||||
|number_documents|number of documents to be retrieved as context|int|1|
|
||||
|template|custom template for prompt|Template|Template("Use the following pieces of context to answer the query at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. \$context Query: $query Helpful Answer:")|
|
||||
|history|include conversation history from your client or database|any (recommendation: list[str])|None
|
||||
|stream|control if response is streamed back to the user|bool|False|
|
||||
|model|OpenAI model|string|gpt-3.5-turbo-0613|
|
||||
|temperature|creativity of the model (0-1)|float|0|
|
||||
|max_tokens|limit maximum tokens used|int|1000|
|
||||
|top_p|diversity of words used by the model (0-1)|float|1|
|
||||
|
||||
#### **Chat Config**
|
||||
|
||||
All options for query and...
|
||||
|
||||
_coming soon_
|
||||
|
||||
history is handled automatically, the config option is not supported.
|
||||
|
||||
## Other methods
|
||||
|
||||
### Reset
|
||||
|
||||
Resets the database and deletes all embeddings. Irreversible. Requires reinitialization afterwards.
|
||||
|
||||
```python
|
||||
app.reset()
|
||||
```
|
||||
|
||||
### Count
|
||||
|
||||
Counts the number of embeddings (chunks) in the database.
|
||||
|
||||
```python
|
||||
print(app.count())
|
||||
# returns: 481
|
||||
```
|
||||
|
||||
# How does it work?
|
||||
|
||||
Creating a chat bot over any dataset needs the following steps to happen
|
||||
|
||||
- load the data
|
||||
- create meaningful chunks
|
||||
- create embeddings for each chunk
|
||||
- store the chunks in vector database
|
||||
|
||||
Whenever a user asks any query, following process happens to find the answer for the query
|
||||
|
||||
- create the embedding for query
|
||||
- find similar documents for this query from vector database
|
||||
- pass similar documents as context to LLM to get the final answer.
|
||||
|
||||
The process of loading the dataset and then querying involves multiple steps and each steps has nuances of it is own.
|
||||
|
||||
- How should I chunk the data? What is a meaningful chunk size?
|
||||
- How should I create embeddings for each chunk? Which embedding model should I use?
|
||||
- How should I store the chunks in vector database? Which vector database should I use?
|
||||
- Should I store meta data along with the embeddings?
|
||||
- How should I find similar documents for a query? Which ranking model should I use?
|
||||
|
||||
These questions may be trivial for some but for a lot of us, it needs research, experimentation and time to find out the accurate answers.
|
||||
|
||||
embedchain is a framework which takes care of all these nuances and provides a simple interface to create bots over any dataset.
|
||||
|
||||
In the first release, we are making it easier for anyone to get a chatbot over any dataset up and running in less than a minute. All you need to do is create an app instance, add the data sets using `.add` function and then use `.query` function to get the relevant answer.
|
||||
|
||||
# Contribution Guidelines
|
||||
|
||||
Thank you for your interest in contributing to the EmbedChain project! We welcome your ideas and contributions to help improve the project. Please follow the instructions below to get started:
|
||||
|
||||
1. **Fork the repository**: Click on the "Fork" button at the top right corner of this repository page. This will create a copy of the repository in your own GitHub account.
|
||||
|
||||
2. **Install the required dependencies**: Ensure that you have the necessary dependencies installed in your Python environment. You can do this by running the following command:
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
3. **Make changes in the code**: Create a new branch in your forked repository and make your desired changes in the codebase.
|
||||
4. **Format code**: Before creating a pull request, it's important to ensure that your code follows our formatting guidelines. Run the following commands to format the code:
|
||||
|
||||
```bash
|
||||
make lint format
|
||||
```
|
||||
|
||||
5. **Create a pull request**: When you are ready to contribute your changes, submit a pull request to the EmbedChain repository. Provide a clear and descriptive title for your pull request, along with a detailed description of the changes you have made.
|
||||
|
||||
# Tech Stack
|
||||
|
||||
embedchain is built on the following stack:
|
||||
|
||||
- [Langchain](https://github.com/hwchase17/langchain) as an LLM framework to load, chunk and index data
|
||||
- [OpenAI's Ada embedding model](https://platform.openai.com/docs/guides/embeddings) to create embeddings
|
||||
- [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/gpt/chat-completions-api) as LLM to get answers given the context
|
||||
- [Chroma](https://github.com/chroma-core/chroma) as the vector database to store embeddings
|
||||
- [gpt4all](https://github.com/nomic-ai/gpt4all) as an open source LLM
|
||||
- [sentence-transformers](https://huggingface.co/sentence-transformers) as open source embedding model
|
||||
|
||||
# Team
|
||||
|
||||
## Author
|
||||
|
||||
- Taranjeet Singh ([@taranjeetio](https://twitter.com/taranjeetio))
|
||||
|
||||
## Maintainer
|
||||
|
||||
- [cachho](https://github.com/cachho)
|
||||
Contributions are welcome! Please check out the issues on the repository, and feel free to open a pull request.
|
||||
For more information, please see the [contributing guidelines](CONTRIBUTING.md).
|
||||
|
||||
## Citation
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Contributing to embedchain docs
|
||||
|
||||
|
||||
### 👩💻 Development
|
||||
|
||||
Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command
|
||||
|
||||
```
|
||||
npm i -g mintlify
|
||||
```
|
||||
|
||||
Run the following command at the root of your documentation (where mint.json is)
|
||||
|
||||
```
|
||||
mintlify dev
|
||||
```
|
||||
|
||||
### 😎 Publishing Changes
|
||||
|
||||
Changes will be deployed to production automatically after your PR is merged to the main branch.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
- Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies.
|
||||
- Page loads as a 404 - Make sure you are running in a folder with `mint.json`
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: '➕ Adding Data'
|
||||
---
|
||||
|
||||
## Add Dataset
|
||||
|
||||
- This step assumes that you have already created an `app` instance by either using `App`, `OpenSourceApp` or `CustomApp`. We are calling our app instance as `naval_chat_bot` 🤖
|
||||
|
||||
- Now use `.add()` function to add any dataset.
|
||||
|
||||
```python
|
||||
# naval_chat_bot = App() or
|
||||
# naval_chat_bot = OpenSourceApp()
|
||||
|
||||
# Embed Online Resources
|
||||
naval_chat_bot.add("youtube_video", "https://www.youtube.com/watch?v=3qHkcs3kG44")
|
||||
naval_chat_bot.add("pdf_file", "https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf")
|
||||
naval_chat_bot.add("web_page", "https://nav.al/feedback")
|
||||
naval_chat_bot.add("web_page", "https://nav.al/agi")
|
||||
|
||||
# Embed Local Resources
|
||||
naval_chat_bot.add_local("qna_pair", ("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."))
|
||||
```
|
||||
|
||||
The possible formats to add data can be found on the [Supported Data Formats](/advanced/data_types) page.
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: '📱 App types'
|
||||
---
|
||||
|
||||
## App Types
|
||||
|
||||
We have three types of App.
|
||||
|
||||
### App
|
||||
|
||||
```python
|
||||
from embedchain import App
|
||||
app = App()
|
||||
```
|
||||
|
||||
- `App` uses OpenAI's model, so these are paid models. 💸 You will be charged for embedding model usage and LLM usage.
|
||||
- `App` uses OpenAI's embedding model to create embeddings for chunks and ChatGPT API as LLM to get answer given the relevant docs. Make sure that you have an OpenAI account and an API key. If you have don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
- `App` is opinionated. It uses the best embedding model and LLM on the market.
|
||||
- Once you have the API key, set it in an environment variable called `OPENAI_API_KEY`
|
||||
|
||||
```python
|
||||
import os
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xxxx"
|
||||
```
|
||||
|
||||
### Llama2App
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
from embedchain import Llama2App
|
||||
|
||||
os.environ['REPLICATE_API_TOKEN'] = "REPLICATE API TOKEN"
|
||||
|
||||
zuck_bot = Llama2App()
|
||||
|
||||
# Embed your data
|
||||
zuck_bot.add("youtube_video", "https://www.youtube.com/watch?v=Ff4fRgnuFgQ")
|
||||
zuck_bot.add("web_page", "https://en.wikipedia.org/wiki/Mark_Zuckerberg")
|
||||
|
||||
# Nice, your bot is ready now. Start asking questions to your bot.
|
||||
zuck_bot.query("Who is Mark Zuckerberg?")
|
||||
# Answer: Mark Zuckerberg is an American internet entrepreneur and business magnate. He is the co-founder and CEO of Facebook. Born in 1984, he dropped out of Harvard University to focus on his social media platform, which has since grown to become one of the largest and most influential technology companies in the world.
|
||||
|
||||
# Enable web search for your bot
|
||||
zuck_bot.online = True # enable internet access for the bot
|
||||
zuck_bot.query("Who owns the new threads app and when it was founded?")
|
||||
# Answer: Based on the context provided, the new Threads app is owned by Meta, the parent company of Facebook, Instagram, and WhatsApp.
|
||||
```
|
||||
|
||||
- `Llama2App` uses Replicate's LLM model, so these are paid models. You can get the `REPLICATE_API_TOKEN` by registering on [their website](https://replicate.com/account).
|
||||
- `Llama2App` uses OpenAI's embedding model to create embeddings for chunks. Make sure that you have an OpenAI account and an API key. If you have don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
|
||||
|
||||
### OpenSourceApp
|
||||
|
||||
```python
|
||||
from embedchain import OpenSourceApp
|
||||
app = OpenSourceApp()
|
||||
```
|
||||
|
||||
- `OpenSourceApp` uses open source embedding and LLM model. It uses `all-MiniLM-L6-v2` from Sentence Transformers library as the embedding model and `gpt4all` as the LLM.
|
||||
- Here there is no need to setup any api keys. You just need to install embedchain package and these will get automatically installed. 📦
|
||||
- Once you have imported and instantiated the app, every functionality from here onwards is the same for either type of app. 📚
|
||||
- `OpenSourceApp` is opinionated. It uses the best open source embedding model and LLM on the market.
|
||||
|
||||
### CustomApp
|
||||
|
||||
```python
|
||||
from embedchain import CustomApp
|
||||
from embedchain.config import CustomAppConfig
|
||||
from embedchain.models import Providers, EmbeddingFunctions
|
||||
|
||||
config = CustomAppConfig(embedding_fn=EmbeddingFunctions.OPENAI, provider=Providers.OPENAI)
|
||||
app = CustomApp(config)
|
||||
```
|
||||
|
||||
- `CustomApp` is not opinionated.
|
||||
- Configuration required. It's for advanced users who want to mix and match different embedding models and LLMs. Configuration required.
|
||||
- while it's doing that, it's still providing abstractions through `Providers`.
|
||||
- paid and free/open source providers included.
|
||||
- Once you have imported and instantiated the app, every functionality from here onwards is the same for either type of app. 📚
|
||||
- Following providers are available for an LLM
|
||||
- OPENAI
|
||||
- ANTHPROPIC
|
||||
- VERTEX_AI
|
||||
- GPT4ALL
|
||||
- Following embedding functions are available for an embedding function
|
||||
- OPENAI
|
||||
- HUGGING_FACE
|
||||
- VERTEX_AI
|
||||
- GPT4ALL
|
||||
|
||||
|
||||
### PersonApp
|
||||
|
||||
```python
|
||||
from embedchain import PersonApp
|
||||
naval_chat_bot = PersonApp("name_of_person_or_character") #Like "Yoda"
|
||||
```
|
||||
|
||||
- `PersonApp` uses OpenAI's model, so these are paid models. 💸 You will be charged for embedding model usage and LLM usage.
|
||||
- `PersonApp` uses OpenAI's embedding model to create embeddings for chunks and ChatGPT API as LLM to get answer given the relevant docs. Make sure that you have an OpenAI account and an API key. If you have don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
- Once you have the API key, set it in an environment variable called `OPENAI_API_KEY`
|
||||
|
||||
```python
|
||||
import os
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xxxx"
|
||||
```
|
||||
|
||||
#### Compatibility with other apps
|
||||
|
||||
- If there is any other app instance in your script or app, you can change the import as
|
||||
|
||||
```python
|
||||
from embedchain import App as EmbedChainApp
|
||||
from embedchain import OpenSourceApp as EmbedChainOSApp
|
||||
from embedchain import PersonApp as EmbedChainPersonApp
|
||||
|
||||
# or
|
||||
|
||||
from embedchain import App as ECApp
|
||||
from embedchain import OpenSourceApp as ECOSApp
|
||||
from embedchain import PersonApp as ECPApp
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: '⚙️ Custom configurations'
|
||||
---
|
||||
|
||||
Embedchain is made to work out of the box. However, for advanced users we're also offering configuration options. All of these configuration options are optional and have sane defaults.
|
||||
|
||||
## Examples
|
||||
|
||||
### General
|
||||
|
||||
Here's the readme example with configuration options.
|
||||
|
||||
```python
|
||||
import os
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig, AddConfig, QueryConfig, ChunkerConfig
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
# Example: set the log level for debugging
|
||||
config = AppConfig(log_level="DEBUG")
|
||||
naval_chat_bot = App(config)
|
||||
|
||||
# Example: define your own chunker config for `youtube_video`
|
||||
chunker_config = ChunkerConfig(chunk_size=1000, chunk_overlap=100, length_function=len)
|
||||
naval_chat_bot.add("youtube_video", "https://www.youtube.com/watch?v=3qHkcs3kG44", AddConfig(chunker=chunker_config))
|
||||
|
||||
add_config = AddConfig()
|
||||
naval_chat_bot.add("pdf_file", "https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf", add_config)
|
||||
naval_chat_bot.add("web_page", "https://nav.al/feedback", add_config)
|
||||
naval_chat_bot.add("web_page", "https://nav.al/agi", add_config)
|
||||
|
||||
naval_chat_bot.add_local("qna_pair", ("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."), add_config)
|
||||
|
||||
query_config = QueryConfig()
|
||||
print(naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?", query_config))
|
||||
```
|
||||
|
||||
### Custom prompt template
|
||||
|
||||
Here's the example of using custom prompt template with `.query`
|
||||
|
||||
```python
|
||||
from embedchain.config import QueryConfig
|
||||
from embedchain.embedchain import App
|
||||
from string import Template
|
||||
import wikipedia
|
||||
|
||||
einstein_chat_bot = App()
|
||||
|
||||
# Embed Wikipedia page
|
||||
page = wikipedia.page("Albert Einstein")
|
||||
einstein_chat_bot.add("text", page.content)
|
||||
|
||||
# Example: use your own custom template with `$context` and `$query`
|
||||
einstein_chat_template = Template("""
|
||||
You are Albert Einstein, a German-born theoretical physicist,
|
||||
widely ranked among the greatest and most influential scientists of all time.
|
||||
|
||||
Use the following information about Albert Einstein to respond to
|
||||
the human's query acting as Albert Einstein.
|
||||
Context: $context
|
||||
|
||||
Keep the response brief. If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
|
||||
Human: $query
|
||||
Albert Einstein:""")
|
||||
query_config = QueryConfig(einstein_chat_template)
|
||||
queries = [
|
||||
"Where did you complete your studies?",
|
||||
"Why did you win nobel prize?",
|
||||
"Why did you divorce your first wife?",
|
||||
]
|
||||
for query in queries:
|
||||
response = einstein_chat_bot.query(query, query_config)
|
||||
print("Query: ", query)
|
||||
print("Response: ", response)
|
||||
|
||||
# Output
|
||||
# Query: Where did you complete your studies?
|
||||
# Response: I completed my secondary education at the Argovian cantonal school in Aarau, Switzerland.
|
||||
# Query: Why did you win nobel prize?
|
||||
# Response: I won the Nobel Prize in Physics in 1921 for my services to Theoretical Physics, particularly for my discovery of the law of the photoelectric effect.
|
||||
# Query: Why did you divorce your first wife?
|
||||
# Response: We divorced due to living apart for five years.
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: '📋 Supported data formats'
|
||||
---
|
||||
|
||||
Embedchain supports following data formats:
|
||||
|
||||
### Youtube video
|
||||
|
||||
To add any youtube video to your app, use the data_type (first argument to `.add()` method) as `youtube_video`. Eg:
|
||||
|
||||
```python
|
||||
app.add('youtube_video', 'a_valid_youtube_url_here')
|
||||
```
|
||||
|
||||
### PDF file
|
||||
|
||||
To add any pdf file, use the data_type as `pdf_file`. Eg:
|
||||
|
||||
```python
|
||||
app.add('pdf_file', 'a_valid_url_where_pdf_file_can_be_accessed')
|
||||
```
|
||||
|
||||
Note that we do not support password protected pdfs.
|
||||
|
||||
### Web page
|
||||
|
||||
To add any web page, use the data_type as `web_page`. Eg:
|
||||
|
||||
```python
|
||||
app.add('web_page', 'a_valid_web_page_url')
|
||||
```
|
||||
|
||||
### Sitemap
|
||||
|
||||
Add all web pages from an xml-sitemap. Filters non-text files. Use the data_type as `sitemap`. Eg:
|
||||
|
||||
```python
|
||||
app.add('sitemap', 'https://example.com/sitemap.xml')
|
||||
```
|
||||
|
||||
### Doc file
|
||||
|
||||
To add any doc/docx file, use the data_type as `docx`. Eg:
|
||||
|
||||
```python
|
||||
app.add('docx', 'a_local_docx_file_path')
|
||||
```
|
||||
|
||||
### Code documentation website loader
|
||||
|
||||
To add any code documentation website as a loader, use the data_type as `docs_site`. Eg:
|
||||
|
||||
```python
|
||||
app.add("docs_site", "https://docs.embedchain.ai/")
|
||||
```
|
||||
|
||||
### Text
|
||||
|
||||
To supply your own text, use the data_type as `text` and enter a string. The text is not processed, this can be very versatile. Eg:
|
||||
|
||||
```python
|
||||
app.add_local('text', 'Seek wealth, not money or status. Wealth is having assets that earn while you sleep. Money is how we transfer time and wealth. Status is your place in the social hierarchy.')
|
||||
```
|
||||
|
||||
Note: This is not used in the examples because in most cases you will supply a whole paragraph or file, which did not fit.
|
||||
|
||||
### QnA pair
|
||||
|
||||
To supply your own QnA pair, use the data_type as `qna_pair` and enter a tuple. Eg:
|
||||
|
||||
```python
|
||||
app.add_local('qna_pair', ("Question", "Answer"))
|
||||
```
|
||||
|
||||
## Reusing a vector database
|
||||
|
||||
Default behavior is to create a persistent vector DB in the directory **./db**. You can split your application into two Python scripts: one to create a local vector DB and the other to reuse this local persistent vector DB. This is useful when you want to index hundreds of documents and separately implement a chat interface.
|
||||
|
||||
Create a local index:
|
||||
|
||||
```python
|
||||
from embedchain import App
|
||||
|
||||
naval_chat_bot = App()
|
||||
naval_chat_bot.add("youtube_video", "https://www.youtube.com/watch?v=3qHkcs3kG44")
|
||||
naval_chat_bot.add("pdf_file", "https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf")
|
||||
```
|
||||
|
||||
You can reuse the local index with the same code, but without adding new documents:
|
||||
|
||||
```python
|
||||
from embedchain import App
|
||||
|
||||
naval_chat_bot = App()
|
||||
print(naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?"))
|
||||
```
|
||||
|
||||
### More formats (coming soon!)
|
||||
|
||||
- If you want to add any other format, please create an [issue](https://github.com/embedchain/embedchain/issues) and we will add it to the list of supported formats.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: '🤝 Interface types'
|
||||
---
|
||||
|
||||
## Interface Types
|
||||
|
||||
The embedchain app exposes the following methods.
|
||||
|
||||
### Query Interface
|
||||
|
||||
- This interface is like a question answering bot. It takes a question and gets the answer. It does not maintain context about the previous chats.❓
|
||||
|
||||
- To use this, call `.query()` function to get the answer for any query.
|
||||
|
||||
```python
|
||||
print(naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?"))
|
||||
# answer: Naval argues that humans possess the unique capacity to understand explanations or concepts to the maximum extent possible in this physical reality.
|
||||
```
|
||||
|
||||
### Chat Interface
|
||||
|
||||
- This interface is chat interface where it remembers previous conversation. Right now it remembers 5 conversation by default. 💬
|
||||
|
||||
- To use this, call `.chat` function to get the answer for any query.
|
||||
|
||||
```python
|
||||
print(naval_chat_bot.chat("How to be happy in life?"))
|
||||
# answer: The most important trick to being happy is to realize happiness is a skill you develop and a choice you make. You choose to be happy, and then you work at it. It's just like building muscles or succeeding at your job. It's about recognizing the abundance and gifts around you at all times.
|
||||
|
||||
print(naval_chat_bot.chat("who is naval ravikant?"))
|
||||
# answer: Naval Ravikant is an Indian-American entrepreneur and investor.
|
||||
|
||||
print(naval_chat_bot.chat("what did the author say about happiness?"))
|
||||
# answer: The author, Naval Ravikant, believes that happiness is a choice you make and a skill you develop. He compares the mind to the body, stating that just as the body can be molded and changed, so can the mind. He emphasizes the importance of being present in the moment and not getting caught up in regrets of the past or worries about the future. By being present and grateful for where you are, you can experience true happiness.
|
||||
```
|
||||
|
||||
#### Dry Run
|
||||
|
||||
Dry Run is an option in the `query` and `chat` methods that allows the user to not send their constructed prompt to the LLM, to save money. It's used for [testing](/advanced/testing#dry-run).
|
||||
|
||||
|
||||
### Stream Response
|
||||
|
||||
- You can add config to your query method to stream responses like ChatGPT does. You would require a downstream handler to render the chunk in your desirable format. Supports both OpenAI model and OpenSourceApp. 📊
|
||||
|
||||
- To use this, instantiate a `QueryConfig` or `ChatConfig` object with `stream=True`. Then pass it to the `.chat()` or `.query()` method. The following example iterates through the chunks and prints them as they appear.
|
||||
|
||||
```python
|
||||
app = App()
|
||||
query_config = QueryConfig(stream = True)
|
||||
resp = app.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?", query_config)
|
||||
|
||||
for chunk in resp:
|
||||
print(chunk, end="", flush=True)
|
||||
# answer: Naval argues that humans possess the unique capacity to understand explanations or concepts to the maximum extent possible in this physical reality.
|
||||
```
|
||||
|
||||
### Other Methods
|
||||
|
||||
#### Reset
|
||||
|
||||
Resets the database and deletes all embeddings. Irreversible. Requires reinitialization afterwards.
|
||||
|
||||
```python
|
||||
app.reset()
|
||||
```
|
||||
|
||||
#### Count
|
||||
|
||||
Counts the number of embeddings (chunks) in the database.
|
||||
|
||||
```python
|
||||
print(app.count())
|
||||
# returns: 481
|
||||
```
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: '🔍 Query configurations'
|
||||
---
|
||||
|
||||
## AppConfig
|
||||
|
||||
| option | description | type | default |
|
||||
|-------------|-----------------------|---------------------------------|------------------------|
|
||||
| log_level | log level | string | WARNING |
|
||||
| embedding_fn| embedding function | chromadb.utils.embedding_functions | \{text-embedding-ada-002\} |
|
||||
| db | vector database (experimental) | BaseVectorDB | ChromaDB |
|
||||
|
||||
|
||||
## AddConfig
|
||||
|
||||
|option|description|type|default|
|
||||
|---|---|---|---|
|
||||
|chunker|chunker config|ChunkerConfig|Default values for chunker depends on the `data_type`. Please refer [ChunkerConfig](#chunker-config)|
|
||||
|loader|loader config|LoaderConfig|None|
|
||||
|
||||
Yes, you are passing `ChunkerConfig` to `AddConfig`, like so:
|
||||
|
||||
```python
|
||||
chunker_config = ChunkerConfig(chunk_size=100)
|
||||
add_config = AddConfig(chunker=chunker_config)
|
||||
app.add_local("text", "lorem ipsum", config=add_config)
|
||||
```
|
||||
|
||||
### ChunkerConfig
|
||||
|
||||
|option|description|type|default|
|
||||
|---|---|---|---|
|
||||
|chunk_size|Maximum size of chunks to return|int|Default value for various `data_type` mentioned below|
|
||||
|chunk_overlap|Overlap in characters between chunks|int|Default value for various `data_type` mentioned below|
|
||||
|length_function|Function that measures the length of given chunks|typing.Callable|Default value for various `data_type` mentioned below|
|
||||
|
||||
Default values of chunker config parameters for different `data_type`:
|
||||
|
||||
|data_type|chunk_size|chunk_overlap|length_function|
|
||||
|---|---|---|---|
|
||||
|docx|1000|0|len|
|
||||
|text|300|0|len|
|
||||
|qna_pair|300|0|len|
|
||||
|web_page|500|0|len|
|
||||
|pdf_file|1000|0|len|
|
||||
|youtube_video|2000|0|len|
|
||||
|docs_site|500|50|len|
|
||||
|
||||
### LoaderConfig
|
||||
|
||||
_coming soon_
|
||||
|
||||
## QueryConfig
|
||||
|
||||
|option|description|type|default|
|
||||
|---|---|---|---|
|
||||
|template|custom template for prompt|Template|Template("Use the following pieces of context to answer the query at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. \$context Query: \$query Helpful Answer:")|
|
||||
|history|include conversation history from your client or database|any (recommendation: list[str])|None
|
||||
|stream|control if response is streamed back to the user|bool|False|
|
||||
|
||||
## ChatConfig
|
||||
|
||||
All options for query and...
|
||||
|
||||
_coming soon_
|
||||
|
||||
History is handled automatically, the config option is not supported.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: '🎪 Community showcase'
|
||||
---
|
||||
|
||||
Embedchain community has been super active in creating demos on top of Embedchain. On this page, we showcase all the apps, blogs, videos, and tutorials created by the community. ❤️
|
||||
|
||||
## Apps
|
||||
|
||||
### Open Source
|
||||
|
||||
- [Discord Bot for LLM chat](https://github.com/Reidond/discord_bots_playground/tree/c8b0c36541e4b393782ee506804c4b6962426dd6/python/chat-channel-bot) by Reidond
|
||||
- [EmbedChain-Streamlit-Docker App](https://github.com/amjadraza/embedchain-streamlit-app) by amjadraza
|
||||
- [Harry Potter Philosphers Stone Bot](https://github.com/vinayak-kempawad/Harry_Potter_Philosphers_Stone_Bot/) by Vinayak Kempawad, ([linkedin post](https://www.linkedin.com/feed/update/urn:li:activity:7080907532155686912/))
|
||||
- [LLM bot trained on own messages](https://github.com/Harin329/harinBot) by Hao Wu
|
||||
|
||||
### Closed Source
|
||||
|
||||
- [Taobot.io](https://taobot.io) - chatbot & knowledgebase hybrid by [cachho](https://github.com/cachho)
|
||||
|
||||
## Templates
|
||||
|
||||
### Replit
|
||||
- [Embedchain Chat Bot](https://replit.com/@taranjeet1/Embedchain-Chat-Bot) by taranjeetio
|
||||
- [Embedchain Memory Chat Bot Template](https://replit.com/@taranjeetio/Embedchain-Memory-Chat-Bot-Template) by taranjeetio
|
||||
|
||||
## Posts
|
||||
|
||||
### Blogs
|
||||
|
||||
- [Customer Service LINE Bot](https://www.evanlin.com/langchain-embedchain/)
|
||||
|
||||
### LinkedIn
|
||||
|
||||
- [What is embedchain](https://www.linkedin.com/posts/activity-7079393104423698432-wRyi/) by Rithesh Sreenivasan
|
||||
- [Building a chatbot with EmbedChain](https://www.linkedin.com/posts/activity-7078434598984060928-Zdso/) by Lior Sinclair
|
||||
- [Making chatbot without vs with embedchain](https://www.linkedin.com/posts/kalyanksnlp_llms-chatbots-langchain-activity-7077453416221863936-7N1L/) by Kalyan KS
|
||||
|
||||
### Twitter
|
||||
|
||||
- [What is embedchain](https://twitter.com/AlphaSignalAI/status/1672668574450847745) by Lior
|
||||
- [Building a chatbot with Embedchain](https://twitter.com/Saboo_Shubham_/status/1673537044419686401) by Shubham Saboo
|
||||
|
||||
## Videos
|
||||
|
||||
- [embedChain Create LLM powered bots over any dataset Python Demo Tesla Neurallink Chatbot Example](https://www.youtube.com/watch?v=bJqAn22a6Gc) by Rithesh Sreenivasan
|
||||
- [Embedchain - NEW 🔥 Langchain BABY to build LLM Bots](https://www.youtube.com/watch?v=qj_GNQ06I8o) by 1littlecoder
|
||||
- [EmbedChain -- NEW!: Build LLM-Powered Bots with Any Dataset](https://www.youtube.com/watch?v=XmaBezzGHu4) by DataInsightEdge
|
||||
- [Chat With Your PDFs in less than 10 lines of code! EMBEDCHAIN tutorial](https://www.youtube.com/watch?v=1ugkcsAcw44) by Phani Reddy
|
||||
- [How To Create A Custom Knowledge AI Powered Bot | Install + How To Use](https://www.youtube.com/watch?v=VfCrIiAst-c) by The Ai Solopreneur
|
||||
- [Build Custom Chatbot in 6 min with this Framework [Beginner Friendly]](https://www.youtube.com/watch?v=-8HxOpaFySM) by Maya Akim
|
||||
- [embedchain-streamlit-app](https://www.youtube.com/watch?v=3-9GVd-3v74) by Amjad Raza
|
||||
|
||||
## Mentions
|
||||
|
||||
### Github repos
|
||||
|
||||
- [awesome-ChatGPT-repositories](https://github.com/taishi-i/awesome-ChatGPT-repositories)
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: '🧪 Testing'
|
||||
---
|
||||
|
||||
## Methods for testing
|
||||
|
||||
### Dry Run
|
||||
|
||||
Before you consume valueable tokens, you should make sure that the embedding you have done works and that it's receiving the correct document from the database.
|
||||
|
||||
For this you can use the `dry_run` option in your `query` or `chat` method.
|
||||
|
||||
Following the example above, add this to your script:
|
||||
|
||||
```python
|
||||
print(naval_chat_bot.query('Can you tell me who Naval Ravikant is?', dry_run=True))
|
||||
|
||||
'''
|
||||
Use the following pieces of context to answer the query at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
Q: Who is Naval Ravikant?
|
||||
A: Naval Ravikant is an Indian-American entrepreneur and investor.
|
||||
Query: Can you tell me who Naval Ravikant is?
|
||||
Helpful Answer:
|
||||
'''
|
||||
```
|
||||
|
||||
_The embedding is confirmed to work as expected. It returns the right document, even if the question is asked slightly different. No prompt tokens have been consumed._
|
||||
|
||||
**The dry run will still consume tokens to embed your query, but it is only ~1/15 of the prompt.**
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: '👨💻 Development'
|
||||
description: 'Contribute to Embedchain framework development'
|
||||
---
|
||||
|
||||
Thank you for your interest in contributing to the EmbedChain project! We welcome your ideas and contributions to help improve the project. Please follow the instructions below to get started:
|
||||
|
||||
1. **Fork the repository**: Click on the "Fork" button at the top right corner of this repository page. This will create a copy of the repository in your own GitHub account.
|
||||
|
||||
2. **Install the required dependencies**: Ensure that you have the necessary dependencies installed in your Python environment. You can do this by running the following command:
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
3. **Make changes in the code**: Create a new branch in your forked repository and make your desired changes in the codebase.
|
||||
4. **Format code**: Before creating a pull request, it's important to ensure that your code follows our formatting guidelines. Run the following commands to format the code:
|
||||
|
||||
```bash
|
||||
make lint format
|
||||
```
|
||||
|
||||
5. **Create a pull request**: When you are ready to contribute your changes, submit a pull request to the EmbedChain repository. Provide a clear and descriptive title for your pull request, along with a detailed description of the changes you have made.
|
||||
|
||||
# Tech Stack
|
||||
|
||||
embedchain is built on the following stack:
|
||||
|
||||
- [Langchain](https://github.com/hwchase17/langchain) as an LLM framework to load, chunk and index data
|
||||
- [OpenAI's Ada embedding model](https://platform.openai.com/docs/guides/embeddings) to create embeddings
|
||||
- [OpenAI's ChatGPT API](https://platform.openai.com/docs/guides/gpt/chat-completions-api) as LLM to get answers given the context
|
||||
- [Chroma](https://github.com/chroma-core/chroma) as the vector database to store embeddings
|
||||
- [gpt4all](https://github.com/nomic-ai/gpt4all) as an open source LLM
|
||||
- [sentence-transformers](https://huggingface.co/sentence-transformers) as open source embedding model
|
||||
|
||||
## Team
|
||||
|
||||
### Author
|
||||
|
||||
- Taranjeet Singh ([@taranjeetio](https://twitter.com/taranjeetio))
|
||||
|
||||
### Maintainer
|
||||
|
||||
- Deshraj Yadav ([@deshrajdry](https://twitter.com/taranjeetio))
|
||||
- [cachho](https://github.com/cachho)
|
||||
|
||||
### Citation
|
||||
|
||||
If you utilize this repository, please consider citing it with:
|
||||
|
||||
```
|
||||
@misc{embedchain,
|
||||
author = {Taranjeet Singh},
|
||||
title = {Embechain: Framework to easily create LLM powered bots over any dataset},
|
||||
year = {2023},
|
||||
publisher = {GitHub},
|
||||
journal = {GitHub repository},
|
||||
howpublished = {\url{https://github.com/embedchain/embedchain}},
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: '📝 Documentation'
|
||||
description: 'Contribute to Embedchain docs'
|
||||
---
|
||||
|
||||
<Info>
|
||||
**Prerequisite** You should have installed Node.js (version 18.10.0 or
|
||||
higher).
|
||||
</Info>
|
||||
|
||||
Step 1. Install Mintlify on your OS:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i -g mintlify
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn global add mintlify
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Step 2. Go to the `docs/` directory (where you can find `mint.json`) and run the following command:
|
||||
|
||||
```bash
|
||||
mintlify dev
|
||||
```
|
||||
|
||||
The documentation website is now available at `http://localhost:3000`.
|
||||
|
||||
### Custom Ports
|
||||
|
||||
Mintlify uses port 3000 by default. You can use the `--port` flag to customize the port Mintlify runs on. For example, use this command to run in port 3333:
|
||||
|
||||
```bash
|
||||
mintlify dev --port 3333
|
||||
```
|
||||
|
||||
You will see an error like this if you try to run Mintlify in a port that's already taken:
|
||||
|
||||
```md
|
||||
Error: listen EADDRINUSE: address already in use :::3000
|
||||
```
|
||||
|
||||
## Mintlify Versions
|
||||
|
||||
Each CLI is linked to a specific version of Mintlify. Please update the CLI if your local website looks different than production.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i -g mintlify@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn global upgrade mintlify
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: 'Development'
|
||||
description: 'Learn how to preview changes locally'
|
||||
---
|
||||
|
||||
<Info>
|
||||
**Prerequisite** You should have installed Node.js (version 18.10.0 or
|
||||
higher).
|
||||
</Info>
|
||||
|
||||
Step 1. Install Mintlify on your OS:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i -g mintlify
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn global add mintlify
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
Step 2. Go to the docs are located (where you can find `mint.json`) and run the following command:
|
||||
|
||||
```bash
|
||||
mintlify dev
|
||||
```
|
||||
|
||||
The documentation website is now available at `http://localhost:3000`.
|
||||
|
||||
### Custom Ports
|
||||
|
||||
Mintlify uses port 3000 by default. You can use the `--port` flag to customize the port Mintlify runs on. For example, use this command to run in port 3333:
|
||||
|
||||
```bash
|
||||
mintlify dev --port 3333
|
||||
```
|
||||
|
||||
You will see an error like this if you try to run Mintlify in a port that's already taken:
|
||||
|
||||
```md
|
||||
Error: listen EADDRINUSE: address already in use :::3000
|
||||
```
|
||||
|
||||
## Mintlify Versions
|
||||
|
||||
Each CLI is linked to a specific version of Mintlify. Please update the CLI if your local website looks different than production.
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm i -g mintlify@latest
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn global upgrade mintlify
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Deployment
|
||||
|
||||
<Tip>
|
||||
Unlimited editors available under the [Startup
|
||||
Plan](https://mintlify.com/pricing)
|
||||
</Tip>
|
||||
|
||||
You should see the following if the deploy successfully went through:
|
||||
|
||||
<Frame>
|
||||
<img src="/images/checks-passed.png" style={{ borderRadius: '0.5rem' }} />
|
||||
</Frame>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Here's how to solve some common problems when working with the CLI.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Mintlify is not loading">
|
||||
Update to Node v18. Run `mintlify install` and try again.
|
||||
</Accordion>
|
||||
<Accordion title="No such file or directory on Windows">
|
||||
Go to the `C:/Users/Username/.mintlify/` directory and remove the `mint`
|
||||
folder. Then Open the Git Bash in this location and run `git clone
|
||||
https://github.com/mintlify/mint.git`.
|
||||
|
||||
Repeat step 3.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Getting an unknown error">
|
||||
Try navigating to the root of your device and delete the ~/.mintlify folder.
|
||||
Then run `mintlify dev` again.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
Curious about what changed in a CLI version? [Check out the CLI changelog.](/changelog/command-line)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 256 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: 📚 Introduction
|
||||
description: '📝 Embedchain is a framework to easily create LLM powered bots over any dataset.'
|
||||
---
|
||||
|
||||
## 🤔 What is Embedchain?
|
||||
|
||||
Embedchain abstracts the entire process of loading a dataset, chunking it, creating embeddings, and storing it in a vector database.
|
||||
|
||||
You can add a single or multiple datasets using the .add and .add_local functions. Then, simply use the .query function to find answers from the added datasets.
|
||||
|
||||
If you want to create a Naval Ravikant bot with a YouTube video, a book in PDF format, two blog posts, and a question and answer pair, all you need to do is add the respective links. Embedchain will take care of the rest, creating a bot for you.
|
||||
|
||||
```python
|
||||
from embedchain import App
|
||||
|
||||
naval_chat_bot = App()
|
||||
# Embed Online Resources
|
||||
naval_chat_bot.add("youtube_video", "https://www.youtube.com/watch?v=3qHkcs3kG44")
|
||||
naval_chat_bot.add("pdf_file", "https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf")
|
||||
naval_chat_bot.add("web_page", "https://nav.al/feedback")
|
||||
naval_chat_bot.add("web_page", "https://nav.al/agi")
|
||||
|
||||
# Embed Local Resources
|
||||
naval_chat_bot.add_local("qna_pair", ("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."))
|
||||
|
||||
naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?")
|
||||
# Answer: Naval argues that humans possess the unique capacity to understand explanations or concepts to the maximum extent possible in this physical reality.
|
||||
```
|
||||
|
||||
## 🚀 How it works?
|
||||
|
||||
Creating a chat bot over any dataset involves the following steps:
|
||||
|
||||
1. Load the data
|
||||
2. Create meaningful chunks
|
||||
3. Create embeddings for each chunk
|
||||
4. Store the chunks in a vector database
|
||||
|
||||
When a user asks a query, the following process happens to find the answer:
|
||||
|
||||
1. Create an embedding for the query
|
||||
2. Find similar documents for the query from the vector database
|
||||
3. Pass the similar documents as context to LLM to get the final answer.
|
||||
|
||||
The process of loading the dataset and querying involves multiple steps, each with its own nuances:
|
||||
|
||||
- How should I chunk the data? What is a meaningful chunk size?
|
||||
- How should I create embeddings for each chunk? Which embedding model should I use?
|
||||
- How should I store the chunks in a vector database? Which vector database should I use?
|
||||
- Should I store metadata along with the embeddings?
|
||||
- How should I find similar documents for a query? Which ranking model should I use?
|
||||
|
||||
Embedchain takes care of all these nuances and provides a simple interface to create bots over any dataset.
|
||||
|
||||
In the first release, we make it easier for anyone to get a chatbot over any dataset up and running in less than a minute. Just create an app instance, add the datasets using the `.add()` function, and use the `.query()` function to get the relevant answers.
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 42 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"$schema": "https://mintlify.com/schema.json",
|
||||
"name": "Embedchain",
|
||||
"logo": {
|
||||
"dark": "/logo/dark.svg",
|
||||
"light": "/logo/light.svg"
|
||||
},
|
||||
"favicon": "/favicon.png",
|
||||
"colors": {
|
||||
"primary": "#12A7D3",
|
||||
"light": "#81D7F7",
|
||||
"dark": "#004E7A"
|
||||
},
|
||||
"topbarLinks": [
|
||||
{
|
||||
"name": "Twitter",
|
||||
"url": "https://twitter.com/embedchain"
|
||||
},
|
||||
{
|
||||
"name": "Discord",
|
||||
"url": "https://discord.gg/6PzXDgEjG5"
|
||||
}
|
||||
],
|
||||
"topbarCtaButton": {
|
||||
"name": "GitHub",
|
||||
"url": "https://embedchain.ai"
|
||||
},
|
||||
"navigation": [
|
||||
{
|
||||
"group": "Getting started",
|
||||
"pages": ["quickstart", "introduction"]
|
||||
},
|
||||
{
|
||||
"group": "Advanced",
|
||||
"pages": ["advanced/app_types", "advanced/interface_types", "advanced/adding_data","advanced/data_types", "advanced/query_configuration", "advanced/configuration", "advanced/testing", "advanced/showcase"]
|
||||
},
|
||||
{
|
||||
"group": "Contribution Guidelines",
|
||||
"pages": ["contribution/dev", "contribution/docs"]
|
||||
}
|
||||
|
||||
],
|
||||
"footerSocials": {
|
||||
"twitter": "https://twitter.com/embedchain",
|
||||
"github": "https://github.com/embedchain/embedchain",
|
||||
"linkedin": "https://www.linkedin.com/company/embedchain"
|
||||
},
|
||||
"backgroundImage": "/background.png",
|
||||
"isWhiteLabeled": true
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: '🚀 Quickstart'
|
||||
description: '💡 Start building LLM powered bots under 30 seconds'
|
||||
---
|
||||
|
||||
Install embedchain python package:
|
||||
|
||||
```bash
|
||||
pip install embedchain
|
||||
```
|
||||
|
||||
Creating a chatbot involves 3 steps:
|
||||
|
||||
- ⚙️ Import the App instance
|
||||
- 🗃️ Add Dataset
|
||||
- 💬 Query or Chat on the dataset and get answers (Interface Types)
|
||||
|
||||
Run your first bot in python using the following code. Make sure to set the `OPENAI_API_KEY` 🔑 environment variable in the code.
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
from embedchain Import App
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "xxx"
|
||||
elon_musk_bot = App()
|
||||
|
||||
# Embed Online Resources
|
||||
elon_musk_bot.add("web_page", "https://en.wikipedia.org/wiki/Elon_Musk")
|
||||
elon_musk_bot.add("web_page", "https://www.tesla.com/elon-musk")
|
||||
|
||||
response = elon_bot.query("How many companies does Elon Musk run?")
|
||||
print(response)
|
||||
# Answer: 'Elon Musk runs four companies: Tesla, SpaceX, Neuralink, and The Boring Company.'
|
||||
```
|
||||
+10
-1
@@ -1 +1,10 @@
|
||||
from .embedchain import App, OpenSourceApp, PersonApp, PersonOpenSourceApp
|
||||
import importlib.metadata
|
||||
|
||||
__version__ = importlib.metadata.version(__package__ or __name__)
|
||||
|
||||
from embedchain.apps.App import App # noqa: F401
|
||||
from embedchain.apps.CustomApp import CustomApp # noqa: F401
|
||||
from embedchain.apps.Llama2App import Llama2App # noqa: F401
|
||||
from embedchain.apps.OpenSourceApp import OpenSourceApp # noqa: F401
|
||||
from embedchain.apps.PersonApp import (PersonApp, # noqa: F401
|
||||
PersonOpenSourceApp)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import openai
|
||||
|
||||
from embedchain.config import AppConfig, ChatConfig
|
||||
from embedchain.embedchain import EmbedChain
|
||||
|
||||
|
||||
class App(EmbedChain):
|
||||
"""
|
||||
The EmbedChain app.
|
||||
Has two functions: add and query.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
dry_run(query): test your prompt without consuming tokens.
|
||||
"""
|
||||
|
||||
def __init__(self, config: AppConfig = None):
|
||||
"""
|
||||
:param config: AppConfig instance to load as configuration. Optional.
|
||||
"""
|
||||
if config is None:
|
||||
config = AppConfig()
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig):
|
||||
messages = []
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
response = openai.ChatCompletion.create(
|
||||
model=config.model or "gpt-3.5-turbo-0613",
|
||||
messages=messages,
|
||||
temperature=config.temperature,
|
||||
max_tokens=config.max_tokens,
|
||||
top_p=config.top_p,
|
||||
stream=config.stream,
|
||||
)
|
||||
|
||||
if config.stream:
|
||||
return self._stream_llm_model_response(response)
|
||||
else:
|
||||
return response["choices"][0]["message"]["content"]
|
||||
|
||||
def _stream_llm_model_response(self, response):
|
||||
"""
|
||||
This is a generator for streaming response from the OpenAI completions API
|
||||
"""
|
||||
for line in response:
|
||||
chunk = line["choices"][0].get("delta", {}).get("content", "")
|
||||
yield chunk
|
||||
@@ -0,0 +1,128 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from langchain.schema import BaseMessage
|
||||
|
||||
from embedchain.config import ChatConfig, CustomAppConfig
|
||||
from embedchain.embedchain import EmbedChain
|
||||
from embedchain.models import Providers
|
||||
|
||||
|
||||
class CustomApp(EmbedChain):
|
||||
"""
|
||||
The custom EmbedChain app.
|
||||
Has two functions: add and query.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
dry_run(query): test your prompt without consuming tokens.
|
||||
"""
|
||||
|
||||
def __init__(self, config: CustomAppConfig = None):
|
||||
"""
|
||||
:param config: Optional. `CustomAppConfig` instance to load as configuration.
|
||||
:raises ValueError: Config must be provided for custom app
|
||||
"""
|
||||
if config is None:
|
||||
raise ValueError("Config must be provided for custom app")
|
||||
|
||||
self.provider = config.provider
|
||||
|
||||
if config.provider == Providers.GPT4ALL:
|
||||
from embedchain import OpenSourceApp
|
||||
|
||||
# Because these models run locally, they should have an instance running when the custom app is created
|
||||
self.open_source_app = OpenSourceApp(config=config.open_source_app_config)
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
def set_llm_model(self, provider: Providers):
|
||||
self.provider = provider
|
||||
if provider == Providers.GPT4ALL:
|
||||
raise ValueError(
|
||||
"GPT4ALL needs to be instantiated with the model known, please create a new app instance instead"
|
||||
)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig):
|
||||
# TODO: Quitting the streaming response here for now.
|
||||
# Idea: https://gist.github.com/jvelezmagic/03ddf4c452d011aae36b2a0f73d72f68
|
||||
if config.stream:
|
||||
raise NotImplementedError(
|
||||
"Streaming responses have not been implemented for this model yet. Please disable."
|
||||
)
|
||||
|
||||
try:
|
||||
if self.provider == Providers.OPENAI:
|
||||
return CustomApp._get_openai_answer(prompt, config)
|
||||
|
||||
if self.provider == Providers.ANTHROPHIC:
|
||||
return CustomApp._get_athrophic_answer(prompt, config)
|
||||
|
||||
if self.provider == Providers.VERTEX_AI:
|
||||
return CustomApp._get_vertex_answer(prompt, config)
|
||||
|
||||
if self.provider == Providers.GPT4ALL:
|
||||
return self.open_source_app._get_gpt4all_answer(prompt, config)
|
||||
|
||||
except ImportError as e:
|
||||
raise ImportError(e.msg) from None
|
||||
|
||||
@staticmethod
|
||||
def _get_openai_answer(prompt: str, config: ChatConfig) -> str:
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
|
||||
logging.info(vars(config))
|
||||
|
||||
chat = ChatOpenAI(
|
||||
temperature=config.temperature,
|
||||
model=config.model or "gpt-3.5-turbo",
|
||||
max_tokens=config.max_tokens,
|
||||
streaming=config.stream,
|
||||
)
|
||||
|
||||
if config.top_p and config.top_p != 1:
|
||||
logging.warning("Config option `top_p` is not supported by this model.")
|
||||
|
||||
messages = CustomApp._get_messages(prompt)
|
||||
|
||||
return chat(messages).content
|
||||
|
||||
@staticmethod
|
||||
def _get_athrophic_answer(prompt: str, config: ChatConfig) -> str:
|
||||
from langchain.chat_models import ChatAnthropic
|
||||
|
||||
chat = ChatAnthropic(temperature=config.temperature, model=config.model)
|
||||
|
||||
if config.max_tokens and config.max_tokens != 1000:
|
||||
logging.warning("Config option `max_tokens` is not supported by this model.")
|
||||
|
||||
messages = CustomApp._get_messages(prompt)
|
||||
|
||||
return chat(messages).content
|
||||
|
||||
@staticmethod
|
||||
def _get_vertex_answer(prompt: str, config: ChatConfig) -> str:
|
||||
from langchain.chat_models import ChatVertexAI
|
||||
|
||||
chat = ChatVertexAI(temperature=config.temperature, model=config.model, max_output_tokens=config.max_tokens)
|
||||
|
||||
if config.top_p and config.top_p != 1:
|
||||
logging.warning("Config option `top_p` is not supported by this model.")
|
||||
|
||||
messages = CustomApp._get_messages(prompt)
|
||||
|
||||
return chat(messages).content
|
||||
|
||||
@staticmethod
|
||||
def _get_messages(prompt: str) -> List[BaseMessage]:
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
|
||||
return [SystemMessage(content="You are a helpful assistant."), HumanMessage(content=prompt)]
|
||||
|
||||
def _stream_llm_model_response(self, response):
|
||||
"""
|
||||
This is a generator for streaming response from the OpenAI completions API
|
||||
"""
|
||||
for line in response:
|
||||
chunk = line["choices"][0].get("delta", {}).get("content", "")
|
||||
yield chunk
|
||||
@@ -0,0 +1,36 @@
|
||||
import os
|
||||
|
||||
from langchain.llms import Replicate
|
||||
|
||||
from embedchain.config import AppConfig
|
||||
from embedchain.embedchain import EmbedChain
|
||||
|
||||
|
||||
class Llama2App(EmbedChain):
|
||||
"""
|
||||
The EmbedChain Llama2App class.
|
||||
Has two functions: add and query.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
"""
|
||||
|
||||
def __init__(self, config: AppConfig = None):
|
||||
"""
|
||||
:param config: AppConfig instance to load as configuration. Optional.
|
||||
"""
|
||||
if "REPLICATE_API_TOKEN" not in os.environ:
|
||||
raise ValueError("Please set the REPLICATE_API_TOKEN environment variable.")
|
||||
|
||||
if config is None:
|
||||
config = AppConfig()
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: AppConfig = None):
|
||||
# TODO: Move the model and other inputs into config
|
||||
llm = Replicate(
|
||||
model="a16z-infra/llama13b-v2-chat:df7690f1994d94e96ad9d568eac121aecf50684a0b0963b25a41cc40061269e5",
|
||||
input={"temperature": 0.75, "max_length": 500, "top_p": 1},
|
||||
)
|
||||
return llm(prompt)
|
||||
@@ -0,0 +1,65 @@
|
||||
import logging
|
||||
from typing import Iterable, Union
|
||||
|
||||
from embedchain.config import ChatConfig, OpenSourceAppConfig
|
||||
from embedchain.embedchain import EmbedChain
|
||||
|
||||
gpt4all_model = None
|
||||
|
||||
|
||||
class OpenSourceApp(EmbedChain):
|
||||
"""
|
||||
The OpenSource app.
|
||||
Same as App, but uses an open source embedding model and LLM.
|
||||
|
||||
Has two function: add and query.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
"""
|
||||
|
||||
def __init__(self, config: OpenSourceAppConfig = None):
|
||||
"""
|
||||
:param config: OpenSourceAppConfig instance to load as configuration. Optional.
|
||||
`ef` defaults to open source.
|
||||
"""
|
||||
logging.info("Loading open source embedding model. This may take some time...") # noqa:E501
|
||||
if not config:
|
||||
config = OpenSourceAppConfig()
|
||||
|
||||
if not config.model:
|
||||
raise ValueError("OpenSourceApp needs a model to be instantiated. Maybe you passed the wrong config type?")
|
||||
|
||||
self.instance = OpenSourceApp._get_instance(config.model)
|
||||
|
||||
logging.info("Successfully loaded open source embedding model.")
|
||||
super().__init__(config)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig):
|
||||
return self._get_gpt4all_answer(prompt=prompt, config=config)
|
||||
|
||||
@staticmethod
|
||||
def _get_instance(model):
|
||||
try:
|
||||
from gpt4all import GPT4All
|
||||
except ModuleNotFoundError:
|
||||
raise ValueError(
|
||||
"The GPT4All python package is not installed. Please install it with `pip install GPT4All`"
|
||||
) from None
|
||||
|
||||
return GPT4All(model)
|
||||
|
||||
def _get_gpt4all_answer(self, prompt: str, config: ChatConfig) -> Union[str, Iterable]:
|
||||
if config.model and config.model != self.config.model:
|
||||
raise RuntimeError(
|
||||
"OpenSourceApp does not support switching models at runtime. Please create a new app instance."
|
||||
)
|
||||
|
||||
response = self.instance.generate(
|
||||
prompt=prompt,
|
||||
streaming=config.stream,
|
||||
top_p=config.top_p,
|
||||
max_tokens=config.max_tokens,
|
||||
temp=config.temperature,
|
||||
)
|
||||
return response
|
||||
@@ -0,0 +1,63 @@
|
||||
from string import Template
|
||||
|
||||
from embedchain.apps.App import App
|
||||
from embedchain.apps.OpenSourceApp import OpenSourceApp
|
||||
from embedchain.config import ChatConfig, QueryConfig
|
||||
from embedchain.config.apps.BaseAppConfig import BaseAppConfig
|
||||
from embedchain.config.QueryConfig import (DEFAULT_PROMPT,
|
||||
DEFAULT_PROMPT_WITH_HISTORY)
|
||||
|
||||
|
||||
class EmbedChainPersonApp:
|
||||
"""
|
||||
Base class to create a person bot.
|
||||
This bot behaves and speaks like a person.
|
||||
|
||||
:param person: name of the person, better if its a well known person.
|
||||
:param config: BaseAppConfig instance to load as configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, person, config: BaseAppConfig = None):
|
||||
self.person = person
|
||||
self.person_prompt = f"You are {person}. Whatever you say, you will always say in {person} style." # noqa:E501
|
||||
super().__init__(config)
|
||||
|
||||
|
||||
class PersonApp(EmbedChainPersonApp, App):
|
||||
"""
|
||||
The Person app.
|
||||
Extends functionality from EmbedChainPersonApp and App
|
||||
"""
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None):
|
||||
self.template = Template(self.person_prompt + " " + DEFAULT_PROMPT)
|
||||
query_config = QueryConfig(
|
||||
template=self.template,
|
||||
)
|
||||
return super().query(input_query, query_config)
|
||||
|
||||
def chat(self, input_query, config: ChatConfig = None):
|
||||
self.template = Template(self.person_prompt + " " + DEFAULT_PROMPT_WITH_HISTORY)
|
||||
chat_config = ChatConfig(
|
||||
template=self.template,
|
||||
)
|
||||
return super().chat(input_query, chat_config)
|
||||
|
||||
|
||||
class PersonOpenSourceApp(EmbedChainPersonApp, OpenSourceApp):
|
||||
"""
|
||||
The Person app.
|
||||
Extends functionality from EmbedChainPersonApp and OpenSourceApp
|
||||
"""
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None):
|
||||
query_config = QueryConfig(
|
||||
template=self.template,
|
||||
)
|
||||
return super().query(input_query, query_config)
|
||||
|
||||
def chat(self, input_query, config: ChatConfig = None):
|
||||
chat_config = ChatConfig(
|
||||
template=self.template,
|
||||
)
|
||||
return super().chat(input_query, chat_config)
|
||||
@@ -5,6 +5,7 @@ class BaseChunker:
|
||||
def __init__(self, text_splitter):
|
||||
"""Initialize the chunker."""
|
||||
self.text_splitter = text_splitter
|
||||
self.data_type = None
|
||||
|
||||
def create_chunks(self, loader, src):
|
||||
"""
|
||||
@@ -22,10 +23,13 @@ class BaseChunker:
|
||||
metadatas = []
|
||||
for data in datas:
|
||||
content = data["content"]
|
||||
|
||||
meta_data = data["meta_data"]
|
||||
# add data type to meta data to allow query using data type
|
||||
meta_data["data_type"] = self.data_type
|
||||
url = meta_data["url"]
|
||||
|
||||
chunks = self.text_splitter.split_text(content)
|
||||
chunks = self.get_chunks(content)
|
||||
|
||||
for chunk in chunks:
|
||||
chunk_id = hashlib.sha256((chunk + url).encode()).hexdigest()
|
||||
@@ -39,3 +43,17 @@ class BaseChunker:
|
||||
"ids": ids,
|
||||
"metadatas": metadatas,
|
||||
}
|
||||
|
||||
def get_chunks(self, content):
|
||||
"""
|
||||
Returns chunks using text splitter instance.
|
||||
|
||||
Override in child class if custom logic.
|
||||
"""
|
||||
return self.text_splitter.split_text(content)
|
||||
|
||||
def set_data_type(self, data_type):
|
||||
"""
|
||||
set the data type of chunker
|
||||
"""
|
||||
self.data_type = data_type
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from typing import Optional
|
||||
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
|
||||
|
||||
class DocsSiteChunker(BaseChunker):
|
||||
"""Chunker for code docs site."""
|
||||
|
||||
def __init__(self, config: Optional[ChunkerConfig] = None):
|
||||
if config is None:
|
||||
config = ChunkerConfig(chunk_size=500, chunk_overlap=50, length_function=len)
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=config.chunk_size,
|
||||
chunk_overlap=config.chunk_overlap,
|
||||
length_function=config.length_function,
|
||||
)
|
||||
super().__init__(text_splitter)
|
||||
@@ -5,18 +5,16 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
|
||||
TEXT_SPLITTER_CHUNK_PARAMS = {
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 0,
|
||||
"length_function": len,
|
||||
}
|
||||
|
||||
|
||||
class DocxFileChunker(BaseChunker):
|
||||
"""Chunker for .docx file."""
|
||||
|
||||
def __init__(self, config: Optional[ChunkerConfig] = None):
|
||||
if config is None:
|
||||
config = TEXT_SPLITTER_CHUNK_PARAMS
|
||||
text_splitter = RecursiveCharacterTextSplitter(**config)
|
||||
config = ChunkerConfig(chunk_size=1000, chunk_overlap=0, length_function=len)
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=config.chunk_size,
|
||||
chunk_overlap=config.chunk_overlap,
|
||||
length_function=config.length_function,
|
||||
)
|
||||
super().__init__(text_splitter)
|
||||
|
||||
@@ -5,18 +5,16 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
|
||||
TEXT_SPLITTER_CHUNK_PARAMS = {
|
||||
"chunk_size": 1000,
|
||||
"chunk_overlap": 0,
|
||||
"length_function": len,
|
||||
}
|
||||
|
||||
|
||||
class PdfFileChunker(BaseChunker):
|
||||
"""Chunker for PDF file."""
|
||||
|
||||
def __init__(self, config: Optional[ChunkerConfig] = None):
|
||||
if config is None:
|
||||
config = TEXT_SPLITTER_CHUNK_PARAMS
|
||||
text_splitter = RecursiveCharacterTextSplitter(**config)
|
||||
config = ChunkerConfig(chunk_size=1000, chunk_overlap=0, length_function=len)
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=config.chunk_size,
|
||||
chunk_overlap=config.chunk_overlap,
|
||||
length_function=config.length_function,
|
||||
)
|
||||
super().__init__(text_splitter)
|
||||
|
||||
@@ -5,18 +5,16 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
|
||||
TEXT_SPLITTER_CHUNK_PARAMS = {
|
||||
"chunk_size": 300,
|
||||
"chunk_overlap": 0,
|
||||
"length_function": len,
|
||||
}
|
||||
|
||||
|
||||
class QnaPairChunker(BaseChunker):
|
||||
"""Chunker for QnA pair."""
|
||||
|
||||
def __init__(self, config: Optional[ChunkerConfig] = None):
|
||||
if config is None:
|
||||
config = TEXT_SPLITTER_CHUNK_PARAMS
|
||||
text_splitter = RecursiveCharacterTextSplitter(**config)
|
||||
config = ChunkerConfig(chunk_size=300, chunk_overlap=0, length_function=len)
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=config.chunk_size,
|
||||
chunk_overlap=config.chunk_overlap,
|
||||
length_function=config.length_function,
|
||||
)
|
||||
super().__init__(text_splitter)
|
||||
|
||||
@@ -5,18 +5,16 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
|
||||
TEXT_SPLITTER_CHUNK_PARAMS = {
|
||||
"chunk_size": 300,
|
||||
"chunk_overlap": 0,
|
||||
"length_function": len,
|
||||
}
|
||||
|
||||
|
||||
class TextChunker(BaseChunker):
|
||||
"""Chunker for text."""
|
||||
|
||||
def __init__(self, config: Optional[ChunkerConfig] = None):
|
||||
if config is None:
|
||||
config = TEXT_SPLITTER_CHUNK_PARAMS
|
||||
text_splitter = RecursiveCharacterTextSplitter(**config)
|
||||
config = ChunkerConfig(chunk_size=300, chunk_overlap=0, length_function=len)
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=config.chunk_size,
|
||||
chunk_overlap=config.chunk_overlap,
|
||||
length_function=config.length_function,
|
||||
)
|
||||
super().__init__(text_splitter)
|
||||
|
||||
@@ -5,18 +5,16 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
|
||||
TEXT_SPLITTER_CHUNK_PARAMS = {
|
||||
"chunk_size": 500,
|
||||
"chunk_overlap": 0,
|
||||
"length_function": len,
|
||||
}
|
||||
|
||||
|
||||
class WebPageChunker(BaseChunker):
|
||||
"""Chunker for web page."""
|
||||
|
||||
def __init__(self, config: Optional[ChunkerConfig] = None):
|
||||
if config is None:
|
||||
config = TEXT_SPLITTER_CHUNK_PARAMS
|
||||
text_splitter = RecursiveCharacterTextSplitter(**config)
|
||||
config = ChunkerConfig(chunk_size=500, chunk_overlap=0, length_function=len)
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=config.chunk_size,
|
||||
chunk_overlap=config.chunk_overlap,
|
||||
length_function=config.length_function,
|
||||
)
|
||||
super().__init__(text_splitter)
|
||||
|
||||
@@ -5,18 +5,16 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
|
||||
TEXT_SPLITTER_CHUNK_PARAMS = {
|
||||
"chunk_size": 2000,
|
||||
"chunk_overlap": 0,
|
||||
"length_function": len,
|
||||
}
|
||||
|
||||
|
||||
class YoutubeVideoChunker(BaseChunker):
|
||||
"""Chunker for Youtube video."""
|
||||
|
||||
def __init__(self, config: Optional[ChunkerConfig] = None):
|
||||
if config is None:
|
||||
config = TEXT_SPLITTER_CHUNK_PARAMS
|
||||
text_splitter = RecursiveCharacterTextSplitter(**config)
|
||||
config = ChunkerConfig(chunk_size=2000, chunk_overlap=0, length_function=len)
|
||||
text_splitter = RecursiveCharacterTextSplitter(
|
||||
chunk_size=config.chunk_size,
|
||||
chunk_overlap=config.chunk_overlap,
|
||||
length_function=config.length_function,
|
||||
)
|
||||
super().__init__(text_splitter)
|
||||
|
||||
@@ -10,13 +10,13 @@ class ChunkerConfig(BaseConfig):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
chunk_size: Optional[int] = 4000,
|
||||
chunk_overlap: Optional[int] = 200,
|
||||
length_function: Optional[Callable[[str], int]] = len,
|
||||
chunk_size: Optional[int] = None,
|
||||
chunk_overlap: Optional[int] = None,
|
||||
length_function: Optional[Callable[[str], int]] = None,
|
||||
):
|
||||
self.chunk_size = chunk_size
|
||||
self.chunk_overlap = chunk_overlap
|
||||
self.length_function = length_function
|
||||
self.chunk_size = chunk_size if chunk_size else 2000
|
||||
self.chunk_overlap = chunk_overlap if chunk_overlap else 0
|
||||
self.length_function = length_function if length_function else len
|
||||
|
||||
|
||||
class LoaderConfig(BaseConfig):
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
from embedchain.config.BaseConfig import BaseConfig
|
||||
|
||||
|
||||
class InitConfig(BaseConfig):
|
||||
"""
|
||||
Config to initialize an embedchain `App` instance.
|
||||
"""
|
||||
|
||||
def __init__(self, log_level=None, ef=None, db=None, host=None, port=None):
|
||||
"""
|
||||
:param log_level: Optional. (String) Debug level
|
||||
['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'].
|
||||
:param ef: Optional. Embedding function to use.
|
||||
:param db: Optional. (Vector) database to use for embeddings.
|
||||
:param host: Optional. Hostname for the database server.
|
||||
:param port: Optional. Port for the database server.
|
||||
"""
|
||||
self._setup_logging(log_level)
|
||||
|
||||
self.ef = ef
|
||||
self.db = db
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
return
|
||||
|
||||
def _set_embedding_function(self, ef):
|
||||
self.ef = ef
|
||||
return
|
||||
|
||||
def _set_embedding_function_to_default(self):
|
||||
"""
|
||||
Sets embedding function to default (`text-embedding-ada-002`).
|
||||
|
||||
:raises ValueError: If the template is not valid as template should contain
|
||||
$context and $query
|
||||
"""
|
||||
if (
|
||||
os.getenv("OPENAI_API_KEY") is None
|
||||
and os.getenv("OPENAI_ORGANIZATION") is None
|
||||
):
|
||||
raise ValueError(
|
||||
"OPENAI_API_KEY or OPENAI_ORGANIZATION environment variables not provided" # noqa:E501
|
||||
)
|
||||
self.ef = embedding_functions.OpenAIEmbeddingFunction(
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
organization_id=os.getenv("OPENAI_ORGANIZATION"),
|
||||
model_name="text-embedding-ada-002",
|
||||
)
|
||||
return
|
||||
|
||||
def _set_db(self, db):
|
||||
if db:
|
||||
self.db = db
|
||||
return
|
||||
|
||||
def _set_db_to_default(self):
|
||||
"""
|
||||
Sets database to default (`ChromaDb`).
|
||||
"""
|
||||
from embedchain.vectordb.chroma_db import ChromaDB
|
||||
|
||||
self.db = ChromaDB(ef=self.ef, host=self.host, port=self.port)
|
||||
|
||||
def _setup_logging(self, debug_level):
|
||||
level = logging.WARNING # Default level
|
||||
if debug_level is not None:
|
||||
level = getattr(logging, debug_level.upper(), None)
|
||||
if not isinstance(level, int):
|
||||
raise ValueError(f"Invalid log level: {debug_level}")
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s [%(name)s] [%(levelname)s] %(message)s", level=level
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
return
|
||||
@@ -17,7 +17,7 @@ DEFAULT_PROMPT = """
|
||||
DEFAULT_PROMPT_WITH_HISTORY = """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
I will provide you with our conversation history.
|
||||
I will provide you with our conversation history.
|
||||
|
||||
$context
|
||||
|
||||
@@ -28,8 +28,20 @@ DEFAULT_PROMPT_WITH_HISTORY = """
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DOCS_SITE_DEFAULT_PROMPT = """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer. Wherever possible, give complete code snippet. Dont make up any code snippet on your own.
|
||||
|
||||
$context
|
||||
|
||||
Query: $query
|
||||
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DEFAULT_PROMPT_TEMPLATE = Template(DEFAULT_PROMPT)
|
||||
DEFAULT_PROMPT_WITH_HISTORY_TEMPLATE = Template(DEFAULT_PROMPT_WITH_HISTORY)
|
||||
DOCS_SITE_PROMPT_TEMPLATE = Template(DOCS_SITE_DEFAULT_PROMPT)
|
||||
query_re = re.compile(r"\$\{*query\}*")
|
||||
context_re = re.compile(r"\$\{*context\}*")
|
||||
history_re = re.compile(r"\$\{*history\}*")
|
||||
@@ -92,7 +104,7 @@ class QueryConfig(BaseConfig):
|
||||
|
||||
self.temperature = temperature if temperature else 0
|
||||
self.max_tokens = max_tokens if max_tokens else 1000
|
||||
self.model = model if model else "gpt-3.5-turbo-0613"
|
||||
self.model = model
|
||||
self.top_p = top_p if top_p else 1
|
||||
|
||||
if self.validate_template(template):
|
||||
@@ -101,9 +113,7 @@ class QueryConfig(BaseConfig):
|
||||
if self.history is None:
|
||||
raise ValueError("`template` should have `query` and `context` keys")
|
||||
else:
|
||||
raise ValueError(
|
||||
"`template` should have `query`, `context` and `history` keys"
|
||||
)
|
||||
raise ValueError("`template` should have `query`, `context` and `history` keys")
|
||||
|
||||
if not isinstance(stream, bool):
|
||||
raise ValueError("`stream` should be bool")
|
||||
@@ -117,9 +127,7 @@ class QueryConfig(BaseConfig):
|
||||
:return: Boolean, valid (true) or invalid (false)
|
||||
"""
|
||||
if self.history is None:
|
||||
return re.search(query_re, template.template) and re.search(
|
||||
context_re, template.template
|
||||
)
|
||||
return re.search(query_re, template.template) and re.search(context_re, template.template)
|
||||
else:
|
||||
return (
|
||||
re.search(query_re, template.template)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from .AddConfig import AddConfig
|
||||
from .BaseConfig import BaseConfig
|
||||
from .ChatConfig import ChatConfig
|
||||
from .InitConfig import InitConfig
|
||||
from .QueryConfig import QueryConfig
|
||||
from .AddConfig import AddConfig, ChunkerConfig # noqa: F401
|
||||
from .apps.AppConfig import AppConfig # noqa: F401
|
||||
from .apps.CustomAppConfig import CustomAppConfig # noqa: F401
|
||||
from .apps.OpenSourceAppConfig import OpenSourceAppConfig # noqa: F401
|
||||
from .BaseConfig import BaseConfig # noqa: F401
|
||||
from .ChatConfig import ChatConfig # noqa: F401
|
||||
from .QueryConfig import QueryConfig # noqa: F401
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
from .BaseAppConfig import BaseAppConfig
|
||||
|
||||
|
||||
class AppConfig(BaseAppConfig):
|
||||
"""
|
||||
Config to initialize an embedchain custom `App` instance, with extra config options.
|
||||
"""
|
||||
|
||||
def __init__(self, log_level=None, host=None, port=None, id=None):
|
||||
"""
|
||||
:param log_level: Optional. (String) Debug level
|
||||
['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'].
|
||||
:param id: Optional. ID of the app. Document metadata will have this id.
|
||||
:param host: Optional. Hostname for the database server.
|
||||
:param port: Optional. Port for the database server.
|
||||
"""
|
||||
super().__init__(
|
||||
log_level=log_level, embedding_fn=AppConfig.default_embedding_function(), host=host, port=port, id=id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def default_embedding_function():
|
||||
"""
|
||||
Sets embedding function to default (`text-embedding-ada-002`).
|
||||
|
||||
:raises ValueError: If the template is not valid as template should contain
|
||||
$context and $query
|
||||
:returns: The default embedding function for the app class.
|
||||
"""
|
||||
if os.getenv("OPENAI_API_KEY") is None and os.getenv("OPENAI_ORGANIZATION") is None:
|
||||
raise ValueError("OPENAI_API_KEY or OPENAI_ORGANIZATION environment variables not provided") # noqa:E501
|
||||
return embedding_functions.OpenAIEmbeddingFunction(
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
organization_id=os.getenv("OPENAI_ORGANIZATION"),
|
||||
model_name="text-embedding-ada-002",
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
import logging
|
||||
|
||||
from embedchain.config.BaseConfig import BaseConfig
|
||||
|
||||
|
||||
class BaseAppConfig(BaseConfig):
|
||||
"""
|
||||
Parent config to initialize an instance of `App`, `OpenSourceApp` or `CustomApp`.
|
||||
"""
|
||||
|
||||
def __init__(self, log_level=None, embedding_fn=None, db=None, host=None, port=None, id=None):
|
||||
"""
|
||||
:param log_level: Optional. (String) Debug level
|
||||
['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'].
|
||||
:param embedding_fn: Embedding function to use.
|
||||
:param db: Optional. (Vector) database instance to use for embeddings.
|
||||
:param id: Optional. ID of the app. Document metadata will have this id.
|
||||
:param host: Optional. Hostname for the database server.
|
||||
:param port: Optional. Port for the database server.
|
||||
"""
|
||||
self._setup_logging(log_level)
|
||||
|
||||
self.db = db if db else BaseAppConfig.default_db(embedding_fn=embedding_fn, host=host, port=port)
|
||||
self.id = id
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def default_db(embedding_fn, host, port):
|
||||
"""
|
||||
Sets database to default (`ChromaDb`).
|
||||
|
||||
:param embedding_fn: Embedding function to use in database.
|
||||
:param host: Optional. Hostname for the database server.
|
||||
:param port: Optional. Port for the database server.
|
||||
:returns: Default database
|
||||
:raises ValueError: BaseAppConfig knows no default embedding function.
|
||||
"""
|
||||
if embedding_fn is None:
|
||||
raise ValueError("ChromaDb cannot be instantiated without an embedding function")
|
||||
from embedchain.vectordb.chroma_db import ChromaDB
|
||||
|
||||
return ChromaDB(embedding_fn=embedding_fn, host=host, port=port)
|
||||
|
||||
def _setup_logging(self, debug_level):
|
||||
level = logging.WARNING # Default level
|
||||
if debug_level is not None:
|
||||
level = getattr(logging, debug_level.upper(), None)
|
||||
if not isinstance(level, int):
|
||||
raise ValueError(f"Invalid log level: {debug_level}")
|
||||
|
||||
logging.basicConfig(format="%(asctime)s [%(name)s] [%(levelname)s] %(message)s", level=level)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
return
|
||||
@@ -0,0 +1,102 @@
|
||||
from typing import Any
|
||||
|
||||
from chromadb.api.types import Documents, Embeddings
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from embedchain.models import EmbeddingFunctions, Providers
|
||||
|
||||
from .BaseAppConfig import BaseAppConfig
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class CustomAppConfig(BaseAppConfig):
|
||||
"""
|
||||
Config to initialize an embedchain custom `App` instance, with extra config options.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
log_level=None,
|
||||
embedding_fn: EmbeddingFunctions = None,
|
||||
embedding_fn_model=None,
|
||||
db=None,
|
||||
host=None,
|
||||
port=None,
|
||||
id=None,
|
||||
provider: Providers = None,
|
||||
model=None,
|
||||
open_source_app_config=None,
|
||||
):
|
||||
"""
|
||||
:param log_level: Optional. (String) Debug level
|
||||
['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'].
|
||||
:param embedding_fn: Optional. Embedding function to use.
|
||||
:param embedding_fn_model: Optional. Model name to use for embedding function.
|
||||
:param db: Optional. (Vector) database to use for embeddings.
|
||||
:param id: Optional. ID of the app. Document metadata will have this id.
|
||||
:param host: Optional. Hostname for the database server.
|
||||
:param port: Optional. Port for the database server.
|
||||
:param provider: Optional. (Providers): LLM Provider to use.
|
||||
:param open_source_app_config: Optional. Config instance needed for open source apps.
|
||||
"""
|
||||
if provider:
|
||||
self.provider = provider
|
||||
else:
|
||||
raise ValueError("CustomApp must have a provider assigned.")
|
||||
|
||||
self.open_source_app_config = open_source_app_config
|
||||
|
||||
super().__init__(
|
||||
log_level=log_level,
|
||||
embedding_fn=CustomAppConfig.embedding_function(embedding_function=embedding_fn, model=embedding_fn_model),
|
||||
db=db,
|
||||
host=host,
|
||||
port=port,
|
||||
id=id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def langchain_default_concept(embeddings: Any):
|
||||
"""
|
||||
Langchains default function layout for embeddings.
|
||||
"""
|
||||
|
||||
def embed_function(texts: Documents) -> Embeddings:
|
||||
return embeddings.embed_documents(texts)
|
||||
|
||||
return embed_function
|
||||
|
||||
@staticmethod
|
||||
def embedding_function(embedding_function: EmbeddingFunctions, model: str = None):
|
||||
if not isinstance(embedding_function, EmbeddingFunctions):
|
||||
raise ValueError(
|
||||
f"Invalid option: '{embedding_function}'. Expecting one of the following options: {list(map(lambda x: x.value, EmbeddingFunctions))}" # noqa: E501
|
||||
)
|
||||
|
||||
if embedding_function == EmbeddingFunctions.OPENAI:
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
|
||||
if model:
|
||||
embeddings = OpenAIEmbeddings(model=model)
|
||||
else:
|
||||
embeddings = OpenAIEmbeddings()
|
||||
return CustomAppConfig.langchain_default_concept(embeddings)
|
||||
|
||||
elif embedding_function == EmbeddingFunctions.HUGGING_FACE:
|
||||
from langchain.embeddings import HuggingFaceEmbeddings
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name=model)
|
||||
return CustomAppConfig.langchain_default_concept(embeddings)
|
||||
|
||||
elif embedding_function == EmbeddingFunctions.VERTEX_AI:
|
||||
from langchain.embeddings import VertexAIEmbeddings
|
||||
|
||||
embeddings = VertexAIEmbeddings(model_name=model)
|
||||
return CustomAppConfig.langchain_default_concept(embeddings)
|
||||
|
||||
elif embedding_function == EmbeddingFunctions.GPT4ALL:
|
||||
# Note: We could use langchains GPT4ALL embedding, but it's not available in all versions.
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
return embedding_functions.SentenceTransformerEmbeddingFunction(model_name=model)
|
||||
@@ -0,0 +1,38 @@
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
from .BaseAppConfig import BaseAppConfig
|
||||
|
||||
|
||||
class OpenSourceAppConfig(BaseAppConfig):
|
||||
"""
|
||||
Config to initialize an embedchain custom `OpenSourceApp` instance, with extra config options.
|
||||
"""
|
||||
|
||||
def __init__(self, log_level=None, host=None, port=None, id=None, model=None):
|
||||
"""
|
||||
:param log_level: Optional. (String) Debug level
|
||||
['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'].
|
||||
:param id: Optional. ID of the app. Document metadata will have this id.
|
||||
:param host: Optional. Hostname for the database server.
|
||||
:param port: Optional. Port for the database server.
|
||||
:param model: Optional. GPT4ALL uses the model to instantiate the class.
|
||||
So unlike `App`, it has to be provided before querying.
|
||||
"""
|
||||
self.model = model or "orca-mini-3b.ggmlv3.q4_0.bin"
|
||||
|
||||
super().__init__(
|
||||
log_level=log_level,
|
||||
embedding_fn=OpenSourceAppConfig.default_embedding_function(),
|
||||
host=host,
|
||||
port=port,
|
||||
id=id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def default_embedding_function():
|
||||
"""
|
||||
Sets embedding function to default (`all-MiniLM-L6-v2`).
|
||||
|
||||
:returns: The default embedding function
|
||||
"""
|
||||
return embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
|
||||
@@ -1 +1 @@
|
||||
from .data_formatter import DataFormatter
|
||||
from .data_formatter import DataFormatter # noqa: F401
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from embedchain.chunkers.docs_site import DocsSiteChunker
|
||||
from embedchain.chunkers.docx_file import DocxFileChunker
|
||||
from embedchain.chunkers.pdf_file import PdfFileChunker
|
||||
from embedchain.chunkers.qna_pair import QnaPairChunker
|
||||
@@ -5,6 +6,7 @@ from embedchain.chunkers.text import TextChunker
|
||||
from embedchain.chunkers.web_page import WebPageChunker
|
||||
from embedchain.chunkers.youtube_video import YoutubeVideoChunker
|
||||
from embedchain.config import AddConfig
|
||||
from embedchain.loaders.docs_site_loader import DocsSiteLoader
|
||||
from embedchain.loaders.docx_file import DocxFileLoader
|
||||
from embedchain.loaders.local_qna_pair import LocalQnaPairLoader
|
||||
from embedchain.loaders.local_text import LocalTextLoader
|
||||
@@ -41,6 +43,7 @@ class DataFormatter:
|
||||
"text": LocalTextLoader(),
|
||||
"docx": DocxFileLoader(),
|
||||
"sitemap": SitemapLoader(),
|
||||
"docs_site": DocsSiteLoader(),
|
||||
}
|
||||
if data_type in loaders:
|
||||
return loaders[data_type]
|
||||
@@ -55,16 +58,20 @@ class DataFormatter:
|
||||
:return: The chunker for the given data type.
|
||||
:raises ValueError: If an unsupported data type is provided.
|
||||
"""
|
||||
chunkers = {
|
||||
"youtube_video": YoutubeVideoChunker(config),
|
||||
"pdf_file": PdfFileChunker(config),
|
||||
"web_page": WebPageChunker(config),
|
||||
"qna_pair": QnaPairChunker(config),
|
||||
"text": TextChunker(config),
|
||||
"docx": DocxFileChunker(config),
|
||||
"sitemap": WebPageChunker(config),
|
||||
chunker_classes = {
|
||||
"youtube_video": YoutubeVideoChunker,
|
||||
"pdf_file": PdfFileChunker,
|
||||
"web_page": WebPageChunker,
|
||||
"qna_pair": QnaPairChunker,
|
||||
"text": TextChunker,
|
||||
"docx": DocxFileChunker,
|
||||
"sitemap": WebPageChunker,
|
||||
"docs_site": DocsSiteChunker,
|
||||
}
|
||||
if data_type in chunkers:
|
||||
return chunkers[data_type]
|
||||
if data_type in chunker_classes:
|
||||
chunker_class = chunker_classes[data_type]
|
||||
chunker = chunker_class(config)
|
||||
chunker.set_data_type(data_type)
|
||||
return chunker
|
||||
else:
|
||||
raise ValueError(f"Unsupported data type: {data_type}")
|
||||
|
||||
+94
-222
@@ -1,19 +1,16 @@
|
||||
import logging
|
||||
import os
|
||||
from string import Template
|
||||
|
||||
import openai
|
||||
from chromadb.utils import embedding_functions
|
||||
from chromadb.errors import InvalidDimensionException
|
||||
from dotenv import load_dotenv
|
||||
from langchain.docstore.document import Document
|
||||
from langchain.memory import ConversationBufferMemory
|
||||
|
||||
from embedchain.config import AddConfig, ChatConfig, InitConfig, QueryConfig
|
||||
from embedchain.config.QueryConfig import DEFAULT_PROMPT
|
||||
from embedchain.config import AddConfig, ChatConfig, QueryConfig
|
||||
from embedchain.config.apps.BaseAppConfig import BaseAppConfig
|
||||
from embedchain.config.QueryConfig import DOCS_SITE_PROMPT_TEMPLATE
|
||||
from embedchain.data_formatter import DataFormatter
|
||||
|
||||
gpt4all_model = None
|
||||
|
||||
load_dotenv()
|
||||
|
||||
ABS_PATH = os.getcwd()
|
||||
@@ -23,18 +20,20 @@ memory = ConversationBufferMemory()
|
||||
|
||||
|
||||
class EmbedChain:
|
||||
def __init__(self, config: InitConfig):
|
||||
def __init__(self, config: BaseAppConfig):
|
||||
"""
|
||||
Initializes the EmbedChain instance, sets up a vector DB client and
|
||||
creates a collection.
|
||||
|
||||
:param config: InitConfig instance to load as configuration.
|
||||
:param config: BaseAppConfig instance to load as configuration.
|
||||
"""
|
||||
|
||||
self.config = config
|
||||
self.db_client = self.config.db.client
|
||||
self.collection = self.config.db.collection
|
||||
self.user_asks = []
|
||||
self.is_docs_site_instance = False
|
||||
self.online = False
|
||||
|
||||
def add(self, data_type, url, metadata=None, config: AddConfig = None):
|
||||
"""
|
||||
@@ -53,9 +52,9 @@ class EmbedChain:
|
||||
|
||||
data_formatter = DataFormatter(data_type, config)
|
||||
self.user_asks.append([data_type, url, metadata])
|
||||
self.load_and_embed(
|
||||
data_formatter.loader, data_formatter.chunker, url, metadata
|
||||
)
|
||||
self.load_and_embed(data_formatter.loader, data_formatter.chunker, url, metadata)
|
||||
if data_type in ("docs_site",):
|
||||
self.is_docs_site_instance = True
|
||||
|
||||
def add_local(self, data_type, content, metadata=None, config: AddConfig = None):
|
||||
"""
|
||||
@@ -96,19 +95,17 @@ class EmbedChain:
|
||||
metadatas = embeddings_data["metadatas"]
|
||||
ids = embeddings_data["ids"]
|
||||
# get existing ids, and discard doc if any common id exist.
|
||||
where = {"app_id": self.config.id} if self.config.id is not None else {}
|
||||
# where={"url": src}
|
||||
existing_docs = self.collection.get(
|
||||
ids=ids,
|
||||
# where={"url": src}
|
||||
where=where, # optional filter
|
||||
)
|
||||
existing_ids = set(existing_docs["ids"])
|
||||
|
||||
if len(existing_ids):
|
||||
data_dict = {
|
||||
id: (doc, meta) for id, doc, meta in zip(ids, documents, metadatas)
|
||||
}
|
||||
data_dict = {
|
||||
id: value for id, value in data_dict.items() if id not in existing_ids
|
||||
}
|
||||
data_dict = {id: (doc, meta) for id, doc, meta in zip(ids, documents, metadatas)}
|
||||
data_dict = {id: value for id, value in data_dict.items() if id not in existing_ids}
|
||||
|
||||
if not data_dict:
|
||||
print(f"All data from {src} already exists in the database.")
|
||||
@@ -117,20 +114,17 @@ class EmbedChain:
|
||||
ids = list(data_dict.keys())
|
||||
documents, metadatas = zip(*data_dict.values())
|
||||
|
||||
# Add app id in metadatas so that they can be queried on later
|
||||
if self.config.id is not None:
|
||||
metadatas = [{**m, "app_id": self.config.id} for m in metadatas]
|
||||
|
||||
chunks_before_addition = self.count()
|
||||
|
||||
# Add metadata to each document
|
||||
metadatas_with_metadata = [meta or metadata for meta in metadatas]
|
||||
metadatas_with_metadata = [{**meta, **metadata} for meta in metadatas]
|
||||
|
||||
self.collection.add(
|
||||
documents=documents, metadatas=list(metadatas_with_metadata), ids=ids
|
||||
)
|
||||
print(
|
||||
(
|
||||
f"Successfully saved {src}. New chunks count: "
|
||||
f"{self.count() - chunks_before_addition}"
|
||||
)
|
||||
)
|
||||
self.collection.add(documents=documents, metadatas=list(metadatas_with_metadata), ids=ids)
|
||||
print((f"Successfully saved {src}. New chunks count: " f"{self.count() - chunks_before_addition}"))
|
||||
|
||||
def _format_result(self, results):
|
||||
return [
|
||||
@@ -142,7 +136,10 @@ class EmbedChain:
|
||||
)
|
||||
]
|
||||
|
||||
def get_llm_model_answer(self, prompt):
|
||||
def get_llm_model_answer(self):
|
||||
"""
|
||||
Usually implemented by child class
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def retrieve_from_database(self, input_query, config: QueryConfig):
|
||||
@@ -154,17 +151,29 @@ class EmbedChain:
|
||||
:param config: The query configuration.
|
||||
:return: The content of the document that matched your query.
|
||||
"""
|
||||
result = self.collection.query(
|
||||
query_texts=[
|
||||
input_query,
|
||||
],
|
||||
n_results=config.number_documents,
|
||||
)
|
||||
try:
|
||||
where = {"app_id": self.config.id} if self.config.id is not None else {} # optional filter
|
||||
result = self.collection.query(
|
||||
query_texts=[
|
||||
input_query,
|
||||
],
|
||||
n_results=config.number_documents,
|
||||
where=where,
|
||||
)
|
||||
except InvalidDimensionException as e:
|
||||
raise InvalidDimensionException(
|
||||
e.message()
|
||||
+ ". This is commonly a side-effect when an embedding function, different from the one used to add the embeddings, is used to retrieve an embedding from the database." # noqa E501
|
||||
) from None
|
||||
|
||||
results_formatted = self._format_result(result)
|
||||
contents = [result[0].page_content for result in results_formatted]
|
||||
return contents
|
||||
|
||||
def generate_prompt(self, input_query, contexts, config: QueryConfig):
|
||||
def _append_search_and_context(self, context, web_search_result):
|
||||
return f"{context}\nWeb Search Result: {web_search_result}"
|
||||
|
||||
def generate_prompt(self, input_query, contexts, config: QueryConfig, **kwargs):
|
||||
"""
|
||||
Generates a prompt based on the given query and context, ready to be
|
||||
passed to an LLM
|
||||
@@ -176,14 +185,13 @@ class EmbedChain:
|
||||
:return: The prompt
|
||||
"""
|
||||
context_string = (" | ").join(contexts)
|
||||
web_search_result = kwargs.get("web_search_result", "")
|
||||
if web_search_result:
|
||||
context_string = self._append_search_and_context(context_string, web_search_result)
|
||||
if not config.history:
|
||||
prompt = config.template.substitute(
|
||||
context=context_string, query=input_query
|
||||
)
|
||||
prompt = config.template.substitute(context=context_string, query=input_query)
|
||||
else:
|
||||
prompt = config.template.substitute(
|
||||
context=context_string, query=input_query, history=config.history
|
||||
)
|
||||
prompt = config.template.substitute(context=context_string, query=input_query, history=config.history)
|
||||
return prompt
|
||||
|
||||
def get_answer_from_llm(self, prompt, config: ChatConfig):
|
||||
@@ -198,7 +206,14 @@ class EmbedChain:
|
||||
|
||||
return self.get_llm_model_answer(prompt, config)
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None):
|
||||
def access_search_and_get_results(self, input_query):
|
||||
from langchain.tools import DuckDuckGoSearchRun
|
||||
|
||||
search = DuckDuckGoSearchRun()
|
||||
logging.info(f"Access search to get answers for {input_query}")
|
||||
return search.run(input_query)
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None, dry_run=False):
|
||||
"""
|
||||
Queries the vector database based on the given input query.
|
||||
Gets relevant doc based on the query and then passes it to an
|
||||
@@ -207,14 +222,29 @@ class EmbedChain:
|
||||
:param input_query: The query to use.
|
||||
:param config: Optional. The `QueryConfig` instance to use as
|
||||
configuration options.
|
||||
:param dry_run: Optional. A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response.
|
||||
You can use it to test your prompt, including the context provided
|
||||
by the vector database's doc retrieval.
|
||||
The only thing the dry run does not consider is the cut-off due to
|
||||
the `max_tokens` parameter.
|
||||
:return: The answer to the query.
|
||||
"""
|
||||
if config is None:
|
||||
config = QueryConfig()
|
||||
if self.is_docs_site_instance:
|
||||
config.template = DOCS_SITE_PROMPT_TEMPLATE
|
||||
config.number_documents = 5
|
||||
k = {}
|
||||
if self.online:
|
||||
k["web_search_result"] = self.access_search_and_get_results(input_query)
|
||||
contexts = self.retrieve_from_database(input_query, config)
|
||||
prompt = self.generate_prompt(input_query, contexts, config)
|
||||
prompt = self.generate_prompt(input_query, contexts, config, **k)
|
||||
logging.info(f"Prompt: {prompt}")
|
||||
|
||||
if dry_run:
|
||||
return prompt
|
||||
|
||||
answer = self.get_answer_from_llm(prompt, config)
|
||||
|
||||
if isinstance(answer, str):
|
||||
@@ -230,7 +260,7 @@ class EmbedChain:
|
||||
yield chunk
|
||||
logging.info(f"Answer: {streamed_answer}")
|
||||
|
||||
def chat(self, input_query, config: ChatConfig = None):
|
||||
def chat(self, input_query, config: ChatConfig = None, dry_run=False):
|
||||
"""
|
||||
Queries the vector database on the given input query.
|
||||
Gets relevant doc based on the query and then passes it to an
|
||||
@@ -240,12 +270,23 @@ class EmbedChain:
|
||||
:param input_query: The query to use.
|
||||
:param config: Optional. The `ChatConfig` instance to use as
|
||||
configuration options.
|
||||
:param dry_run: Optional. A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response.
|
||||
You can use it to test your prompt, including the context provided
|
||||
by the vector database's doc retrieval.
|
||||
The only thing the dry run does not consider is the cut-off due to
|
||||
the `max_tokens` parameter.
|
||||
:return: The answer to the query.
|
||||
"""
|
||||
if config is None:
|
||||
config = ChatConfig()
|
||||
|
||||
contexts = self.retrieve_from_database(input_query, config)
|
||||
if self.is_docs_site_instance:
|
||||
config.template = DOCS_SITE_PROMPT_TEMPLATE
|
||||
config.number_documents = 5
|
||||
k = {}
|
||||
if self.online:
|
||||
k["web_search_result"] = self.access_search_and_get_results(input_query)
|
||||
contexts = self.retrieve_from_database(input_query, config, **k)
|
||||
|
||||
global memory
|
||||
chat_history = memory.load_memory_variables({})["history"]
|
||||
@@ -253,8 +294,12 @@ class EmbedChain:
|
||||
if chat_history:
|
||||
config.set_history(chat_history)
|
||||
|
||||
prompt = self.generate_prompt(input_query, contexts, config)
|
||||
prompt = self.generate_prompt(input_query, contexts, config, **k)
|
||||
logging.info(f"Prompt: {prompt}")
|
||||
|
||||
if dry_run:
|
||||
return prompt
|
||||
|
||||
answer = self.get_answer_from_llm(prompt, config)
|
||||
|
||||
memory.chat_memory.add_user_message(input_query)
|
||||
@@ -275,27 +320,6 @@ class EmbedChain:
|
||||
memory.chat_memory.add_ai_message(streamed_answer)
|
||||
logging.info(f"Answer: {streamed_answer}")
|
||||
|
||||
def dry_run(self, input_query, config: QueryConfig = None):
|
||||
"""
|
||||
A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response.
|
||||
You can use it to test your prompt, including the context provided
|
||||
by the vector database's doc retrieval.
|
||||
The only thing the dry run does not consider is the cut-off due to
|
||||
the `max_tokens` parameter.
|
||||
|
||||
:param input_query: The query to use.
|
||||
:param config: Optional. The `QueryConfig` instance to use as
|
||||
configuration options.
|
||||
:return: The prompt that would be sent to the LLM
|
||||
"""
|
||||
if config is None:
|
||||
config = QueryConfig()
|
||||
contexts = self.retrieve_from_database(input_query, config)
|
||||
prompt = self.generate_prompt(input_query, contexts, config)
|
||||
logging.info(f"Prompt: {prompt}")
|
||||
return prompt
|
||||
|
||||
def count(self):
|
||||
"""
|
||||
Count the number of embeddings.
|
||||
@@ -310,155 +334,3 @@ class EmbedChain:
|
||||
`App` has to be reinitialized after using this method.
|
||||
"""
|
||||
self.db_client.reset()
|
||||
|
||||
|
||||
class App(EmbedChain):
|
||||
"""
|
||||
The EmbedChain app.
|
||||
Has two functions: add and query.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
dry_run(query): test your prompt without consuming tokens.
|
||||
"""
|
||||
|
||||
def __init__(self, config: InitConfig = None):
|
||||
"""
|
||||
:param config: InitConfig instance to load as configuration. Optional.
|
||||
"""
|
||||
if config is None:
|
||||
config = InitConfig()
|
||||
|
||||
if not config.ef:
|
||||
config._set_embedding_function_to_default()
|
||||
|
||||
if not config.db:
|
||||
config._set_db_to_default()
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig):
|
||||
messages = []
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
response = openai.ChatCompletion.create(
|
||||
model=config.model,
|
||||
messages=messages,
|
||||
temperature=config.temperature,
|
||||
max_tokens=config.max_tokens,
|
||||
top_p=config.top_p,
|
||||
stream=config.stream,
|
||||
)
|
||||
|
||||
if config.stream:
|
||||
return self._stream_llm_model_response(response)
|
||||
else:
|
||||
return response["choices"][0]["message"]["content"]
|
||||
|
||||
def _stream_llm_model_response(self, response):
|
||||
"""
|
||||
This is a generator for streaming response from the OpenAI completions API
|
||||
"""
|
||||
for line in response:
|
||||
chunk = line["choices"][0].get("delta", {}).get("content", "")
|
||||
yield chunk
|
||||
|
||||
|
||||
class OpenSourceApp(EmbedChain):
|
||||
"""
|
||||
The OpenSource app.
|
||||
Same as App, but uses an open source embedding model and LLM.
|
||||
|
||||
Has two function: add and query.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
"""
|
||||
|
||||
def __init__(self, config: InitConfig = None):
|
||||
"""
|
||||
:param config: InitConfig instance to load as configuration. Optional.
|
||||
`ef` defaults to open source.
|
||||
"""
|
||||
print(
|
||||
"Loading open source embedding model. This may take some time..."
|
||||
) # noqa:E501
|
||||
if not config:
|
||||
config = InitConfig()
|
||||
|
||||
if not config.ef:
|
||||
config._set_embedding_function(
|
||||
embedding_functions.SentenceTransformerEmbeddingFunction(
|
||||
model_name="all-MiniLM-L6-v2"
|
||||
)
|
||||
)
|
||||
|
||||
if not config.db:
|
||||
config._set_db_to_default()
|
||||
|
||||
print("Successfully loaded open source embedding model.")
|
||||
super().__init__(config)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig):
|
||||
from gpt4all import GPT4All
|
||||
|
||||
global gpt4all_model
|
||||
if gpt4all_model is None:
|
||||
gpt4all_model = GPT4All("orca-mini-3b.ggmlv3.q4_0.bin")
|
||||
response = gpt4all_model.generate(prompt=prompt, streaming=config.stream)
|
||||
return response
|
||||
|
||||
|
||||
class EmbedChainPersonApp:
|
||||
"""
|
||||
Base class to create a person bot.
|
||||
This bot behaves and speaks like a person.
|
||||
|
||||
:param person: name of the person, better if its a well known person.
|
||||
:param config: InitConfig instance to load as configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, person, config: InitConfig = None):
|
||||
self.person = person
|
||||
self.person_prompt = f"You are {person}. Whatever you say, you will always say in {person} style." # noqa:E501
|
||||
self.template = Template(self.person_prompt + " " + DEFAULT_PROMPT)
|
||||
if config is None:
|
||||
config = InitConfig()
|
||||
super().__init__(config)
|
||||
|
||||
|
||||
class PersonApp(EmbedChainPersonApp, App):
|
||||
"""
|
||||
The Person app.
|
||||
Extends functionality from EmbedChainPersonApp and App
|
||||
"""
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None):
|
||||
query_config = QueryConfig(
|
||||
template=self.template,
|
||||
)
|
||||
return super().query(input_query, query_config)
|
||||
|
||||
def chat(self, input_query, config: ChatConfig = None):
|
||||
chat_config = ChatConfig(
|
||||
template=self.template,
|
||||
)
|
||||
return super().chat(input_query, chat_config)
|
||||
|
||||
|
||||
class PersonOpenSourceApp(EmbedChainPersonApp, OpenSourceApp):
|
||||
"""
|
||||
The Person app.
|
||||
Extends functionality from EmbedChainPersonApp and OpenSourceApp
|
||||
"""
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None):
|
||||
query_config = QueryConfig(
|
||||
template=self.template,
|
||||
)
|
||||
return super().query(input_query, query_config)
|
||||
|
||||
def chat(self, input_query, config: ChatConfig = None):
|
||||
chat_config = ChatConfig(
|
||||
template=self.template,
|
||||
)
|
||||
return super().chat(input_query, chat_config)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import logging
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class DocsSiteLoader:
|
||||
def __init__(self):
|
||||
self.visited_links = set()
|
||||
|
||||
def _get_child_links_recursive(self, url):
|
||||
parsed_url = urlparse(url)
|
||||
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
current_path = parsed_url.path
|
||||
|
||||
response = requests.get(url)
|
||||
if response.status_code != 200:
|
||||
logging.info(f"Failed to fetch the website: {response.status_code}")
|
||||
return
|
||||
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
all_links = [link.get("href") for link in soup.find_all("a")]
|
||||
|
||||
child_links = [link for link in all_links if link and link.startswith(current_path) and link != current_path]
|
||||
|
||||
absolute_paths = [urljoin(base_url, link) for link in child_links]
|
||||
|
||||
for link in absolute_paths:
|
||||
if link not in self.visited_links:
|
||||
self.visited_links.add(link)
|
||||
self._get_child_links_recursive(link)
|
||||
|
||||
def _get_all_urls(self, url):
|
||||
self.visited_links = set()
|
||||
self._get_child_links_recursive(url)
|
||||
urls = [link for link in self.visited_links if urlparse(link).netloc == urlparse(url).netloc]
|
||||
return urls
|
||||
|
||||
def _load_data_from_url(self, url):
|
||||
response = requests.get(url)
|
||||
if response.status_code != 200:
|
||||
logging.info(f"Failed to fetch the website: {response.status_code}")
|
||||
return []
|
||||
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
selectors = [
|
||||
"article.bd-article",
|
||||
'article[role="main"]',
|
||||
"div.md-content",
|
||||
'div[role="main"]',
|
||||
"div.container",
|
||||
"div.section",
|
||||
"article",
|
||||
"main",
|
||||
]
|
||||
|
||||
output = []
|
||||
for selector in selectors:
|
||||
element = soup.select_one(selector)
|
||||
if element:
|
||||
content = element.prettify()
|
||||
break
|
||||
else:
|
||||
content = soup.get_text()
|
||||
|
||||
soup = BeautifulSoup(content, "html.parser")
|
||||
ignored_tags = [
|
||||
"nav",
|
||||
"aside",
|
||||
"form",
|
||||
"header",
|
||||
"noscript",
|
||||
"svg",
|
||||
"canvas",
|
||||
"footer",
|
||||
"script",
|
||||
"style",
|
||||
]
|
||||
for tag in soup(ignored_tags):
|
||||
tag.decompose()
|
||||
|
||||
content = " ".join(soup.stripped_strings)
|
||||
output.append(
|
||||
{
|
||||
"content": content,
|
||||
"meta_data": {"url": url},
|
||||
}
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
def load_data(self, url):
|
||||
all_urls = self._get_all_urls(url)
|
||||
output = []
|
||||
for u in all_urls:
|
||||
output.extend(self._load_data_from_url(u))
|
||||
return output
|
||||
@@ -1,7 +1,11 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from bs4.builder import ParserRejectedMarkup
|
||||
|
||||
from embedchain.loaders.web_page import WebPageLoader
|
||||
from embedchain.utils import is_readable
|
||||
|
||||
|
||||
class SitemapLoader:
|
||||
@@ -17,8 +21,20 @@ class SitemapLoader:
|
||||
response.raise_for_status()
|
||||
|
||||
soup = BeautifulSoup(response.text, "xml")
|
||||
links = [link.text for link in soup.find_all("loc")]
|
||||
|
||||
links = [link.text for link in soup.find_all("loc") if link.parent.name == "url"]
|
||||
if len(links) == 0:
|
||||
# Get all <loc> tags as a fallback. This might include images.
|
||||
links = [link.text for link in soup.find_all("loc")]
|
||||
|
||||
for link in links:
|
||||
each_load_data = web_page_loader.load_data(link)
|
||||
output.append(each_load_data)
|
||||
try:
|
||||
each_load_data = web_page_loader.load_data(link)
|
||||
|
||||
if is_readable(each_load_data[0].get("content")):
|
||||
output.append(each_load_data)
|
||||
else:
|
||||
logging.warning(f"Page is not readable (too many invalid characters): {link}")
|
||||
except ParserRejectedMarkup as e:
|
||||
logging.error(f"Failed to parse {link}: {e}")
|
||||
return [data[0] for data in output]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
@@ -10,31 +12,58 @@ class WebPageLoader:
|
||||
response = requests.get(url)
|
||||
data = response.content
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
for tag in soup(
|
||||
[
|
||||
"nav",
|
||||
"aside",
|
||||
"form",
|
||||
"header",
|
||||
"noscript",
|
||||
"svg",
|
||||
"canvas",
|
||||
"footer",
|
||||
"script",
|
||||
"style",
|
||||
]
|
||||
):
|
||||
tag.string = " "
|
||||
output = []
|
||||
original_size = len(str(soup.get_text()))
|
||||
|
||||
tags_to_exclude = [
|
||||
"nav",
|
||||
"aside",
|
||||
"form",
|
||||
"header",
|
||||
"noscript",
|
||||
"svg",
|
||||
"canvas",
|
||||
"footer",
|
||||
"script",
|
||||
"style",
|
||||
]
|
||||
for tag in soup(tags_to_exclude):
|
||||
tag.decompose()
|
||||
|
||||
ids_to_exclude = ["sidebar", "main-navigation", "menu-main-menu"]
|
||||
for id in ids_to_exclude:
|
||||
tags = soup.find_all(id=id)
|
||||
for tag in tags:
|
||||
tag.decompose()
|
||||
|
||||
classes_to_exclude = [
|
||||
"elementor-location-header",
|
||||
"navbar-header",
|
||||
"nav",
|
||||
"header-sidebar-wrapper",
|
||||
"blog-sidebar-wrapper",
|
||||
"related-posts",
|
||||
]
|
||||
for class_name in classes_to_exclude:
|
||||
tags = soup.find_all(class_=class_name)
|
||||
for tag in tags:
|
||||
tag.decompose()
|
||||
|
||||
content = soup.get_text()
|
||||
content = clean_string(content)
|
||||
|
||||
cleaned_size = len(content)
|
||||
if original_size != 0:
|
||||
logging.info(
|
||||
f"[{url}] Cleaned page size: {cleaned_size} characters, down from {original_size} (shrunk: {original_size-cleaned_size} chars, {round((1-(cleaned_size/original_size)) * 100, 2)}%)" # noqa:E501
|
||||
)
|
||||
|
||||
meta_data = {
|
||||
"url": url,
|
||||
}
|
||||
output.append(
|
||||
|
||||
return [
|
||||
{
|
||||
"content": content,
|
||||
"meta_data": meta_data,
|
||||
}
|
||||
)
|
||||
return output
|
||||
]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class EmbeddingFunctions(Enum):
|
||||
OPENAI = "OPENAI"
|
||||
HUGGING_FACE = "HUGGING_FACE"
|
||||
VERTEX_AI = "VERTEX_AI"
|
||||
GPT4ALL = "GPT4ALL"
|
||||
@@ -0,0 +1,8 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Providers(Enum):
|
||||
OPENAI = "OPENAI"
|
||||
ANTHROPHIC = "ANTHPROPIC"
|
||||
VERTEX_AI = "VERTEX_AI"
|
||||
GPT4ALL = "GPT4ALL"
|
||||
@@ -0,0 +1,2 @@
|
||||
from .EmbeddingFunctions import EmbeddingFunctions # noqa: F401
|
||||
from .Providers import Providers # noqa: F401
|
||||
@@ -1,4 +1,5 @@
|
||||
import re
|
||||
import string
|
||||
|
||||
|
||||
def clean_string(text):
|
||||
@@ -33,3 +34,14 @@ def clean_string(text):
|
||||
cleaned_text = re.sub(r"([^\w\s])\1*", r"\1", cleaned_text)
|
||||
|
||||
return cleaned_text
|
||||
|
||||
|
||||
def is_readable(s):
|
||||
"""
|
||||
Heuristic to determine if a string is "readable" (mostly contains printable characters and forms meaningful words)
|
||||
|
||||
:param s: string
|
||||
:return: True if the string is more than 95% printable.
|
||||
"""
|
||||
printable_ratio = sum(c in string.printable for c in s) / len(s)
|
||||
return printable_ratio > 0.95 # 95% of characters are printable
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import chromadb
|
||||
from chromadb.utils import embedding_functions
|
||||
from chromadb.config import Settings
|
||||
|
||||
from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
|
||||
@@ -10,33 +9,33 @@ from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
class ChromaDB(BaseVectorDB):
|
||||
"""Vector database using ChromaDB."""
|
||||
|
||||
def __init__(self, db_dir=None, ef=None, host=None, port=None):
|
||||
self.ef = ef
|
||||
def __init__(self, db_dir=None, embedding_fn=None, host=None, port=None):
|
||||
self.embedding_fn = embedding_fn
|
||||
|
||||
if not hasattr(embedding_fn, "__call__"):
|
||||
raise ValueError("Embedding function is not a function")
|
||||
|
||||
if host and port:
|
||||
logging.info(f"Connecting to ChromaDB server: {host}:{port}")
|
||||
self.client_settings = chromadb.config.Settings(
|
||||
chroma_api_impl="rest",
|
||||
chroma_server_host=host,
|
||||
chroma_server_http_port=port,
|
||||
)
|
||||
self.settings = Settings(chroma_server_host=host, chroma_server_http_port=port)
|
||||
self.client = chromadb.HttpClient(self.settings)
|
||||
else:
|
||||
if db_dir is None:
|
||||
db_dir = "db"
|
||||
self.client_settings = chromadb.config.Settings(
|
||||
chroma_db_impl="duckdb+parquet",
|
||||
persist_directory=db_dir,
|
||||
anonymized_telemetry=False,
|
||||
self.settings = Settings(anonymized_telemetry=False, allow_reset=True)
|
||||
self.client = chromadb.PersistentClient(
|
||||
path=db_dir,
|
||||
settings=self.settings,
|
||||
)
|
||||
super().__init__()
|
||||
|
||||
def _get_or_create_db(self):
|
||||
"""Get or create the database."""
|
||||
return chromadb.Client(self.client_settings)
|
||||
return self.client
|
||||
|
||||
def _get_or_create_collection(self):
|
||||
"""Get or create the collection."""
|
||||
return self.client.get_or_create_collection(
|
||||
"embedchain_store",
|
||||
embedding_function=self.ef,
|
||||
embedding_function=self.embedding_fn,
|
||||
)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
__version__ = "0.0.22"
|
||||
@@ -34,13 +34,13 @@
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from embedchain import App\n",
|
||||
"from embedchain.config import InitConfig\n",
|
||||
"from embedchain.config import AppConfig\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"chromadb_host = \"localhost\"\n",
|
||||
"chromadb_port = 8000\n",
|
||||
"\n",
|
||||
"config = InitConfig(host=chromadb_host, port=chromadb_port)\n",
|
||||
"config = AppConfig(host=chromadb_host, port=chromadb_port)\n",
|
||||
"elon_bot = App(config)"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "e9a9dc6a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from embedchain import App\n",
|
||||
"\n",
|
||||
"embedchain_docs_bot = App()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "c1c24d68",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"All data from https://docs.embedchain.ai/ already exists in the database.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"embedchain_docs_bot.add(\"docs_site\", \"https://docs.embedchain.ai/\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "48cdaecf",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"answer = embedchain_docs_bot.query(\"Write a flask API for embedchain bot\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "0fe18085",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/markdown": [
|
||||
"To write a Flask API for the embedchain bot, you can use the following code snippet:\n",
|
||||
"\n",
|
||||
"```python\n",
|
||||
"from flask import Flask, request, jsonify\n",
|
||||
"from embedchain import App\n",
|
||||
"\n",
|
||||
"app = Flask(__name__)\n",
|
||||
"bot = App()\n",
|
||||
"\n",
|
||||
"# Add datasets to the bot\n",
|
||||
"bot.add(\"youtube_video\", \"https://www.youtube.com/watch?v=3qHkcs3kG44\")\n",
|
||||
"bot.add(\"pdf_file\", \"https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf\")\n",
|
||||
"\n",
|
||||
"@app.route('/query', methods=['POST'])\n",
|
||||
"def query():\n",
|
||||
" data = request.get_json()\n",
|
||||
" question = data['question']\n",
|
||||
" response = bot.query(question)\n",
|
||||
" return jsonify({'response': response})\n",
|
||||
"\n",
|
||||
"if __name__ == '__main__':\n",
|
||||
" app.run()\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"In this code, we create a Flask app and initialize an instance of the embedchain bot. We then add the desired datasets to the bot using the `add()` function.\n",
|
||||
"\n",
|
||||
"Next, we define a route `/query` that accepts POST requests. The request body should contain a JSON object with a `question` field. The bot's `query()` function is called with the provided question, and the response is returned as a JSON object.\n",
|
||||
"\n",
|
||||
"Finally, we run the Flask app using `app.run()`.\n",
|
||||
"\n",
|
||||
"Note: Make sure to install Flask and embedchain packages before running this code."
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.Markdown object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from IPython.display import Markdown\n",
|
||||
"# Create a Markdown object and display it\n",
|
||||
"markdown_answer = Markdown(answer)\n",
|
||||
"display(markdown_answer)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
[virtualenvs]
|
||||
in-project = true
|
||||
path = "."
|
||||
+50
-3
@@ -1,3 +1,11 @@
|
||||
[tool.poetry]
|
||||
name = "embedchain"
|
||||
version = "0.0.23"
|
||||
description = "embedchain is a framework to easily create LLM powered bots over any dataset"
|
||||
authors = ["Taranjeet Singh"]
|
||||
license = "Apache License"
|
||||
readme = "README.md"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
@@ -5,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
||||
[tool.ruff]
|
||||
select = ["E", "F"]
|
||||
ignore = []
|
||||
fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
exclude = [
|
||||
".bzr",
|
||||
@@ -30,15 +38,19 @@ exclude = [
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
line-length = 88
|
||||
line-length = 120
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
target-version = "py38"
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
|
||||
[tool.ruff.per-file-ignores]
|
||||
"embedchain/__init__.py" = ["E401"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
line-length = 120
|
||||
target-version = ["py38", "py39", "py310", "py311"]
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
@@ -66,3 +78,38 @@ exclude = '''
|
||||
|
||||
[tool.black.format]
|
||||
color = true
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<3.9.7 || >3.9.7,<4.0"
|
||||
python-dotenv = "^1.0.0"
|
||||
langchain = "^0.0.237"
|
||||
requests = "^2.31.0"
|
||||
openai = "^0.27.5"
|
||||
chromadb ="^0.4.2"
|
||||
youtube-transcript-api = "^0.6.1"
|
||||
beautifulsoup4 = "^4.12.2"
|
||||
pypdf = "^3.11.0"
|
||||
pytube = "^15.0.0"
|
||||
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^23.3.0"
|
||||
pre-commit = "^3.2.2"
|
||||
ruff = "^0.0.220"
|
||||
pytest = "^7.3.1"
|
||||
pytest-mock = "^3.10.0"
|
||||
pytest-env = "^0.8.1"
|
||||
click = "^8.1.3"
|
||||
isort = "^5.12.0"
|
||||
|
||||
[tool.poetry.extras]
|
||||
streamlit = ["streamlit"]
|
||||
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
|
||||
|
||||
|
||||
[tool.poetry.scripts]
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
pip
|
||||
black==23.3.0
|
||||
isort==5.8.0
|
||||
ruff==0.0.277
|
||||
pytest==7.4.0
|
||||
@@ -1,14 +1,11 @@
|
||||
import setuptools
|
||||
|
||||
import embedchain.version
|
||||
|
||||
|
||||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
setuptools.setup(
|
||||
name="embedchain",
|
||||
version=embedchain.version.__version__,
|
||||
version="0.0.28",
|
||||
author="Taranjeet Singh",
|
||||
author_email="reachtotj@gmail.com",
|
||||
description="embedchain is a framework to easily create LLM powered bots over any dataset", # noqa:E501
|
||||
@@ -27,7 +24,7 @@ setuptools.setup(
|
||||
"langchain>=0.0.205",
|
||||
"requests",
|
||||
"openai",
|
||||
"chromadb==0.3.26",
|
||||
"chromadb>=0.4.2",
|
||||
"youtube-transcript-api",
|
||||
"beautifulsoup4",
|
||||
"pypdf",
|
||||
@@ -37,6 +34,8 @@ setuptools.setup(
|
||||
"sentence_transformers",
|
||||
"docx2txt",
|
||||
"pydantic==1.10.8",
|
||||
"replicate==0.9.0",
|
||||
"duckduckgo-search==3.8.4",
|
||||
],
|
||||
extras_require={"dev": ["black", "ruff", "isort", "pytest"]},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# ruff: noqa: E501
|
||||
|
||||
import unittest
|
||||
|
||||
from embedchain.chunkers.text import TextChunker
|
||||
from embedchain.config import ChunkerConfig
|
||||
|
||||
|
||||
class TestTextChunker(unittest.TestCase):
|
||||
def test_chunks(self):
|
||||
"""
|
||||
Test the chunks generated by TextChunker.
|
||||
# TODO: Not a very precise test.
|
||||
"""
|
||||
chunker_config = ChunkerConfig(chunk_size=10, chunk_overlap=0, length_function=len)
|
||||
chunker = TextChunker(config=chunker_config)
|
||||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
|
||||
result = chunker.create_chunks(MockLoader(), text)
|
||||
|
||||
documents = result["documents"]
|
||||
|
||||
self.assertGreaterEqual(len(documents), 5)
|
||||
|
||||
# Additional test cases can be added to cover different scenarios
|
||||
|
||||
|
||||
class MockLoader:
|
||||
def load_data(self, src):
|
||||
"""
|
||||
Mock loader that returns a list of data dictionaries.
|
||||
Adjust this method to return different data for testing.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"content": src,
|
||||
"meta_data": {"url": "none"},
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from embedchain import App
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
|
||||
def setUp(self):
|
||||
self.app = App()
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_add(self):
|
||||
"""
|
||||
This test checks the functionality of the 'add' method in the App class.
|
||||
It begins by simulating the addition of a web page with a specific URL to the application instance.
|
||||
The 'add' method is expected to append the input type and URL to the 'user_asks' attribute of the App instance.
|
||||
By asserting that 'user_asks' is updated correctly after the 'add' method is called, we can confirm that the
|
||||
method is working as intended.
|
||||
The Collection.add method from the chromadb library is mocked during this test to isolate the behavior of the
|
||||
'add' method.
|
||||
"""
|
||||
self.app.add("web_page", "https://example.com", {"meta": "meta-data"})
|
||||
self.assertEqual(self.app.user_asks, [["web_page", "https://example.com", {"meta": "meta-data"}]])
|
||||
@@ -0,0 +1,48 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from embedchain import App
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
|
||||
def setUp(self):
|
||||
self.app = App()
|
||||
|
||||
@patch("embedchain.embedchain.memory", autospec=True)
|
||||
@patch.object(App, "retrieve_from_database", return_value=["Test context"])
|
||||
@patch.object(App, "get_answer_from_llm", return_value="Test answer")
|
||||
def test_chat_with_memory(self, mock_answer, mock_retrieve, mock_memory):
|
||||
"""
|
||||
This test checks the functionality of the 'chat' method in the App class with respect to the chat history
|
||||
memory.
|
||||
The 'chat' method is called twice. The first call initializes the chat history memory.
|
||||
The second call is expected to use the chat history from the first call.
|
||||
|
||||
Key assumptions tested:
|
||||
- After the first call, 'memory.chat_memory.add_user_message' and 'memory.chat_memory.add_ai_message' are
|
||||
called with correct arguments, adding the correct chat history.
|
||||
- During the second call, the 'chat' method uses the chat history from the first call.
|
||||
|
||||
The test isolates the 'chat' method behavior by mocking out 'retrieve_from_database', 'get_answer_from_llm' and
|
||||
'memory' methods.
|
||||
"""
|
||||
mock_memory.load_memory_variables.return_value = {"history": []}
|
||||
app = App()
|
||||
|
||||
# First call to chat
|
||||
first_answer = app.chat("Test query 1")
|
||||
self.assertEqual(first_answer, "Test answer")
|
||||
mock_memory.chat_memory.add_user_message.assert_called_once_with("Test query 1")
|
||||
mock_memory.chat_memory.add_ai_message.assert_called_once_with("Test answer")
|
||||
|
||||
mock_memory.chat_memory.add_user_message.reset_mock()
|
||||
mock_memory.chat_memory.add_ai_message.reset_mock()
|
||||
|
||||
# Second call to chat
|
||||
second_answer = app.chat("Test query 2")
|
||||
self.assertEqual(second_answer, "Test answer")
|
||||
mock_memory.chat_memory.add_user_message.assert_called_once_with("Test query 2")
|
||||
mock_memory.chat_memory.add_ai_message.assert_called_once_with("Test answer")
|
||||
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig
|
||||
|
||||
|
||||
class TestChromaDbHostsLoglevel(unittest.TestCase):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add")
|
||||
@patch("chromadb.api.models.Collection.Collection.get")
|
||||
@patch("embedchain.embedchain.EmbedChain.retrieve_from_database")
|
||||
@patch("embedchain.embedchain.EmbedChain.get_answer_from_llm")
|
||||
@patch("embedchain.embedchain.EmbedChain.get_llm_model_answer")
|
||||
def test_whole_app(
|
||||
self,
|
||||
_mock_get,
|
||||
_mock_add,
|
||||
_mock_ec_retrieve_from_database,
|
||||
_mock_get_answer_from_llm,
|
||||
mock_ec_get_llm_model_answer,
|
||||
):
|
||||
"""
|
||||
Test if the `App` instance is initialized without a config that does not contain default hosts and ports.
|
||||
"""
|
||||
config = AppConfig(log_level="DEBUG")
|
||||
|
||||
app = App(config)
|
||||
|
||||
knowledge = "lorem ipsum dolor sit amet, consectetur adipiscing"
|
||||
|
||||
app.add_local("text", knowledge)
|
||||
|
||||
app.query("What text did I give you?")
|
||||
app.chat("What text did I give you?")
|
||||
|
||||
self.assertEqual(mock_ec_get_llm_model_answer.call_args[1]["documents"], [knowledge])
|
||||
@@ -0,0 +1,66 @@
|
||||
import unittest
|
||||
from string import Template
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.embedchain import QueryConfig
|
||||
|
||||
|
||||
class TestGeneratePrompt(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = App()
|
||||
|
||||
def test_generate_prompt_with_template(self):
|
||||
"""
|
||||
Tests that the generate_prompt method correctly formats the prompt using
|
||||
a custom template provided in the QueryConfig instance.
|
||||
|
||||
This test sets up a scenario with an input query and a list of contexts,
|
||||
and a custom template, and then calls generate_prompt. It checks that the
|
||||
returned prompt correctly incorporates all the contexts and the query into
|
||||
the format specified by the template.
|
||||
"""
|
||||
# Setup
|
||||
input_query = "Test query"
|
||||
contexts = ["Context 1", "Context 2", "Context 3"]
|
||||
template = "You are a bot. Context: ${context} - Query: ${query} - Helpful answer:"
|
||||
config = QueryConfig(template=Template(template))
|
||||
|
||||
# Execute
|
||||
result = self.app.generate_prompt(input_query, contexts, config)
|
||||
|
||||
# Assert
|
||||
expected_result = (
|
||||
"You are a bot. Context: Context 1 | Context 2 | Context 3 - Query: Test query - Helpful answer:"
|
||||
)
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
def test_generate_prompt_with_contexts_list(self):
|
||||
"""
|
||||
Tests that the generate_prompt method correctly handles a list of contexts.
|
||||
|
||||
This test sets up a scenario with an input query and a list of contexts,
|
||||
and then calls generate_prompt. It checks that the returned prompt
|
||||
correctly includes all the contexts and the query.
|
||||
"""
|
||||
# Setup
|
||||
input_query = "Test query"
|
||||
contexts = ["Context 1", "Context 2", "Context 3"]
|
||||
config = QueryConfig()
|
||||
|
||||
# Execute
|
||||
result = self.app.generate_prompt(input_query, contexts, config)
|
||||
|
||||
# Assert
|
||||
expected_result = config.template.substitute(context="Context 1 | Context 2 | Context 3", query=input_query)
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
def test_generate_prompt_with_history(self):
|
||||
"""
|
||||
Test the 'generate_prompt' method with QueryConfig containing a history attribute.
|
||||
"""
|
||||
config = QueryConfig(history=["Past context 1", "Past context 2"])
|
||||
config.template = Template("Context: $context | Query: $query | History: $history")
|
||||
prompt = self.app.generate_prompt("Test query", ["Test context"], config)
|
||||
|
||||
expected_prompt = "Context: Test context | Query: Test query | History: ['Past context 1', 'Past context 2']"
|
||||
self.assertEqual(prompt, expected_prompt)
|
||||
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.embedchain import QueryConfig
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
|
||||
def setUp(self):
|
||||
self.app = App()
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_query(self):
|
||||
"""
|
||||
This test checks the functionality of the 'query' method in the App class.
|
||||
It simulates a scenario where the 'retrieve_from_database' method returns a context list and
|
||||
'get_llm_model_answer' returns an expected answer string.
|
||||
|
||||
The 'query' method is expected to call 'retrieve_from_database' and 'get_llm_model_answer' methods
|
||||
appropriately and return the right answer.
|
||||
|
||||
Key assumptions tested:
|
||||
- 'retrieve_from_database' method is called exactly once with arguments: "Test query" and an instance of
|
||||
QueryConfig.
|
||||
- 'get_llm_model_answer' is called exactly once. The specific arguments are not checked in this test.
|
||||
- 'query' method returns the value it received from 'get_llm_model_answer'.
|
||||
|
||||
The test isolates the 'query' method behavior by mocking out 'retrieve_from_database' and
|
||||
'get_llm_model_answer' methods.
|
||||
"""
|
||||
with patch.object(self.app, "retrieve_from_database") as mock_retrieve:
|
||||
mock_retrieve.return_value = ["Test context"]
|
||||
with patch.object(self.app, "get_llm_model_answer") as mock_answer:
|
||||
mock_answer.return_value = "Test answer"
|
||||
answer = self.app.query("Test query")
|
||||
|
||||
self.assertEqual(answer, "Test answer")
|
||||
self.assertEqual(mock_retrieve.call_args[0][0], "Test query")
|
||||
self.assertIsInstance(mock_retrieve.call_args[0][1], QueryConfig)
|
||||
mock_answer.assert_called_once()
|
||||
@@ -1,29 +0,0 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from embedchain import App
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
|
||||
def setUp(self):
|
||||
self.app = App()
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_add(self):
|
||||
self.app.add("web_page", "https://example.com")
|
||||
self.assertEqual(self.app.user_asks, [["web_page", "https://example.com"]])
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_query(self):
|
||||
with patch.object(self.app, "retrieve_from_database") as mock_retrieve:
|
||||
mock_retrieve.return_value = "Test context"
|
||||
with patch.object(self.app, "get_llm_model_answer") as mock_answer:
|
||||
mock_answer.return_value = "Test answer"
|
||||
answer = self.app.query("Test query")
|
||||
|
||||
self.assertEqual(answer, "Test answer")
|
||||
mock_retrieve.assert_called_once_with("Test query")
|
||||
mock_answer.assert_called_once()
|
||||
@@ -0,0 +1,73 @@
|
||||
# ruff: noqa: E501
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig
|
||||
from embedchain.vectordb.chroma_db import ChromaDB, chromadb
|
||||
from chromadb.config import Settings
|
||||
|
||||
|
||||
class TestChromaDbHosts(unittest.TestCase):
|
||||
def test_init_with_host_and_port(self):
|
||||
"""
|
||||
Test if the `ChromaDB` instance is initialized with the correct host and port values.
|
||||
"""
|
||||
host = "test-host"
|
||||
port = "1234"
|
||||
|
||||
with patch.object(chromadb, "Client") as mock_client:
|
||||
_db = ChromaDB(host=host, port=port, embedding_fn=len)
|
||||
|
||||
expected_settings = Settings(
|
||||
chroma_server_host="test-host",
|
||||
chroma_server_http_port="1234",
|
||||
)
|
||||
|
||||
mock_client.assert_called_once_with(expected_settings)
|
||||
|
||||
|
||||
# Review this test
|
||||
class TestChromaDbHostsInit(unittest.TestCase):
|
||||
@patch("embedchain.vectordb.chroma_db.chromadb.Client")
|
||||
def test_init_with_host_and_port(self, mock_client):
|
||||
"""
|
||||
Test if the `App` instance is initialized with the correct host and port values.
|
||||
"""
|
||||
host = "test-host"
|
||||
port = "1234"
|
||||
|
||||
config = AppConfig(host=host, port=port)
|
||||
|
||||
_app = App(config)
|
||||
|
||||
# self.assertEqual(mock_client.call_args[0][0].chroma_server_host, host)
|
||||
# self.assertEqual(mock_client.call_args[0][0].chroma_server_http_port, port)
|
||||
|
||||
|
||||
class TestChromaDbHostsNone(unittest.TestCase):
|
||||
@patch("embedchain.vectordb.chroma_db.chromadb.Client")
|
||||
def test_init_with_host_and_port(self, mock_client):
|
||||
"""
|
||||
Test if the `App` instance is initialized without default hosts and ports.
|
||||
"""
|
||||
|
||||
_app = App()
|
||||
|
||||
self.assertEqual(mock_client.call_args[0][0].chroma_server_host, None)
|
||||
self.assertEqual(mock_client.call_args[0][0].chroma_server_http_port, None)
|
||||
|
||||
|
||||
class TestChromaDbHostsLoglevel(unittest.TestCase):
|
||||
@patch("embedchain.vectordb.chroma_db.chromadb.Client")
|
||||
def test_init_with_host_and_port(self, mock_client):
|
||||
"""
|
||||
Test if the `App` instance is initialized without a config that does not contain default hosts and ports.
|
||||
"""
|
||||
config = AppConfig(log_level="DEBUG")
|
||||
|
||||
_app = App(config)
|
||||
|
||||
self.assertEqual(mock_client.call_args[0][0].chroma_server_host, None)
|
||||
self.assertEqual(mock_client.call_args[0][0].chroma_server_http_port, None)
|
||||
Reference in New Issue
Block a user