Compare commits

..

69 Commits

Author SHA1 Message Date
Taranjeet Singh 12eb16d60b bump version to 0.0.51 (#540) 2023-09-04 01:04:10 +05:30
Dev Khant ec9f454ad1 System prompt at App level (#484)
Co-authored-by: Taranjeet Singh <reachtotj@gmail.com>
2023-09-04 00:55:43 +05:30
Ikko Eltociear Ashimine 9f1f17a611 Fix typo in README.md (#513)
Co-authored-by: cachho <admin@ch-webdev.com>
Co-authored-by: Taranjeet Singh <reachtotj@gmail.com>
2023-09-04 00:54:18 +05:30
aryankhanna475 94369657ef Showcase Update 01-09 (#527)
Co-authored-by: Sahil Kumar Yadav <sahilyadav902@gmail.com>
Co-authored-by: Taranjeet Singh <reachtotj@gmail.com>
2023-09-04 00:52:35 +05:30
Taranjeet Singh 490d7db601 Bump version to 0.0.50 (#530) 2023-09-02 05:56:06 +05:30
Taranjeet Singh ea18b80f90 fix: update chroma init for host & port (#529) 2023-09-02 05:52:10 +05:30
Deshraj Yadav 70077f4e46 Update package version to 0.0.49 (#515) 2023-08-30 11:25:46 -07:00
cachho 85106c7c7e fix: remove stale code (#514) 2023-08-30 11:19:41 -07:00
Taranjeet Singh 1b19d0d19c bump version to 0.0.48 (#503) 2023-08-29 00:31:50 +05:30
Taranjeet Singh 13f01e399c fix: add tiktoken as required dependency in pyproject (#502) 2023-08-29 00:29:12 +05:30
Taranjeet Singh 261e2d088c Bump version to 0.0.47 (#501) 2023-08-29 00:00:35 +05:30
Taranjeet Singh b0ae3e95c7 fix: update poe bot creation docs (#500) 2023-08-28 23:59:34 +05:30
cachho fc633dadeb feat: poe bot (#492)
Co-authored-by: Taranjeet Singh <reachtotj@gmail.com>
2023-08-28 23:47:01 +05:30
Taranjeet Singh aafb334916 fix: improve CTA of book meeting (#496) 2023-08-28 08:19:47 +05:30
Taranjeet Singh 04d851e802 fix: update discord link (#494) 2023-08-28 05:33:03 +05:30
Taranjeet Singh de31c63dac feat: Add cal.com link for feedback (#493) 2023-08-28 05:12:54 +05:30
Deshraj Yadav c068f58543 [version]: bump package version to 0.0.46 2023-08-27 12:24:37 -07:00
Deshraj Yadav 70df373807 [feat]: add support for sending anonymous user_id in telemetry (#491) 2023-08-28 00:53:42 +05:30
Deshraj Yadav 4388f6bfc2 [bug-fix] fix issue related to bot memory when using multiple bots at the same time (#486) 2023-08-25 21:59:39 -07:00
Taranjeet Singh d0956a0dc1 Bump version to 0.0.45 (#479) 2023-08-25 03:22:04 +05:30
cachho ccf515cadd Fix/use poetry for build (#478) 2023-08-25 03:20:57 +05:30
Taranjeet Singh a6e4235bb0 fix: update docs (#477) 2023-08-25 03:16:15 +05:30
Taranjeet Singh 9ba408086e Bump version to 0.0.44 (#476) 2023-08-25 03:12:22 +05:30
cachho bbb3bca1c7 fix: python version string (#475) 2023-08-25 03:10:48 +05:30
Taranjeet Singh 564036a166 Bump version to 0.0.43 (#474) 2023-08-25 03:05:27 +05:30
cachho abf99ce5ea chore: add release workflow (#397) 2023-08-25 03:04:07 +05:30
Taranjeet Singh f0f5c34acb Bump version to 0.0.42 (#473) 2023-08-25 02:55:29 +05:30
cachho ed319531bf fix: dependencies (#416) 2023-08-25 02:53:25 +05:30
Dev Khant d8d0e0e5d1 Add whatsapp bot poetry dependencies (#466) 2023-08-23 10:15:25 -07:00
Girish S 27c91bbd2d Create documentation_issue.yml as Github Issue Template for documentation (#460) 2023-08-20 13:36:44 +05:30
Taranjeet Singh f76d9740c6 Bump version to 0.0.41 (#459) 2023-08-20 02:44:22 +05:30
Deshraj Yadav f29443a0fc [feat] Add support for creating whatsapp bot using embedchain (#458) 2023-08-20 02:42:48 +05:30
Taranjeet Singh 35b022d6bc Add discord link (#456) 2023-08-19 11:05:00 +05:30
Taranjeet Singh 0dd1faf57f Github: Improve issue template. (#455) 2023-08-19 10:31:05 +05:30
Taranjeet Singh b57f096b27 Bump version to 0.0.40 (#454) 2023-08-17 01:53:03 +05:30
cachho 4c8876f032 feat: add method - detect format / data_type (#380) 2023-08-17 01:48:24 +05:30
Taranjeet Singh f92e890aa1 Bump version to 0.0.39 (#453) 2023-08-17 01:39:22 +05:30
cachho 849de5e8ab feat: system prompt (#448) 2023-08-17 01:27:01 +05:30
Dev Khant 7585bc557b Add guide links to readme (#451) 2023-08-16 22:55:13 +05:30
cachho 4021d93168 chore: linting (#449) 2023-08-16 04:57:19 +05:30
Jonas 28e06be26f fix: reset destroys app (#319)
Co-authored-by: cachho <admin@ch-webdev.com>
2023-08-15 05:22:02 +05:30
cachho 39861ec1e8 fix: add telemetry to add_local (#437) 2023-08-15 03:12:10 +05:30
Sahil Kumar Yadav e3ae84b80d add: WhatsApp, Slack and Telegram bots (#438) 2023-08-15 03:10:25 +05:30
Taranjeet Singh 09c02954ba Bump version to 0.0.38 (#442) 2023-08-15 03:01:11 +05:30
cachho 66b661660b feat: session id for telemetry (#440) 2023-08-15 02:58:09 +05:30
cachho c26559a2d3 fix: notion install error message (#439) 2023-08-15 02:57:23 +05:30
Taranjeet Singh c5da46f8b0 update: discord bot docs (#435) 2023-08-12 06:00:21 +05:30
Sahil Kumar Yadav 0b72269e18 add: support for discord bot (#412) 2023-08-12 05:52:27 +05:30
cachho a232d1b779 refactor: do not instantiate all loaders (#418) 2023-08-12 05:32:40 +05:30
Taranjeet Singh 3cab4415b7 Bump version to 0.0.37 (#431) 2023-08-12 05:07:51 +05:30
Taranjeet Singh d494d99c06 docs: update variable name (#430) 2023-08-12 04:58:37 +05:30
cachho 163f437582 feat: anonymous telemetry (#423) 2023-08-12 04:57:11 +05:30
cachho 1e0d967bb5 chore: linting (#428) 2023-08-10 23:42:38 -07:00
Sahil Kumar Yadav a86deb2675 add: API server(#422)
Add an example of api server so that devs can quickly get up a bot running along with its api
2023-08-11 11:30:51 +05:30
Taranjeet Singh d51c508b40 Bump version to 0.0.36 (#426) 2023-08-11 09:50:39 +05:30
aryankhanna475 af8b3081fa feat: Update showcase section in the docs (#421) 2023-08-11 09:44:45 +05:30
Taranjeet Singh 1dbe7daac1 fix: Update embedding field name for Elastiscearch mapping (#425) 2023-08-11 09:43:52 +05:30
Taranjeet Singh e56f91a239 Bump version to 0.0.35 (#424) 2023-08-11 09:29:31 +05:30
Prashant Chaudhary 0179141b2e feat: add support for Elastcisearch as vector data source (#402) 2023-08-11 09:23:56 +05:30
cachho f0abfea55d chore: linting (#414) 2023-08-11 01:53:42 +05:30
Taranjeet Singh 77e223be52 Bump version to 0.0.34 (#420) 2023-08-10 04:48:34 +05:30
Taranjeet Singh c96df72cd0 Fix: lazy load Notion loader (#419) 2023-08-10 04:44:02 +05:30
cachho ce6eb39009 feat: notion loader (#405) 2023-08-09 13:15:22 +05:30
Jonas eeac84e2d9 feat: collection name everywhere (#310)
Co-authored-by: cachho <admin@ch-webdev.com>
2023-08-09 13:08:35 +05:30
Taranjeet Singh 1ee1e671d1 Bump version to 0.0.33 (#411) 2023-08-09 12:59:45 +05:30
cachho 2ef7c0b736 fix: escape pysqlite swapping (#410) 2023-08-09 12:54:41 +05:30
aryankhanna475 f2b563e42a Additions to the community showcase (#401)
Co-authored-by: Sahil Kumar Yadav <sahilyadav902@gmail.com>
2023-08-09 12:39:36 +05:30
Taranjeet Singh 7a718643a3 bump version to 0.0.32 (#409) 2023-08-09 12:28:35 +05:30
Taranjeet Singh 1f0f0c93b7 fix: Pass deployment name as param for azure api (#406) 2023-08-09 12:25:26 +05:30
95 changed files with 2529 additions and 329 deletions
+7
View File
@@ -1 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: 1-on-1 Session
url: https://cal.com/taranjeetio/ec
about: Speak directly with Taranjeet, the founder, to discuss issues, share feedback, or explore improvements for Embedchain
- name: Discord
url: https://discord.gg/6PzXDgEjG5
about: General community discussions
@@ -0,0 +1,11 @@
name: Documentation
description: Report an issue related to the Embedchain docs.
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
body:
- type: textarea
attributes:
label: "Issue with current documentation:"
description: >
Please make sure to leave a reference to the document/code you're
referring to.
+2 -11
View File
@@ -1,8 +1,9 @@
name: 🚀 Feature request
description: Submit a proposal/request for a new embedchain feature
description: Submit a proposal/request for a new Embedchain feature
body:
- type: textarea
id: feature-request
attributes:
label: 🚀 The feature
description: >
@@ -16,16 +17,6 @@ body:
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: >
+32 -16
View File
@@ -1,24 +1,40 @@
name: cd
name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
on:
release:
types:
- published
permissions:
id-token: write
contents: read
types: [published] # This will trigger the workflow when you create a new release
jobs:
publish_to_pypi:
name: publish to pypi on new release
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
runs-on: ubuntu-latest
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
steps:
- uses: actions/checkout@v3
- uses: JRubics/poetry-publish@v1.16
name: Build and publish to PyPI
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
pypi_token: ${{ secrets.PYPI_TOKEN }}
ignore_dev_requirements: "yes"
repository_url: https://upload.pypi.org/legacy/
repository_name: embedchain
python-version: '3.11'
- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Install dependencies
run: poetry install
- name: Build a binary wheel and a source tarball
run: poetry build
- name: Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository_url: https://test.pypi.org/legacy/
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
+21 -27
View File
@@ -1,42 +1,23 @@
# embedchain
[![PyPI](https://img.shields.io/pypi/v/embedchain)](https://pypi.org/project/embedchain/)
[![Discord](https://dcbadge.vercel.app/api/server/6PzXDgEjG5?style=flat)](https://discord.gg/6PzXDgEjG5)
[![Discord](https://dcbadge.vercel.app/api/server/6PzXDgEjG5?style=flat)](https://discord.gg/CUU9FPhRNt)
[![Twitter](https://img.shields.io/twitter/follow/embedchain)](https://twitter.com/embedchain)
[![Substack](https://img.shields.io/badge/Substack-%23006f5c.svg?logo=substack)](https://embedchain.substack.com/)
[![Open in Colab](https://camo.githubusercontent.com/84f0493939e0c4de4e6dbe113251b4bfb5353e57134ffd9fcab6b8714514d4d1/68747470733a2f2f636f6c61622e72657365617263682e676f6f676c652e636f6d2f6173736574732f636f6c61622d62616467652e737667)](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)
## 🤝 Schedule a 1-on-1 Session
Book a [1-on-1 Session](https://cal.com/taranjeetio/ec) with Taranjeet, the founder, to discuss any issues, provide feedback, or explore how we can improve Embedchain for you.
## 🔧 Quick install
```bash
pip install embedchain
```
## 🔥 Latest
- **[2023/07/19]** Released support for 🦙 `llama2` model. Start creating your `llama2` based bots like this:
```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.
```
## 🔍 Demo
Try out embedchain in your browser:
@@ -51,6 +32,16 @@ The documentation for embedchain can be found at [docs.embedchain.ai](https://do
Embedchain empowers you to create chatbot models similar to ChatGPT, using your own evolving dataset.
### Data Types Supported
* Youtube video
* PDF file
* Web page
* Sitemap
* Doc file
* Code documentation website loader
* Notion
### Queries
For example, you can use Embedchain to create an Elon Musk bot using the following code:
@@ -64,9 +55,9 @@ os.environ["OPENAI_API_KEY"] = "YOUR API KEY"
elon_bot = App()
# 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")
elon_bot.add("https://en.wikipedia.org/wiki/Elon_Musk")
elon_bot.add("https://tesla.com/elon-musk")
elon_bot.add("https://www.youtube.com/watch?v=MxZpaJK74Y4")
# Query the bot
elon_bot.query("How many companies does Elon Musk run?")
@@ -78,6 +69,9 @@ elon_bot.query("How many companies does Elon Musk run?")
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).
For more reference, please go through [Development Guide](https://docs.embedchain.ai/contribution/dev) and [Documentation Guide](https://docs.embedchain.ai/contribution/docs).
## Citation
If you utilize this repository, please consider citing it with:
+6 -6
View File
@@ -6,20 +6,20 @@ title: '➕ Adding Data'
- 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.
- Now use `.add` method 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")
naval_chat_bot.add("https://www.youtube.com/watch?v=3qHkcs3kG44")
naval_chat_bot.add("https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf")
naval_chat_bot.add("https://nav.al/feedback")
naval_chat_bot.add("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.add(("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.
+3 -2
View File
@@ -35,8 +35,8 @@ 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")
zuck_bot.add("https://www.youtube.com/watch?v=Ff4fRgnuFgQ")
zuck_bot.add("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?")
@@ -63,6 +63,7 @@ app = OpenSourceApp()
- 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.
- extra dependencies are required for this app type. Install them with `pip install embedchain[opensource]`.
### CustomApp
+13 -9
View File
@@ -20,19 +20,23 @@ from chromadb.utils import embedding_functions
config = AppConfig(log_level="DEBUG")
naval_chat_bot = App(config)
# Example: specify a custom collection name
config = AppConfig(collection_name="naval_chat_bot")
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))
naval_chat_bot.add("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("https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf", config=add_config)
naval_chat_bot.add("https://nav.al/feedback", config=add_config)
naval_chat_bot.add("https://nav.al/agi", config=add_config)
naval_chat_bot.add_local("qna_pair", ("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."), add_config)
naval_chat_bot.add(("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."), config=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))
print(naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?", config=query_config))
```
### Custom prompt template
@@ -49,7 +53,7 @@ einstein_chat_bot = App()
# Embed Wikipedia page
page = wikipedia.page("Albert Einstein")
einstein_chat_bot.add("text", page.content)
einstein_chat_bot.add(page.content)
# Example: use your own custom template with `$context` and `$query`
einstein_chat_template = Template("""
@@ -64,14 +68,14 @@ einstein_chat_template = Template("""
Human: $query
Albert Einstein:""")
query_config = QueryConfig(template=einstein_chat_template)
query_config = QueryConfig(template=einstein_chat_template, system_prompt="You are Albert Einstein.")
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)
response = einstein_chat_bot.query(query, config=query_config)
print("Query: ", query)
print("Response: ", response)
+55 -14
View File
@@ -2,14 +2,40 @@
title: '📋 Supported data formats'
---
Embedchain supports following data formats:
## Automatic data type detection
The add method automatically tries to detect the data_type, based on your input for the source argument. So `app.add('https://www.youtube.com/watch?v=dQw4w9WgXcQ')` is enough to embed a YouTube video.
This detection is implemented for all formats. It is based on factors such as whether it's a URL, a local file, the source data type, etc.
### Debugging automatic detection
Set `log_level=DEBUG` (in [AppConfig](http://localhost:3000/advanced/query_configuration#appconfig)) and make sure it's working as intended.
Otherwise, you will not know when, for instance, an invalid filepath is interpreted as raw text instead.
### Forcing a data type
To omit any issues with the data type detection, you can **force** a data_type by adding it as a `add` method argument.
The examples below show you the keyword to force the respective `data_type`.
Forcing can also be used for edge cases, such as interpreting a sitemap as a web_page, for reading it's raw text instead of following links.
## Remote Data Types
<Tip>
**Use local files in remote data types**
Some data_types are meant for remote content and only work with URLs.
You can pass local files by formatting the path using the `file:` [URI scheme](https://en.wikipedia.org/wiki/File_URI_scheme), e.g. `file:///info.pdf`.
</Tip>
### 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')
app.add('a_valid_youtube_url_here', data_type='youtube_video')
```
### PDF file
@@ -17,7 +43,7 @@ app.add('youtube_video', 'a_valid_youtube_url_here')
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')
app.add('a_valid_url_where_pdf_file_can_be_accessed', data_type='pdf_file')
```
Note that we do not support password protected pdfs.
@@ -27,7 +53,7 @@ Note that we do not support password protected pdfs.
To add any web page, use the data_type as `web_page`. Eg:
```python
app.add('web_page', 'a_valid_web_page_url')
app.add('a_valid_web_page_url', data_type='web_page')
```
### Sitemap
@@ -35,15 +61,16 @@ app.add('web_page', 'a_valid_web_page_url')
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')
app.add('https://example.com/sitemap.xml', data_type='sitemap')
```
### Doc file
To add any doc/docx file, use the data_type as `docx`. Eg:
To add any doc/docx file, use the data_type as `docx`. `docx` allows remote urls and conventional file paths. Eg:
```python
app.add('docx', 'a_local_docx_file_path')
app.add('https://example.com/content/intro.docx', data_type="docx")
app.add('content/intro.docx', data_type="docx")
```
### Code documentation website loader
@@ -51,15 +78,29 @@ app.add('docx', 'a_local_docx_file_path')
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/")
app.add("https://docs.embedchain.ai/", data_type="docs_site")
```
### Notion
To use notion you must install the extra dependencies with `pip install embedchain[notion]`.
To load a notion page, use the data_type as `notion`. Since it is hard to automatically detect, forcing this is advised.
The next argument must **end** with the `notion page id`. The id is a 32-character string. Eg:
```python
app.add("cfbc134ca6464fc980d0391613959196", "notion")
app.add("my-page-cfbc134ca6464fc980d0391613959196", "notion")
app.add("https://www.notion.so/my-page-cfbc134ca6464fc980d0391613959196", "notion")
```
## Local Data Types
### 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.')
app.add('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.', data_type='text')
```
Note: This is not used in the examples because in most cases you will supply a whole paragraph or file, which did not fit.
@@ -69,7 +110,7 @@ Note: This is not used in the examples because in most cases you will supply a w
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"))
app.add(("Question", "Answer"), data_type="qna_pair")
```
## Reusing a vector database
@@ -82,8 +123,8 @@ Create a local index:
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")
naval_chat_bot.add("https://www.youtube.com/watch?v=3qHkcs3kG44")
naval_chat_bot.add("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:
@@ -95,6 +136,6 @@ 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!)
## 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.
- 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.
+10 -5
View File
@@ -4,11 +4,13 @@ title: '🔍 Query configurations'
## AppConfig
| option | description | type | default |
|-------------|-----------------------|---------------------------------|------------------------|
| log_level | log level | string | WARNING |
| 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 |
| db | vector database (experimental) | BaseVectorDB | ChromaDB |
| collection_name | initial collection name for the database | string | embedchain_store |
| collect_metrics | collect anonymous telemetry data to improve embedchain | boolean | true |
## AddConfig
@@ -23,7 +25,7 @@ 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)
app.add("lorem ipsum", config=add_config)
```
### ChunkerConfig
@@ -45,6 +47,7 @@ Default values of chunker config parameters for different `data_type`:
|pdf_file|1000|0|len|
|youtube_video|2000|0|len|
|docs_site|500|50|len|
|notion|300|0|len|
### LoaderConfig
@@ -62,6 +65,8 @@ _coming soon_
|top_p|Controls the diversity of words. Higher values (closer to 1) make word selection more diverse, lower values make words less diverse.|float|1|
|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|
|deployment_name|t.b.a.|str|None|
|system_prompt|System prompt string. Unused if none.|str|None|
## ChatConfig
+36
View File
@@ -17,6 +17,10 @@ Embedchain community has been super active in creating demos on top of Embedchai
- [Taobot.io](https://taobot.io) - chatbot & knowledgebase hybrid by [cachho](https://github.com/cachho)
- [Create Instant ChatBot 🤖 using embedchain](https://databutton.com/v/h3e680h9) by Avra, ([Tweet](https://twitter.com/Avra_b/status/1674704745154641920/))
- [JOBO 🤖 — The AI-driven sidekick to craft your resume](https://try-jobo.com/) by Enrico Willemse, ([LinkedIn Post](https://www.linkedin.com/posts/enrico-willemse_jobai-gptfun-embedchain-activity-7090340080879374336-ueLB/))
- [Explore Your Knowledge Base: Interactive chats over various forms of documents](https://chatdocs.dkedar.com/) by Kedar Dabhadkar, ([LinkedIn Post](https://www.linkedin.com/posts/dkedar7_machinelearning-llmops-activity-7092524836639424513-2O3L/))
- [Chatbot trained on 1000+ videos of Ester hicks the co-author behind the famous book Secret](https://ask-abraham.thoughtseed.repl.co) by Mohan Kumar
## Templates
@@ -33,6 +37,10 @@ Embedchain community has been super active in creating demos on top of Embedchai
- [Chatbot in Under 5 mins using Embedchain](https://medium.com/@ayush.wattal/chatbot-in-under-5-mins-using-embedchain-a4f161fcf9c5) by Ayush Wattal
- [Understanding what the LLM framework embedchain does](https://zenn.dev/hijikix/articles/4bc8d60156a436) by Daisuke Hashimoto
- [In bed with GPT and Node.js](https://dev.to/worldlinetech/in-bed-with-gpt-and-nodejs-4kh2) by Raphaël Semeteys, ([LinkedIn Post](https://www.linkedin.com/posts/raphaelsemeteys_in-bed-with-gpt-and-nodejs-activity-7088113552326029313-nn87/))
- [Using Embedchain — A powerful LangChain Python wrapper to build Chat Bots even faster!⚡](https://medium.com/@avra42/using-embedchain-a-powerful-langchain-python-wrapper-to-build-chat-bots-even-faster-35c12994a360) by Avra, ([Tweet](https://twitter.com/Avra_b/status/1686767751560310784/))
- [What is the Embedchain library?](https://jahaniwww.com/%da%a9%d8%aa%d8%a7%d8%a8%d8%ae%d8%a7%d9%86%d9%87-embedchain/) by Ali Jahani, ([LinkedIn Post](https://www.linkedin.com/posts/ajahani_aepaetaeqaexaggahyaeu-aetaexaesabraeaaeqaepaeu-activity-7097605202135904256-ppU-/))
- [LangChain is Nice, But Have You Tried EmbedChain ?](https://medium.com/thoughts-on-machine-learning/langchain-is-nice-but-have-you-tried-embedchain-215a34421cde) by FS Ndzomga, ([Tweet](https://twitter.com/ndzfs/status/1695583640372035951/))
- [Simplest Method to Build a Custom Chatbot with GPT-3.5 (via Embedchain)](https://www.ainewsletter.today/p/simplest-method-to-build-a-custom) by Arjun, ([Tweet](https://twitter.com/aiguy_arjun/status/1696393808467091758/))
### LinkedIn
@@ -45,6 +53,7 @@ Embedchain community has been super active in creating demos on top of Embedchai
- [About embedchain](https://www.linkedin.com/feed/update/urn:li:activity:7080984218914189312/) by Morris Lee
- [How to use Embedchain](https://www.linkedin.com/posts/nehaabansal_github-embedchainembedchain-framework-activity-7085830340136595456-kbW5/) by Neha Bansal
- [Youtube/Webpage summary for Energy Study](https://www.linkedin.com/posts/bar%C4%B1%C5%9F-sanl%C4%B1-34b82715_enerji-python-activity-7082735341563977730-Js0U/) by Barış Sanlı, ([Tweet](https://twitter.com/barissanli/status/1676968784979193857/))
- [Demo: How to use Embedchain? (Contains Collab Notebook link)](https://www.linkedin.com/posts/liorsinclair_embedchain-is-getting-a-lot-of-traction-because-activity-7103044695995424768-RckT/) by Lior Sinclair
### Twitter
@@ -56,6 +65,7 @@ Embedchain community has been super active in creating demos on top of Embedchai
- [Highlights of EmbedChain](https://twitter.com/carl_AIwarts/status/1673542204328120321/) by carl_AIwarts
- [Build Llama-2 chatbot in less than 5 minutes](https://twitter.com/Saboo_Shubham_/status/1682168956918833152/) by Shubham Saboo
- [All cool features of embedchain](https://twitter.com/DhravyaShah/status/1683497882438217728/) by Dhravya Shah, ([LinkedIn Post](https://www.linkedin.com/posts/dhravyashah_what-if-i-tell-you-that-you-can-make-an-ai-activity-7089459599287726080-ZIYm/))
- [Read paid Medium articles for Free using embedchain](https://twitter.com/kumarkaushal_/status/1688952961622585344) by Kaushal Kumar
## Videos
@@ -69,9 +79,35 @@ Embedchain community has been super active in creating demos on top of Embedchai
- [🤖CHAT with ANY ONLINE RESOURCES using EMBEDCHAIN - a LangChain wrapper, in few lines of code !](https://www.youtube.com/watch?v=Mp7zJe4TIdM) by Avra
- [Building resource-driven LLM-powered bots with Embedchain](https://www.youtube.com/watch?v=IVfcAgxTO4I) by BugBytes
- [embedchain-streamlit-demo](https://www.youtube.com/watch?v=yJAWB13FhYQ) by Amjad Raza
- [Embedchain - create your own AI chatbots using open source models](https://www.youtube.com/shorts/O3rJWKwSrWE) by Dhravya Shah
- [AI ChatBot in 5 lines Python Code](https://www.youtube.com/watch?v=zjWvLJLksv8) by Data Engineering
- [Interview with Karl Marx](https://www.youtube.com/watch?v=5Y4Tscwj1xk) by Alexander Ray Williams
- [Vlog where we try to build a bot based on our content on the internet](https://www.youtube.com/watch?v=I2w8CWM3bx4) by DV, ([Tweet](https://twitter.com/dvcoolster/status/1688387017544261632))
- [CHAT with ANY ONLINE RESOURCES using EMBEDCHAIN|STREAMLIT with MEMORY |All OPENSOURCE](https://www.youtube.com/watch?v=TqQIHWoWTDQ&pp=ygUKZW1iZWRjaGFpbg%3D%3D) by DataInsightEdge
- [Build POWERFUL LLM Bots EASILY with Your Own Data - Embedchain - Langchain 2.0? (Tutorial)](https://www.youtube.com/watch?v=jE24Y_GasE8) by WorldofAI, ([Tweet](https://twitter.com/intheworldofai/status/1696229166922780737))
- [Embedchain: An AI knowledge base assistant for customizing enterprise private data, which can be connected to discord, whatsapp, slack, tele and other terminals (with gradio to build a request interface) in Chinese](https://www.youtube.com/watch?v=5RZzCJRk-d0) by AIGC LINK
- [Embedchain Introduction](https://www.youtube.com/watch?v=Jet9zAqyggI) by Fahd Mirza
## Mentions
### Github repos
- [Awesome-LLM](https://github.com/Hannibal046/Awesome-LLM)
- [awesome-chatgpt-api](https://github.com/reorx/awesome-chatgpt-api)
- [awesome-langchain](https://github.com/kyrolabs/awesome-langchain)
- [Awesome-Prompt-Engineering](https://github.com/promptslab/Awesome-Prompt-Engineering)
- [awesome-chatgpt](https://github.com/eon01/awesome-chatgpt)
- [Awesome-LLMOps](https://github.com/tensorchord/Awesome-LLMOps)
- [awesome-generative-ai](https://github.com/filipecalegario/awesome-generative-ai)
- [awesome-gpt](https://github.com/formulahendry/awesome-gpt)
- [awesome-ChatGPT-repositories](https://github.com/taishi-i/awesome-ChatGPT-repositories)
- [awesome-gpt-prompt-engineering](https://github.com/snwfdhmp/awesome-gpt-prompt-engineering)
- [awesome-chatgpt](https://github.com/awesome-chatgpt/awesome-chatgpt)
- [awesome-llm-and-aigc](https://github.com/sjinzh/awesome-llm-and-aigc)
- [awesome-compbio-chatgpt](https://github.com/csbl-br/awesome-compbio-chatgpt)
- [Awesome-LLM4Tool](https://github.com/OpenGVLab/Awesome-LLM4Tool)
## Meetups
- [Dash and ChatGPT: Future of AI-enabled apps 30/08/23](https://go.plotly.com/dash-chatgpt)
- [Pie & AI: Bangalore - Build end-to-end LLM app using Embedchain 01/09/23](https://www.eventbrite.com/e/pie-ai-bangalore-build-end-to-end-llm-app-using-embedchain-tickets-698045722547)
+34
View File
@@ -0,0 +1,34 @@
---
title: '💾 Vector Database'
---
We support `Chroma` and `Elasticsearch` as two vector database.
`Chroma` is used as a default database.
### Elasticsearch
In order to use `Elasticsearch` as vector database we need to use App type `CustomApp`.
```python
import os
from embedchain import CustomApp
from embedchain.config import CustomAppConfig, ElasticsearchDBConfig
from embedchain.models import Providers, EmbeddingFunctions, VectorDatabases
os.environ["OPENAI_API_KEY"] = 'OPENAI_API_KEY'
es_config = ElasticsearchDBConfig(
# elasticsearch url or list of nodes url with different hosts and ports.
es_url='http://localhost:9200',
# pass named parameters supported by Python Elasticsearch client
ca_certs="/path/to/http_ca.crt",
basic_auth=("username", "password")
)
config = CustomAppConfig(
embedding_fn=EmbeddingFunctions.OPENAI,
provider=Providers.OPENAI,
db_type=VectorDatabases.ELASTICSEARCH,
es_config=es_config,
)
es_app = CustomApp(config)
```
- Set `db_type=VectorDatabases.ELASTICSEARCH` and `es_config=ElasticsearchDBConfig(es_url='')` in `CustomAppConfig`.
- `ElasticsearchDBConfig` accepts `es_url` as elasticsearch url or as list of nodes url with different hosts and ports. Additionally we can pass named paramaters supported by Python Elasticsearch client.
+91
View File
@@ -0,0 +1,91 @@
---
title: '🌍 API Server'
---
The API Server based on Flask integrates the `embedchain` package, offering endpoints to add, query, and chat to engage in conversations with a chatbot using JSON requests.
### 🐳 Docker Setup
- Open variables.env, and edit it to add your 🔑 `OPENAI_API_KEY`.
- To setup your api server using docker, run the following command inside this folder using your terminal.
```bash
docker-compose up --build
```
📝 Note: The build command might take a while to install all the packages depending on your system resources.
### 🚀 Usage Instructions
- Your api server is running on [http://localhost:5000/](http://localhost:5000/)
- To use the api server, make an api call to the endpoints `/add`, `/query` and `/chat` using the json formats discussed below.
- To add data sources to the bot (/add):
```json
// Request
{
"data_type": "your_data_type_here",
"url_or_text": "your_url_or_text_here"
}
// Response
{
"data": "Added data_type: url_or_text"
}
```
- To ask queries from the bot (/query):
```json
// Request
{
"question": "your_question_here"
}
// Response
{
"data": "your_answer_here"
}
```
- To chat with the bot (/chat):
```json
// Request
{
"question": "your_question_here"
}
// Response
{
"data": "your_answer_here"
}
```
### 📡 Curl Call Formats
- To add data sources to the bot (/add):
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"data_type": "your_data_type_here",
"url_or_text": "your_url_or_text_here"
}' \
http://localhost:5000/add
```
- To ask queries from the bot (/query):
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"question": "your_question_here"
}' \
http://localhost:5000/query
```
- To chat with the bot (/chat):
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"question": "your_question_here"
}' \
http://localhost:5000/chat
```
🎉 Happy Chatting! 🎉
+48
View File
@@ -0,0 +1,48 @@
---
title: '🤖 Discord Bot'
---
### 🔑 Keys Setup
- Set your `OPENAI_API_KEY` in your variables.env file.
- Go to [https://discord.com/developers/applications/](https://discord.com/developers/applications/) and click on `New Application`.
- Enter the name for your bot, accept the terms and click on `Create`. On the resulting page, enter the details of your bot as you like.
- On the left sidebar, click on `Bot`. Under the heading `Privileged Gateway Intents`, toggle all 3 options to ON position. Save your changes.
- Now click on `Reset Token` and copy the token value. Set it as `DISCORD_BOT_TOKEN` in variables.env file.
- On the left sidebar, click on `OAuth2` and go to `General`.
- Set `Authorization Method` to `In-app Authorization`. Under `Scopes` select `bot`.
- Under `Bot Permissions` allow the following and then click on `Save Changes`.
```text
Read Messages/View Channel (under General Permissions)
Send Messages (under Text Permissions)
Read Message History (under Text Permissions)
Mention everyone (under Text Permissions)
```
- Now under `OAuth2` and go to `URL Generator`. Under `Scopes` select `bot`.
- Under `Bot Permissions` set the same permissions as above.
- Now scroll down and copy the `Generated URL`. Paste it in a browser window and select the Server where you want to add the bot.
- Click on `Continue` and authorize the bot.
- 🎉 The bot has been successfully added to your server.
### 🐳 Docker Setup
- To setup your discord bot using docker, run the following command inside this folder using your terminal.
```bash
docker-compose up --build
```
📝 Note: The build command might take a while to install all the packages depending on your system resources.
### 🚀 Usage Instructions
- Go to the server where you have added your bot.
- You can add data sources to the bot using the command:
```text
/ec add <data_type> <url_or_text>
```
- You can ask your queries from the bot using the command:
```text
/ec query <question>
```
📝 Note: To use the bot privately, you can message the bot directly by right clicking the bot and selecting `Message`.
🎉 Happy Chatting! 🎉
+48
View File
@@ -0,0 +1,48 @@
---
title: '🔮 Poe Bot'
---
### 🚀 Getting started
1. Install embedchain python package:
```bash
pip install embedchain[poe]
```
2. Create a free account on [Poe](https://www.poe.com?utm_source=embedchain).
3. Click "Create Bot" button on top left
4. Give it a handle and an optional description.
5. Select `Use API`.
6. Under `API URL` enter your server or ngrok address. You can use your machine's public IP or DNS. Otherwise, employ a proxy server like [ngrok](https://ngrok.com/) to make your local bot accessible.
7. Copy your api key and paste it in `.env` as `POE_API_KEY`.
8. Start the bot.
```bash
python -m embedchain.bots.poe
```
If you want to run the bot on another port, you can pass `--port option` like
```bash
python -m embedchain.bots.poe --port 5000
```
9. Click `Run check` to make sure your machine can be reached.
10. Make sure your bot is private if that's what you want.
11. Click `Create bot` at the bottom to finally create the bot
12. Now you bot is created.
### 💬 How to use
- To include data sources, use this command:
```text
/add <url_or_text>
```
- You can refer the [Supported Data formats](https://docs.embedchain.ai/advanced/data_types) section to refer the supported data types in embedchain.
- To ask the bot questions, just type your query:
```text
<your-question-here>
```
+39
View File
@@ -0,0 +1,39 @@
---
title: '💼 Slack Bot'
---
### 🖼️ Template Setup
- Fork [this](https://replit.com/@taranjeetio/EC-Slack-Bot-Template?v=1#README.md) replit template.
- Set your `OPENAI_API_KEY` in Secrets.
- Create a workspace on Slack if you don't have one already by clicking [here](https://slack.com/intl/en-in/).
- Create a new App on your Slack account by going [here](https://api.slack.com/apps).
- Select `From Scratch`, then enter the Bot Name and select your workspace.
- On the `Basic Information` page copy the `Signing Secret` and set it in your secrets as `SLACK_SIGNING_SECRET`.
- On the left Sidebar, go to `OAuth and Permissions` and add the following scopes under `Bot Token Scopes`:
```text
app_mentions:read
channels:history
channels:read
chat:write
```
- Now select the option `Install to Workspace` and after it's done, copy the `Bot User OAuth Token` and set it in your secrets as `SLACK_BOT_TOKEN`.
- Start your replit container now by clicking on `Run`.
- On the Slack API website go to `Event Subscriptions` on the left Sidebar and turn on `Enable Events`.
- Copy the generated server URL in replit, append `/chat` at its end and paste it in `Request URL` box.
- After it gets verified, click on `Subscribe to bot events`, add `message.channels` Bot User Event and click on `Save Changes`.
- Now go to your workspace, click on the bot name in the Sidebar and then add the bot to any channel you want.
### 🚀 Usage Instructions
- Go to the channel where you have added your bot.
- To add data sources to the bot, use the command:
```text
add <data_type> <url_or_text>
```
- To ask queries from the bot, use the command:
```text
query <question>
```
🎉 Happy Chatting! 🎉
+26
View File
@@ -0,0 +1,26 @@
---
title: '📱 Telegram Bot'
---
### 🖼️ Template Setup
- Fork [this](https://replit.com/@taranjeetio/EC-Telegram-Bot-Template?v=1#README.md) replit template.
- Set your `OPENAI_API_KEY` in Secrets.
- Open the Telegram app and search for the `BotFather` user.
- Start a chat with BotFather and use the `/newbot` command to create a new bot.
- Follow the instructions to choose a name and username for your bot.
- Once the bot is created, BotFather will provide you with a unique token for your bot.
- Set this token as `TELEGRAM_BOT_TOKEN` in Secrets.
- Click on `Run` in the replit container and a URL will get generated for your bot.
- Now set your webhook by running the following link in your browser:
```url
https://api.telegram.org/bot<Your_Telegram_Bot_Token>/setWebhook?url=<Replit_Generated_URL>
```
- When you get a successful response in your browser, your bot is ready to be used.
### 🚀 Usage Instructions
- Open your bot by searching for it using the bot name or bot username.
- Click on `Start` or type `/start` and follow the on screen instructions.
🎉 Happy Chatting! 🎉
+46
View File
@@ -0,0 +1,46 @@
---
title: '💬 WhatsApp Bot'
---
### 🚀 Getting started
1. Install embedchain python package:
```bash
pip install embedchain
```
2. Launch your WhatsApp bot:
```bash
python -m embedchain.bots.whatsapp --port 5000
```
If your bot needs to be accessible online, use your machine's public IP or DNS. Otherwise, employ a proxy server like [ngrok](https://ngrok.com/) to make your local bot accessible.
3. Create a free account on [Twilio](https://www.twilio.com/try-twilio)
- Set up a WhatsApp Sandbox in your Twilio dashboard. Access it via the left sidebar: `Messaging > Try it out > Send a WhatsApp Message`.
- Follow on-screen instructions to link a phone number for chatting with your bot
- Copy your bot's public URL, add /chat at the end, and paste it in Twilio's WhatsApp Sandbox settings under "When a message comes in". Save the settings.
- Copy your bot's public url, append `/chat` at the end and paste it under `When a message comes in` under the `Sandbox settings` for Whatsapp in Twilio. Save your settings.
### 💬 How to use
- To connect a new number or reconnect an old one in the Sandbox, follow Twilio's instructions.
- To include data sources, use this command:
```text
add <url_or_text>
```
- To ask the bot questions, just type your query:
```text
<your-question-here>
```
### Example
Here is an example of Elon Musk WhatsApp Bot that we created:
<img src="/images/whatsapp.jpg"/>
Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+8 -8
View File
@@ -7,7 +7,7 @@ description: '📝 Embedchain is a framework to easily create LLM powered bots o
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.
You can add a single or multiple datasets using the `.add` method. Then, simply use the `.query` method 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.
@@ -16,13 +16,13 @@ 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")
naval_chat_bot.add("https://www.youtube.com/watch?v=3qHkcs3kG44")
naval_chat_bot.add("https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf")
naval_chat_bot.add("https://nav.al/feedback")
naval_chat_bot.add("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.add(("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.
@@ -32,7 +32,7 @@ naval_chat_bot.query("What unique capacity does Naval argue humans possess when
Creating a chat bot over any dataset involves the following steps:
1. Load the data
1. Detect the data type and load the data
2. Create meaningful chunks
3. Create embeddings for each chunk
4. Store the chunks in a vector database
@@ -53,4 +53,4 @@ The process of loading the dataset and querying involves multiple steps, each wi
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.
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` method, and use the `.query` method to get the relevant answers.
+4 -3
View File
@@ -32,11 +32,11 @@
},
{
"group": "Advanced",
"pages": ["advanced/app_types", "advanced/interface_types", "advanced/adding_data","advanced/data_types", "advanced/query_configuration", "advanced/configuration", "advanced/testing", "advanced/showcase"]
"pages": ["advanced/app_types", "advanced/interface_types", "advanced/adding_data", "advanced/data_types", "advanced/query_configuration", "advanced/configuration", "advanced/testing", "advanced/vector_database", "advanced/showcase"]
},
{
"group": "Examples",
"pages": ["examples/full_stack"]
"pages": ["examples/full_stack", "examples/api_server", "examples/discord_bot", "examples/slack_bot", "examples/telegram_bot", "examples/whatsapp_bot", "examples/poe_bot"]
},
{
"group": "Contribution Guidelines",
@@ -47,7 +47,8 @@
"footerSocials": {
"twitter": "https://twitter.com/embedchain",
"github": "https://github.com/embedchain/embedchain",
"linkedin": "https://www.linkedin.com/company/embedchain"
"linkedin": "https://www.linkedin.com/company/embedchain",
"website": "https://embedchain.ai"
},
"backgroundImage": "/background.png",
"isWhiteLabeled": true
+2 -2
View File
@@ -26,8 +26,8 @@ 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")
elon_musk_bot.add("https://en.wikipedia.org/wiki/Elon_Musk")
elon_musk_bot.add("https://www.tesla.com/elon-musk")
response = elon_musk_bot.query("How many companies does Elon Musk run?")
print(response)
+14 -2
View File
@@ -1,3 +1,5 @@
from typing import Optional
import openai
from embedchain.config import AppConfig, ChatConfig
@@ -14,17 +16,27 @@ class App(EmbedChain):
dry_run(query): test your prompt without consuming tokens.
"""
def __init__(self, config: AppConfig = None):
def __init__(self, config: AppConfig = None, system_prompt: Optional[str] = None):
"""
:param config: AppConfig instance to load as configuration. Optional.
:param system_prompt: System prompt string. Optional.
"""
if config is None:
config = AppConfig()
super().__init__(config)
super().__init__(config, system_prompt)
def get_llm_model_answer(self, prompt, config: ChatConfig):
messages = []
system_prompt = (
self.system_prompt
if self.system_prompt is not None
else config.system_prompt
if config.system_prompt is not None
else None
)
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
response = openai.ChatCompletion.create(
model=config.model or "gpt-3.5-turbo-0613",
+24 -12
View File
@@ -1,5 +1,5 @@
import logging
from typing import List
from typing import List, Optional
from langchain.schema import BaseMessage
@@ -18,10 +18,11 @@ class CustomApp(EmbedChain):
dry_run(query): test your prompt without consuming tokens.
"""
def __init__(self, config: CustomAppConfig = None):
def __init__(self, config: CustomAppConfig = None, system_prompt: Optional[str] = None):
"""
:param config: Optional. `CustomAppConfig` instance to load as configuration.
:raises ValueError: Config must be provided for custom app
:param system_prompt: Optional. System prompt string.
"""
if config is None:
raise ValueError("Config must be provided for custom app")
@@ -34,7 +35,7 @@ class CustomApp(EmbedChain):
# 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)
super().__init__(config, system_prompt)
def set_llm_model(self, provider: Providers):
self.provider = provider
@@ -51,6 +52,9 @@ class CustomApp(EmbedChain):
"Streaming responses have not been implemented for this model yet. Please disable."
)
if config.system_prompt is None and self.system_prompt is not None:
config.system_prompt = self.system_prompt
try:
if self.provider == Providers.OPENAI:
return CustomApp._get_openai_answer(prompt, config)
@@ -68,7 +72,7 @@ class CustomApp(EmbedChain):
return CustomApp._get_azure_openai_answer(prompt, config)
except ImportError as e:
raise ImportError(e.msg) from None
raise ModuleNotFoundError(e.msg) from None
@staticmethod
def _get_openai_answer(prompt: str, config: ChatConfig) -> str:
@@ -84,7 +88,7 @@ class CustomApp(EmbedChain):
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)
messages = CustomApp._get_messages(prompt, system_prompt=config.system_prompt)
return chat(messages).content
@@ -97,7 +101,7 @@ class CustomApp(EmbedChain):
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)
messages = CustomApp._get_messages(prompt, system_prompt=config.system_prompt)
return chat(messages).content
@@ -110,7 +114,7 @@ class CustomApp(EmbedChain):
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)
messages = CustomApp._get_messages(prompt, system_prompt=config.system_prompt)
return chat(messages).content
@@ -118,9 +122,13 @@ class CustomApp(EmbedChain):
def _get_azure_openai_answer(prompt: str, config: ChatConfig) -> str:
from langchain.chat_models import AzureChatOpenAI
if not config.deployment_name:
raise ValueError("Deployment name must be provided for Azure OpenAI")
chat = AzureChatOpenAI(
deployment_name="td2",
model_name=config.model or "text-davinci-002",
deployment_name=config.deployment_name,
openai_api_version="2023-05-15",
model_name=config.model or "gpt-3.5-turbo",
temperature=config.temperature,
max_tokens=config.max_tokens,
streaming=config.stream,
@@ -129,15 +137,19 @@ class CustomApp(EmbedChain):
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)
messages = CustomApp._get_messages(prompt, system_prompt=config.system_prompt)
return chat(messages).content
@staticmethod
def _get_messages(prompt: str) -> List[BaseMessage]:
def _get_messages(prompt: str, system_prompt: Optional[str] = None) -> List[BaseMessage]:
from langchain.schema import HumanMessage, SystemMessage
return [SystemMessage(content="You are a helpful assistant."), HumanMessage(content=prompt)]
messages = []
if system_prompt:
messages.append(SystemMessage(content=system_prompt))
messages.append(HumanMessage(content=prompt))
return messages
def _stream_llm_model_response(self, response):
"""
+8 -4
View File
@@ -1,8 +1,9 @@
import os
from typing import Optional
from langchain.llms import Replicate
from embedchain.config import AppConfig
from embedchain.config import AppConfig, ChatConfig
from embedchain.embedchain import EmbedChain
@@ -15,9 +16,10 @@ class Llama2App(EmbedChain):
query(query): finds answer to the given query using vector database and LLM.
"""
def __init__(self, config: AppConfig = None):
def __init__(self, config: AppConfig = None, system_prompt: Optional[str] = None):
"""
:param config: AppConfig instance to load as configuration. Optional.
:param system_prompt: System prompt string. Optional.
"""
if "REPLICATE_API_TOKEN" not in os.environ:
raise ValueError("Please set the REPLICATE_API_TOKEN environment variable.")
@@ -25,10 +27,12 @@ class Llama2App(EmbedChain):
if config is None:
config = AppConfig()
super().__init__(config)
super().__init__(config, system_prompt)
def get_llm_model_answer(self, prompt, config: AppConfig = None):
def get_llm_model_answer(self, prompt, config: ChatConfig = None):
# TODO: Move the model and other inputs into config
if self.system_prompt or config.system_prompt:
raise ValueError("Llama2App does not support `system_prompt`")
llm = Replicate(
model="a16z-infra/llama13b-v2-chat:df7690f1994d94e96ad9d568eac121aecf50684a0b0963b25a41cc40061269e5",
input={"temperature": 0.75, "max_length": 500, "top_p": 1},
+9 -5
View File
@@ -1,5 +1,5 @@
import logging
from typing import Iterable, Union
from typing import Iterable, Union, Optional
from embedchain.config import ChatConfig, OpenSourceAppConfig
from embedchain.embedchain import EmbedChain
@@ -18,10 +18,11 @@ class OpenSourceApp(EmbedChain):
query(query): finds answer to the given query using vector database and LLM.
"""
def __init__(self, config: OpenSourceAppConfig = None):
def __init__(self, config: OpenSourceAppConfig = None, system_prompt: Optional[str] = None):
"""
:param config: OpenSourceAppConfig instance to load as configuration. Optional.
`ef` defaults to open source.
:param system_prompt: System prompt string. Optional.
"""
logging.info("Loading open source embedding model. This may take some time...") # noqa:E501
if not config:
@@ -33,7 +34,7 @@ class OpenSourceApp(EmbedChain):
self.instance = OpenSourceApp._get_instance(config.model)
logging.info("Successfully loaded open source embedding model.")
super().__init__(config)
super().__init__(config, system_prompt)
def get_llm_model_answer(self, prompt, config: ChatConfig):
return self._get_gpt4all_answer(prompt=prompt, config=config)
@@ -43,8 +44,8 @@ class OpenSourceApp(EmbedChain):
try:
from gpt4all import GPT4All
except ModuleNotFoundError:
raise ValueError(
"The GPT4All python package is not installed. Please install it with `pip install GPT4All`"
raise ModuleNotFoundError(
"The GPT4All python package is not installed. Please install it with `pip install embedchain[opensource]`" # noqa E501
) from None
return GPT4All(model)
@@ -55,6 +56,9 @@ class OpenSourceApp(EmbedChain):
"OpenSourceApp does not support switching models at runtime. Please create a new app instance."
)
if self.system_prompt or config.system_prompt:
raise ValueError("OpenSourceApp does not support `system_prompt`")
response = self.instance.generate(
prompt=prompt,
streaming=config.stream,
View File
+25
View File
@@ -0,0 +1,25 @@
from embedchain import CustomApp
from embedchain.config import AddConfig, CustomAppConfig, QueryConfig
from embedchain.models import EmbeddingFunctions, Providers
class BaseBot:
def __init__(self, app_config=None):
if app_config is None:
app_config = CustomAppConfig(embedding_fn=EmbeddingFunctions.OPENAI, provider=Providers.OPENAI)
self.app_config = app_config
self.app = CustomApp(config=self.app_config)
def add(self, data, config: AddConfig = None):
"""Add data to the bot"""
config = config if config else AddConfig()
self.app.add(data, config=config)
def query(self, query, config: QueryConfig = None):
"""Query bot"""
config = config if config else QueryConfig()
return self.app.query(query, config=config)
def start(self):
"""Start the bot's functionality."""
raise NotImplementedError("Subclasses must implement the start method.")
+79
View File
@@ -0,0 +1,79 @@
import argparse
import logging
import os
from typing import List, Optional
from fastapi_poe import PoeBot, run
from embedchain.config import QueryConfig
from .base import BaseBot
class EcPoeBot(BaseBot, PoeBot):
def __init__(self):
self.history_length = 5
super().__init__()
async def get_response(self, query):
last_message = query.query[-1].content
try:
history = (
[f"{m.role}: {m.content}" for m in query.query[-(self.history_length + 1) : -1]]
if len(query.query) > 0
else None
)
except Exception as e:
logging.error(f"Error when processing the chat history. Message is being sent without history. Error: {e}")
logging.warning(history)
answer = self.handle_message(last_message, history)
yield self.text_event(answer)
def handle_message(self, message, history: Optional[List[str]] = None):
if message.startswith("/add "):
response = self.add_data(message)
else:
response = self.ask_bot(message, history)
return response
def add_data(self, message):
data = message.split(" ")[-1]
try:
self.add(data)
response = f"Added data from: {data}"
except Exception:
logging.exception(f"Failed to add data {data}.")
response = "Some error occurred while adding data."
return response
def ask_bot(self, message, history: List[str]):
try:
config = QueryConfig(history=history)
response = self.query(message, config)
except Exception:
logging.exception(f"Failed to query {message}.")
response = "An error occurred. Please try again!"
return response
def start_command():
parser = argparse.ArgumentParser(description="EmbedChain PoeBot command line interface")
# parser.add_argument("--host", default="0.0.0.0", help="Host IP to bind")
parser.add_argument("--port", default=8080, type=int, help="Port to bind")
parser.add_argument("--api-key", type=str, help="Poe API key")
# parser.add_argument(
# "--history-length",
# default=5,
# type=int,
# help="Set the max size of the chat history. Multiplies cost, but improves conversation awareness.",
# )
args = parser.parse_args()
# FIXME: Arguments are automatically loaded by Poebot's ArgumentParser which causes it to fail.
# the port argument here is also just for show, it actually works because poe has the same argument.
run(EcPoeBot(), api_key=args.api_key or os.environ.get("POE_API_KEY"))
if __name__ == "__main__":
start_command()
+72
View File
@@ -0,0 +1,72 @@
import argparse
import logging
import signal
import sys
from flask import Flask, request
from twilio.twiml.messaging_response import MessagingResponse
from .base import BaseBot
class WhatsAppBot(BaseBot):
def __init__(self):
super().__init__()
def handle_message(self, message):
if message.startswith("add "):
response = self.add_data(message)
else:
response = self.ask_bot(message)
return response
def add_data(self, message):
data = message.split(" ")[-1]
try:
self.add(data)
response = f"Added data from: {data}"
except Exception:
logging.exception(f"Failed to add data {data}.")
response = "Some error occurred while adding data."
return response
def ask_bot(self, message):
try:
response = self.query(message)
except Exception:
logging.exception(f"Failed to query {message}.")
response = "An error occurred. Please try again!"
return response
def start(self, host="0.0.0.0", port=5000, debug=True):
app = Flask(__name__)
def signal_handler(sig, frame):
logging.info("\nGracefully shutting down the WhatsAppBot...")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
@app.route("/chat", methods=["POST"])
def chat():
incoming_message = request.values.get("Body", "").lower()
response = self.handle_message(incoming_message)
twilio_response = MessagingResponse()
twilio_response.message(response)
return str(twilio_response)
app.run(host=host, port=port, debug=debug)
def start_command():
parser = argparse.ArgumentParser(description="EmbedChain WhatsAppBot command line interface")
parser.add_argument("--host", default="0.0.0.0", help="Host IP to bind")
parser.add_argument("--port", default=5000, type=int, help="Port to bind")
args = parser.parse_args()
whatsapp_bot = WhatsAppBot()
whatsapp_bot.start(host=args.host, port=args.port)
if __name__ == "__main__":
start_command()
+6 -2
View File
@@ -1,5 +1,7 @@
import hashlib
from embedchain.models.data_type import DataType
class BaseChunker:
def __init__(self, text_splitter):
@@ -26,7 +28,7 @@ class BaseChunker:
meta_data = data["meta_data"]
# add data type to meta data to allow query using data type
meta_data["data_type"] = self.data_type
meta_data["data_type"] = self.data_type.value
url = meta_data["url"]
chunks = self.get_chunks(content)
@@ -52,8 +54,10 @@ class BaseChunker:
"""
return self.text_splitter.split_text(content)
def set_data_type(self, data_type):
def set_data_type(self, data_type: DataType):
"""
set the data type of chunker
"""
self.data_type = data_type
# TODO: This should be done during initialization. This means it has to be done in the child classes.
+20
View File
@@ -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 NotionChunker(BaseChunker):
"""Chunker for notion."""
def __init__(self, config: Optional[ChunkerConfig] = None):
if config is None:
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)
+7
View File
@@ -1,4 +1,5 @@
from string import Template
from typing import Optional
from embedchain.config.QueryConfig import QueryConfig
@@ -33,6 +34,8 @@ class ChatConfig(QueryConfig):
max_tokens=None,
top_p=None,
stream: bool = False,
deployment_name=None,
system_prompt: Optional[str] = None,
):
"""
Initializes the ChatConfig instance.
@@ -50,6 +53,8 @@ class ChatConfig(QueryConfig):
(closer to 1) make word selection more diverse, lower values make words less
diverse.
:param stream: Optional. Control if response is streamed back to the user
:param deployment_name: t.b.a.
:param system_prompt: Optional. System prompt string.
:raises ValueError: If the template is not valid as template should contain
$context and $query and $history
"""
@@ -68,6 +73,8 @@ class ChatConfig(QueryConfig):
top_p=top_p,
history=[0],
stream=stream,
deployment_name=deployment_name,
system_prompt=system_prompt,
)
def set_history(self, history):
+7
View File
@@ -1,5 +1,6 @@
import re
from string import Template
from typing import Optional
from embedchain.config.BaseConfig import BaseConfig
@@ -62,6 +63,8 @@ class QueryConfig(BaseConfig):
top_p=None,
history=None,
stream: bool = False,
deployment_name=None,
system_prompt: Optional[str] = None,
):
"""
Initializes the QueryConfig instance.
@@ -80,6 +83,8 @@ class QueryConfig(BaseConfig):
diverse.
:param history: Optional. A list of strings to consider as history.
:param stream: Optional. Control if response is streamed back to user
:param deployment_name: t.b.a.
:param system_prompt: Optional. System prompt string.
:raises ValueError: If the template is not valid as template should
contain $context and $query (and optionally $history).
"""
@@ -106,6 +111,8 @@ class QueryConfig(BaseConfig):
self.max_tokens = max_tokens if max_tokens else 1000
self.model = model
self.top_p = top_p if top_p else 1
self.deployment_name = deployment_name
self.system_prompt = system_prompt
if self.validate_template(template):
self.template = template
+2
View File
@@ -5,3 +5,5 @@ 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
from .vectordbs.ElasticsearchDBConfig import \
ElasticsearchDBConfig # noqa: F401
+19 -2
View File
@@ -1,4 +1,5 @@
import os
from typing import Optional
try:
from chromadb.utils import embedding_functions
@@ -16,16 +17,32 @@ 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):
def __init__(
self,
log_level=None,
host=None,
port=None,
id=None,
collection_name=None,
collect_metrics: Optional[bool] = None,
):
"""
:param log_level: Optional. (String) Debug level
['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'].
:param host: Optional. Hostname for the database server.
:param port: Optional. Port for the database server.
:param id: Optional. ID of the app. Document metadata will have this id.
:param collection_name: Optional. Collection name for the database.
:param collect_metrics: Defaults to True. Send anonymous telemetry to improve embedchain.
"""
super().__init__(
log_level=log_level, embedding_fn=AppConfig.default_embedding_function(), host=host, port=port, id=id
log_level=log_level,
embedding_fn=AppConfig.default_embedding_function(),
host=host,
port=port,
id=id,
collection_name=collection_name,
collect_metrics=collect_metrics,
)
@staticmethod
+53 -8
View File
@@ -1,6 +1,8 @@
import logging
from embedchain.config.BaseConfig import BaseConfig
from embedchain.config.vectordbs import ElasticsearchDBConfig
from embedchain.models import VectorDatabases, VectorDimensions
class BaseAppConfig(BaseConfig):
@@ -8,35 +10,78 @@ 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):
def __init__(
self,
log_level=None,
embedding_fn=None,
db=None,
host=None,
port=None,
id=None,
collection_name=None,
collect_metrics: bool = True,
db_type: VectorDatabases = None,
vector_dim: VectorDimensions = None,
es_config: ElasticsearchDBConfig = 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.
:param id: Optional. ID of the app. Document metadata will have this id.
:param collection_name: Optional. Collection name for the database.
:param collect_metrics: Defaults to True. Send anonymous telemetry to improve embedchain.
:param db_type: Optional. type of Vector database to use
:param vector_dim: Vector dimension generated by embedding fn
:param es_config: Optional. elasticsearch database config to be used for connection
"""
self._setup_logging(log_level)
self.db = db if db else BaseAppConfig.default_db(embedding_fn=embedding_fn, host=host, port=port)
self.collection_name = collection_name if collection_name else "embedchain_store"
self.db = BaseAppConfig.get_db(
db=db,
embedding_fn=embedding_fn,
host=host,
port=port,
db_type=db_type,
vector_dim=vector_dim,
collection_name=self.collection_name,
es_config=es_config,
)
self.id = id
self.collect_metrics = True if (collect_metrics is True or collect_metrics is None) else False
return
@staticmethod
def default_db(embedding_fn, host, port):
def get_db(db, embedding_fn, host, port, db_type, vector_dim, collection_name, es_config):
"""
Sets database to default (`ChromaDb`).
Get db based on db_type, db with default database (`ChromaDb`)
:param Optional. (Vector) database to use for embeddings.
: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
:param db_type: Optional. db type to use. Supported values (`es`, `chroma`)
:param vector_dim: Vector dimension generated by embedding fn
:param collection_name: Optional. Collection name for the database.
:param es_config: Optional. elasticsearch database config to be used for connection
:raises ValueError: BaseAppConfig knows no default embedding function.
:returns: database instance
"""
if db:
return db
if embedding_fn is None:
raise ValueError("ChromaDb cannot be instantiated without an embedding function")
if db_type == VectorDatabases.ELASTICSEARCH:
from embedchain.vectordb.elasticsearch_db import ElasticsearchDB
return ElasticsearchDB(
embedding_fn=embedding_fn, vector_dim=vector_dim, collection_name=collection_name, es_config=es_config
)
from embedchain.vectordb.chroma_db import ChromaDB
return ChromaDB(embedding_fn=embedding_fn, host=host, port=port)
+44 -7
View File
@@ -1,9 +1,11 @@
from typing import Any
from typing import Any, Optional
from chromadb.api.types import Documents, Embeddings
from dotenv import load_dotenv
from embedchain.models import EmbeddingFunctions, Providers
from embedchain.config.vectordbs import ElasticsearchDBConfig
from embedchain.models import (EmbeddingFunctions, Providers, VectorDatabases,
VectorDimensions)
from .BaseAppConfig import BaseAppConfig
@@ -24,9 +26,13 @@ class CustomAppConfig(BaseAppConfig):
host=None,
port=None,
id=None,
collection_name=None,
provider: Providers = None,
model=None,
open_source_app_config=None,
deployment_name=None,
collect_metrics: Optional[bool] = None,
db_type: VectorDatabases = None,
es_config: ElasticsearchDBConfig = None,
):
"""
:param log_level: Optional. (String) Debug level
@@ -34,11 +40,15 @@ class CustomAppConfig(BaseAppConfig):
: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 id: Optional. ID of the app. Document metadata will have this id.
:param collection_name: Optional. Collection name for the database.
:param provider: Optional. (Providers): LLM Provider to use.
:param open_source_app_config: Optional. Config instance needed for open source apps.
:param collect_metrics: Defaults to True. Send anonymous telemetry to improve embedchain.
:param db_type: Optional. type of Vector database to use.
:param es_config: Optional. elasticsearch database config to be used for connection
"""
if provider:
self.provider = provider
@@ -49,11 +59,18 @@ class CustomAppConfig(BaseAppConfig):
super().__init__(
log_level=log_level,
embedding_fn=CustomAppConfig.embedding_function(embedding_function=embedding_fn, model=embedding_fn_model),
embedding_fn=CustomAppConfig.embedding_function(
embedding_function=embedding_fn, model=embedding_fn_model, deployment_name=deployment_name
),
db=db,
host=host,
port=port,
id=id,
collection_name=collection_name,
collect_metrics=collect_metrics,
db_type=db_type,
vector_dim=CustomAppConfig.get_vector_dimension(embedding_function=embedding_fn),
es_config=es_config,
)
@staticmethod
@@ -68,7 +85,7 @@ class CustomAppConfig(BaseAppConfig):
return embed_function
@staticmethod
def embedding_function(embedding_function: EmbeddingFunctions, model: str = None):
def embedding_function(embedding_function: EmbeddingFunctions, model: str = None, deployment_name: 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
@@ -80,7 +97,10 @@ class CustomAppConfig(BaseAppConfig):
if model:
embeddings = OpenAIEmbeddings(model=model)
else:
embeddings = OpenAIEmbeddings()
if deployment_name:
embeddings = OpenAIEmbeddings(deployment=deployment_name)
else:
embeddings = OpenAIEmbeddings()
return CustomAppConfig.langchain_default_concept(embeddings)
elif embedding_function == EmbeddingFunctions.HUGGING_FACE:
@@ -100,3 +120,20 @@ class CustomAppConfig(BaseAppConfig):
from chromadb.utils import embedding_functions
return embedding_functions.SentenceTransformerEmbeddingFunction(model_name=model)
@staticmethod
def get_vector_dimension(embedding_function: EmbeddingFunctions):
if not isinstance(embedding_function, EmbeddingFunctions):
raise ValueError(f"Invalid option: '{embedding_function}'.")
if embedding_function == EmbeddingFunctions.OPENAI:
return VectorDimensions.OPENAI.value
elif embedding_function == EmbeddingFunctions.HUGGING_FACE:
return VectorDimensions.HUGGING_FACE.value
elif embedding_function == EmbeddingFunctions.VERTEX_AI:
return VectorDimensions.VERTEX_AI.value
elif embedding_function == EmbeddingFunctions.GPT4ALL:
return VectorDimensions.GPT4ALL.value
+23 -2
View File
@@ -1,3 +1,5 @@
from typing import Optional
from chromadb.utils import embedding_functions
from .BaseAppConfig import BaseAppConfig
@@ -8,13 +10,24 @@ 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):
def __init__(
self,
log_level=None,
host=None,
port=None,
id=None,
collection_name=None,
collect_metrics: Optional[bool] = 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 collection_name: Optional. Collection name for the database.
:param host: Optional. Hostname for the database server.
:param port: Optional. Port for the database server.
:param collect_metrics: Defaults to True. Send anonymous telemetry to improve embedchain.
:param model: Optional. GPT4ALL uses the model to instantiate the class.
So unlike `App`, it has to be provided before querying.
"""
@@ -26,6 +39,8 @@ class OpenSourceAppConfig(BaseAppConfig):
host=host,
port=port,
id=id,
collection_name=collection_name,
collect_metrics=collect_metrics,
)
@staticmethod
@@ -35,4 +50,10 @@ class OpenSourceAppConfig(BaseAppConfig):
:returns: The default embedding function
"""
return embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
try:
return embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
except ValueError as e:
print(e)
raise ModuleNotFoundError(
"The open source app requires extra dependencies. Install with `pip install embedchain[opensource]`"
) from None
@@ -0,0 +1,15 @@
from typing import Dict, List, Union
from embedchain.config.BaseConfig import BaseConfig
class ElasticsearchDBConfig(BaseConfig):
"""
Config to initialize an elasticsearch client.
:param es_url. elasticsearch url or list of nodes url to be used for connection
:param ES_EXTRA_PARAMS: extra params dict that can be passed to elasticsearch.
"""
def __init__(self, es_url: Union[str, List[str]] = None, **ES_EXTRA_PARAMS: Dict[str, any]):
self.ES_URL = es_url
self.ES_EXTRA_PARAMS = ES_EXTRA_PARAMS
+33 -20
View File
@@ -1,5 +1,6 @@
from embedchain.chunkers.docs_site import DocsSiteChunker
from embedchain.chunkers.docx_file import DocxFileChunker
from embedchain.chunkers.notion import NotionChunker
from embedchain.chunkers.pdf_file import PdfFileChunker
from embedchain.chunkers.qna_pair import QnaPairChunker
from embedchain.chunkers.text import TextChunker
@@ -14,6 +15,7 @@ from embedchain.loaders.pdf_file import PdfFileLoader
from embedchain.loaders.sitemap import SitemapLoader
from embedchain.loaders.web_page import WebPageLoader
from embedchain.loaders.youtube_video import YoutubeVideoLoader
from embedchain.models.data_type import DataType
class DataFormatter:
@@ -23,11 +25,11 @@ class DataFormatter:
.add or .add_local method call
"""
def __init__(self, data_type: str, config: AddConfig):
def __init__(self, data_type: DataType, config: AddConfig):
self.loader = self._get_loader(data_type, config.loader)
self.chunker = self._get_chunker(data_type, config.chunker)
def _get_loader(self, data_type, config):
def _get_loader(self, data_type: DataType, config):
"""
Returns the appropriate data loader for the given data type.
@@ -36,21 +38,31 @@ class DataFormatter:
:raises ValueError: If an unsupported data type is provided.
"""
loaders = {
"youtube_video": YoutubeVideoLoader(),
"pdf_file": PdfFileLoader(),
"web_page": WebPageLoader(),
"qna_pair": LocalQnaPairLoader(),
"text": LocalTextLoader(),
"docx": DocxFileLoader(),
"sitemap": SitemapLoader(),
"docs_site": DocsSiteLoader(),
DataType.YOUTUBE_VIDEO: YoutubeVideoLoader,
DataType.PDF_FILE: PdfFileLoader,
DataType.WEB_PAGE: WebPageLoader,
DataType.QNA_PAIR: LocalQnaPairLoader,
DataType.TEXT: LocalTextLoader,
DataType.DOCX: DocxFileLoader,
DataType.SITEMAP: SitemapLoader,
DataType.DOCS_SITE: DocsSiteLoader,
}
lazy_loaders = {DataType.NOTION}
if data_type in loaders:
return loaders[data_type]
loader_class = loaders[data_type]
loader = loader_class()
return loader
elif data_type in lazy_loaders:
if data_type == DataType.NOTION:
from embedchain.loaders.notion import NotionLoader
return NotionLoader()
else:
raise ValueError(f"Unsupported data type: {data_type}")
else:
raise ValueError(f"Unsupported data type: {data_type}")
def _get_chunker(self, data_type, config):
def _get_chunker(self, data_type: DataType, config):
"""
Returns the appropriate chunker for the given data type.
@@ -59,14 +71,15 @@ class DataFormatter:
:raises ValueError: If an unsupported data type is provided.
"""
chunker_classes = {
"youtube_video": YoutubeVideoChunker,
"pdf_file": PdfFileChunker,
"web_page": WebPageChunker,
"qna_pair": QnaPairChunker,
"text": TextChunker,
"docx": DocxFileChunker,
"sitemap": WebPageChunker,
"docs_site": DocsSiteChunker,
DataType.YOUTUBE_VIDEO: YoutubeVideoChunker,
DataType.PDF_FILE: PdfFileChunker,
DataType.WEB_PAGE: WebPageChunker,
DataType.QNA_PAIR: QnaPairChunker,
DataType.TEXT: TextChunker,
DataType.DOCX: DocxFileChunker,
DataType.WEB_PAGE: WebPageChunker,
DataType.DOCS_SITE: DocsSiteChunker,
DataType.NOTION: NotionChunker,
}
if data_type in chunker_classes:
chunker_class = chunker_classes[data_type]
+208 -68
View File
@@ -1,10 +1,18 @@
import hashlib
import importlib.metadata
import json
import logging
import os
import threading
import uuid
from pathlib import Path
from typing import Dict, Optional
from chromadb.errors import InvalidDimensionException
import requests
from dotenv import load_dotenv
from langchain.docstore.document import Document
from langchain.memory import ConversationBufferMemory
from tenacity import retry, stop_after_attempt, wait_fixed
from embedchain.chunkers.base_chunker import BaseChunker
from embedchain.config import AddConfig, ChatConfig, QueryConfig
@@ -12,77 +20,155 @@ from embedchain.config.apps.BaseAppConfig import BaseAppConfig
from embedchain.config.QueryConfig import DOCS_SITE_PROMPT_TEMPLATE
from embedchain.data_formatter import DataFormatter
from embedchain.loaders.base_loader import BaseLoader
from embedchain.models.data_type import DataType
from embedchain.utils import detect_datatype
load_dotenv()
ABS_PATH = os.getcwd()
DB_DIR = os.path.join(ABS_PATH, "db")
memory = ConversationBufferMemory()
HOME_DIR = str(Path.home())
CONFIG_DIR = os.path.join(HOME_DIR, ".embedchain")
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
class EmbedChain:
def __init__(self, config: BaseAppConfig):
def __init__(self, config: BaseAppConfig, system_prompt: Optional[str] = None):
"""
Initializes the EmbedChain instance, sets up a vector DB client and
creates a collection.
:param config: BaseAppConfig instance to load as configuration.
:param system_prompt: Optional. System prompt string.
"""
self.config = config
self.db_client = self.config.db.client
self.collection = self.config.db.collection
self.system_prompt = system_prompt
self.collection = self.config.db._get_or_create_collection(self.config.collection_name)
self.db = self.config.db
self.user_asks = []
self.is_docs_site_instance = False
self.online = False
self.memory = ConversationBufferMemory()
def add(self, data_type, url, metadata=None, config: AddConfig = None):
# Send anonymous telemetry
self.s_id = self.config.id if self.config.id else str(uuid.uuid4())
self.u_id = self._load_or_generate_user_id()
thread_telemetry = threading.Thread(target=self._send_telemetry_event, args=("init",))
thread_telemetry.start()
def _load_or_generate_user_id(self):
"""
Loads the user id from the config file if it exists, otherwise generates a new
one and saves it to the config file.
"""
if not os.path.exists(CONFIG_DIR):
os.makedirs(CONFIG_DIR)
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r") as f:
data = json.load(f)
if "user_id" in data:
return data["user_id"]
u_id = str(uuid.uuid4())
with open(CONFIG_FILE, "w") as f:
json.dump({"user_id": u_id}, f)
return u_id
def add(
self,
source,
data_type: Optional[DataType] = None,
metadata: Optional[Dict] = None,
config: Optional[AddConfig] = None,
):
"""
Adds the data from the given URL to the vector db.
Loads the data, chunks it, create embedding for each chunk
and then stores the embedding to vector database.
:param data_type: The type of the data to add.
:param url: The URL where the data is located.
:param source: The data to embed, can be a URL, local file or raw content, depending on the data type.
:param data_type: Optional. Automatically detected, but can be forced with this argument.
The type of the data to add.
:param metadata: Optional. Metadata associated with the data source.
:param config: Optional. The `AddConfig` instance to use as configuration
options.
:return: source_id, a md5-hash of the source, in hexadecimal representation.
"""
if config is None:
config = AddConfig()
try:
DataType(source)
logging.warning(
f"""Starting from version v0.0.40, Embedchain can automatically detect the data type. So, in the `add` method, the argument order has changed. You no longer need to specify '{source}' for the `source` argument. So the code snippet will be `.add("{data_type}", "{source}")`""" # noqa #E501
)
logging.warning(
"Embedchain is swapping the arguments for you. This functionality might be deprecated in the future, so please adjust your code." # noqa #E501
)
source, data_type = data_type, source
except ValueError:
pass
if data_type:
try:
data_type = DataType(data_type)
except ValueError:
raise ValueError(
f"Invalid data_type: '{data_type}'.",
f"Please use one of the following: {[data_type.value for data_type in DataType]}",
) from None
if not data_type:
data_type = detect_datatype(source)
# `source_id` is the hash of the source argument
hash_object = hashlib.md5(str(source).encode("utf-8"))
source_id = hash_object.hexdigest()
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)
if data_type in ("docs_site",):
self.user_asks.append([source, data_type.value, metadata])
documents, _metadatas, _ids, new_chunks = self.load_and_embed(
data_formatter.loader, data_formatter.chunker, source, metadata, source_id
)
if data_type in {DataType.DOCS_SITE}:
self.is_docs_site_instance = True
def add_local(self, data_type, content, metadata=None, config: AddConfig = None):
# Send anonymous telemetry
if self.config.collect_metrics:
# it's quicker to check the variable twice than to count words when they won't be submitted.
word_count = sum([len(document.split(" ")) for document in documents])
extra_metadata = {"data_type": data_type.value, "word_count": word_count, "chunks_count": new_chunks}
thread_telemetry = threading.Thread(target=self._send_telemetry_event, args=("add", extra_metadata))
thread_telemetry.start()
return source_id
def add_local(self, source, data_type=None, metadata=None, config: AddConfig = None):
"""
Adds the data you supply to the vector db.
Warning:
This method is deprecated and will be removed in future versions. Use `add` instead.
Adds the data from the given URL to the vector db.
Loads the data, chunks it, create embedding for each chunk
and then stores the embedding to vector database.
:param data_type: The type of the data to add.
:param content: The local data. Refer to the `README` for formatting.
:param source: The data to embed, can be a URL, local file or raw content, depending on the data type.
:param data_type: Optional. Automatically detected, but can be forced with this argument.
The type of the data to add.
:param metadata: Optional. Metadata associated with the data source.
:param config: Optional. The `AddConfig` instance to use as
configuration options.
:param config: Optional. The `AddConfig` instance to use as configuration
options.
:return: md5-hash of the source, in hexadecimal representation.
"""
if config is None:
config = AddConfig()
data_formatter = DataFormatter(data_type, config)
self.user_asks.append([data_type, content])
self.load_and_embed(
data_formatter.loader,
data_formatter.chunker,
content,
metadata,
logging.warning(
"The `add_local` method is deprecated and will be removed in future versions. Please use the `add` method for both local and remote files." # noqa: E501
)
return self.add(source=source, data_type=data_type, metadata=metadata, config=config)
def load_and_embed(self, loader: BaseLoader, chunker: BaseChunker, src, metadata=None):
def load_and_embed(self, loader: BaseLoader, chunker: BaseChunker, src, metadata=None, source_id=None):
"""
Loads the data from the given URL, chunks it, and adds it to database.
@@ -91,19 +177,23 @@ class EmbedChain:
:param src: The data to be handled by the loader. Can be a URL for
remote sources or local content for local loaders.
:param metadata: Optional. Metadata associated with the data source.
:param source_id: Hexadecimal hash of the source.
:return: (List) documents (embedded text), (List) metadata, (list) ids, (int) number of chunks
"""
embeddings_data = chunker.create_chunks(loader, src)
# spread chunking results
documents = embeddings_data["documents"]
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(
existing_ids = self.db.get(
ids=ids,
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)}
@@ -111,25 +201,37 @@ class EmbedChain:
if not data_dict:
print(f"All data from {src} already exists in the database.")
return
# Make sure to return a matching return type
return [], [], [], 0
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]
# Loop though all metadatas and add extras.
new_metadatas = []
for m in metadatas:
# Add app id in metadatas so that they can be queried on later
if self.config.id:
m["app_id"] = self.config.id
# FIXME: Fix the error handling logic when metadatas or metadata is None
metadatas = metadatas if metadatas else []
metadata = metadata if metadata else {}
# Add hashed source
m["hash"] = source_id
# Note: Metadata is the function argument
if metadata:
# Spread whatever is in metadata into the new object.
m.update(metadata)
new_metadatas.append(m)
metadatas = new_metadatas
# Count before, to calculate a delta in the end.
chunks_before_addition = self.count()
# Add metadata to each document
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.db.add(documents=documents, metadatas=metadatas, ids=ids)
count_new_chunks = self.count() - chunks_before_addition
print((f"Successfully saved {src} ({chunker.data_type}). New chunks count: {count_new_chunks}"))
return list(documents), metadatas, ids, count_new_chunks
def _format_result(self, results):
return [
@@ -156,23 +258,13 @@ class EmbedChain:
:param config: The query configuration.
:return: The content of the document that matched your query.
"""
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
where = {"app_id": self.config.id} if self.config.id is not None else {} # optional filter
contents = self.db.query(
input_query=input_query,
n_results=config.number_documents,
where=where,
)
results_formatted = self._format_result(result)
contents = [result[0].page_content for result in results_formatted]
return contents
def _append_search_and_context(self, context, web_search_result):
@@ -252,6 +344,10 @@ class EmbedChain:
answer = self.get_answer_from_llm(prompt, config)
# Send anonymous telemetry
thread_telemetry = threading.Thread(target=self._send_telemetry_event, args=("query",))
thread_telemetry.start()
if isinstance(answer, str):
logging.info(f"Answer: {answer}")
return answer
@@ -293,8 +389,7 @@ class EmbedChain:
k["web_search_result"] = self.access_search_and_get_results(input_query)
contexts = self.retrieve_from_database(input_query, config)
global memory
chat_history = memory.load_memory_variables({})["history"]
chat_history = self.memory.load_memory_variables({})["history"]
if chat_history:
config.set_history(chat_history)
@@ -307,10 +402,14 @@ class EmbedChain:
answer = self.get_answer_from_llm(prompt, config)
memory.chat_memory.add_user_message(input_query)
self.memory.chat_memory.add_user_message(input_query)
# Send anonymous telemetry
thread_telemetry = threading.Thread(target=self._send_telemetry_event, args=("chat",))
thread_telemetry.start()
if isinstance(answer, str):
memory.chat_memory.add_ai_message(answer)
self.memory.chat_memory.add_ai_message(answer)
logging.info(f"Answer: {answer}")
return answer
else:
@@ -322,20 +421,61 @@ class EmbedChain:
for chunk in answer:
streamed_answer = streamed_answer + chunk
yield chunk
memory.chat_memory.add_ai_message(streamed_answer)
self.memory.chat_memory.add_ai_message(streamed_answer)
logging.info(f"Answer: {streamed_answer}")
def count(self):
def set_collection(self, collection_name):
"""
Set the collection to use.
:param collection_name: The name of the collection to use.
"""
self.collection = self.config.db._get_or_create_collection(collection_name)
def count(self) -> int:
"""
Count the number of embeddings.
:return: The number of embeddings.
"""
return self.collection.count()
return self.db.count()
def reset(self):
"""
Resets the database. Deletes all embeddings irreversibly.
`App` has to be reinitialized after using this method.
`App` does not have to be reinitialized after using this method.
"""
self.db_client.reset()
# Send anonymous telemetry
thread_telemetry = threading.Thread(target=self._send_telemetry_event, args=("reset",))
thread_telemetry.start()
collection_name = self.collection.name
self.db.reset()
self.collection = self.config.db._get_or_create_collection(collection_name)
# Todo: Automatically recreating a collection with the same name cannot be the best way to handle a reset.
# A downside of this implementation is, if you have two instances,
# the other instance will not get the updated `self.collection` attribute.
# A better way would be to create the collection if it is called again after being reset.
# That means, checking if collection exists in the db-consuming methods, and creating it if it doesn't.
# That's an extra steps for all uses, just to satisfy a niche use case in a niche method. For now, this will do.
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
def _send_telemetry_event(self, method: str, extra_metadata: Optional[dict] = None):
if not self.config.collect_metrics:
return
with threading.Lock():
url = "https://api.embedchain.ai/api/v1/telemetry/"
metadata = {
"s_id": self.s_id,
"version": importlib.metadata.version(__package__ or __name__),
"method": method,
"language": "py",
"u_id": self.u_id,
}
if extra_metadata:
metadata.update(extra_metadata)
response = requests.post(url, json={"metadata": metadata})
if response.status_code != 200:
logging.warning(f"Telemetry event failed with status code {response.status_code}")
+41
View File
@@ -0,0 +1,41 @@
import logging
import os
try:
from llama_index import download_loader
except ImportError:
raise ImportError("Notion requires extra dependencies. Install with `pip install embedchain[community]`") from None
from embedchain.loaders.base_loader import BaseLoader
from embedchain.utils import clean_string
class NotionLoader(BaseLoader):
def load_data(self, source):
"""Load data from a PDF file."""
NotionPageReader = download_loader("NotionPageReader")
# Reformat Id to match notion expectation
id = source[-32:]
formatted_id = f"{id[:8]}-{id[8:12]}-{id[12:16]}-{id[16:20]}-{id[20:]}"
logging.debug(f"Extracted notion page id as: {formatted_id}")
# Get page through the notion api
integration_token = os.getenv("NOTION_INTEGRATION_TOKEN")
reader = NotionPageReader(integration_token=integration_token)
documents = reader.load_data(page_ids=[formatted_id])
# Extract text
raw_text = documents[0].text
# Clean text
text = clean_string(raw_text)
return [
{
"content": text,
"meta_data": {"url": f"notion-{formatted_id}"},
}
]
+6
View File
@@ -0,0 +1,6 @@
from enum import Enum
class VectorDatabases(Enum):
CHROMADB = "CHROMADB"
ELASTICSEARCH = "ELASTICSEARCH"
+9
View File
@@ -0,0 +1,9 @@
from enum import Enum
# vector length created by embedding fn
class VectorDimensions(Enum):
GPT4ALL = 384
OPENAI = 1536
VERTEX_AI = 768
HUGGING_FACE = 384
+2
View File
@@ -1,2 +1,4 @@
from .EmbeddingFunctions import EmbeddingFunctions # noqa: F401
from .Providers import Providers # noqa: F401
from .VectorDatabases import VectorDatabases # noqa: F401
from .VectorDimensions import VectorDimensions # noqa: F401
+13
View File
@@ -0,0 +1,13 @@
from enum import Enum
class DataType(Enum):
YOUTUBE_VIDEO = "youtube_video"
PDF_FILE = "pdf_file"
WEB_PAGE = "web_page"
SITEMAP = "sitemap"
DOCX = "docx"
DOCS_SITE = "docs_site"
TEXT = "text"
QNA_PAIR = "qna_pair"
NOTION = "notion"
+129 -14
View File
@@ -1,6 +1,10 @@
import logging
import os
import re
import string
from typing import Any
from embedchain.models.data_type import DataType
def clean_string(text):
@@ -60,22 +64,133 @@ def use_pysqlite3():
import sqlite3
if platform.system() == "Linux" and sqlite3.sqlite_version_info < (3, 35, 0):
# According to the Chroma team, this patch only works on Linux
import datetime
import subprocess
import sys
try:
# According to the Chroma team, this patch only works on Linux
import datetime
import subprocess
import sys
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "pysqlite3-binary", "--quiet", "--disable-pip-version-check"]
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "pysqlite3-binary", "--quiet", "--disable-pip-version-check"]
)
__import__("pysqlite3")
sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
# Let the user know what happened.
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
print(
f"{current_time} [embedchain] [INFO]",
"Swapped std-lib sqlite3 with pysqlite3 for ChromaDb compatibility.",
f"Your original version was {sqlite3.sqlite_version}.",
)
except Exception as e:
# Escape all exceptions
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
print(
f"{current_time} [embedchain] [ERROR]",
"Failed to swap std-lib sqlite3 with pysqlite3 for ChromaDb compatibility.",
"Error:",
e,
)
def format_source(source: str, limit: int = 20) -> str:
"""
Format a string to only take the first x and last x letters.
This makes it easier to display a URL, keeping familiarity while ensuring a consistent length.
If the string is too short, it is not sliced.
"""
if len(source) > 2 * limit:
return source[:limit] + "..." + source[-limit:]
return source
def detect_datatype(source: Any) -> DataType:
"""
Automatically detect the datatype of the given source.
:param source: the source to base the detection on
:return: data_type string
"""
from urllib.parse import urlparse
try:
if not isinstance(source, str):
raise ValueError("Source is not a string and thus cannot be a URL.")
url = urlparse(source)
# Check if both scheme and netloc are present. Local file system URIs are acceptable too.
if not all([url.scheme, url.netloc]) and url.scheme != "file":
raise ValueError("Not a valid URL.")
except ValueError:
url = False
formatted_source = format_source(str(source), 30)
if url:
from langchain.document_loaders.youtube import \
ALLOWED_NETLOCK as YOUTUBE_ALLOWED_NETLOCS
if url.netloc in YOUTUBE_ALLOWED_NETLOCS:
logging.debug(f"Source of `{formatted_source}` detected as `youtube_video`.")
return DataType.YOUTUBE_VIDEO
if url.netloc in {"notion.so", "notion.site"}:
logging.debug(f"Source of `{formatted_source}` detected as `notion`.")
return DataType.NOTION
if url.path.endswith(".pdf"):
logging.debug(f"Source of `{formatted_source}` detected as `pdf_file`.")
return DataType.PDF_FILE
if url.path.endswith(".xml"):
logging.debug(f"Source of `{formatted_source}` detected as `sitemap`.")
return DataType.SITEMAP
if url.path.endswith(".docx"):
logging.debug(f"Source of `{formatted_source}` detected as `docx`.")
return DataType.DOCX
if "docs" in url.netloc or ("docs" in url.path and url.scheme != "file"):
# `docs_site` detection via path is not accepted for local filesystem URIs,
# because that would mean all paths that contain `docs` are now doc sites, which is too aggressive.
logging.debug(f"Source of `{formatted_source}` detected as `docs_site`.")
return DataType.DOCS_SITE
# If none of the above conditions are met, it's a general web page
logging.debug(f"Source of `{formatted_source}` detected as `web_page`.")
return DataType.WEB_PAGE
elif not isinstance(source, str):
# For datatypes where source is not a string.
if isinstance(source, tuple) and len(source) == 2 and isinstance(source[0], str) and isinstance(source[1], str):
logging.debug(f"Source of `{formatted_source}` detected as `qna_pair`.")
return DataType.QNA_PAIR
# Raise an error if it isn't a string and also not a valid non-string type (one of the previous).
# We could stringify it, but it is better to raise an error and let the user decide how they want to do that.
raise TypeError(
"Source is not a string and a valid non-string type could not be detected. If you want to embed it, please stringify it, for instance by using `str(source)` or `(', ').join(source)`." # noqa: E501
)
__import__("pysqlite3")
sys.modules["sqlite3"] = sys.modules.pop("pysqlite3")
elif os.path.isfile(source):
# For datatypes that support conventional file references.
# Note: checking for string is not necessary anymore.
# Let the user know what happened.
current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S,%f")[:-3]
print(
f"{current_time} [embedchain] [INFO]",
"Swapped std-lib sqlite3 with pysqlite3 for ChromaDb compatibility.",
f"Your original version was {sqlite3.sqlite_version}.",
if source.endswith(".docx"):
logging.debug(f"Source of `{formatted_source}` detected as `docx`.")
return DataType.DOCX
# If the source is a valid file, that's not detectable as a type, an error is raised.
# It does not fallback to text.
raise ValueError(
"Source points to a valid file, but based on the filename, no `data_type` can be detected. Please be aware, that not all data_types allow conventional file references, some require the use of the `file URI scheme`. Please refer to the embedchain documentation (https://docs.embedchain.ai/advanced/data_types#remote-data-types)." # noqa: E501
)
else:
# Source is not a URL.
# Use text as final fallback.
logging.debug(f"Source of `{formatted_source}` detected as `text`.")
return DataType.TEXT
+15 -1
View File
@@ -3,7 +3,6 @@ class BaseVectorDB:
def __init__(self):
self.client = self._get_or_create_db()
self.collection = self._get_or_create_collection()
def _get_or_create_db(self):
"""Get or create the database."""
@@ -11,3 +10,18 @@ class BaseVectorDB:
def _get_or_create_collection(self):
raise NotImplementedError
def get(self):
raise NotImplementedError
def add(self):
raise NotImplementedError
def query(self):
raise NotImplementedError
def count(self):
raise NotImplementedError
def reset(self):
raise NotImplementedError
+75 -5
View File
@@ -1,4 +1,8 @@
import logging
from typing import Any, Dict, List
from chromadb.errors import InvalidDimensionException
from langchain.docstore.document import Document
try:
import chromadb
@@ -7,6 +11,7 @@ except RuntimeError:
use_pysqlite3()
import chromadb
from chromadb.config import Settings
from embedchain.vectordb.base_vector_db import BaseVectorDB
@@ -23,8 +28,7 @@ class ChromaDB(BaseVectorDB):
if host and port:
logging.info(f"Connecting to ChromaDB server: {host}:{port}")
self.settings = Settings(chroma_server_host=host, chroma_server_http_port=port)
self.client = chromadb.HttpClient(self.settings)
self.client = chromadb.HttpClient(host=host, port=8000)
else:
if db_dir is None:
db_dir = "db"
@@ -39,9 +43,75 @@ class ChromaDB(BaseVectorDB):
"""Get or create the database."""
return self.client
def _get_or_create_collection(self):
def _get_or_create_collection(self, name):
"""Get or create the collection."""
return self.client.get_or_create_collection(
"embedchain_store",
self.collection = self.client.get_or_create_collection(
name=name,
embedding_function=self.embedding_fn,
)
return self.collection
def get(self, ids: List[str], where: Dict[str, any]) -> List[str]:
"""
Get existing doc ids present in vector database
:param ids: list of doc ids to check for existance
:param where: Optional. to filter data
"""
existing_docs = self.collection.get(
ids=ids,
where=where, # optional filter
)
return set(existing_docs["ids"])
def add(self, documents: List[str], metadatas: List[object], ids: List[str]) -> Any:
"""
add data in vector database
:param documents: list of texts to add
:param metadatas: list of metadata associated with docs
:param ids: ids of docs
"""
self.collection.add(documents=documents, metadatas=metadatas, ids=ids)
def _format_result(self, results):
return [
(Document(page_content=result[0], metadata=result[1] or {}), result[2])
for result in zip(
results["documents"][0],
results["metadatas"][0],
results["distances"][0],
)
]
def query(self, input_query: List[str], n_results: int, where: Dict[str, any]) -> List[str]:
"""
query contents from vector data base based on vector similarity
:param input_query: list of query string
:param n_results: no of similar documents to fetch from database
:param where: Optional. to filter data
:return: The content of the document that matched your query.
"""
try:
result = self.collection.query(
query_texts=[
input_query,
],
n_results=n_results,
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 count(self) -> int:
return self.collection.count()
def reset(self):
# Delete all data from the database
self.client.reset()
+136
View File
@@ -0,0 +1,136 @@
from typing import Any, Callable, Dict, List
try:
from elasticsearch import Elasticsearch
from elasticsearch.helpers import bulk
except ImportError:
raise ImportError(
"Elasticsearch requires extra dependencies. Install with `pip install embedchain[elasticsearch]`"
) from None
from embedchain.config import ElasticsearchDBConfig
from embedchain.models.VectorDimensions import VectorDimensions
from embedchain.vectordb.base_vector_db import BaseVectorDB
class ElasticsearchDB(BaseVectorDB):
def __init__(
self,
es_config: ElasticsearchDBConfig = None,
embedding_fn: Callable[[list[str]], list[str]] = None,
vector_dim: VectorDimensions = None,
collection_name: str = None,
):
"""
Elasticsearch as vector database
:param es_config. elasticsearch database config to be used for connection
:param embedding_fn: Function to generate embedding vectors.
:param vector_dim: Vector dimension generated by embedding fn
:param collection_name: Optional. Collection name for the database.
"""
if not hasattr(embedding_fn, "__call__"):
raise ValueError("Embedding function is not a function")
if es_config is None:
raise ValueError("ElasticsearchDBConfig is required")
if vector_dim is None:
raise ValueError("Vector Dimension is required to refer correct index and mapping")
if collection_name is None:
raise ValueError("collection name is required. It cannot be empty")
self.embedding_fn = embedding_fn
self.client = Elasticsearch(es_config.ES_URL, **es_config.ES_EXTRA_PARAMS)
self.vector_dim = vector_dim
self.es_index = f"{collection_name}_{self.vector_dim}"
index_settings = {
"mappings": {
"properties": {
"text": {"type": "text"},
"embeddings": {"type": "dense_vector", "index": False, "dims": self.vector_dim},
}
}
}
if not self.client.indices.exists(index=self.es_index):
# create index if not exist
print("Creating index", self.es_index, index_settings)
self.client.indices.create(index=self.es_index, body=index_settings)
super().__init__()
def _get_or_create_db(self):
return self.client
def _get_or_create_collection(self, name):
"""Note: nothing to return here. Discuss later"""
def get(self, ids: List[str], where: Dict[str, any]) -> List[str]:
"""
Get existing doc ids present in vector database
:param ids: list of doc ids to check for existance
:param where: Optional. to filter data
"""
query = {"bool": {"must": [{"ids": {"values": ids}}]}}
if "app_id" in where:
app_id = where["app_id"]
query["bool"]["must"].append({"term": {"metadata.app_id": app_id}})
response = self.client.search(index=self.es_index, query=query, _source=False)
docs = response["hits"]["hits"]
ids = [doc["_id"] for doc in docs]
return set(ids)
def add(self, documents: List[str], metadatas: List[object], ids: List[str]) -> Any:
"""
add data in vector database
:param documents: list of texts to add
:param metadatas: list of metadata associated with docs
:param ids: ids of docs
"""
docs = []
embeddings = self.embedding_fn(documents)
for id, text, metadata, embeddings in zip(ids, documents, metadatas, embeddings):
docs.append(
{
"_index": self.es_index,
"_id": id,
"_source": {"text": text, "metadata": metadata, "embeddings": embeddings},
}
)
bulk(self.client, docs)
self.client.indices.refresh(index=self.es_index)
return
def query(self, input_query: List[str], n_results: int, where: Dict[str, any]) -> List[str]:
"""
query contents from vector data base based on vector similarity
:param input_query: list of query string
:param n_results: no of similar documents to fetch from database
:param where: Optional. to filter data
"""
input_query_vector = self.embedding_fn(input_query)
query_vector = input_query_vector[0]
query = {
"script_score": {
"query": {"bool": {"must": [{"exists": {"field": "text"}}]}},
"script": {
"source": "cosineSimilarity(params.input_query_vector, 'embeddings') + 1.0",
"params": {"input_query_vector": query_vector},
},
}
}
if "app_id" in where:
app_id = where["app_id"]
query["script_score"]["query"]["bool"]["must"] = [{"term": {"metadata.app_id": app_id}}]
_source = ["text"]
response = self.client.search(index=self.es_index, query=query, _source=_source, size=n_results)
docs = response["hits"]["hits"]
contents = [doc["_source"]["text"] for doc in docs]
return contents
def count(self) -> int:
query = {"match_all": {}}
response = self.client.count(index=self.es_index, query=query)
doc_count = response["count"]
return doc_count
def reset(self):
# Delete all data from the database
if self.client.indices.exists(index=self.es_index):
# delete index in Es
self.client.indices.delete(index=self.es_index)
+8
View File
@@ -0,0 +1,8 @@
__pycache__/
database
db
pyenv
venv
.env
.git
trash_files/
+8
View File
@@ -0,0 +1,8 @@
__pycache__
db
database
pyenv
venv
.env
trash_files/
.ideas.md
+11
View File
@@ -0,0 +1,11 @@
FROM python:3.11 AS backend
WORKDIR /usr/src/api
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "api_server.py"]
+3
View File
@@ -0,0 +1,3 @@
# API Server
This is a docker template to create your own API Server using the embedchain package. To know more about the API Server and how to use it, go [here](https://docs.embedchain.ai/examples/api_server).
+55
View File
@@ -0,0 +1,55 @@
from flask import Flask, jsonify, request
from embedchain import App
app = Flask(__name__)
def initialize_chat_bot():
global chat_bot
chat_bot = App()
@app.route("/add", methods=["POST"])
def add():
data = request.get_json()
data_type = data.get("data_type")
url_or_text = data.get("url_or_text")
if data_type and url_or_text:
try:
chat_bot.add(data_type, url_or_text)
return jsonify({"data": f"Added {data_type}: {url_or_text}"}), 200
except Exception:
return jsonify({"error": f"Failed to add {data_type}: {url_or_text}"}), 500
return jsonify({"error": "Invalid request. Please provide 'data_type' and 'url_or_text' in JSON format."}), 400
@app.route("/query", methods=["POST"])
def query():
data = request.get_json()
question = data.get("question")
if question:
try:
response = chat_bot.query(question)
return jsonify({"data": response}), 200
except Exception:
return jsonify({"error": "An error occurred. Please try again!"}), 500
return jsonify({"error": "Invalid request. Please provide 'question' in JSON format."}), 400
@app.route("/chat", methods=["POST"])
def chat():
data = request.get_json()
question = data.get("question")
if question:
try:
response = chat_bot.chat(question)
return jsonify({"data": response}), 200
except Exception:
return jsonify({"error": "An error occurred. Please try again!"}), 500
return jsonify({"error": "Invalid request. Please provide 'question' in JSON format."}), 400
if __name__ == "__main__":
initialize_chat_bot()
app.run(host="0.0.0.0", port=5000, debug=False)
+13
View File
@@ -0,0 +1,13 @@
version: "3.9"
services:
backend:
container_name: embedchain_api
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile
env_file:
- variables.env
ports:
- "5000:5000"
+2
View File
@@ -0,0 +1,2 @@
flask==2.3.2
embedchain==0.0.30
+1
View File
@@ -0,0 +1 @@
OPENAI_API_KEY=""
+8
View File
@@ -0,0 +1,8 @@
__pycache__/
database
db
pyenv
venv
.env
.git
trash_files/
+7
View File
@@ -0,0 +1,7 @@
__pycache__
db
database
pyenv
venv
.env
trash_files/
+9
View File
@@ -0,0 +1,9 @@
FROM python:3.11 AS backend
WORKDIR /usr/src/discord_bot
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "discord_bot.py"]
+3
View File
@@ -0,0 +1,3 @@
# Discord Bot
This is a docker template to create your own Discord bot using the embedchain package. To know more about the bot and how to use it, go [here](https://docs.embedchain.ai/examples/discord_bot).
+65
View File
@@ -0,0 +1,65 @@
import os
import discord
from discord.ext import commands
from dotenv import load_dotenv
from embedchain import App
load_dotenv()
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="/ec ", intents=intents)
root_folder = os.getcwd()
def initialize_chat_bot():
global chat_bot
chat_bot = App()
@bot.event
async def on_ready():
print(f"Logged in as {bot.user.name}")
initialize_chat_bot()
@bot.event
async def on_command_error(ctx, error):
if isinstance(error, commands.CommandNotFound):
await send_response(ctx, "Invalid command. Please refer to the documentation for correct syntax.")
else:
print("Error occurred during command execution:", error)
@bot.command()
async def add(ctx, data_type: str, *, url_or_text: str):
print(f"User: {ctx.author.name}, Data Type: {data_type}, URL/Text: {url_or_text}")
try:
chat_bot.add(data_type, url_or_text)
await send_response(ctx, f"Added {data_type} : {url_or_text}")
except Exception as e:
await send_response(ctx, f"Failed to add {data_type} : {url_or_text}")
print("Error occurred during 'add' command:", e)
@bot.command()
async def query(ctx, *, question: str):
print(f"User: {ctx.author.name}, Query: {question}")
try:
response = chat_bot.chat(question)
await send_response(ctx, response)
except Exception as e:
await send_response(ctx, "An error occurred. Please try again!")
print("Error occurred during 'query' command:", e)
async def send_response(ctx, message):
if ctx.guild is None:
await ctx.send(message)
else:
await ctx.reply(message)
bot.run(os.environ["DISCORD_BOT_TOKEN"])
+11
View File
@@ -0,0 +1,11 @@
version: "3.9"
services:
backend:
container_name: embedchain_discord_bot
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile
env_file:
- variables.env
+3
View File
@@ -0,0 +1,3 @@
discord==2.3.1
embedchain==0.0.30
python-dotenv==1.0.0
+2
View File
@@ -0,0 +1,2 @@
OPENAI_API_KEY=""
DISCORD_BOT_TOKEN=""
+7
View File
@@ -0,0 +1,7 @@
__pycache__
db
database
pyenv
venv
.env
trash_files/
+3
View File
@@ -0,0 +1,3 @@
# Slack Bot
This is a replit template to create your own Slack bot using the embedchain package. To know more about the bot and how to use it, go [here](https://docs.embedchain.ai/examples/slack_bot).
+5
View File
@@ -0,0 +1,5 @@
flask==2.3.2
slackeventsapi==3.0.1
slacksdk==3.21.3
python-dotenv==1.0.0
embedchain
+58
View File
@@ -0,0 +1,58 @@
import os
from dotenv import load_dotenv
from flask import Flask
from slack_sdk import WebClient
from slackeventsapi import SlackEventAdapter
from embedchain import App
load_dotenv()
app = Flask(__name__)
slack_signing_secret = os.environ.get("SLACK_SIGNING_SECRET")
slack_events_adapter = SlackEventAdapter(slack_signing_secret, "/chat", app)
slack_bot_token = os.environ.get("SLACK_BOT_TOKEN")
client = WebClient(token=slack_bot_token)
chat_bot = App()
recent_message = {"ts": 0, "channel": ""}
@slack_events_adapter.on("message")
def handle_message(event_data):
message = event_data["event"]
if "text" in message and message.get("subtype") != "bot_message":
text = message["text"]
if float(message.get("ts")) > float(recent_message["ts"]):
recent_message["ts"] = message["ts"]
recent_message["channel"] = message["channel"]
if text.startswith("query"):
_, question = text.split(" ", 1)
try:
response = chat_bot.chat(question)
send_slack_message(message["channel"], response)
print("Query answered successfully!")
except Exception as e:
send_slack_message(message["channel"], "An error occurred. Please try again!")
print("Error occurred during 'query' command:", e)
elif text.startswith("add"):
_, data_type, url_or_text = text.split(" ", 2)
if url_or_text.startswith("<") and url_or_text.endswith(">"):
url_or_text = url_or_text[1:-1]
try:
chat_bot.add(data_type, url_or_text)
send_slack_message(message["channel"], f"Added {data_type} : {url_or_text}")
except Exception as e:
send_slack_message(message["channel"], f"Failed to add {data_type} : {url_or_text}")
print("Error occurred during 'add' command:", e)
def send_slack_message(channel, message):
response = client.chat_postMessage(channel=channel, text=message)
return response
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
+3
View File
@@ -0,0 +1,3 @@
SLACK_SIGNING_SECRET=""
SLACK_BOT_TOKEN=""
OPENAI_API_KEY=""
+7
View File
@@ -0,0 +1,7 @@
__pycache__
db
database
pyenv
venv
.env
trash_files/
+3
View File
@@ -0,0 +1,3 @@
# Telegram Bot
This is a replit template to create your own Telegram bot using the embedchain package. To know more about the bot and how to use it, go [here](https://docs.embedchain.ai/examples/telegram_bot).
+4
View File
@@ -0,0 +1,4 @@
flask==2.3.2
requests==2.31.0
python-dotenv==1.0.0
embedchain
+66
View File
@@ -0,0 +1,66 @@
import os
import requests
from dotenv import load_dotenv
from flask import Flask, request
from embedchain import App
app = Flask(__name__)
load_dotenv()
bot_token = os.environ["TELEGRAM_BOT_TOKEN"]
chat_bot = App()
@app.route("/", methods=["POST"])
def telegram_webhook():
data = request.json
message = data["message"]
chat_id = message["chat"]["id"]
text = message["text"]
if text.startswith("/start"):
response_text = (
"Welcome to Embedchain Bot! Try the following commands to use the bot:\n"
"For adding data sources:\n /add <data_type> <url_or_text>\n"
"For asking queries:\n /query <question>"
)
elif text.startswith("/add"):
_, data_type, url_or_text = text.split(maxsplit=2)
response_text = add_to_chat_bot(data_type, url_or_text)
elif text.startswith("/query"):
_, question = text.split(maxsplit=1)
response_text = query_chat_bot(question)
else:
response_text = "Invalid command. Please refer to the documentation for correct syntax."
send_message(chat_id, response_text)
return "OK"
def add_to_chat_bot(data_type, url_or_text):
try:
chat_bot.add(data_type, url_or_text)
response_text = f"Added {data_type} : {url_or_text}"
except Exception as e:
response_text = f"Failed to add {data_type} : {url_or_text}"
print("Error occurred during 'add' command:", e)
return response_text
def query_chat_bot(question):
try:
response = chat_bot.chat(question)
response_text = response
except Exception as e:
response_text = "An error occurred. Please try again!"
print("Error occurred during 'query' command:", e)
return response_text
def send_message(chat_id, text):
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
data = {"chat_id": chat_id, "text": text}
requests.post(url, json=data)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
+2
View File
@@ -0,0 +1,2 @@
TELEGRAM_BOT_TOKEN=""
OPENAI_API_KEY=""
+8
View File
@@ -0,0 +1,8 @@
__pycache__
db
database
pyenv
venv
.env
trash_files/
.ideas.md
+3
View File
@@ -0,0 +1,3 @@
# WhatsApp Bot
This is a replit template to create your own WhatsApp bot using the embedchain package. To know more about the bot and how to use it, go [here](https://docs.embedchain.ai/examples/whatsapp_bot).
+3
View File
@@ -0,0 +1,3 @@
Flask==2.3.2
twilio==8.5.0
embedchain
+10
View File
@@ -0,0 +1,10 @@
from embedchain.bots.whatsapp import WhatsAppBot
def main():
whatsapp_bot = WhatsAppBot()
whatsapp_bot.start()
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
OPENAI_API_KEY=""
+51
View File
@@ -0,0 +1,51 @@
from flask import Flask, request
from twilio.twiml.messaging_response import MessagingResponse
from embedchain import App
app = Flask(__name__)
chat_bot = App()
@app.route("/chat", methods=["POST"])
def chat():
incoming_message = request.values.get("Body", "").lower()
response = handle_message(incoming_message)
twilio_response = MessagingResponse()
twilio_response.message(response)
return str(twilio_response)
def handle_message(message):
if message.startswith("add "):
response = add_sources(message)
else:
response = query(message)
return response
def add_sources(message):
message_parts = message.split(" ", 2)
if len(message_parts) == 3:
data_type = message_parts[1]
url_or_text = message_parts[2]
try:
chat_bot.add(data_type, url_or_text)
response = f"Added {data_type}: {url_or_text}"
except Exception as e:
response = f"Failed to add {data_type}: {url_or_text}.\nError: {str(e)}"
else:
response = "Invalid 'add' command format.\nUse: add <data_type> <url_or_text>"
return response
def query(message):
try:
response = chat_bot.chat(message)
except Exception:
response = "An error occurred. Please try again!"
return response
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=False)
+15 -3
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "embedchain"
version = "0.0.23"
version = "0.0.51"
description = "embedchain is a framework to easily create LLM powered bots over any dataset"
authors = ["Taranjeet Singh"]
license = "Apache License"
@@ -85,11 +85,21 @@ python-dotenv = "^1.0.0"
langchain = "^0.0.237"
requests = "^2.31.0"
openai = "^0.27.5"
tiktoken = "^0.4.0"
chromadb ="^0.4.2"
youtube-transcript-api = "^0.6.1"
beautifulsoup4 = "^4.12.2"
pypdf = "^3.11.0"
pytube = "^15.0.0"
llama-index = { version = "^0.7.21", optional = true }
sentence-transformers = { version = "^2.2.2", optional = true }
torch = { version = ">=2.0.0, !=2.0.1", optional = true }
# Torch 2.0.1 is not compatible with poetry (https://github.com/pytorch/pytorch/issues/100974)
gpt4all = { version = "^1.0.8", optional = true }
elasticsearch = { version = "^8.9.0", optional = true }
flask = "^2.3.3"
twilio = "^8.5.0"
fastapi-poe = { version = "0.0.16", optional = true }
@@ -105,11 +115,13 @@ isort = "^5.12.0"
[tool.poetry.extras]
streamlit = ["streamlit"]
community = ["llama-index"]
opensource = ["sentence-transformers", "torch", "gpt4all"]
elasticsearch = ["elasticsearch"]
poe = ["fastapi-poe"]
[tool.poetry.group.docs.dependencies]
[tool.poetry.scripts]
-41
View File
@@ -1,41 +0,0 @@
import setuptools
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setuptools.setup(
name="embedchain",
version="0.0.31",
author="Taranjeet Singh",
author_email="reachtotj@gmail.com",
description="embedchain is a framework to easily create LLM powered bots over any dataset", # noqa:E501
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/embedchain/embedchain",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
],
python_requires=">=3.8",
py_modules=["embedchain"],
install_requires=[
"langchain>=0.0.205",
"requests",
"openai",
"chromadb>=0.4.2",
"youtube-transcript-api",
"beautifulsoup4",
"pypdf",
"pytube",
"lxml",
"gpt4all",
"sentence_transformers",
"docx2txt",
"pydantic==1.10.8",
"replicate==0.9.0",
"duckduckgo-search==3.8.4",
],
extras_require={"dev": ["black", "ruff", "isort", "pytest"]},
)
+7
View File
@@ -4,6 +4,7 @@ import unittest
from embedchain.chunkers.text import TextChunker
from embedchain.config import ChunkerConfig
from embedchain.models.data_type import DataType
class TestTextChunker(unittest.TestCase):
@@ -15,6 +16,8 @@ class TestTextChunker(unittest.TestCase):
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."
# Data type must be set manually in the test
chunker.set_data_type(DataType.TEXT)
result = chunker.create_chunks(MockLoader(), text)
@@ -31,6 +34,8 @@ class TestTextChunker(unittest.TestCase):
chunker_config = ChunkerConfig(chunk_size=9999999999, chunk_overlap=0, length_function=len)
chunker = TextChunker(config=chunker_config)
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
# Data type must be set manually in the test
chunker.set_data_type(DataType.TEXT)
result = chunker.create_chunks(MockLoader(), text)
@@ -46,6 +51,8 @@ class TestTextChunker(unittest.TestCase):
chunker = TextChunker(config=chunker_config)
# We can't test with lorem ipsum because chunks are deduped, so would be recurring characters.
text = """0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"""
# Data type must be set manually in the test
chunker.set_data_type(DataType.TEXT)
result = chunker.create_chunks(MockLoader(), text)
+13 -3
View File
@@ -3,13 +3,14 @@ import unittest
from unittest.mock import MagicMock, patch
from embedchain import App
from embedchain.config import AppConfig
class TestApp(unittest.TestCase):
os.environ["OPENAI_API_KEY"] = "test_key"
def setUp(self):
self.app = App()
self.app = App(config=AppConfig(collect_metrics=False))
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
def test_add(self):
@@ -22,5 +23,14 @@ class TestApp(unittest.TestCase):
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"}]])
self.app.add("https://example.com", metadata={"meta": "meta-data"})
self.assertEqual(self.app.user_asks, [["https://example.com", "web_page", {"meta": "meta-data"}]])
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
def test_add_forced_type(self):
"""
Test that you can also force a data_type with `add`.
"""
data_type = "text"
self.app.add("https://example.com", data_type=data_type, metadata={"meta": "meta-data"})
self.assertEqual(self.app.user_asks, [["https://example.com", data_type, {"meta": "meta-data"}]])
+7 -18
View File
@@ -3,18 +3,17 @@ import unittest
from unittest.mock import patch
from embedchain import App
from embedchain.config import AppConfig
class TestApp(unittest.TestCase):
os.environ["OPENAI_API_KEY"] = "test_key"
def setUp(self):
self.app = App()
os.environ["OPENAI_API_KEY"] = "test_key"
self.app = App(config=AppConfig(collect_metrics=False))
@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):
def test_chat_with_memory(self, mock_get_answer, mock_retrieve):
"""
This test checks the functionality of the 'chat' method in the App class with respect to the chat history
memory.
@@ -22,27 +21,17 @@ class TestApp(unittest.TestCase):
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.
- After the first call, 'memory.chat_memory.add_user_message' and 'memory.chat_memory.add_ai_message' are
- 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
self.assertEqual(len(app.memory.chat_memory.messages), 2)
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")
self.assertEqual(len(app.memory.chat_memory.messages), 4)
+24 -2
View File
@@ -25,15 +25,37 @@ class TestChromaDbHostsLoglevel(unittest.TestCase):
"""
Test if the `App` instance is initialized without a config that does not contain default hosts and ports.
"""
config = AppConfig(log_level="DEBUG")
config = AppConfig(log_level="DEBUG", collect_metrics=False)
app = App(config)
knowledge = "lorem ipsum dolor sit amet, consectetur adipiscing"
app.add_local("text", knowledge)
app.add(knowledge, data_type="text")
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])
def test_add_after_reset(self):
"""
Test if the `App` instance is correctly reconstructed after a reset.
"""
app = App()
app.reset()
# Make sure the client is still healthy
app.db.client.heartbeat()
# Make sure the collection exists, and can be added to
app.collection.add(
embeddings=[[1.1, 2.3, 3.2], [4.5, 6.9, 4.4], [1.1, 2.3, 3.2]],
metadatas=[
{"chapter": "3", "verse": "16"},
{"chapter": "3", "verse": "5"},
{"chapter": "29", "verse": "11"},
],
ids=["id1", "id2", "id3"],
)
app.reset()
+2 -2
View File
@@ -2,12 +2,12 @@ import unittest
from string import Template
from embedchain import App
from embedchain.embedchain import QueryConfig
from embedchain.config import AppConfig, QueryConfig
class TestGeneratePrompt(unittest.TestCase):
def setUp(self):
self.app = App()
self.app = App(config=AppConfig(collect_metrics=False))
def test_generate_prompt_with_template(self):
"""
+34 -2
View File
@@ -3,14 +3,14 @@ import unittest
from unittest.mock import MagicMock, patch
from embedchain import App
from embedchain.embedchain import QueryConfig
from embedchain.config import AppConfig, QueryConfig
class TestApp(unittest.TestCase):
os.environ["OPENAI_API_KEY"] = "test_key"
def setUp(self):
self.app = App()
self.app = App(config=AppConfig(collect_metrics=False))
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
def test_query(self):
@@ -41,3 +41,35 @@ class TestApp(unittest.TestCase):
self.assertEqual(mock_retrieve.call_args[0][0], "Test query")
self.assertIsInstance(mock_retrieve.call_args[0][1], QueryConfig)
mock_answer.assert_called_once()
@patch("openai.ChatCompletion.create")
def test_query_config_app_passing(self, mock_create):
mock_create.return_value = {"choices": [{"message": {"content": "response"}}]} # Mock response
config = AppConfig()
chat_config = QueryConfig(system_prompt="Test system prompt")
app = App(config=config)
app.get_llm_model_answer("Test query", chat_config)
# Test system_prompt: Check that the 'create' method was called with the correct 'messages' argument
messages_arg = mock_create.call_args.kwargs["messages"]
self.assertEqual(messages_arg[0]["role"], "system")
self.assertEqual(messages_arg[0]["content"], "Test system prompt")
# TODO: Add tests for other config variables
@patch("openai.ChatCompletion.create")
def test_app_passing(self, mock_create):
mock_create.return_value = {"choices": [{"message": {"content": "response"}}]} # Mock response
config = AppConfig()
chat_config = QueryConfig()
app = App(config=config, system_prompt="Test system prompt")
app.get_llm_model_answer("Test query", chat_config)
# Test system_prompt: Check that the 'create' method was called with the correct 'messages' argument
messages_arg = mock_create.call_args.kwargs["messages"]
self.assertEqual(messages_arg[0]["role"], "system")
self.assertEqual(messages_arg[0]["content"], "Test system prompt")
+129
View File
@@ -0,0 +1,129 @@
import tempfile
import unittest
from unittest.mock import patch
from embedchain.models.data_type import DataType
from embedchain.utils import detect_datatype
class TestApp(unittest.TestCase):
"""Test that the datatype detection is working, based on the input."""
def test_detect_datatype_youtube(self):
self.assertEqual(detect_datatype("https://www.youtube.com/watch?v=dQw4w9WgXcQ"), DataType.YOUTUBE_VIDEO)
self.assertEqual(detect_datatype("https://m.youtube.com/watch?v=dQw4w9WgXcQ"), DataType.YOUTUBE_VIDEO)
self.assertEqual(
detect_datatype("https://www.youtube-nocookie.com/watch?v=dQw4w9WgXcQ"), DataType.YOUTUBE_VIDEO
)
self.assertEqual(detect_datatype("https://vid.plus/watch?v=dQw4w9WgXcQ"), DataType.YOUTUBE_VIDEO)
self.assertEqual(detect_datatype("https://youtu.be/dQw4w9WgXcQ"), DataType.YOUTUBE_VIDEO)
def test_detect_datatype_local_file(self):
self.assertEqual(detect_datatype("file:///home/user/file.txt"), DataType.WEB_PAGE)
def test_detect_datatype_pdf(self):
self.assertEqual(detect_datatype("https://www.example.com/document.pdf"), DataType.PDF_FILE)
def test_detect_datatype_local_pdf(self):
self.assertEqual(detect_datatype("file:///home/user/document.pdf"), DataType.PDF_FILE)
def test_detect_datatype_xml(self):
self.assertEqual(detect_datatype("https://www.example.com/sitemap.xml"), DataType.SITEMAP)
def test_detect_datatype_local_xml(self):
self.assertEqual(detect_datatype("file:///home/user/sitemap.xml"), DataType.SITEMAP)
def test_detect_datatype_docx(self):
self.assertEqual(detect_datatype("https://www.example.com/document.docx"), DataType.DOCX)
def test_detect_datatype_local_docx(self):
self.assertEqual(detect_datatype("file:///home/user/document.docx"), DataType.DOCX)
@patch("os.path.isfile")
def test_detect_datatype_regular_filesystem_docx(self, mock_isfile):
with tempfile.NamedTemporaryFile(suffix=".docx", delete=True) as tmp:
mock_isfile.return_value = True
self.assertEqual(detect_datatype(tmp.name), DataType.DOCX)
def test_detect_datatype_docs_site(self):
self.assertEqual(detect_datatype("https://docs.example.com"), DataType.DOCS_SITE)
def test_detect_datatype_docs_sitein_path(self):
self.assertEqual(detect_datatype("https://www.example.com/docs/index.html"), DataType.DOCS_SITE)
self.assertNotEqual(detect_datatype("file:///var/www/docs/index.html"), DataType.DOCS_SITE) # NOT equal
def test_detect_datatype_web_page(self):
self.assertEqual(detect_datatype("https://nav.al/agi"), DataType.WEB_PAGE)
def test_detect_datatype_invalid_url(self):
self.assertEqual(detect_datatype("not a url"), DataType.TEXT)
def test_detect_datatype_qna_pair(self):
self.assertEqual(
detect_datatype(("Question?", "Answer. Content of the string is irrelevant.")), DataType.QNA_PAIR
) #
def test_detect_datatype_qna_pair_types(self):
"""Test that a QnA pair needs to be a tuple of length two, and both items have to be strings."""
with self.assertRaises(TypeError):
self.assertNotEqual(
detect_datatype(("How many planets are in our solar system?", 8)), DataType.QNA_PAIR
) # NOT equal
def test_detect_datatype_text(self):
self.assertEqual(detect_datatype("Just some text."), DataType.TEXT)
def test_detect_datatype_non_string_error(self):
"""Test type error if the value passed is not a string, and not a valid non-string data_type"""
with self.assertRaises(TypeError):
detect_datatype(["foo", "bar"])
@patch("os.path.isfile")
def test_detect_datatype_regular_filesystem_file_not_detected(self, mock_isfile):
"""Test error if a valid file is referenced, but it isn't a valid data_type"""
with tempfile.NamedTemporaryFile(suffix=".txt", delete=True) as tmp:
mock_isfile.return_value = True
with self.assertRaises(ValueError):
detect_datatype(tmp.name)
def test_detect_datatype_regular_filesystem_no_file(self):
"""Test that if a filepath is not actually an existing file, it is not handled as a file path."""
self.assertEqual(detect_datatype("/var/not-an-existing-file.txt"), DataType.TEXT)
def test_doc_examples_quickstart(self):
"""Test examples used in the documentation."""
self.assertEqual(detect_datatype("https://en.wikipedia.org/wiki/Elon_Musk"), DataType.WEB_PAGE)
self.assertEqual(detect_datatype("https://www.tesla.com/elon-musk"), DataType.WEB_PAGE)
def test_doc_examples_introduction(self):
"""Test examples used in the documentation."""
self.assertEqual(detect_datatype("https://www.youtube.com/watch?v=3qHkcs3kG44"), DataType.YOUTUBE_VIDEO)
self.assertEqual(
detect_datatype(
"https://navalmanack.s3.amazonaws.com/Eric-Jorgenson_The-Almanack-of-Naval-Ravikant_Final.pdf"
),
DataType.PDF_FILE,
)
self.assertEqual(detect_datatype("https://nav.al/feedback"), DataType.WEB_PAGE)
def test_doc_examples_app_types(self):
"""Test examples used in the documentation."""
self.assertEqual(detect_datatype("https://www.youtube.com/watch?v=Ff4fRgnuFgQ"), DataType.YOUTUBE_VIDEO)
self.assertEqual(detect_datatype("https://en.wikipedia.org/wiki/Mark_Zuckerberg"), DataType.WEB_PAGE)
def test_doc_examples_configuration(self):
"""Test examples used in the documentation."""
import subprocess
import sys
subprocess.check_call([sys.executable, "-m", "pip", "install", "wikipedia"])
import wikipedia
page = wikipedia.page("Albert Einstein")
# TODO: Add a wikipedia type, so wikipedia is a dependency and we don't need this slow test.
# (timings: import: 1.4s, fetch wiki: 0.7s)
self.assertEqual(detect_datatype(page.content), DataType.TEXT)
if __name__ == "__main__":
unittest.main()
+186 -3
View File
@@ -39,7 +39,7 @@ class TestChromaDbHostsInit(unittest.TestCase):
host = "test-host"
port = "1234"
config = AppConfig(host=host, port=port)
config = AppConfig(host=host, port=port, collect_metrics=False)
_app = App(config)
@@ -54,7 +54,7 @@ class TestChromaDbHostsNone(unittest.TestCase):
Test if the `App` instance is initialized without default hosts and ports.
"""
_app = App()
_app = App(config=AppConfig(collect_metrics=False))
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)
@@ -68,7 +68,190 @@ class TestChromaDbHostsLoglevel(unittest.TestCase):
"""
config = AppConfig(log_level="DEBUG")
_app = App(config)
_app = App(config=AppConfig(collect_metrics=False))
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 TestChromaDbDuplicateHandling:
def test_duplicates_throw_warning(self, caplog):
"""
Test that add duplicates throws an error.
"""
# Start with a clean app
App().reset()
app = App(config=AppConfig(collect_metrics=False))
app.collection.add(embeddings=[[0, 0, 0]], ids=["0"])
app.collection.add(embeddings=[[0, 0, 0]], ids=["0"])
assert "Insert of existing embedding ID: 0" in caplog.text
assert "Add of existing embedding ID: 0" in caplog.text
def test_duplicates_collections_no_warning(self, caplog):
"""
Test that different collections can have duplicates.
"""
# NOTE: Not part of the TestChromaDbCollection because `unittest.TestCase` doesn't have caplog.
# Start with a clean app
App().reset()
app = App(config=AppConfig(collect_metrics=False))
app.set_collection("test_collection_1")
app.collection.add(embeddings=[[0, 0, 0]], ids=["0"])
app.set_collection("test_collection_2")
app.collection.add(embeddings=[[0, 0, 0]], ids=["0"])
assert "Insert of existing embedding ID: 0" not in caplog.text # not
assert "Add of existing embedding ID: 0" not in caplog.text # not
class TestChromaDbCollection(unittest.TestCase):
def test_init_with_default_collection(self):
"""
Test if the `App` instance is initialized with the correct default collection name.
"""
app = App(config=AppConfig(collect_metrics=False))
self.assertEqual(app.collection.name, "embedchain_store")
def test_init_with_custom_collection(self):
"""
Test if the `App` instance is initialized with the correct custom collection name.
"""
config = AppConfig(collection_name="test_collection", collect_metrics=False)
app = App(config)
self.assertEqual(app.collection.name, "test_collection")
def test_set_collection(self):
"""
Test if the `App` collection is correctly switched using the `set_collection` method.
"""
app = App(config=AppConfig(collect_metrics=False))
app.set_collection("test_collection")
self.assertEqual(app.collection.name, "test_collection")
def test_changes_encapsulated(self):
"""
Test that changes to one collection do not affect the other collection
"""
# Start with a clean app
App().reset()
app = App(config=AppConfig(collect_metrics=False))
app.set_collection("test_collection_1")
# Collection should be empty when created
self.assertEqual(app.count(), 0)
app.collection.add(embeddings=[0, 0, 0], ids=["0"])
# After adding, should contain one item
self.assertEqual(app.count(), 1)
app.set_collection("test_collection_2")
# New collection is empty
self.assertEqual(app.count(), 0)
# Adding to new collection should not effect existing collection
app.collection.add(embeddings=[0, 0, 0], ids=["0"])
app.set_collection("test_collection_1")
# Should still be 1, not 2.
self.assertEqual(app.count(), 1)
def test_collections_are_persistent(self):
"""
Test that a collection can be picked up later.
"""
# Start with a clean app
App().reset()
app = App(config=AppConfig(collect_metrics=False))
app.set_collection("test_collection_1")
app.collection.add(embeddings=[[0, 0, 0]], ids=["0"])
del app
app = App(config=AppConfig(collect_metrics=False))
app.set_collection("test_collection_1")
self.assertEqual(app.count(), 1)
def test_parallel_collections(self):
"""
Test that two apps can have different collections open in parallel.
Switching the names will allow instant access to the collection of
the other app.
"""
# Start clean
App().reset()
# Create two apps
app1 = App(AppConfig(collection_name="test_collection_1", collect_metrics=False))
app2 = App(AppConfig(collection_name="test_collection_2", collect_metrics=False))
# app2 has been created last, but adding to app1 will still write to collection 1.
app1.collection.add(embeddings=[0, 0, 0], ids=["0"])
self.assertEqual(app1.count(), 1)
self.assertEqual(app2.count(), 0)
# Add data
app1.collection.add(embeddings=[[0, 0, 0], [1, 1, 1]], ids=["1", "2"])
app2.collection.add(embeddings=[0, 0, 0], ids=["0"])
# Swap names and test
app1.set_collection("test_collection_2")
self.assertEqual(app1.count(), 1)
app2.set_collection("test_collection_1")
self.assertEqual(app2.count(), 3)
def test_ids_share_collections(self):
"""
Different ids should still share collections.
"""
# Start clean
App().reset()
# Create two apps
app1 = App(AppConfig(collection_name="one_collection", id="new_app_id_1", collect_metrics=False))
app2 = App(AppConfig(collection_name="one_collection", id="new_app_id_2", collect_metrics=False))
# Add data
app1.collection.add(embeddings=[[0, 0, 0], [1, 1, 1]], ids=["0", "1"])
app2.collection.add(embeddings=[0, 0, 0], ids=["2"])
# Both should have the same collection
self.assertEqual(app1.count(), 3)
self.assertEqual(app2.count(), 3)
def test_reset(self):
"""
Resetting should hit all collections and ids.
"""
# Start clean
App().reset()
# Create four apps.
# app1, which we are about to reset, shares an app with one, and an id with the other, none with the last.
app1 = App(AppConfig(collection_name="one_collection", id="new_app_id_1", collect_metrics=False))
app2 = App(AppConfig(collection_name="one_collection", id="new_app_id_2", collect_metrics=False))
app3 = App(AppConfig(collection_name="three_collection", id="new_app_id_1", collect_metrics=False))
app4 = App(AppConfig(collection_name="four_collection", id="new_app_id_4", collect_metrics=False))
# Each one of them get data
app1.collection.add(embeddings=[0, 0, 0], ids=["1"])
app2.collection.add(embeddings=[0, 0, 0], ids=["2"])
app3.collection.add(embeddings=[0, 0, 0], ids=["3"])
app4.collection.add(embeddings=[0, 0, 0], ids=["4"])
# Resetting the first one should reset them all.
app1.reset()
# Reinstantiate app2-4, app1 doesn't have to be reinstantiated (PR #319)
app2 = App(AppConfig(collection_name="one_collection", id="new_app_id_2", collect_metrics=False))
app3 = App(AppConfig(collection_name="three_collection", id="new_app_id_3", collect_metrics=False))
app4 = App(AppConfig(collection_name="four_collection", id="new_app_id_3", collect_metrics=False))
# All should be empty
self.assertEqual(app1.count(), 0)
self.assertEqual(app2.count(), 0)
self.assertEqual(app3.count(), 0)
self.assertEqual(app4.count(), 0)
+33
View File
@@ -0,0 +1,33 @@
import unittest
from unittest.mock import Mock
from embedchain.config import ElasticsearchDBConfig
from embedchain.vectordb.elasticsearch_db import ElasticsearchDB
class TestEsDB(unittest.TestCase):
def setUp(self):
self.es_config = ElasticsearchDBConfig()
self.vector_dim = 384
def test_init_with_invalid_embedding_fn(self):
# Test if an exception is raised when an invalid embedding_fn is provided
with self.assertRaises(ValueError):
ElasticsearchDB(embedding_fn=None)
def test_init_with_invalid_es_config(self):
# Test if an exception is raised when an invalid es_config is provided
with self.assertRaises(ValueError):
ElasticsearchDB(embedding_fn=Mock(), es_config=None)
def test_init_with_invalid_vector_dim(self):
# Test if an exception is raised when an invalid vector_dim is provided
with self.assertRaises(ValueError):
ElasticsearchDB(embedding_fn=Mock(), es_config=self.es_config, vector_dim=None)
def test_init_with_invalid_collection_name(self):
# Test if an exception is raised when an invalid collection_name is provided
with self.assertRaises(ValueError):
ElasticsearchDB(
embedding_fn=Mock(), es_config=self.es_config, vector_dim=self.vector_dim, collection_name=None
)