Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f582d70031 | ||
|
|
1ac8aef4de | ||
|
|
4754372fcd | ||
|
|
eac85779eb | ||
|
|
b0d8711b65 | ||
|
|
f0844ed923 | ||
|
|
129242534d | ||
|
|
6481b555b4 | ||
|
|
794e51494e | ||
|
|
3059e96041 | ||
|
|
bd595f84e8 | ||
|
|
344e7470f6 | ||
|
|
387b042a49 | ||
|
|
b3837572be | ||
|
|
d4e6462e4d | ||
|
|
60d5daaaf5 | ||
|
|
3e66ddf69a | ||
|
|
202fd2d5b6 | ||
|
|
eecdbc5e06 | ||
|
|
433c4157e0 | ||
|
|
8be8990507 | ||
|
|
2cfeb5ed80 | ||
|
|
0d4ad07d7b | ||
|
|
2aa25a5169 | ||
|
|
c07fbc07b1 | ||
|
|
12eb16d60b | ||
|
|
ec9f454ad1 | ||
|
|
9f1f17a611 | ||
|
|
94369657ef | ||
|
|
490d7db601 | ||
|
|
ea18b80f90 | ||
|
|
70077f4e46 | ||
|
|
85106c7c7e | ||
|
|
1b19d0d19c | ||
|
|
13f01e399c |
@@ -15,7 +15,7 @@ Book a [1-on-1 Session](https://cal.com/taranjeetio/ec) with Taranjeet, the foun
|
||||
## 🔧 Quick install
|
||||
|
||||
```bash
|
||||
pip install embedchain
|
||||
pip install --upgrade embedchain
|
||||
```
|
||||
|
||||
## 🔍 Demo
|
||||
@@ -69,8 +69,11 @@ 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 refrence, please go through [Development Guide](https://docs.embedchain.ai/contribution/dev) and [Documentation Guide](https://docs.embedchain.ai/contribution/docs).
|
||||
For more reference, please go through [Development Guide](https://docs.embedchain.ai/contribution/dev) and [Documentation Guide](https://docs.embedchain.ai/contribution/docs).
|
||||
|
||||
<a href="https://github.com/embedchain/embedchain/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=embedchain/embedchain" />
|
||||
</a>
|
||||
|
||||
## Citation
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ app = App()
|
||||
```
|
||||
|
||||
- `App` uses OpenAI's model, so these are paid models. 💸 You will be charged for embedding model usage and LLM usage.
|
||||
- `App` uses OpenAI's embedding model to create embeddings for chunks and ChatGPT API as LLM to get answer given the relevant docs. Make sure that you have an OpenAI account and an API key. If you have don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
- `App` uses OpenAI's embedding model to create embeddings for chunks and ChatGPT API as LLM to get answer given the relevant docs. Make sure that you have an OpenAI account and an API key. If you don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
- `App` is opinionated. It uses the best embedding model and LLM on the market.
|
||||
- Once you have the API key, set it in an environment variable called `OPENAI_API_KEY`
|
||||
|
||||
@@ -49,7 +49,7 @@ zuck_bot.query("Who owns the new threads app and when it was founded?")
|
||||
```
|
||||
|
||||
- `Llama2App` uses Replicate's LLM model, so these are paid models. You can get the `REPLICATE_API_TOKEN` by registering on [their website](https://replicate.com/account).
|
||||
- `Llama2App` uses OpenAI's embedding model to create embeddings for chunks. Make sure that you have an OpenAI account and an API key. If you have don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
- `Llama2App` uses OpenAI's embedding model to create embeddings for chunks. Make sure that you have an OpenAI account and an API key. If you don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
|
||||
|
||||
### OpenSourceApp
|
||||
@@ -69,16 +69,27 @@ app = OpenSourceApp()
|
||||
|
||||
```python
|
||||
from embedchain import CustomApp
|
||||
from embedchain.config import CustomAppConfig
|
||||
from embedchain.models import Providers, EmbeddingFunctions
|
||||
from embedchain.config import (CustomAppConfig, ElasticsearchDBConfig,
|
||||
EmbedderConfig, LlmConfig)
|
||||
from embedchain.embedder.vertexai_embedder import VertexAiEmbedder
|
||||
from embedchain.llm.vertex_ai_llm import VertexAiLlm
|
||||
from embedchain.models import EmbeddingFunctions, Providers
|
||||
from embedchain.vectordb.elasticsearch_db import Elasticsearch
|
||||
|
||||
config = CustomAppConfig(embedding_fn=EmbeddingFunctions.OPENAI, provider=Providers.OPENAI)
|
||||
app = CustomApp(config)
|
||||
# short
|
||||
app = CustomApp(llm=VertexAiLlm(), db=Elasticsearch(), embedder=VertexAiEmbedder())
|
||||
# with configs
|
||||
app = CustomApp(
|
||||
config=CustomAppConfig(log_level="INFO"),
|
||||
llm=VertexAiLlm(config=LlmConfig(number_documents=5)),
|
||||
db=Elasticsearch(config=ElasticsearchDBConfig(es_url="...")),
|
||||
embedder=VertexAiEmbedder(config=EmbedderConfig()),
|
||||
)
|
||||
```
|
||||
|
||||
- `CustomApp` is not opinionated.
|
||||
- Configuration required. It's for advanced users who want to mix and match different embedding models and LLMs. Configuration required.
|
||||
- while it's doing that, it's still providing abstractions through `Providers`.
|
||||
- Configuration required. It's for advanced users who want to mix and match different embedding models and LLMs.
|
||||
- while it's doing that, it's still providing abstractions by allowing you to import Classes from `embedchain.llm`, `embedchain.vectordb`, and `embedchain.embedder`.
|
||||
- paid and free/open source providers included.
|
||||
- Once you have imported and instantiated the app, every functionality from here onwards is the same for either type of app. 📚
|
||||
- Following providers are available for an LLM
|
||||
@@ -87,6 +98,7 @@ app = CustomApp(config)
|
||||
- VERTEX_AI
|
||||
- GPT4ALL
|
||||
- AZURE_OPENAI
|
||||
- LLAMA2
|
||||
- Following embedding functions are available for an embedding function
|
||||
- OPENAI
|
||||
- HUGGING_FACE
|
||||
@@ -103,7 +115,7 @@ naval_chat_bot = PersonApp("name_of_person_or_character") #Like "Yoda"
|
||||
```
|
||||
|
||||
- `PersonApp` uses OpenAI's model, so these are paid models. 💸 You will be charged for embedding model usage and LLM usage.
|
||||
- `PersonApp` uses OpenAI's embedding model to create embeddings for chunks and ChatGPT API as LLM to get answer given the relevant docs. Make sure that you have an OpenAI account and an API key. If you have don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
- `PersonApp` uses OpenAI's embedding model to create embeddings for chunks and ChatGPT API as LLM to get answer given the relevant docs. Make sure that you have an OpenAI account and an API key. If you don't have an API key, you can create one by visiting [this link](https://platform.openai.com/account/api-keys).
|
||||
- Once you have the API key, set it in an environment variable called `OPENAI_API_KEY`
|
||||
|
||||
```python
|
||||
|
||||
@@ -4,6 +4,16 @@ title: '⚙️ Custom configurations'
|
||||
|
||||
Embedchain is made to work out of the box. However, for advanced users we're also offering configuration options. All of these configuration options are optional and have sane defaults.
|
||||
|
||||
## Concept
|
||||
The main `App` class is available in the following varieties: `CustomApp`, `OpenSourceApp` and `Llama2App` and `App`. The first is fully configurable, the others are opinionated in some aspects.
|
||||
|
||||
The `App` class has three subclasses: `llm`, `db` and `embedder`. These are the core ingredients that make up an EmbedChain app.
|
||||
App plus each one of the subclasses have a `config` attribute.
|
||||
You can pass a `Config` instance as an argument during initialization to persistently configure a class.
|
||||
These configs can be imported from `embedchain.config`
|
||||
|
||||
There are `set` methods for some things that should not (only) be set at start-up, like `app.db.set_collection_name`.
|
||||
|
||||
## Examples
|
||||
|
||||
### General
|
||||
@@ -11,31 +21,31 @@ Embedchain is made to work out of the box. However, for advanced users we're als
|
||||
Here's the readme example with configuration options.
|
||||
|
||||
```python
|
||||
import os
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig, AddConfig, QueryConfig, ChunkerConfig
|
||||
from chromadb.utils import embedding_functions
|
||||
from embedchain.config import AppConfig, AddConfig, LlmConfig, ChunkerConfig
|
||||
|
||||
# Example: set the log level for debugging
|
||||
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)
|
||||
naval_chat_bot.db.set_collection_name("naval_chat_bot")
|
||||
|
||||
# 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("https://www.youtube.com/watch?v=3qHkcs3kG44", AddConfig(chunker=chunker_config))
|
||||
# Example: Add your chunker config to an AddConfig to actually use it
|
||||
add_config = AddConfig(chunker=chunker_config)
|
||||
naval_chat_bot.add("https://www.youtube.com/watch?v=3qHkcs3kG44", config=add_config)
|
||||
|
||||
# Example: Reset to default
|
||||
add_config = AddConfig()
|
||||
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(("Who is Naval Ravikant?", "Naval Ravikant is an Indian-American entrepreneur and investor."), config=add_config)
|
||||
|
||||
query_config = QueryConfig()
|
||||
# Change the number of documents.
|
||||
query_config = LlmConfig(number_documents=5)
|
||||
print(naval_chat_bot.query("What unique capacity does Naval argue humans possess when it comes to understanding explanations or concepts?", config=query_config))
|
||||
```
|
||||
|
||||
@@ -44,11 +54,13 @@ print(naval_chat_bot.query("What unique capacity does Naval argue humans possess
|
||||
Here's the example of using custom prompt template with `.query`
|
||||
|
||||
```python
|
||||
from embedchain.config import QueryConfig
|
||||
from embedchain.embedchain import App
|
||||
from string import Template
|
||||
|
||||
import wikipedia
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import LlmConfig
|
||||
|
||||
einstein_chat_bot = App()
|
||||
|
||||
# Embed Wikipedia page
|
||||
@@ -56,7 +68,8 @@ page = wikipedia.page("Albert Einstein")
|
||||
einstein_chat_bot.add(page.content)
|
||||
|
||||
# Example: use your own custom template with `$context` and `$query`
|
||||
einstein_chat_template = Template("""
|
||||
einstein_chat_template = Template(
|
||||
"""
|
||||
You are Albert Einstein, a German-born theoretical physicist,
|
||||
widely ranked among the greatest and most influential scientists of all time.
|
||||
|
||||
@@ -67,17 +80,19 @@ einstein_chat_template = Template("""
|
||||
Keep the response brief. If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
|
||||
Human: $query
|
||||
Albert Einstein:""")
|
||||
query_config = QueryConfig(template=einstein_chat_template, system_prompt="You are Albert Einstein.")
|
||||
Albert Einstein:"""
|
||||
)
|
||||
# Example: Use the template, also add a system prompt.
|
||||
llm_config = LlmConfig(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?",
|
||||
"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, config=query_config)
|
||||
print("Query: ", query)
|
||||
print("Response: ", response)
|
||||
response = einstein_chat_bot.query(query, config=llm_config)
|
||||
print("Query: ", query)
|
||||
print("Response: ", response)
|
||||
|
||||
# Output
|
||||
# Query: Where did you complete your studies?
|
||||
|
||||
@@ -19,7 +19,7 @@ Otherwise, you will not know when, for instance, an invalid filepath is interpre
|
||||
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.
|
||||
Forcing can also be used for edge cases, such as interpreting a sitemap as a web_page, for reading its raw text instead of following links.
|
||||
|
||||
## Remote Data Types
|
||||
|
||||
@@ -73,6 +73,15 @@ app.add('https://example.com/content/intro.docx', data_type="docx")
|
||||
app.add('content/intro.docx', data_type="docx")
|
||||
```
|
||||
|
||||
### CSV file
|
||||
|
||||
To add any csv file, use the data_type as `csv`. `csv` allows remote urls and conventional file paths. Headers are included for each line, so if you have an `age` column, `18` will be added as `age: 18`. Eg:
|
||||
|
||||
```python
|
||||
app.add('https://example.com/content/sheet.csv', data_type="csv")
|
||||
app.add('content/sheet.csv', data_type="csv")
|
||||
```
|
||||
|
||||
### Code documentation website loader
|
||||
|
||||
To add any code documentation website as a loader, use the data_type as `docs_site`. Eg:
|
||||
@@ -138,4 +147,4 @@ print(naval_chat_bot.query("What unique capacity does Naval argue humans possess
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -19,7 +19,7 @@ print(naval_chat_bot.query("What unique capacity does Naval argue humans possess
|
||||
|
||||
### Chat Interface
|
||||
|
||||
- This interface is chat interface where it remembers previous conversation. Right now it remembers 5 conversation by default. 💬
|
||||
- This interface is a chat interface that remembers previous conversations. Right now it remembers 5 conversations by default. 💬
|
||||
|
||||
- To use this, call `.chat` function to get the answer for any query.
|
||||
|
||||
@@ -72,4 +72,4 @@ Counts the number of embeddings (chunks) in the database.
|
||||
```python
|
||||
print(app.count())
|
||||
# returns: 481
|
||||
```
|
||||
```
|
||||
|
||||
@@ -53,7 +53,7 @@ Default values of chunker config parameters for different `data_type`:
|
||||
|
||||
_coming soon_
|
||||
|
||||
## QueryConfig
|
||||
## LlmConfig
|
||||
|
||||
|option|description|type|default|
|
||||
|---|---|---|---|
|
||||
|
||||
@@ -8,6 +8,7 @@ Embedchain community has been super active in creating demos on top of Embedchai
|
||||
|
||||
### Open Source
|
||||
|
||||
- [My GSoC23 bot- Streamlit chat](https://github.com/lucifertrj/EmbedChain_GSoC23_BOT) by Tarun Jain
|
||||
- [Discord Bot for LLM chat](https://github.com/Reidond/discord_bots_playground/tree/c8b0c36541e4b393782ee506804c4b6962426dd6/python/chat-channel-bot) by Reidond
|
||||
- [EmbedChain-Streamlit-Docker App](https://github.com/amjadraza/embedchain-streamlit-app) by amjadraza
|
||||
- [Harry Potter Philosphers Stone Bot](https://github.com/vinayak-kempawad/Harry_Potter_Philosphers_Stone_Bot/) by Vinayak Kempawad, ([LinkedIn post](https://www.linkedin.com/feed/update/urn:li:activity:7080907532155686912/))
|
||||
@@ -38,6 +39,9 @@ Embedchain community has been super active in creating demos on top of Embedchai
|
||||
- [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
|
||||
|
||||
@@ -50,6 +54,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
|
||||
|
||||
@@ -65,6 +70,7 @@ Embedchain community has been super active in creating demos on top of Embedchai
|
||||
|
||||
## Videos
|
||||
|
||||
- [Embedchain in one shot](https://www.youtube.com/watch?v=vIhDh7H73Ww&t=82s) by AI with Tarun
|
||||
- [embedChain Create LLM powered bots over any dataset Python Demo Tesla Neurallink Chatbot Example](https://www.youtube.com/watch?v=bJqAn22a6Gc) by Rithesh Sreenivasan
|
||||
- [Embedchain - NEW 🔥 Langchain BABY to build LLM Bots](https://www.youtube.com/watch?v=qj_GNQ06I8o) by 1littlecoder
|
||||
- [EmbedChain -- NEW!: Build LLM-Powered Bots with Any Dataset](https://www.youtube.com/watch?v=XmaBezzGHu4) by DataInsightEdge
|
||||
@@ -79,9 +85,31 @@ Embedchain community has been super active in creating demos on top of Embedchai
|
||||
- [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)
|
||||
|
||||
@@ -31,4 +31,4 @@ config = CustomAppConfig(
|
||||
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.
|
||||
- `ElasticsearchDBConfig` accepts `es_url` as elasticsearch url or as list of nodes url with different hosts and ports. Additionally we can pass named parameters supported by Python Elasticsearch client.
|
||||
|
||||
@@ -8,40 +8,50 @@ title: '🤖 Discord Bot'
|
||||
- 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.
|
||||
- Now click on `Reset Token` and copy the token value. Set it as `DISCORD_BOT_TOKEN` in .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.
|
||||
- 🎉 The bot has been successfully added to your server. But it's still offline.
|
||||
|
||||
### 🐳 Docker Setup
|
||||
### Take the bot online
|
||||
|
||||
1. Install embedchain python package:
|
||||
|
||||
- To setup your discord bot using docker, run the following command inside this folder using your terminal.
|
||||
```bash
|
||||
docker-compose up --build
|
||||
pip install "embedchain[discord]"
|
||||
```
|
||||
|
||||
2. Launch your Discord bot:
|
||||
|
||||
|
||||
```bash
|
||||
python -m embedchain.bots.discord
|
||||
```
|
||||
|
||||
If you prefer to see the question and not only the answer, run it with
|
||||
|
||||
```bash
|
||||
python -m embedchain.bots.discord --include-question
|
||||
```
|
||||
📝 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:
|
||||
- You can add data sources to the bot using the slash command:
|
||||
```text
|
||||
/ec add <data_type> <url_or_text>
|
||||
/add <data_type> <url_or_text>
|
||||
```
|
||||
- You can ask your queries from the bot using the command:
|
||||
- You can ask your queries from the bot using the slash command:
|
||||
```text
|
||||
/ec query <question>
|
||||
/query <question>
|
||||
```
|
||||
📝 Note: To use the bot privately, you can message the bot directly by right clicking the bot and selecting `Message`.
|
||||
|
||||
|
||||
+29
-18
@@ -7,42 +7,53 @@ title: '🔮 Poe Bot'
|
||||
1. Install embedchain python package:
|
||||
|
||||
```bash
|
||||
pip install embedchain[poe]
|
||||
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
|
||||
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.
|
||||
8. You will need to set `OPENAI_API_KEY` for generating embeddings and using LLM. Copy your OpenAI API key from [here](https://platform.openai.com/account/api-keys) and paste it in `.env` as `OPENAI_API_KEY`.
|
||||
9. Now create your bot using the following code snippet.
|
||||
|
||||
```bash
|
||||
python -m embedchain.bots.poe
|
||||
# make sure that you have set OPENAI_API_KEY and POE_API_KEY in .env file
|
||||
from embedchain.bots import PoeBot
|
||||
|
||||
poe_bot = PoeBot()
|
||||
|
||||
# add as many data sources as you want
|
||||
poe_bot.add("https://en.wikipedia.org/wiki/Adam_D%27Angelo")
|
||||
poe_bot.add("https://www.youtube.com/watch?v=pJQVAqmKua8")
|
||||
|
||||
# start the bot
|
||||
# this start the poe bot server on port 8080 by default
|
||||
poe_bot.start()
|
||||
```
|
||||
|
||||
If you want to run the bot on another port, you can pass `--port option` like
|
||||
10. You can paste the above in a file called `your_script.py` and then simply do
|
||||
|
||||
```bash
|
||||
python -m embedchain.bots.poe --port 5000
|
||||
python your_script.py
|
||||
```
|
||||
|
||||
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.
|
||||
Now your bot will start running at port `8080` by default.
|
||||
|
||||
11. You can refer the [Supported Data formats](https://docs.embedchain.ai/advanced/data_types) section to refer the supported data types in embedchain.
|
||||
|
||||
12. Click `Run check` to make sure your machine can be reached.
|
||||
13. Make sure your bot is private if that's what you want.
|
||||
14. Click `Create bot` at the bottom to finally create the bot
|
||||
15. Now your 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:
|
||||
- To ask the bot questions, just type your query in the Poe interface:
|
||||
```text
|
||||
<your-question-here>
|
||||
```
|
||||
|
||||
- If you wish to add more data source to the bot, simply update your script and add as many `.add` as you like. You need to restart the server.
|
||||
|
||||
+12
-14
@@ -2,27 +2,25 @@
|
||||
title: '💼 Slack Bot'
|
||||
---
|
||||
|
||||
### 🖼️ Template Setup
|
||||
### 🖼️ 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`:
|
||||
1. Create a workspace on Slack if you don't have one already by clicking [here](https://slack.com/intl/en-in/).
|
||||
2. Create a new App on your Slack account by going [here](https://api.slack.com/apps).
|
||||
3. Select `From Scratch`, then enter the Bot Name and select your workspace.
|
||||
4. 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.
|
||||
5. 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`.
|
||||
6. Run your bot now with `python3 -m embedchain.bots.slack`
|
||||
7. Expose your bot to the internet. Default port is `5000`, which can be changed by adding `port --8080` to the startup command. 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.
|
||||
8. On the Slack API website go to `Event Subscriptions` on the left Sidebar and turn on `Enable Events`.
|
||||
9. In `Request URL`, enter your server or ngrok address.
|
||||
10. After it gets verified, click on `Subscribe to bot events`, add `message.channels` Bot User Event and click on `Save Changes`.
|
||||
11. Now go to your workspace, right click on the bot name in the sidebar, click `view app details`, then `add this app to a channel`.
|
||||
|
||||
### 🚀 Usage Instructions
|
||||
|
||||
|
||||
@@ -8,3 +8,5 @@ from embedchain.apps.Llama2App import Llama2App # noqa: F401
|
||||
from embedchain.apps.OpenSourceApp import OpenSourceApp # noqa: F401
|
||||
from embedchain.apps.PersonApp import (PersonApp, # noqa: F401
|
||||
PersonOpenSourceApp)
|
||||
from embedchain.vectordb.chroma_db import ChromaDB # noqa: F401
|
||||
from embedchain.vectordb.elasticsearch_db import ElasticsearchDB # noqa: F401
|
||||
|
||||
+38
-35
@@ -1,51 +1,54 @@
|
||||
import openai
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config import AppConfig, ChatConfig
|
||||
from embedchain.config import (AppConfig, BaseEmbedderConfig, BaseLlmConfig,
|
||||
ChromaDbConfig)
|
||||
from embedchain.embedchain import EmbedChain
|
||||
from embedchain.embedder.openai_embedder import OpenAiEmbedder
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.openai_llm import OpenAiLlm
|
||||
from embedchain.vectordb.chroma_db import ChromaDB
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class App(EmbedChain):
|
||||
"""
|
||||
The EmbedChain app.
|
||||
Has two functions: add and query.
|
||||
The EmbedChain app in it's simplest and most straightforward form.
|
||||
An opinionated choice of LLM, vector database and embedding model.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
Methods:
|
||||
add(source, data_type): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
dry_run(query): test your prompt without consuming tokens.
|
||||
chat(query): finds answer to the given query using vector database and LLM, with conversation history.
|
||||
"""
|
||||
|
||||
def __init__(self, config: AppConfig = None):
|
||||
def __init__(
|
||||
self,
|
||||
config: AppConfig = None,
|
||||
llm_config: BaseLlmConfig = None,
|
||||
chromadb_config: Optional[ChromaDbConfig] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
:param config: AppConfig instance to load as configuration. Optional.
|
||||
Initialize a new `CustomApp` instance. You only have a few choices to make.
|
||||
|
||||
:param config: Config for the app instance.
|
||||
This is the most basic configuration, that does not fall into the LLM, database or embedder category,
|
||||
defaults to None
|
||||
:type config: AppConfig, optional
|
||||
:param llm_config: Allows you to configure the LLM, e.g. how many documents to return,
|
||||
example: `from embedchain.config import LlmConfig`, defaults to None
|
||||
:type llm_config: BaseLlmConfig, optional
|
||||
:param chromadb_config: Allows you to configure the vector database,
|
||||
example: `from embedchain.config import ChromaDbConfig`, defaults to None
|
||||
:type chromadb_config: Optional[ChromaDbConfig], optional
|
||||
:param system_prompt: System prompt that will be provided to the LLM as such, defaults to None
|
||||
:type system_prompt: Optional[str], optional
|
||||
"""
|
||||
if config is None:
|
||||
config = AppConfig()
|
||||
|
||||
super().__init__(config)
|
||||
llm = OpenAiLlm(config=llm_config)
|
||||
embedder = OpenAiEmbedder(config=BaseEmbedderConfig(model="text-embedding-ada-002"))
|
||||
database = ChromaDB(config=chromadb_config)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig):
|
||||
messages = []
|
||||
if config.system_prompt:
|
||||
messages.append({"role": "system", "content": config.system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
response = openai.ChatCompletion.create(
|
||||
model=config.model or "gpt-3.5-turbo-0613",
|
||||
messages=messages,
|
||||
temperature=config.temperature,
|
||||
max_tokens=config.max_tokens,
|
||||
top_p=config.top_p,
|
||||
stream=config.stream,
|
||||
)
|
||||
|
||||
if config.stream:
|
||||
return self._stream_llm_model_response(response)
|
||||
else:
|
||||
return response["choices"][0]["message"]["content"]
|
||||
|
||||
def _stream_llm_model_response(self, response):
|
||||
"""
|
||||
This is a generator for streaming response from the OpenAI completions API
|
||||
"""
|
||||
for line in response:
|
||||
chunk = line["choices"][0].get("delta", {}).get("content", "")
|
||||
yield chunk
|
||||
super().__init__(config, llm, db=database, embedder=embedder, system_prompt=system_prompt)
|
||||
|
||||
+66
-139
@@ -1,156 +1,83 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from langchain.schema import BaseMessage
|
||||
|
||||
from embedchain.config import ChatConfig, CustomAppConfig
|
||||
from embedchain.config import CustomAppConfig
|
||||
from embedchain.embedchain import EmbedChain
|
||||
from embedchain.models import Providers
|
||||
from embedchain.embedder.base_embedder import BaseEmbedder
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class CustomApp(EmbedChain):
|
||||
"""
|
||||
The custom EmbedChain app.
|
||||
Has two functions: add and query.
|
||||
Embedchain's custom app allows for most flexibility.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
You can craft your own mix of various LLMs, vector databases and embedding model/functions.
|
||||
|
||||
Methods:
|
||||
add(source, data_type): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
dry_run(query): test your prompt without consuming tokens.
|
||||
chat(query): finds answer to the given query using vector database and LLM, with conversation history.
|
||||
"""
|
||||
|
||||
def __init__(self, config: CustomAppConfig = None):
|
||||
def __init__(
|
||||
self,
|
||||
config: Optional[CustomAppConfig] = None,
|
||||
llm: BaseLlm = None,
|
||||
db: BaseVectorDB = None,
|
||||
embedder: BaseEmbedder = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
:param config: Optional. `CustomAppConfig` instance to load as configuration.
|
||||
:raises ValueError: Config must be provided for custom app
|
||||
Initialize a new `CustomApp` instance. You have to choose a LLM, database and embedder.
|
||||
|
||||
:param config: Config for the app instance. This is the most basic configuration,
|
||||
that does not fall into the LLM, database or embedder category, defaults to None
|
||||
:type config: Optional[CustomAppConfig], optional
|
||||
:param llm: LLM Class instance. example: `from embedchain.llm.openai_llm import OpenAiLlm`, defaults to None
|
||||
:type llm: BaseLlm
|
||||
:param db: The database to use for storing and retrieving embeddings,
|
||||
example: `from embedchain.vectordb.chroma_db import ChromaDb`, defaults to None
|
||||
:type db: BaseVectorDB
|
||||
:param embedder: The embedder (embedding model and function) use to calculate embeddings.
|
||||
example: `from embedchain.embedder.gpt4all_embedder import GPT4AllEmbedder`, defaults to None
|
||||
:type embedder: BaseEmbedder
|
||||
:param system_prompt: System prompt that will be provided to the LLM as such, defaults to None
|
||||
:type system_prompt: Optional[str], optional
|
||||
:raises ValueError: LLM, database or embedder has not been defined.
|
||||
:raises TypeError: LLM, database or embedder is not a valid class instance.
|
||||
"""
|
||||
# Config is not required, it has a default
|
||||
if config is None:
|
||||
raise ValueError("Config must be provided for custom app")
|
||||
config = CustomAppConfig()
|
||||
|
||||
self.provider = config.provider
|
||||
if llm is None:
|
||||
raise ValueError("LLM must be provided for custom app. Please import from `embedchain.llm`.")
|
||||
if db is None:
|
||||
raise ValueError("Database must be provided for custom app. Please import from `embedchain.vectordb`.")
|
||||
if embedder is None:
|
||||
raise ValueError("Embedder must be provided for custom app. Please import from `embedchain.embedder`.")
|
||||
|
||||
if config.provider == Providers.GPT4ALL:
|
||||
from embedchain import OpenSourceApp
|
||||
|
||||
# Because these models run locally, they should have an instance running when the custom app is created
|
||||
self.open_source_app = OpenSourceApp(config=config.open_source_app_config)
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
def set_llm_model(self, provider: Providers):
|
||||
self.provider = provider
|
||||
if provider == Providers.GPT4ALL:
|
||||
raise ValueError(
|
||||
"GPT4ALL needs to be instantiated with the model known, please create a new app instance instead"
|
||||
if not isinstance(config, CustomAppConfig):
|
||||
raise TypeError(
|
||||
"Config is not a `CustomAppConfig` instance. "
|
||||
"Please make sure the type is right and that you are passing an instance."
|
||||
)
|
||||
if not isinstance(llm, BaseLlm):
|
||||
raise TypeError(
|
||||
"LLM is not a `BaseLlm` instance. "
|
||||
"Please make sure the type is right and that you are passing an instance."
|
||||
)
|
||||
if not isinstance(db, BaseVectorDB):
|
||||
raise TypeError(
|
||||
"Database is not a `BaseVectorDB` instance. "
|
||||
"Please make sure the type is right and that you are passing an instance."
|
||||
)
|
||||
if not isinstance(embedder, BaseEmbedder):
|
||||
raise TypeError(
|
||||
"Embedder is not a `BaseEmbedder` instance. "
|
||||
"Please make sure the type is right and that you are passing an instance."
|
||||
)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig):
|
||||
# TODO: Quitting the streaming response here for now.
|
||||
# Idea: https://gist.github.com/jvelezmagic/03ddf4c452d011aae36b2a0f73d72f68
|
||||
if config.stream:
|
||||
raise NotImplementedError(
|
||||
"Streaming responses have not been implemented for this model yet. Please disable."
|
||||
)
|
||||
|
||||
try:
|
||||
if self.provider == Providers.OPENAI:
|
||||
return CustomApp._get_openai_answer(prompt, config)
|
||||
|
||||
if self.provider == Providers.ANTHROPHIC:
|
||||
return CustomApp._get_athrophic_answer(prompt, config)
|
||||
|
||||
if self.provider == Providers.VERTEX_AI:
|
||||
return CustomApp._get_vertex_answer(prompt, config)
|
||||
|
||||
if self.provider == Providers.GPT4ALL:
|
||||
return self.open_source_app._get_gpt4all_answer(prompt, config)
|
||||
|
||||
if self.provider == Providers.AZURE_OPENAI:
|
||||
return CustomApp._get_azure_openai_answer(prompt, config)
|
||||
|
||||
except ImportError as e:
|
||||
raise ModuleNotFoundError(e.msg) from None
|
||||
|
||||
@staticmethod
|
||||
def _get_openai_answer(prompt: str, config: ChatConfig) -> str:
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
|
||||
chat = ChatOpenAI(
|
||||
temperature=config.temperature,
|
||||
model=config.model or "gpt-3.5-turbo",
|
||||
max_tokens=config.max_tokens,
|
||||
streaming=config.stream,
|
||||
)
|
||||
|
||||
if config.top_p and config.top_p != 1:
|
||||
logging.warning("Config option `top_p` is not supported by this model.")
|
||||
|
||||
messages = CustomApp._get_messages(prompt, system_prompt=config.system_prompt)
|
||||
|
||||
return chat(messages).content
|
||||
|
||||
@staticmethod
|
||||
def _get_athrophic_answer(prompt: str, config: ChatConfig) -> str:
|
||||
from langchain.chat_models import ChatAnthropic
|
||||
|
||||
chat = ChatAnthropic(temperature=config.temperature, model=config.model)
|
||||
|
||||
if config.max_tokens and config.max_tokens != 1000:
|
||||
logging.warning("Config option `max_tokens` is not supported by this model.")
|
||||
|
||||
messages = CustomApp._get_messages(prompt, system_prompt=config.system_prompt)
|
||||
|
||||
return chat(messages).content
|
||||
|
||||
@staticmethod
|
||||
def _get_vertex_answer(prompt: str, config: ChatConfig) -> str:
|
||||
from langchain.chat_models import ChatVertexAI
|
||||
|
||||
chat = ChatVertexAI(temperature=config.temperature, model=config.model, max_output_tokens=config.max_tokens)
|
||||
|
||||
if config.top_p and config.top_p != 1:
|
||||
logging.warning("Config option `top_p` is not supported by this model.")
|
||||
|
||||
messages = CustomApp._get_messages(prompt, system_prompt=config.system_prompt)
|
||||
|
||||
return chat(messages).content
|
||||
|
||||
@staticmethod
|
||||
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=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,
|
||||
)
|
||||
|
||||
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, system_prompt=config.system_prompt)
|
||||
|
||||
return chat(messages).content
|
||||
|
||||
@staticmethod
|
||||
def _get_messages(prompt: str, system_prompt: Optional[str] = None) -> List[BaseMessage]:
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append(SystemMessage(content=system_prompt))
|
||||
messages.append(HumanMessage(content=prompt))
|
||||
return messages
|
||||
|
||||
def _stream_llm_model_response(self, response):
|
||||
"""
|
||||
This is a generator for streaming response from the OpenAI completions API
|
||||
"""
|
||||
for line in response:
|
||||
chunk = line["choices"][0].get("delta", {}).get("content", "")
|
||||
yield chunk
|
||||
super().__init__(config=config, llm=llm, db=db, embedder=embedder, system_prompt=system_prompt)
|
||||
|
||||
@@ -1,38 +1,33 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from langchain.llms import Replicate
|
||||
|
||||
from embedchain.config import AppConfig, ChatConfig
|
||||
from embedchain.embedchain import EmbedChain
|
||||
from embedchain.apps.CustomApp import CustomApp
|
||||
from embedchain.config import CustomAppConfig
|
||||
from embedchain.embedder.openai_embedder import OpenAiEmbedder
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.llama2_llm import Llama2Llm
|
||||
from embedchain.vectordb.chroma_db import ChromaDB
|
||||
|
||||
|
||||
class Llama2App(EmbedChain):
|
||||
@register_deserializable
|
||||
class Llama2App(CustomApp):
|
||||
"""
|
||||
The EmbedChain Llama2App class.
|
||||
Has two functions: add and query.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
Methods:
|
||||
add(source, data_type): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
chat(query): finds answer to the given query using vector database and LLM, with conversation history.
|
||||
"""
|
||||
|
||||
def __init__(self, config: AppConfig = None):
|
||||
def __init__(self, config: CustomAppConfig = None, system_prompt: Optional[str] = None):
|
||||
"""
|
||||
:param config: AppConfig instance to load as configuration. Optional.
|
||||
:param config: CustomAppConfig 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.")
|
||||
|
||||
if config is None:
|
||||
config = AppConfig()
|
||||
config = CustomAppConfig()
|
||||
|
||||
super().__init__(config)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig = None):
|
||||
# TODO: Move the model and other inputs into config
|
||||
if 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},
|
||||
super().__init__(
|
||||
config=config, llm=Llama2Llm(), db=ChromaDB(), embedder=OpenAiEmbedder(), system_prompt=system_prompt
|
||||
)
|
||||
return llm(prompt)
|
||||
|
||||
@@ -1,68 +1,78 @@
|
||||
import logging
|
||||
from typing import Iterable, Union
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config import ChatConfig, OpenSourceAppConfig
|
||||
from embedchain.config import (BaseEmbedderConfig, BaseLlmConfig,
|
||||
ChromaDbConfig, OpenSourceAppConfig)
|
||||
from embedchain.embedchain import EmbedChain
|
||||
from embedchain.embedder.gpt4all_embedder import GPT4AllEmbedder
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.gpt4all_llm import GPT4ALLLlm
|
||||
from embedchain.vectordb.chroma_db import ChromaDB
|
||||
|
||||
gpt4all_model = None
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class OpenSourceApp(EmbedChain):
|
||||
"""
|
||||
The OpenSource app.
|
||||
Same as App, but uses an open source embedding model and LLM.
|
||||
The embedchain Open Source App.
|
||||
Comes preconfigured with the best open source LLM, embedding model, database.
|
||||
|
||||
Has two function: add and query.
|
||||
|
||||
adds(data_type, url): adds the data from the given URL to the vector db.
|
||||
Methods:
|
||||
add(source, data_type): adds the data from the given URL to the vector db.
|
||||
query(query): finds answer to the given query using vector database and LLM.
|
||||
chat(query): finds answer to the given query using vector database and LLM, with conversation history.
|
||||
"""
|
||||
|
||||
def __init__(self, config: OpenSourceAppConfig = None):
|
||||
def __init__(
|
||||
self,
|
||||
config: OpenSourceAppConfig = None,
|
||||
llm_config: BaseLlmConfig = None,
|
||||
chromadb_config: Optional[ChromaDbConfig] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
:param config: OpenSourceAppConfig instance to load as configuration. Optional.
|
||||
`ef` defaults to open source.
|
||||
Initialize a new `CustomApp` instance.
|
||||
Since it's opinionated you don't have to choose a LLM, database and embedder.
|
||||
However, you can configure those.
|
||||
|
||||
:param config: Config for the app instance. This is the most basic configuration,
|
||||
that does not fall into the LLM, database or embedder category, defaults to None
|
||||
:type config: OpenSourceAppConfig, optional
|
||||
:param llm_config: Allows you to configure the LLM, e.g. how many documents to return.
|
||||
example: `from embedchain.config import LlmConfig`, defaults to None
|
||||
:type llm_config: BaseLlmConfig, optional
|
||||
:param chromadb_config: Allows you to configure the open source database,
|
||||
example: `from embedchain.config import ChromaDbConfig`, defaults to None
|
||||
:type chromadb_config: Optional[ChromaDbConfig], optional
|
||||
:param system_prompt: System prompt that will be provided to the LLM as such.
|
||||
Please don't use for the time being, as it's not supported., defaults to None
|
||||
:type system_prompt: Optional[str], optional
|
||||
:raises TypeError: `OpenSourceAppConfig` or `LlmConfig` invalid.
|
||||
"""
|
||||
logging.info("Loading open source embedding model. This may take some time...") # noqa:E501
|
||||
if not config:
|
||||
config = OpenSourceAppConfig()
|
||||
|
||||
if not config.model:
|
||||
raise ValueError("OpenSourceApp needs a model to be instantiated. Maybe you passed the wrong config type?")
|
||||
|
||||
self.instance = OpenSourceApp._get_instance(config.model)
|
||||
|
||||
logging.info("Successfully loaded open source embedding model.")
|
||||
super().__init__(config)
|
||||
|
||||
def get_llm_model_answer(self, prompt, config: ChatConfig):
|
||||
return self._get_gpt4all_answer(prompt=prompt, config=config)
|
||||
|
||||
@staticmethod
|
||||
def _get_instance(model):
|
||||
try:
|
||||
from gpt4all import GPT4All
|
||||
except ModuleNotFoundError:
|
||||
raise ModuleNotFoundError(
|
||||
"The GPT4All python package is not installed. Please install it with `pip install embedchain[opensource]`" # noqa E501
|
||||
) from None
|
||||
|
||||
return GPT4All(model)
|
||||
|
||||
def _get_gpt4all_answer(self, prompt: str, config: ChatConfig) -> Union[str, Iterable]:
|
||||
if config.model and config.model != self.config.model:
|
||||
raise RuntimeError(
|
||||
"OpenSourceApp does not support switching models at runtime. Please create a new app instance."
|
||||
if not isinstance(config, OpenSourceAppConfig):
|
||||
raise TypeError(
|
||||
"OpenSourceApp needs a OpenSourceAppConfig passed to it. "
|
||||
"You can import it with `from embedchain.config import OpenSourceAppConfig`"
|
||||
)
|
||||
|
||||
if config.system_prompt:
|
||||
raise ValueError("OpenSourceApp does not support `system_prompt`")
|
||||
if not llm_config:
|
||||
llm_config = BaseLlmConfig(model="orca-mini-3b.ggmlv3.q4_0.bin")
|
||||
elif not isinstance(llm_config, BaseLlmConfig):
|
||||
raise TypeError(
|
||||
"The LlmConfig passed to OpenSourceApp is invalid. "
|
||||
"You can import it with `from embedchain.config import LlmConfig`"
|
||||
)
|
||||
elif not llm_config.model:
|
||||
llm_config.model = "orca-mini-3b.ggmlv3.q4_0.bin"
|
||||
|
||||
response = self.instance.generate(
|
||||
prompt=prompt,
|
||||
streaming=config.stream,
|
||||
top_p=config.top_p,
|
||||
max_tokens=config.max_tokens,
|
||||
temp=config.temperature,
|
||||
)
|
||||
return response
|
||||
llm = GPT4ALLLlm(config=llm_config)
|
||||
embedder = GPT4AllEmbedder(config=BaseEmbedderConfig(model="all-MiniLM-L6-v2"))
|
||||
logging.error("Successfully loaded open source embedding model.")
|
||||
database = ChromaDB(config=chromadb_config)
|
||||
|
||||
super().__init__(config, llm=llm, db=database, embedder=embedder, system_prompt=system_prompt)
|
||||
|
||||
@@ -2,12 +2,14 @@ from string import Template
|
||||
|
||||
from embedchain.apps.App import App
|
||||
from embedchain.apps.OpenSourceApp import OpenSourceApp
|
||||
from embedchain.config import ChatConfig, QueryConfig
|
||||
from embedchain.config import BaseLlmConfig
|
||||
from embedchain.config.apps.BaseAppConfig import BaseAppConfig
|
||||
from embedchain.config.QueryConfig import (DEFAULT_PROMPT,
|
||||
DEFAULT_PROMPT_WITH_HISTORY)
|
||||
from embedchain.config.llm.base_llm_config import (DEFAULT_PROMPT,
|
||||
DEFAULT_PROMPT_WITH_HISTORY)
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class EmbedChainPersonApp:
|
||||
"""
|
||||
Base class to create a person bot.
|
||||
@@ -17,20 +19,30 @@ class EmbedChainPersonApp:
|
||||
:param config: BaseAppConfig instance to load as configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, person, config: BaseAppConfig = None):
|
||||
def __init__(self, person: str, config: BaseAppConfig = None):
|
||||
"""Initialize a new person app
|
||||
|
||||
:param person: Name of the person that's imitated.
|
||||
:type person: str
|
||||
:param config: Configuration class instance, defaults to None
|
||||
:type config: BaseAppConfig, optional
|
||||
"""
|
||||
self.person = person
|
||||
self.person_prompt = f"You are {person}. Whatever you say, you will always say in {person} style." # noqa:E501
|
||||
super().__init__(config)
|
||||
|
||||
def add_person_template_to_config(self, default_prompt: str, config: ChatConfig = None):
|
||||
def add_person_template_to_config(self, default_prompt: str, config: BaseLlmConfig = None):
|
||||
"""
|
||||
This method checks if the config object contains a prompt template
|
||||
if yes it adds the person prompt to it and return the updated config
|
||||
else it creates a config object with the default prompt added to the person prompt
|
||||
|
||||
:param default_prompt: it is the default prompt for query or chat methods
|
||||
:param config: Optional. The `ChatConfig` instance to use as
|
||||
configuration options.
|
||||
:param default_prompt: it is the default prompt for query or chat methods
|
||||
:type default_prompt: str
|
||||
:param config: _description_, defaults to None
|
||||
:type config: BaseLlmConfig, optional
|
||||
:return: The `ChatConfig` instance to use as configuration options.
|
||||
:rtype: _type_
|
||||
"""
|
||||
template = Template(self.person_prompt + " " + default_prompt)
|
||||
|
||||
@@ -43,38 +55,40 @@ class EmbedChainPersonApp:
|
||||
config.template = template
|
||||
else:
|
||||
# if no config is present at all, initialize the config with person prompt and default template
|
||||
config = QueryConfig(
|
||||
config = BaseLlmConfig(
|
||||
template=template,
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class PersonApp(EmbedChainPersonApp, App):
|
||||
"""
|
||||
The Person app.
|
||||
Extends functionality from EmbedChainPersonApp and App
|
||||
"""
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None, dry_run=False):
|
||||
config = self.add_person_template_to_config(DEFAULT_PROMPT, config)
|
||||
return super().query(input_query, config, dry_run)
|
||||
def query(self, input_query, config: BaseLlmConfig = None, dry_run=False):
|
||||
config = self.add_person_template_to_config(DEFAULT_PROMPT, config, where=None)
|
||||
return super().query(input_query, config, dry_run, where=None)
|
||||
|
||||
def chat(self, input_query, config: ChatConfig = None, dry_run=False):
|
||||
def chat(self, input_query, config: BaseLlmConfig = None, dry_run=False, where=None):
|
||||
config = self.add_person_template_to_config(DEFAULT_PROMPT_WITH_HISTORY, config)
|
||||
return super().chat(input_query, config, dry_run)
|
||||
return super().chat(input_query, config, dry_run, where)
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class PersonOpenSourceApp(EmbedChainPersonApp, OpenSourceApp):
|
||||
"""
|
||||
The Person app.
|
||||
Extends functionality from EmbedChainPersonApp and OpenSourceApp
|
||||
"""
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None, dry_run=False):
|
||||
def query(self, input_query, config: BaseLlmConfig = None, dry_run=False):
|
||||
config = self.add_person_template_to_config(DEFAULT_PROMPT, config)
|
||||
return super().query(input_query, config, dry_run)
|
||||
|
||||
def chat(self, input_query, config: ChatConfig = None, dry_run=False):
|
||||
def chat(self, input_query, config: BaseLlmConfig = None, dry_run=False):
|
||||
config = self.add_person_template_to_config(DEFAULT_PROMPT_WITH_HISTORY, config)
|
||||
return super().chat(input_query, config, dry_run)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from embedchain.bots.poe import PoeBot # noqa: F401
|
||||
from embedchain.bots.whatsapp import WhatsAppBot # noqa: F401
|
||||
|
||||
# TODO: fix discord import
|
||||
# from embedchain.bots.discord import DiscordBot
|
||||
|
||||
+34
-13
@@ -1,23 +1,44 @@
|
||||
from typing import Any
|
||||
|
||||
from embedchain import CustomApp
|
||||
from embedchain.config import AddConfig, CustomAppConfig, QueryConfig
|
||||
from embedchain.models import EmbeddingFunctions, Providers
|
||||
from embedchain.config import AddConfig, CustomAppConfig, LlmConfig
|
||||
from embedchain.embedder.openai_embedder import OpenAiEmbedder
|
||||
from embedchain.helper_classes.json_serializable import (
|
||||
JSONSerializable, register_deserializable)
|
||||
from embedchain.llm.openai_llm import OpenAiLlm
|
||||
from embedchain.vectordb.chroma_db import ChromaDB
|
||||
|
||||
|
||||
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)
|
||||
@register_deserializable
|
||||
class BaseBot(JSONSerializable):
|
||||
def __init__(self):
|
||||
self.app = CustomApp(config=CustomAppConfig(), llm=OpenAiLlm(), db=ChromaDB(), embedder=OpenAiEmbedder())
|
||||
|
||||
def add(self, data, config: AddConfig = None):
|
||||
"""Add data to the bot"""
|
||||
def add(self, data: Any, config: AddConfig = None):
|
||||
"""
|
||||
Add data to the bot (to the vector database).
|
||||
Auto-dectects type only, so some data types might not be usable.
|
||||
|
||||
:param data: data to embed
|
||||
:type data: Any
|
||||
:param config: configuration class instance, defaults to None
|
||||
:type config: AddConfig, optional
|
||||
"""
|
||||
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()
|
||||
def query(self, query: str, config: LlmConfig = None) -> str:
|
||||
"""
|
||||
Query the bot
|
||||
|
||||
:param query: the user query
|
||||
:type query: str
|
||||
:param config: configuration class instance, defaults to None
|
||||
:type config: LlmConfig, optional
|
||||
:return: Answer
|
||||
:rtype: str
|
||||
"""
|
||||
config = config
|
||||
return self.app.query(query, config=config)
|
||||
|
||||
def start(self):
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
from .base import BaseBot
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.message_content = True
|
||||
client = discord.Client(intents=intents)
|
||||
tree = app_commands.CommandTree(client)
|
||||
|
||||
# Invite link example
|
||||
# https://discord.com/api/oauth2/authorize?client_id={DISCORD_CLIENT_ID}&permissions=2048&scope=bot
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class DiscordBot(BaseBot):
|
||||
def __init__(self, *args, **kwargs):
|
||||
BaseBot.__init__(self, *args, **kwargs)
|
||||
|
||||
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):
|
||||
client.run(os.environ["DISCORD_BOT_TOKEN"])
|
||||
|
||||
|
||||
# @tree decorator cannot be used in a class. A global discord_bot is used as a workaround.
|
||||
|
||||
|
||||
@tree.command(name="question", description="ask embedchain")
|
||||
async def query_command(interaction: discord.Interaction, question: str):
|
||||
await interaction.response.defer()
|
||||
member = client.guilds[0].get_member(client.user.id)
|
||||
logging.info(f"User: {member}, Query: {question}")
|
||||
try:
|
||||
answer = discord_bot.ask_bot(question)
|
||||
if args.include_question:
|
||||
response = f"> {question}\n\n{answer}"
|
||||
else:
|
||||
response = answer
|
||||
await interaction.followup.send(response)
|
||||
except Exception as e:
|
||||
await interaction.followup.send("An error occurred. Please try again!")
|
||||
logging.error("Error occurred during 'query' command:", e)
|
||||
|
||||
|
||||
@tree.command(name="add", description="add new content to the embedchain database")
|
||||
async def add_command(interaction: discord.Interaction, url_or_text: str):
|
||||
await interaction.response.defer()
|
||||
member = client.guilds[0].get_member(client.user.id)
|
||||
logging.info(f"User: {member}, Add: {url_or_text}")
|
||||
try:
|
||||
response = discord_bot.add_data(url_or_text)
|
||||
await interaction.followup.send(response)
|
||||
except Exception as e:
|
||||
await interaction.followup.send("An error occurred. Please try again!")
|
||||
logging.error("Error occurred during 'add' command:", e)
|
||||
|
||||
|
||||
@tree.command(name="ping", description="Simple ping pong command")
|
||||
async def ping(interaction: discord.Interaction):
|
||||
await interaction.response.send_message("Pong", ephemeral=True)
|
||||
|
||||
|
||||
@tree.error
|
||||
async def on_app_command_error(interaction: discord.Interaction, error: discord.app_commands.AppCommandError) -> None:
|
||||
if isinstance(error, commands.CommandNotFound):
|
||||
await interaction.followup.send("Invalid command. Please refer to the documentation for correct syntax.")
|
||||
else:
|
||||
logging.error("Error occurred during command execution:", error)
|
||||
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
# TODO: Sync in admin command, to not hit rate limits.
|
||||
# This might be overkill for most users, and it would require to set a guild or user id, where sync is allowed.
|
||||
await tree.sync()
|
||||
logging.debug("Command tree synced")
|
||||
logging.info(f"Logged in as {client.user.name}")
|
||||
|
||||
|
||||
def start_command():
|
||||
parser = argparse.ArgumentParser(description="EmbedChain DiscordBot command line interface")
|
||||
parser.add_argument(
|
||||
"--include-question",
|
||||
help="include question in query reply, otherwise it is hidden behind the slash command.",
|
||||
action="store_true",
|
||||
)
|
||||
global args
|
||||
args = parser.parse_args()
|
||||
|
||||
global discord_bot
|
||||
discord_bot = DiscordBot()
|
||||
discord_bot.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_command()
|
||||
+51
-48
@@ -5,57 +5,11 @@ from typing import List, Optional
|
||||
|
||||
from fastapi_poe import PoeBot, run
|
||||
|
||||
from embedchain.config import QueryConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
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")
|
||||
@@ -72,7 +26,56 @@ def start_command():
|
||||
# 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"))
|
||||
run(PoeBot(), api_key=args.api_key or os.environ.get("POE_API_KEY"))
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class PoeBot(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}")
|
||||
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:
|
||||
self.app.llm.set_history(history=history)
|
||||
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):
|
||||
start_command()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
from .base import BaseBot
|
||||
|
||||
try:
|
||||
from flask import Flask, request
|
||||
from slack_sdk import WebClient
|
||||
except ModuleNotFoundError:
|
||||
raise ModuleNotFoundError(
|
||||
"The required dependencies for Slack are not installed." 'Please install with `pip install "embedchain[slack]"`'
|
||||
) from None
|
||||
|
||||
|
||||
SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class SlackBot(BaseBot):
|
||||
def __init__(self):
|
||||
self.client = WebClient(token=SLACK_BOT_TOKEN)
|
||||
self.chat_bot = App()
|
||||
self.recent_message = {"ts": 0, "channel": ""}
|
||||
super().__init__()
|
||||
|
||||
def handle_message(self, event_data):
|
||||
message = event_data.get("event")
|
||||
if message and "text" in message and message.get("subtype") != "bot_message":
|
||||
text: str = message["text"]
|
||||
if float(message.get("ts")) > float(self.recent_message["ts"]):
|
||||
self.recent_message["ts"] = message["ts"]
|
||||
self.recent_message["channel"] = message["channel"]
|
||||
if text.startswith("query"):
|
||||
_, question = text.split(" ", 1)
|
||||
try:
|
||||
response = self.chat_bot.chat(question)
|
||||
self.send_slack_message(message["channel"], response)
|
||||
logging.info("Query answered successfully!")
|
||||
except Exception as e:
|
||||
self.send_slack_message(message["channel"], "An error occurred. Please try again!")
|
||||
logging.error("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:
|
||||
self.chat_bot.add(url_or_text, data_type)
|
||||
self.send_slack_message(message["channel"], f"Added {data_type} : {url_or_text}")
|
||||
except ValueError as e:
|
||||
self.send_slack_message(message["channel"], f"Error: {str(e)}")
|
||||
logging.error("Error occurred during 'add' command:", e)
|
||||
except Exception as e:
|
||||
self.send_slack_message(message["channel"], f"Failed to add {data_type} : {url_or_text}")
|
||||
logging.error("Error occurred during 'add' command:", e)
|
||||
|
||||
def send_slack_message(self, channel, message):
|
||||
response = self.client.chat_postMessage(channel=channel, text=message)
|
||||
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 SlackBot...")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
@app.route("/", methods=["POST"])
|
||||
def chat():
|
||||
# Check if the request is a verification request
|
||||
if request.json.get("challenge"):
|
||||
return str(request.json.get("challenge"))
|
||||
|
||||
response = self.handle_message(request.json)
|
||||
return str(response)
|
||||
|
||||
app.run(host=host, port=port, debug=debug)
|
||||
|
||||
|
||||
def start_command():
|
||||
parser = argparse.ArgumentParser(description="EmbedChain SlackBot 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()
|
||||
|
||||
slack_bot = SlackBot()
|
||||
slack_bot.start(host=args.host, port=args.port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_command()
|
||||
@@ -1,16 +1,25 @@
|
||||
import argparse
|
||||
import importlib
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from flask import Flask, request
|
||||
from twilio.twiml.messaging_response import MessagingResponse
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
from .base import BaseBot
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class WhatsAppBot(BaseBot):
|
||||
def __init__(self):
|
||||
try:
|
||||
self.flask = importlib.import_module("flask")
|
||||
self.twilio = importlib.import_module("twilio")
|
||||
except ModuleNotFoundError:
|
||||
raise ModuleNotFoundError(
|
||||
"The required dependencies for WhatsApp are not installed. "
|
||||
'Please install with `pip install --upgrade "embedchain[whatsapp]"`'
|
||||
) from None
|
||||
super().__init__()
|
||||
|
||||
def handle_message(self, message):
|
||||
@@ -39,7 +48,7 @@ class WhatsAppBot(BaseBot):
|
||||
return response
|
||||
|
||||
def start(self, host="0.0.0.0", port=5000, debug=True):
|
||||
app = Flask(__name__)
|
||||
app = self.flask.Flask(__name__)
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
logging.info("\nGracefully shutting down the WhatsAppBot...")
|
||||
@@ -49,9 +58,9 @@ class WhatsAppBot(BaseBot):
|
||||
|
||||
@app.route("/chat", methods=["POST"])
|
||||
def chat():
|
||||
incoming_message = request.values.get("Body", "").lower()
|
||||
incoming_message = self.flask.request.values.get("Body", "").lower()
|
||||
response = self.handle_message(incoming_message)
|
||||
twilio_response = MessagingResponse()
|
||||
twilio_response = self.twilio.twiml.messaging_response.MessagingResponse()
|
||||
twilio_response.message(response)
|
||||
return str(twilio_response)
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import hashlib
|
||||
|
||||
from embedchain.helper_classes.json_serializable import JSONSerializable
|
||||
from embedchain.models.data_type import DataType
|
||||
|
||||
|
||||
class BaseChunker:
|
||||
class BaseChunker(JSONSerializable):
|
||||
def __init__(self, text_splitter):
|
||||
"""Initialize the chunker."""
|
||||
self.text_splitter = text_splitter
|
||||
|
||||
@@ -4,8 +4,10 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class DocsSiteChunker(BaseChunker):
|
||||
"""Chunker for code docs site."""
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class DocxFileChunker(BaseChunker):
|
||||
"""Chunker for .docx file."""
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class NotionChunker(BaseChunker):
|
||||
"""Chunker for notion."""
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class PdfFileChunker(BaseChunker):
|
||||
"""Chunker for PDF file."""
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class QnaPairChunker(BaseChunker):
|
||||
"""Chunker for QnA pair."""
|
||||
|
||||
|
||||
@@ -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 TableChunker(BaseChunker):
|
||||
"""Chunker for tables, for instance csv, google sheets or databases."""
|
||||
|
||||
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)
|
||||
@@ -4,8 +4,10 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class TextChunker(BaseChunker):
|
||||
"""Chunker for text."""
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class WebPageChunker(BaseChunker):
|
||||
"""Chunker for web page."""
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
from embedchain.config.AddConfig import ChunkerConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class YoutubeVideoChunker(BaseChunker):
|
||||
"""Chunker for Youtube video."""
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from typing import Callable, Optional
|
||||
|
||||
from embedchain.config.BaseConfig import BaseConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class ChunkerConfig(BaseConfig):
|
||||
"""
|
||||
Config for the chunker used in `add` method
|
||||
@@ -19,6 +21,7 @@ class ChunkerConfig(BaseConfig):
|
||||
self.length_function = length_function if length_function else len
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class LoaderConfig(BaseConfig):
|
||||
"""
|
||||
Config for the chunker used in `add` method
|
||||
@@ -28,6 +31,7 @@ class LoaderConfig(BaseConfig):
|
||||
pass
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class AddConfig(BaseConfig):
|
||||
"""
|
||||
Config for the `add` method.
|
||||
@@ -38,5 +42,13 @@ class AddConfig(BaseConfig):
|
||||
chunker: Optional[ChunkerConfig] = None,
|
||||
loader: Optional[LoaderConfig] = None,
|
||||
):
|
||||
"""
|
||||
Initializes a configuration class instance for the `add` method.
|
||||
|
||||
:param chunker: Chunker config, defaults to None
|
||||
:type chunker: Optional[ChunkerConfig], optional
|
||||
:param loader: Loader config, defaults to None
|
||||
:type loader: Optional[LoaderConfig], optional
|
||||
"""
|
||||
self.loader = loader
|
||||
self.chunker = chunker
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
class BaseConfig:
|
||||
from typing import Any, Dict
|
||||
|
||||
from embedchain.helper_classes.json_serializable import JSONSerializable
|
||||
|
||||
|
||||
class BaseConfig(JSONSerializable):
|
||||
"""
|
||||
Base config.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes a configuration class for a class."""
|
||||
pass
|
||||
|
||||
def as_dict(self):
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
"""Return config object as a dict
|
||||
|
||||
:return: config object as dict
|
||||
:rtype: Dict[str, Any]
|
||||
"""
|
||||
return vars(self)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
from string import Template
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config.QueryConfig import QueryConfig
|
||||
|
||||
DEFAULT_PROMPT = """
|
||||
You are a chatbot having a conversation with a human. You are given chat
|
||||
history and context.
|
||||
You need to answer the query considering context, chat history and your knowledge base. If you don't know the answer or the answer is neither contained in the context nor in history, then simply say "I don't know".
|
||||
|
||||
$context
|
||||
|
||||
History: $history
|
||||
|
||||
Query: $query
|
||||
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DEFAULT_PROMPT_TEMPLATE = Template(DEFAULT_PROMPT)
|
||||
|
||||
|
||||
class ChatConfig(QueryConfig):
|
||||
"""
|
||||
Config for the `chat` method, inherits from `QueryConfig`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
number_documents=None,
|
||||
template: Template = None,
|
||||
model=None,
|
||||
temperature=None,
|
||||
max_tokens=None,
|
||||
top_p=None,
|
||||
stream: bool = False,
|
||||
deployment_name=None,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initializes the ChatConfig instance.
|
||||
|
||||
:param number_documents: Number of documents to pull from the database as
|
||||
context.
|
||||
:param template: Optional. The `Template` instance to use as a template for
|
||||
prompt.
|
||||
:param model: Optional. Controls the OpenAI model used.
|
||||
:param temperature: Optional. Controls the randomness of the model's output.
|
||||
Higher values (closer to 1) make output more random,lower values make it more
|
||||
deterministic.
|
||||
:param max_tokens: Optional. Controls how many tokens are generated.
|
||||
:param top_p: Optional. Controls the diversity of words.Higher values
|
||||
(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
|
||||
"""
|
||||
if template is None:
|
||||
template = DEFAULT_PROMPT_TEMPLATE
|
||||
|
||||
# History is set as 0 to ensure that there is always a history, that way,
|
||||
# there don't have to be two templates. Having two templates would make it
|
||||
# complicated because the history is not user controlled.
|
||||
super().__init__(
|
||||
number_documents=number_documents,
|
||||
template=template,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
top_p=top_p,
|
||||
history=[0],
|
||||
stream=stream,
|
||||
deployment_name=deployment_name,
|
||||
system_prompt=system_prompt,
|
||||
)
|
||||
|
||||
def set_history(self, history):
|
||||
"""
|
||||
Chat history is not user provided and not set at initialization time
|
||||
|
||||
:param history: (string) history to set
|
||||
"""
|
||||
self.history = history
|
||||
return
|
||||
@@ -1,143 +0,0 @@
|
||||
import re
|
||||
from string import Template
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config.BaseConfig import BaseConfig
|
||||
|
||||
DEFAULT_PROMPT = """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
|
||||
$context
|
||||
|
||||
Query: $query
|
||||
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DEFAULT_PROMPT_WITH_HISTORY = """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
I will provide you with our conversation history.
|
||||
|
||||
$context
|
||||
|
||||
History: $history
|
||||
|
||||
Query: $query
|
||||
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DOCS_SITE_DEFAULT_PROMPT = """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer. Wherever possible, give complete code snippet. Dont make up any code snippet on your own.
|
||||
|
||||
$context
|
||||
|
||||
Query: $query
|
||||
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DEFAULT_PROMPT_TEMPLATE = Template(DEFAULT_PROMPT)
|
||||
DEFAULT_PROMPT_WITH_HISTORY_TEMPLATE = Template(DEFAULT_PROMPT_WITH_HISTORY)
|
||||
DOCS_SITE_PROMPT_TEMPLATE = Template(DOCS_SITE_DEFAULT_PROMPT)
|
||||
query_re = re.compile(r"\$\{*query\}*")
|
||||
context_re = re.compile(r"\$\{*context\}*")
|
||||
history_re = re.compile(r"\$\{*history\}*")
|
||||
|
||||
|
||||
class QueryConfig(BaseConfig):
|
||||
"""
|
||||
Config for the `query` method.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
number_documents=None,
|
||||
template: Template = None,
|
||||
model=None,
|
||||
temperature=None,
|
||||
max_tokens=None,
|
||||
top_p=None,
|
||||
history=None,
|
||||
stream: bool = False,
|
||||
deployment_name=None,
|
||||
system_prompt: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initializes the QueryConfig instance.
|
||||
|
||||
:param number_documents: Number of documents to pull from the database as
|
||||
context.
|
||||
:param template: Optional. The `Template` instance to use as a template for
|
||||
prompt.
|
||||
:param model: Optional. Controls the OpenAI model used.
|
||||
:param temperature: Optional. Controls the randomness of the model's output.
|
||||
Higher values (closer to 1) make output more random, lower values make it more
|
||||
deterministic.
|
||||
:param max_tokens: Optional. Controls how many tokens are generated.
|
||||
:param top_p: Optional. Controls the diversity of words. Higher values
|
||||
(closer to 1) make word selection more diverse, lower values make words less
|
||||
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).
|
||||
"""
|
||||
if number_documents is None:
|
||||
self.number_documents = 1
|
||||
else:
|
||||
self.number_documents = number_documents
|
||||
|
||||
if not history:
|
||||
self.history = None
|
||||
else:
|
||||
if len(history) == 0:
|
||||
self.history = None
|
||||
else:
|
||||
self.history = history
|
||||
|
||||
if template is None:
|
||||
if self.history is None:
|
||||
template = DEFAULT_PROMPT_TEMPLATE
|
||||
else:
|
||||
template = DEFAULT_PROMPT_WITH_HISTORY_TEMPLATE
|
||||
|
||||
self.temperature = temperature if temperature else 0
|
||||
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
|
||||
else:
|
||||
if self.history is None:
|
||||
raise ValueError("`template` should have `query` and `context` keys")
|
||||
else:
|
||||
raise ValueError("`template` should have `query`, `context` and `history` keys")
|
||||
|
||||
if not isinstance(stream, bool):
|
||||
raise ValueError("`stream` should be bool")
|
||||
self.stream = stream
|
||||
|
||||
def validate_template(self, template: Template):
|
||||
"""
|
||||
validate the template
|
||||
|
||||
:param template: the template to validate
|
||||
:return: Boolean, valid (true) or invalid (false)
|
||||
"""
|
||||
if self.history is None:
|
||||
return re.search(query_re, template.template) and re.search(context_re, template.template)
|
||||
else:
|
||||
return (
|
||||
re.search(query_re, template.template)
|
||||
and re.search(context_re, template.template)
|
||||
and re.search(history_re, template.template)
|
||||
)
|
||||
@@ -1,9 +1,13 @@
|
||||
from .AddConfig import AddConfig, ChunkerConfig # noqa: F401
|
||||
from .apps.AppConfig import AppConfig # noqa: F401
|
||||
from .apps.CustomAppConfig import CustomAppConfig # noqa: F401
|
||||
from .apps.OpenSourceAppConfig import OpenSourceAppConfig # noqa: F401
|
||||
from .BaseConfig import BaseConfig # noqa: F401
|
||||
from .ChatConfig import ChatConfig # noqa: F401
|
||||
from .QueryConfig import QueryConfig # noqa: F401
|
||||
from .vectordbs.ElasticsearchDBConfig import \
|
||||
ElasticsearchDBConfig # noqa: F401
|
||||
# flake8: noqa: F401
|
||||
|
||||
from .AddConfig import AddConfig, ChunkerConfig
|
||||
from .apps.AppConfig import AppConfig
|
||||
from .apps.CustomAppConfig import CustomAppConfig
|
||||
from .apps.OpenSourceAppConfig import OpenSourceAppConfig
|
||||
from .BaseConfig import BaseConfig
|
||||
from .embedder.BaseEmbedderConfig import BaseEmbedderConfig
|
||||
from .embedder.BaseEmbedderConfig import BaseEmbedderConfig as EmbedderConfig
|
||||
from .llm.base_llm_config import BaseLlmConfig
|
||||
from .llm.base_llm_config import BaseLlmConfig as LlmConfig
|
||||
from .vectordbs.ChromaDbConfig import ChromaDbConfig
|
||||
from .vectordbs.ElasticsearchDBConfig import ElasticsearchDBConfig
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
try:
|
||||
from chromadb.utils import embedding_functions
|
||||
except RuntimeError:
|
||||
from embedchain.utils import use_pysqlite3
|
||||
|
||||
use_pysqlite3()
|
||||
from chromadb.utils import embedding_functions
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
from .BaseAppConfig import BaseAppConfig
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class AppConfig(BaseAppConfig):
|
||||
"""
|
||||
Config to initialize an embedchain custom `App` instance, with extra config options.
|
||||
@@ -19,45 +13,23 @@ class AppConfig(BaseAppConfig):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
log_level=None,
|
||||
host=None,
|
||||
port=None,
|
||||
id=None,
|
||||
collection_name=None,
|
||||
log_level: str = "WARNING",
|
||||
id: Optional[str] = None,
|
||||
collect_metrics: Optional[bool] = None,
|
||||
collection_name: Optional[str] = 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,
|
||||
collection_name=collection_name,
|
||||
collect_metrics=collect_metrics,
|
||||
)
|
||||
Initializes a configuration class instance for an App. This is the simplest form of an embedchain app.
|
||||
Most of the configuration is done in the `App` class itself.
|
||||
|
||||
@staticmethod
|
||||
def default_embedding_function():
|
||||
:param log_level: Debug level ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], defaults to "WARNING"
|
||||
:type log_level: str, optional
|
||||
:param id: ID of the app. Document metadata will have this id., defaults to None
|
||||
:type id: Optional[str], optional
|
||||
:param collect_metrics: Send anonymous telemetry to improve embedchain, defaults to True
|
||||
:type collect_metrics: Optional[bool], optional
|
||||
:param collection_name: Default collection name. It's recommended to use app.db.set_collection_name() instead,
|
||||
defaults to None
|
||||
:type collection_name: Optional[str], optional
|
||||
"""
|
||||
Sets embedding function to default (`text-embedding-ada-002`).
|
||||
|
||||
:raises ValueError: If the template is not valid as template should contain
|
||||
$context and $query
|
||||
:returns: The default embedding function for the app class.
|
||||
"""
|
||||
if os.getenv("OPENAI_API_KEY") is None and os.getenv("OPENAI_ORGANIZATION") is None:
|
||||
raise ValueError("OPENAI_API_KEY or OPENAI_ORGANIZATION environment variables not provided") # noqa:E501
|
||||
return embedding_functions.OpenAIEmbeddingFunction(
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
organization_id=os.getenv("OPENAI_ORGANIZATION"),
|
||||
model_name="text-embedding-ada-002",
|
||||
)
|
||||
super().__init__(log_level=log_level, id=id, collect_metrics=collect_metrics, collection_name=collection_name)
|
||||
|
||||
@@ -1,90 +1,56 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config.BaseConfig import BaseConfig
|
||||
from embedchain.config.vectordbs import ElasticsearchDBConfig
|
||||
from embedchain.models import VectorDatabases, VectorDimensions
|
||||
from embedchain.helper_classes.json_serializable import JSONSerializable
|
||||
from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
|
||||
|
||||
class BaseAppConfig(BaseConfig):
|
||||
class BaseAppConfig(BaseConfig, JSONSerializable):
|
||||
"""
|
||||
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,
|
||||
collection_name=None,
|
||||
log_level: str = "WARNING",
|
||||
db: Optional[BaseVectorDB] = None,
|
||||
id: Optional[str] = None,
|
||||
collect_metrics: bool = True,
|
||||
db_type: VectorDatabases = None,
|
||||
vector_dim: VectorDimensions = None,
|
||||
es_config: ElasticsearchDBConfig = None,
|
||||
collection_name: Optional[str] = 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 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
|
||||
Initializes a configuration class instance for an App.
|
||||
Most of the configuration is done in the `App` class itself.
|
||||
|
||||
:param log_level: Debug level ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], defaults to "WARNING"
|
||||
:type log_level: str, optional
|
||||
:param db: A database class. It is recommended to set this directly in the `App` class, not this config,
|
||||
defaults to None
|
||||
:type db: Optional[BaseVectorDB], optional
|
||||
:param id: ID of the app. Document metadata will have this id., defaults to None
|
||||
:type id: Optional[str], optional
|
||||
:param collect_metrics: Send anonymous telemetry to improve embedchain, defaults to True
|
||||
:type collect_metrics: Optional[bool], optional
|
||||
:param collection_name: Default collection name. It's recommended to use app.db.set_collection_name() instead,
|
||||
defaults to None
|
||||
:type collection_name: Optional[str], optional
|
||||
"""
|
||||
self._setup_logging(log_level)
|
||||
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
|
||||
self.collection_name = collection_name
|
||||
|
||||
@staticmethod
|
||||
def get_db(db, embedding_fn, host, port, db_type, vector_dim, collection_name, es_config):
|
||||
"""
|
||||
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.
|
||||
: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
|
||||
self._db = db
|
||||
logging.warning(
|
||||
"DEPRECATION WARNING: Please supply the database as the second parameter during app init. "
|
||||
"Such as `app(config=config, db=db)`."
|
||||
)
|
||||
|
||||
from embedchain.vectordb.chroma_db import ChromaDB
|
||||
|
||||
return ChromaDB(embedding_fn=embedding_fn, host=host, port=port)
|
||||
if collection_name:
|
||||
logging.warning("DEPRECATION WARNING: Please supply the collection name to the database config.")
|
||||
return
|
||||
|
||||
def _setup_logging(self, debug_level):
|
||||
level = logging.WARNING # Default level
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
from typing import Any, Optional
|
||||
from typing import Optional
|
||||
|
||||
from chromadb.api.types import Documents, Embeddings
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from embedchain.config.vectordbs import ElasticsearchDBConfig
|
||||
from embedchain.models import (EmbeddingFunctions, Providers, VectorDatabases,
|
||||
VectorDimensions)
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
|
||||
from .BaseAppConfig import BaseAppConfig
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class CustomAppConfig(BaseAppConfig):
|
||||
"""
|
||||
Config to initialize an embedchain custom `App` instance, with extra config options.
|
||||
@@ -19,121 +18,29 @@ class CustomAppConfig(BaseAppConfig):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
log_level=None,
|
||||
embedding_fn: EmbeddingFunctions = None,
|
||||
embedding_fn_model=None,
|
||||
db=None,
|
||||
host=None,
|
||||
port=None,
|
||||
id=None,
|
||||
collection_name=None,
|
||||
provider: Providers = None,
|
||||
open_source_app_config=None,
|
||||
deployment_name=None,
|
||||
log_level: str = "WARNING",
|
||||
db: Optional[BaseVectorDB] = None,
|
||||
id: Optional[str] = None,
|
||||
collect_metrics: Optional[bool] = None,
|
||||
db_type: VectorDatabases = None,
|
||||
es_config: ElasticsearchDBConfig = None,
|
||||
collection_name: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
:param log_level: Optional. (String) Debug level
|
||||
['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'].
|
||||
:param embedding_fn: Optional. Embedding function to use.
|
||||
:param embedding_fn_model: Optional. Model name to use for embedding function.
|
||||
:param db: Optional. (Vector) database to use for embeddings.
|
||||
:param 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
|
||||
Initializes a configuration class instance for an Custom App.
|
||||
Most of the configuration is done in the `CustomApp` class itself.
|
||||
|
||||
:param log_level: Debug level ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], defaults to "WARNING"
|
||||
:type log_level: str, optional
|
||||
:param db: A database class. It is recommended to set this directly in the `CustomApp` class, not this config,
|
||||
defaults to None
|
||||
:type db: Optional[BaseVectorDB], optional
|
||||
:param id: ID of the app. Document metadata will have this id., defaults to None
|
||||
:type id: Optional[str], optional
|
||||
:param collect_metrics: Send anonymous telemetry to improve embedchain, defaults to True
|
||||
:type collect_metrics: Optional[bool], optional
|
||||
:param collection_name: Default collection name. It's recommended to use app.db.set_collection_name() instead,
|
||||
defaults to None
|
||||
:type collection_name: Optional[str], optional
|
||||
"""
|
||||
if provider:
|
||||
self.provider = provider
|
||||
else:
|
||||
raise ValueError("CustomApp must have a provider assigned.")
|
||||
|
||||
self.open_source_app_config = open_source_app_config
|
||||
|
||||
super().__init__(
|
||||
log_level=log_level,
|
||||
embedding_fn=CustomAppConfig.embedding_function(
|
||||
embedding_function=embedding_fn, model=embedding_fn_model, 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,
|
||||
log_level=log_level, db=db, id=id, collect_metrics=collect_metrics, collection_name=collection_name
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def langchain_default_concept(embeddings: Any):
|
||||
"""
|
||||
Langchains default function layout for embeddings.
|
||||
"""
|
||||
|
||||
def embed_function(texts: Documents) -> Embeddings:
|
||||
return embeddings.embed_documents(texts)
|
||||
|
||||
return embed_function
|
||||
|
||||
@staticmethod
|
||||
def embedding_function(embedding_function: EmbeddingFunctions, model: str = None, 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
|
||||
)
|
||||
|
||||
if embedding_function == EmbeddingFunctions.OPENAI:
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
|
||||
if model:
|
||||
embeddings = OpenAIEmbeddings(model=model)
|
||||
else:
|
||||
if deployment_name:
|
||||
embeddings = OpenAIEmbeddings(deployment=deployment_name)
|
||||
else:
|
||||
embeddings = OpenAIEmbeddings()
|
||||
return CustomAppConfig.langchain_default_concept(embeddings)
|
||||
|
||||
elif embedding_function == EmbeddingFunctions.HUGGING_FACE:
|
||||
from langchain.embeddings import HuggingFaceEmbeddings
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name=model)
|
||||
return CustomAppConfig.langchain_default_concept(embeddings)
|
||||
|
||||
elif embedding_function == EmbeddingFunctions.VERTEX_AI:
|
||||
from langchain.embeddings import VertexAIEmbeddings
|
||||
|
||||
embeddings = VertexAIEmbeddings(model_name=model)
|
||||
return CustomAppConfig.langchain_default_concept(embeddings)
|
||||
|
||||
elif embedding_function == EmbeddingFunctions.GPT4ALL:
|
||||
# Note: We could use langchains GPT4ALL embedding, but it's not available in all versions.
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
return embedding_functions.SentenceTransformerEmbeddingFunction(model_name=model)
|
||||
|
||||
@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
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from typing import Optional
|
||||
|
||||
from chromadb.utils import embedding_functions
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
from .BaseAppConfig import BaseAppConfig
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class OpenSourceAppConfig(BaseAppConfig):
|
||||
"""
|
||||
Config to initialize an embedchain custom `OpenSourceApp` instance, with extra config options.
|
||||
@@ -12,48 +13,28 @@ class OpenSourceAppConfig(BaseAppConfig):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
log_level=None,
|
||||
host=None,
|
||||
port=None,
|
||||
id=None,
|
||||
collection_name=None,
|
||||
log_level: str = "WARNING",
|
||||
id: Optional[str] = None,
|
||||
collect_metrics: Optional[bool] = None,
|
||||
model=None,
|
||||
model: str = "orca-mini-3b.ggmlv3.q4_0.bin",
|
||||
collection_name: Optional[str] = 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.
|
||||
Initializes a configuration class instance for an Open Source App.
|
||||
|
||||
:param log_level: Debug level ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], defaults to "WARNING"
|
||||
:type log_level: str, optional
|
||||
:param id: ID of the app. Document metadata will have this id., defaults to None
|
||||
:type id: Optional[str], optional
|
||||
:param collect_metrics: Send anonymous telemetry to improve embedchain, defaults to True
|
||||
:type collect_metrics: Optional[bool], optional
|
||||
:param model: GPT4ALL uses the model to instantiate the class.
|
||||
Unlike `App`, it has to be provided before querying, defaults to "orca-mini-3b.ggmlv3.q4_0.bin"
|
||||
:type model: str, optional
|
||||
:param collection_name: Default collection name. It's recommended to use app.db.set_collection_name() instead,
|
||||
defaults to None
|
||||
:type collection_name: Optional[str], optional
|
||||
"""
|
||||
self.model = model or "orca-mini-3b.ggmlv3.q4_0.bin"
|
||||
|
||||
super().__init__(
|
||||
log_level=log_level,
|
||||
embedding_fn=OpenSourceAppConfig.default_embedding_function(),
|
||||
host=host,
|
||||
port=port,
|
||||
id=id,
|
||||
collection_name=collection_name,
|
||||
collect_metrics=collect_metrics,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def default_embedding_function():
|
||||
"""
|
||||
Sets embedding function to default (`all-MiniLM-L6-v2`).
|
||||
|
||||
:returns: The default embedding function
|
||||
"""
|
||||
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
|
||||
super().__init__(log_level=log_level, id=id, collect_metrics=collect_metrics, collection_name=collection_name)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class BaseEmbedderConfig:
|
||||
def __init__(self, model: Optional[str] = None, deployment_name: Optional[str] = None):
|
||||
"""
|
||||
Initialize a new instance of an embedder config class.
|
||||
|
||||
:param model: model name of the llm embedding model (not applicable to all providers), defaults to None
|
||||
:type model: Optional[str], optional
|
||||
:param deployment_name: deployment name for llm embedding model, defaults to None
|
||||
:type deployment_name: Optional[str], optional
|
||||
"""
|
||||
self.model = model
|
||||
self.deployment_name = deployment_name
|
||||
@@ -0,0 +1,146 @@
|
||||
import re
|
||||
from string import Template
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from embedchain.config.BaseConfig import BaseConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
DEFAULT_PROMPT = """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
|
||||
$context
|
||||
|
||||
Query: $query
|
||||
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DEFAULT_PROMPT_WITH_HISTORY = """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer.
|
||||
I will provide you with our conversation history.
|
||||
|
||||
$context
|
||||
|
||||
History: $history
|
||||
|
||||
Query: $query
|
||||
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DOCS_SITE_DEFAULT_PROMPT = """
|
||||
Use the following pieces of context to answer the query at the end.
|
||||
If you don't know the answer, just say that you don't know, don't try to make up an answer. Wherever possible, give complete code snippet. Dont make up any code snippet on your own.
|
||||
|
||||
$context
|
||||
|
||||
Query: $query
|
||||
|
||||
Helpful Answer:
|
||||
""" # noqa:E501
|
||||
|
||||
DEFAULT_PROMPT_TEMPLATE = Template(DEFAULT_PROMPT)
|
||||
DEFAULT_PROMPT_WITH_HISTORY_TEMPLATE = Template(DEFAULT_PROMPT_WITH_HISTORY)
|
||||
DOCS_SITE_PROMPT_TEMPLATE = Template(DOCS_SITE_DEFAULT_PROMPT)
|
||||
query_re = re.compile(r"\$\{*query\}*")
|
||||
context_re = re.compile(r"\$\{*context\}*")
|
||||
history_re = re.compile(r"\$\{*history\}*")
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class BaseLlmConfig(BaseConfig):
|
||||
"""
|
||||
Config for the `query` method.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
number_documents: int = 1,
|
||||
template: Optional[Template] = None,
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0,
|
||||
max_tokens: int = 1000,
|
||||
top_p: float = 1,
|
||||
stream: bool = False,
|
||||
deployment_name: Optional[str] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
where: Dict[str, Any] = None,
|
||||
):
|
||||
"""
|
||||
Initializes a configuration class instance for the LLM.
|
||||
|
||||
Takes the place of the former `QueryConfig` or `ChatConfig`.
|
||||
Use `LlmConfig` as an alias to `BaseLlmConfig`.
|
||||
|
||||
:param number_documents: Number of documents to pull from the database as
|
||||
context, defaults to 1
|
||||
:type number_documents: int, optional
|
||||
:param template: The `Template` instance to use as a template for
|
||||
prompt, defaults to None
|
||||
:type template: Optional[Template], optional
|
||||
:param model: Controls the OpenAI model used, defaults to None
|
||||
:type model: Optional[str], optional
|
||||
:param temperature: Controls the randomness of the model's output.
|
||||
Higher values (closer to 1) make output more random, lower values make it more deterministic, defaults to 0
|
||||
:type temperature: float, optional
|
||||
:param max_tokens: Controls how many tokens are generated, defaults to 1000
|
||||
:type max_tokens: int, optional
|
||||
:param top_p: Controls the diversity of words. Higher values (closer to 1) make word selection more diverse,
|
||||
defaults to 1
|
||||
:type top_p: float, optional
|
||||
:param stream: Control if response is streamed back to user, defaults to False
|
||||
:type stream: bool, optional
|
||||
:param deployment_name: t.b.a., defaults to None
|
||||
:type deployment_name: Optional[str], optional
|
||||
:param system_prompt: System prompt string, defaults to None
|
||||
:type system_prompt: Optional[str], optional
|
||||
:param where: A dictionary of key-value pairs to filter the database results., defaults to None
|
||||
:type where: Dict[str, Any], optional
|
||||
:raises ValueError: If the template is not valid as template should
|
||||
contain $context and $query (and optionally $history)
|
||||
:raises ValueError: Stream is not boolean
|
||||
"""
|
||||
if template is None:
|
||||
template = DEFAULT_PROMPT_TEMPLATE
|
||||
|
||||
self.number_documents = number_documents
|
||||
self.temperature = temperature
|
||||
self.max_tokens = max_tokens
|
||||
self.model = model
|
||||
self.top_p = top_p
|
||||
self.deployment_name = deployment_name
|
||||
self.system_prompt = system_prompt
|
||||
|
||||
if self.validate_template(template):
|
||||
self.template = template
|
||||
else:
|
||||
raise ValueError("`template` should have `query` and `context` keys and potentially `history` (if used).")
|
||||
|
||||
if not isinstance(stream, bool):
|
||||
raise ValueError("`stream` should be bool")
|
||||
self.stream = stream
|
||||
self.where = where
|
||||
|
||||
def validate_template(self, template: Template) -> bool:
|
||||
"""
|
||||
validate the template
|
||||
|
||||
:param template: the template to validate
|
||||
:type template: Template
|
||||
:return: valid (true) or invalid (false)
|
||||
:rtype: bool
|
||||
"""
|
||||
return re.search(query_re, template.template) and re.search(context_re, template.template)
|
||||
|
||||
def _validate_template_history(self, template: Template) -> bool:
|
||||
"""
|
||||
validate the template with history
|
||||
|
||||
:param template: the template to validate
|
||||
:type template: Template
|
||||
:return: valid (true) or invalid (false)
|
||||
:rtype: bool
|
||||
"""
|
||||
return re.search(history_re, template.template)
|
||||
@@ -0,0 +1,29 @@
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config.BaseConfig import BaseConfig
|
||||
|
||||
|
||||
class BaseVectorDbConfig(BaseConfig):
|
||||
def __init__(
|
||||
self,
|
||||
collection_name: Optional[str] = None,
|
||||
dir: str = "db",
|
||||
host: Optional[str] = None,
|
||||
port: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initializes a configuration class instance for the vector database.
|
||||
|
||||
:param collection_name: Default name for the collection, defaults to None
|
||||
:type collection_name: Optional[str], optional
|
||||
:param dir: Path to the database directory, where the database is stored, defaults to "db"
|
||||
:type dir: str, optional
|
||||
:param host: Database connection remote host. Use this if you run Embedchain as a client, defaults to None
|
||||
:type host: Optional[str], optional
|
||||
:param host: Database connection remote port. Use this if you run Embedchain as a client, defaults to None
|
||||
:type port: Optional[str], optional
|
||||
"""
|
||||
self.collection_name = collection_name or "embedchain_store"
|
||||
self.dir = dir
|
||||
self.host = host
|
||||
self.port = port
|
||||
@@ -0,0 +1,35 @@
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config.vectordbs.BaseVectorDbConfig import BaseVectorDbConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class ChromaDbConfig(BaseVectorDbConfig):
|
||||
def __init__(
|
||||
self,
|
||||
collection_name: Optional[str] = None,
|
||||
dir: Optional[str] = None,
|
||||
host: Optional[str] = None,
|
||||
port: Optional[str] = None,
|
||||
chroma_settings: Optional[dict] = None,
|
||||
):
|
||||
"""
|
||||
Initializes a configuration class instance for ChromaDB.
|
||||
|
||||
:param collection_name: Default name for the collection, defaults to None
|
||||
:type collection_name: Optional[str], optional
|
||||
:param dir: Path to the database directory, where the database is stored, defaults to None
|
||||
:type dir: Optional[str], optional
|
||||
:param host: Database connection remote host. Use this if you run Embedchain as a client, defaults to None
|
||||
:type host: Optional[str], optional
|
||||
:param port: Database connection remote port. Use this if you run Embedchain as a client, defaults to None
|
||||
:type port: Optional[str], optional
|
||||
:param chroma_settings: Chroma settings dict, defaults to None
|
||||
:type chroma_settings: Optional[dict], optional
|
||||
"""
|
||||
"""
|
||||
:param chroma_settings: Optional. Chroma settings for connection.
|
||||
"""
|
||||
self.chroma_settings = chroma_settings
|
||||
super().__init__(collection_name=collection_name, dir=dir, host=host, port=port)
|
||||
@@ -1,15 +1,32 @@
|
||||
from typing import Dict, List, Union
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from embedchain.config.BaseConfig import BaseConfig
|
||||
from embedchain.config.vectordbs.BaseVectorDbConfig import BaseVectorDbConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
@register_deserializable
|
||||
class ElasticsearchDBConfig(BaseVectorDbConfig):
|
||||
def __init__(
|
||||
self,
|
||||
collection_name: Optional[str] = None,
|
||||
dir: Optional[str] = None,
|
||||
es_url: Union[str, List[str]] = None,
|
||||
**ES_EXTRA_PARAMS: Dict[str, any],
|
||||
):
|
||||
"""
|
||||
Initializes a configuration class instance for an Elasticsearch client.
|
||||
|
||||
def __init__(self, es_url: Union[str, List[str]] = None, **ES_EXTRA_PARAMS: Dict[str, any]):
|
||||
:param collection_name: Default name for the collection, defaults to None
|
||||
:type collection_name: Optional[str], optional
|
||||
:param dir: Path to the database directory, where the database is stored, defaults to None
|
||||
:type dir: Optional[str], optional
|
||||
:param es_url: elasticsearch url or list of nodes url to be used for connection, defaults to None
|
||||
:type es_url: Union[str, List[str]], optional
|
||||
:param ES_EXTRA_PARAMS: extra params dict that can be passed to elasticsearch.
|
||||
:type ES_EXTRA_PARAMS: Dict[str, Any], optional
|
||||
"""
|
||||
# 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
|
||||
|
||||
super().__init__(collection_name=collection_name, dir=dir)
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
from embedchain.chunkers.base_chunker import BaseChunker
|
||||
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.table import TableChunker
|
||||
from embedchain.chunkers.text import TextChunker
|
||||
from embedchain.chunkers.web_page import WebPageChunker
|
||||
from embedchain.chunkers.youtube_video import YoutubeVideoChunker
|
||||
from embedchain.config import AddConfig
|
||||
from embedchain.config.AddConfig import ChunkerConfig, LoaderConfig
|
||||
from embedchain.helper_classes.json_serializable import JSONSerializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
from embedchain.loaders.csv import CsvLoader
|
||||
from embedchain.loaders.docs_site_loader import DocsSiteLoader
|
||||
from embedchain.loaders.docx_file import DocxFileLoader
|
||||
from embedchain.loaders.local_qna_pair import LocalQnaPairLoader
|
||||
@@ -18,7 +24,7 @@ from embedchain.loaders.youtube_video import YoutubeVideoLoader
|
||||
from embedchain.models.data_type import DataType
|
||||
|
||||
|
||||
class DataFormatter:
|
||||
class DataFormatter(JSONSerializable):
|
||||
"""
|
||||
DataFormatter is an internal utility class which abstracts the mapping for
|
||||
loaders and chunkers to the data_type entered by the user in their
|
||||
@@ -26,16 +32,28 @@ class DataFormatter:
|
||||
"""
|
||||
|
||||
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)
|
||||
"""
|
||||
Initialize a dataformatter, set data type and chunker based on datatype.
|
||||
|
||||
def _get_loader(self, data_type: DataType, config):
|
||||
:param data_type: The type of the data to load and chunk.
|
||||
:type data_type: DataType
|
||||
:param config: AddConfig instance with nested loader and chunker config attributes.
|
||||
:type config: AddConfig
|
||||
"""
|
||||
self.loader = self._get_loader(data_type=data_type, config=config.loader)
|
||||
self.chunker = self._get_chunker(data_type=data_type, config=config.chunker)
|
||||
|
||||
def _get_loader(self, data_type: DataType, config: LoaderConfig) -> BaseLoader:
|
||||
"""
|
||||
Returns the appropriate data loader for the given data type.
|
||||
|
||||
:param data_type: The type of the data to load.
|
||||
:return: The loader for the given data type.
|
||||
:type data_type: DataType
|
||||
:param config: Config to initialize the loader with.
|
||||
:type config: LoaderConfig
|
||||
:raises ValueError: If an unsupported data type is provided.
|
||||
:return: The loader for the given data type.
|
||||
:rtype: BaseLoader
|
||||
"""
|
||||
loaders = {
|
||||
DataType.YOUTUBE_VIDEO: YoutubeVideoLoader,
|
||||
@@ -46,11 +64,12 @@ class DataFormatter:
|
||||
DataType.DOCX: DocxFileLoader,
|
||||
DataType.SITEMAP: SitemapLoader,
|
||||
DataType.DOCS_SITE: DocsSiteLoader,
|
||||
DataType.CSV: CsvLoader,
|
||||
}
|
||||
lazy_loaders = {DataType.NOTION}
|
||||
if data_type in loaders:
|
||||
loader_class = loaders[data_type]
|
||||
loader = loader_class()
|
||||
loader_class: type = loaders[data_type]
|
||||
loader: BaseLoader = loader_class()
|
||||
return loader
|
||||
elif data_type in lazy_loaders:
|
||||
if data_type == DataType.NOTION:
|
||||
@@ -62,13 +81,16 @@ class DataFormatter:
|
||||
else:
|
||||
raise ValueError(f"Unsupported data type: {data_type}")
|
||||
|
||||
def _get_chunker(self, data_type: DataType, config):
|
||||
"""
|
||||
Returns the appropriate chunker for the given data type.
|
||||
def _get_chunker(self, data_type: DataType, config: ChunkerConfig) -> BaseChunker:
|
||||
"""Returns the appropriate chunker for the given data type.
|
||||
|
||||
:param data_type: The type of the data to chunk.
|
||||
:return: The chunker for the given data type.
|
||||
:type data_type: DataType
|
||||
:param config: Config to initialize the chunker with.
|
||||
:type config: ChunkerConfig
|
||||
:raises ValueError: If an unsupported data type is provided.
|
||||
:return: The chunker for the given data type.
|
||||
:rtype: BaseChunker
|
||||
"""
|
||||
chunker_classes = {
|
||||
DataType.YOUTUBE_VIDEO: YoutubeVideoChunker,
|
||||
@@ -80,10 +102,11 @@ class DataFormatter:
|
||||
DataType.WEB_PAGE: WebPageChunker,
|
||||
DataType.DOCS_SITE: DocsSiteChunker,
|
||||
DataType.NOTION: NotionChunker,
|
||||
DataType.CSV: TableChunker,
|
||||
}
|
||||
if data_type in chunker_classes:
|
||||
chunker_class = chunker_classes[data_type]
|
||||
chunker = chunker_class(config)
|
||||
chunker_class: type = chunker_classes[data_type]
|
||||
chunker: BaseChunker = chunker_class(config)
|
||||
chunker.set_data_type(data_type)
|
||||
return chunker
|
||||
else:
|
||||
|
||||
+192
-204
@@ -6,59 +6,103 @@ import os
|
||||
import threading
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
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
|
||||
from embedchain.config import AddConfig, BaseLlmConfig
|
||||
from embedchain.config.apps.BaseAppConfig import BaseAppConfig
|
||||
from embedchain.config.QueryConfig import DOCS_SITE_PROMPT_TEMPLATE
|
||||
from embedchain.data_formatter import DataFormatter
|
||||
from embedchain.embedder.base_embedder import BaseEmbedder
|
||||
from embedchain.helper_classes.json_serializable import JSONSerializable
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
from embedchain.models.data_type import DataType
|
||||
from embedchain.utils import detect_datatype
|
||||
from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
|
||||
load_dotenv()
|
||||
|
||||
ABS_PATH = os.getcwd()
|
||||
DB_DIR = os.path.join(ABS_PATH, "db")
|
||||
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):
|
||||
class EmbedChain(JSONSerializable):
|
||||
def __init__(
|
||||
self,
|
||||
config: BaseAppConfig,
|
||||
llm: BaseLlm,
|
||||
db: BaseVectorDB = None,
|
||||
embedder: BaseEmbedder = None,
|
||||
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 config: Configuration just for the app, not the db or llm or embedder.
|
||||
:type config: BaseAppConfig
|
||||
:param llm: Instance of the LLM you want to use.
|
||||
:type llm: BaseLlm
|
||||
:param db: Instance of the Database to use, defaults to None
|
||||
:type db: BaseVectorDB, optional
|
||||
:param embedder: instance of the embedder to use, defaults to None
|
||||
:type embedder: BaseEmbedder, optional
|
||||
:param system_prompt: System prompt to use in the llm query, defaults to None
|
||||
:type system_prompt: Optional[str], optional
|
||||
:raises ValueError: No database or embedder provided.
|
||||
"""
|
||||
|
||||
self.config = config
|
||||
self.collection = self.config.db._get_or_create_collection(self.config.collection_name)
|
||||
self.db = self.config.db
|
||||
|
||||
# Add subclasses
|
||||
## Llm
|
||||
self.llm = llm
|
||||
## Database
|
||||
# Database has support for config assignment for backwards compatibility
|
||||
if db is None and (not hasattr(self.config, "db") or self.config.db is None):
|
||||
raise ValueError("App requires Database.")
|
||||
self.db = db or self.config.db
|
||||
## Embedder
|
||||
if embedder is None:
|
||||
raise ValueError("App requires Embedder.")
|
||||
self.embedder = embedder
|
||||
|
||||
# Initialize database
|
||||
self.db._set_embedder(self.embedder)
|
||||
self.db._initialize()
|
||||
# Set collection name from app config for backwards compatibility.
|
||||
if config.collection_name:
|
||||
self.db.set_collection_name(config.collection_name)
|
||||
|
||||
# Add variables that are "shortcuts"
|
||||
if system_prompt:
|
||||
self.llm.config.system_prompt = system_prompt
|
||||
|
||||
# Attributes that aren't subclass related.
|
||||
self.user_asks = []
|
||||
self.is_docs_site_instance = False
|
||||
self.online = False
|
||||
self.memory = ConversationBufferMemory()
|
||||
|
||||
# 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()
|
||||
# NOTE: Uncomment the next two lines when running tests to see if any test fires a telemetry event.
|
||||
# if (self.config.collect_metrics):
|
||||
# raise ConnectionRefusedError("Collection of metrics should not be allowed.")
|
||||
thread_telemetry = threading.Thread(target=self._send_telemetry_event, args=("init",))
|
||||
thread_telemetry.start()
|
||||
|
||||
def _load_or_generate_user_id(self):
|
||||
def _load_or_generate_user_id(self) -> str:
|
||||
"""
|
||||
Loads the user id from the config file if it exists, otherwise generates a new
|
||||
one and saves it to the config file.
|
||||
|
||||
:return: user id
|
||||
:rtype: str
|
||||
"""
|
||||
if not os.path.exists(CONFIG_DIR):
|
||||
os.makedirs(CONFIG_DIR)
|
||||
@@ -77,9 +121,9 @@ class EmbedChain:
|
||||
|
||||
def add(
|
||||
self,
|
||||
source,
|
||||
source: Any,
|
||||
data_type: Optional[DataType] = None,
|
||||
metadata: Optional[Dict] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
config: Optional[AddConfig] = None,
|
||||
):
|
||||
"""
|
||||
@@ -88,12 +132,17 @@ class EmbedChain:
|
||||
and then stores the embedding to vector database.
|
||||
|
||||
: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.
|
||||
:type source: Any
|
||||
:param data_type: Automatically detected, but can be forced with this argument. The type of the data to add,
|
||||
defaults to None
|
||||
:type data_type: Optional[DataType], optional
|
||||
:param metadata: Metadata associated with the data source., defaults to None
|
||||
:type metadata: Optional[Dict[str, Any]], optional
|
||||
:param config: The `AddConfig` instance to use as configuration options., defaults to None
|
||||
:type config: Optional[AddConfig], optional
|
||||
:raises ValueError: Invalid data type
|
||||
:return: source_id, a md5-hash of the source, in hexadecimal representation.
|
||||
:rtype: str
|
||||
"""
|
||||
if config is None:
|
||||
config = AddConfig()
|
||||
@@ -144,39 +193,62 @@ class EmbedChain:
|
||||
|
||||
return source_id
|
||||
|
||||
def add_local(self, source, data_type=None, metadata=None, config: AddConfig = None):
|
||||
def add_local(
|
||||
self,
|
||||
source: Any,
|
||||
data_type: Optional[DataType] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
config: Optional[AddConfig] = None,
|
||||
):
|
||||
"""
|
||||
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.
|
||||
|
||||
Warning:
|
||||
This method is deprecated and will be removed in future versions. Use `add` instead.
|
||||
|
||||
: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: md5-hash of the source, in hexadecimal representation.
|
||||
:type source: Any
|
||||
:param data_type: Automatically detected, but can be forced with this argument. The type of the data to add,
|
||||
defaults to None
|
||||
:type data_type: Optional[DataType], optional
|
||||
:param metadata: Metadata associated with the data source., defaults to None
|
||||
:type metadata: Optional[Dict[str, Any]], optional
|
||||
:param config: The `AddConfig` instance to use as configuration options., defaults to None
|
||||
:type config: Optional[AddConfig], optional
|
||||
:raises ValueError: Invalid data type
|
||||
:return: source_id, a md5-hash of the source, in hexadecimal representation.
|
||||
:rtype: str
|
||||
"""
|
||||
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, source_id=None):
|
||||
"""
|
||||
Loads the data from the given URL, chunks it, and adds it to database.
|
||||
def load_and_embed(
|
||||
self,
|
||||
loader: BaseLoader,
|
||||
chunker: BaseChunker,
|
||||
src: Any,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
source_id: Optional[str] = None,
|
||||
) -> Tuple[List[str], Dict[str, Any], List[str], int]:
|
||||
"""The loader to use to load the data.
|
||||
|
||||
:param loader: The loader to use to load the data.
|
||||
:type loader: BaseLoader
|
||||
:param chunker: The chunker to use to chunk the data.
|
||||
: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.
|
||||
:type chunker: BaseChunker
|
||||
:param src: The data to be handled by the loader.
|
||||
Can be a URL for remote sources or local content for local loaders.
|
||||
:type src: Any
|
||||
:param metadata: Metadata associated with the data source., defaults to None
|
||||
:type metadata: Dict[str, Any], optional
|
||||
:param source_id: Hexadecimal hash of the source., defaults to None
|
||||
:type source_id: str, optional
|
||||
:return: (List) documents (embedded text), (List) metadata, (list) ids, (int) number of chunks
|
||||
:rtype: Tuple[List[str], Dict[str, Any], List[str], int]
|
||||
"""
|
||||
embeddings_data = chunker.create_chunks(loader, src)
|
||||
|
||||
@@ -224,241 +296,157 @@ class EmbedChain:
|
||||
metadatas = new_metadatas
|
||||
|
||||
# Count before, to calculate a delta in the end.
|
||||
chunks_before_addition = self.count()
|
||||
chunks_before_addition = self.db.count()
|
||||
|
||||
self.db.add(documents=documents, metadatas=metadatas, ids=ids)
|
||||
count_new_chunks = self.count() - chunks_before_addition
|
||||
count_new_chunks = self.db.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 [
|
||||
(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 get_llm_model_answer(self):
|
||||
"""
|
||||
Usually implemented by child class
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def retrieve_from_database(self, input_query, config: QueryConfig):
|
||||
def retrieve_from_database(self, input_query: str, config: Optional[BaseLlmConfig] = None, where=None) -> List[str]:
|
||||
"""
|
||||
Queries the vector database based on the given input query.
|
||||
Gets relevant doc based on the query
|
||||
|
||||
:param input_query: The query to use.
|
||||
:param config: The query configuration.
|
||||
:return: The content of the document that matched your query.
|
||||
:type input_query: str
|
||||
:param config: The query configuration, defaults to None
|
||||
:type config: Optional[BaseLlmConfig], optional
|
||||
:param where: A dictionary of key-value pairs to filter the database results, defaults to None
|
||||
:type where: _type_, optional
|
||||
:return: List of contents of the document that matched your query
|
||||
:rtype: List[str]
|
||||
"""
|
||||
where = {"app_id": self.config.id} if self.config.id is not None else {} # optional filter
|
||||
query_config = config or self.llm.config
|
||||
|
||||
if where is not None:
|
||||
where = where
|
||||
elif query_config is not None and query_config.where is not None:
|
||||
where = query_config.where
|
||||
else:
|
||||
where = {}
|
||||
|
||||
if self.config.id is not None:
|
||||
where.update({"app_id": self.config.id})
|
||||
|
||||
contents = self.db.query(
|
||||
input_query=input_query,
|
||||
n_results=config.number_documents,
|
||||
n_results=query_config.number_documents,
|
||||
where=where,
|
||||
)
|
||||
|
||||
return contents
|
||||
|
||||
def _append_search_and_context(self, context, web_search_result):
|
||||
return f"{context}\nWeb Search Result: {web_search_result}"
|
||||
|
||||
def generate_prompt(self, input_query, contexts, config: QueryConfig, **kwargs):
|
||||
"""
|
||||
Generates a prompt based on the given query and context, ready to be
|
||||
passed to an LLM
|
||||
|
||||
:param input_query: The query to use.
|
||||
:param contexts: List of similar documents to the query used as context.
|
||||
:param config: Optional. The `QueryConfig` instance to use as
|
||||
configuration options.
|
||||
:return: The prompt
|
||||
"""
|
||||
context_string = (" | ").join(contexts)
|
||||
web_search_result = kwargs.get("web_search_result", "")
|
||||
if web_search_result:
|
||||
context_string = self._append_search_and_context(context_string, web_search_result)
|
||||
if not config.history:
|
||||
prompt = config.template.substitute(context=context_string, query=input_query)
|
||||
else:
|
||||
prompt = config.template.substitute(context=context_string, query=input_query, history=config.history)
|
||||
return prompt
|
||||
|
||||
def get_answer_from_llm(self, prompt, config: ChatConfig):
|
||||
"""
|
||||
Gets an answer based on the given query and context by passing it
|
||||
to an LLM.
|
||||
|
||||
:param query: The query to use.
|
||||
:param context: Similar documents to the query used as context.
|
||||
:return: The answer.
|
||||
"""
|
||||
|
||||
return self.get_llm_model_answer(prompt, config)
|
||||
|
||||
def access_search_and_get_results(self, input_query):
|
||||
from langchain.tools import DuckDuckGoSearchRun
|
||||
|
||||
search = DuckDuckGoSearchRun()
|
||||
logging.info(f"Access search to get answers for {input_query}")
|
||||
return search.run(input_query)
|
||||
|
||||
def query(self, input_query, config: QueryConfig = None, dry_run=False):
|
||||
def query(self, input_query: str, config: BaseLlmConfig = None, dry_run=False, where: Optional[Dict] = None) -> str:
|
||||
"""
|
||||
Queries the vector database based on the given input query.
|
||||
Gets relevant doc based on the query and then passes it to an
|
||||
LLM as context to get the answer.
|
||||
|
||||
:param input_query: The query to use.
|
||||
:param config: Optional. The `QueryConfig` instance to use as
|
||||
configuration options.
|
||||
:param dry_run: Optional. A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response.
|
||||
You can use it to test your prompt, including the context provided
|
||||
by the vector database's doc retrieval.
|
||||
The only thing the dry run does not consider is the cut-off due to
|
||||
the `max_tokens` parameter.
|
||||
:return: The answer to the query.
|
||||
:type input_query: str
|
||||
:param config: The `LlmConfig` instance to use as configuration options. This is used for one method call.
|
||||
To persistently use a config, declare it during app init., defaults to None
|
||||
:type config: Optional[BaseLlmConfig], optional
|
||||
:param dry_run: A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response., defaults to False
|
||||
:type dry_run: bool, optional
|
||||
:param where: A dictionary of key-value pairs to filter the database results., defaults to None
|
||||
:type where: Optional[Dict[str, str]], optional
|
||||
:return: The answer to the query or the dry run result
|
||||
:rtype: str
|
||||
"""
|
||||
if config is None:
|
||||
config = QueryConfig()
|
||||
if self.is_docs_site_instance:
|
||||
config.template = DOCS_SITE_PROMPT_TEMPLATE
|
||||
config.number_documents = 5
|
||||
k = {}
|
||||
if self.online:
|
||||
k["web_search_result"] = self.access_search_and_get_results(input_query)
|
||||
contexts = self.retrieve_from_database(input_query, config)
|
||||
prompt = self.generate_prompt(input_query, contexts, config, **k)
|
||||
logging.info(f"Prompt: {prompt}")
|
||||
|
||||
if dry_run:
|
||||
return prompt
|
||||
|
||||
answer = self.get_answer_from_llm(prompt, config)
|
||||
contexts = self.retrieve_from_database(input_query=input_query, config=config, where=where)
|
||||
answer = self.llm.query(input_query=input_query, contexts=contexts, config=config, dry_run=dry_run)
|
||||
|
||||
# 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
|
||||
else:
|
||||
return self._stream_query_response(answer)
|
||||
return answer
|
||||
|
||||
def _stream_query_response(self, answer):
|
||||
streamed_answer = ""
|
||||
for chunk in answer:
|
||||
streamed_answer = streamed_answer + chunk
|
||||
yield chunk
|
||||
logging.info(f"Answer: {streamed_answer}")
|
||||
|
||||
def chat(self, input_query, config: ChatConfig = None, dry_run=False):
|
||||
def chat(
|
||||
self,
|
||||
input_query: str,
|
||||
config: Optional[BaseLlmConfig] = None,
|
||||
dry_run=False,
|
||||
where: Optional[Dict[str, str]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Queries the vector database on the given input query.
|
||||
Gets relevant doc based on the query and then passes it to an
|
||||
LLM as context to get the answer.
|
||||
|
||||
Maintains the whole conversation in memory.
|
||||
|
||||
:param input_query: The query to use.
|
||||
:param config: Optional. The `ChatConfig` instance to use as
|
||||
configuration options.
|
||||
:param dry_run: Optional. A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response.
|
||||
You can use it to test your prompt, including the context provided
|
||||
by the vector database's doc retrieval.
|
||||
The only thing the dry run does not consider is the cut-off due to
|
||||
the `max_tokens` parameter.
|
||||
:return: The answer to the query.
|
||||
:type input_query: str
|
||||
:param config: The `LlmConfig` instance to use as configuration options. This is used for one method call.
|
||||
To persistently use a config, declare it during app init., defaults to None
|
||||
:type config: Optional[BaseLlmConfig], optional
|
||||
:param dry_run: A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response., defaults to False
|
||||
:type dry_run: bool, optional
|
||||
:param where: A dictionary of key-value pairs to filter the database results., defaults to None
|
||||
:type where: Optional[Dict[str, str]], optional
|
||||
:return: The answer to the query or the dry run result
|
||||
:rtype: str
|
||||
"""
|
||||
if config is None:
|
||||
config = ChatConfig()
|
||||
if self.is_docs_site_instance:
|
||||
config.template = DOCS_SITE_PROMPT_TEMPLATE
|
||||
config.number_documents = 5
|
||||
k = {}
|
||||
if self.online:
|
||||
k["web_search_result"] = self.access_search_and_get_results(input_query)
|
||||
contexts = self.retrieve_from_database(input_query, config)
|
||||
|
||||
chat_history = self.memory.load_memory_variables({})["history"]
|
||||
|
||||
if chat_history:
|
||||
config.set_history(chat_history)
|
||||
|
||||
prompt = self.generate_prompt(input_query, contexts, config, **k)
|
||||
logging.info(f"Prompt: {prompt}")
|
||||
|
||||
if dry_run:
|
||||
return prompt
|
||||
|
||||
answer = self.get_answer_from_llm(prompt, config)
|
||||
|
||||
self.memory.chat_memory.add_user_message(input_query)
|
||||
contexts = self.retrieve_from_database(input_query=input_query, config=config, where=where)
|
||||
answer = self.llm.chat(input_query=input_query, contexts=contexts, config=config, dry_run=dry_run)
|
||||
|
||||
# Send anonymous telemetry
|
||||
thread_telemetry = threading.Thread(target=self._send_telemetry_event, args=("chat",))
|
||||
thread_telemetry.start()
|
||||
|
||||
if isinstance(answer, str):
|
||||
self.memory.chat_memory.add_ai_message(answer)
|
||||
logging.info(f"Answer: {answer}")
|
||||
return answer
|
||||
else:
|
||||
# this is a streamed response and needs to be handled differently.
|
||||
return self._stream_chat_response(answer)
|
||||
return answer
|
||||
|
||||
def _stream_chat_response(self, answer):
|
||||
streamed_answer = ""
|
||||
for chunk in answer:
|
||||
streamed_answer = streamed_answer + chunk
|
||||
yield chunk
|
||||
self.memory.chat_memory.add_ai_message(streamed_answer)
|
||||
logging.info(f"Answer: {streamed_answer}")
|
||||
|
||||
def set_collection(self, collection_name):
|
||||
def set_collection_name(self, name: str):
|
||||
"""
|
||||
Set the collection to use.
|
||||
Set the name of the collection. A collection is an isolated space for vectors.
|
||||
|
||||
:param collection_name: The name of the collection to use.
|
||||
Using `app.db.set_collection_name` method is preferred to this.
|
||||
|
||||
:param name: Name of the collection.
|
||||
:type name: str
|
||||
"""
|
||||
self.collection = self.config.db._get_or_create_collection(collection_name)
|
||||
self.db.set_collection_name(name)
|
||||
# Create the collection if it does not exist
|
||||
self.db._get_or_create_collection(name)
|
||||
# TODO: Check whether it is necessary to assign to the `self.collection` attribute,
|
||||
# since the main purpose is the creation.
|
||||
|
||||
def count(self) -> int:
|
||||
"""
|
||||
Count the number of embeddings.
|
||||
|
||||
DEPRECATED IN FAVOR OF `db.count()`
|
||||
|
||||
:return: The number of embeddings.
|
||||
:rtype: int
|
||||
"""
|
||||
logging.warning("DEPRECATION WARNING: Please use `app.db.count()` instead of `app.count()`.")
|
||||
return self.db.count()
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the database. Deletes all embeddings irreversibly.
|
||||
`App` does not have to be reinitialized after using this method.
|
||||
|
||||
DEPRECATED IN FAVOR OF `db.reset()`
|
||||
"""
|
||||
# Send anonymous telemetry
|
||||
thread_telemetry = threading.Thread(target=self._send_telemetry_event, args=("reset",))
|
||||
thread_telemetry.start()
|
||||
|
||||
collection_name = self.collection.name
|
||||
logging.warning("DEPRECATION WARNING: Please use `app.db.reset()` instead of `App.reset()`.")
|
||||
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):
|
||||
"""
|
||||
Send telemetry event to the embedchain server. This is anonymous. It can be toggled off in `AppConfig`.
|
||||
"""
|
||||
if not self.config.collect_metrics:
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from embedchain.config.embedder.BaseEmbedderConfig import BaseEmbedderConfig
|
||||
|
||||
try:
|
||||
from chromadb.api.types import Documents, Embeddings
|
||||
except RuntimeError:
|
||||
from embedchain.utils import use_pysqlite3
|
||||
|
||||
use_pysqlite3()
|
||||
from chromadb.api.types import Documents, Embeddings
|
||||
|
||||
|
||||
class BaseEmbedder:
|
||||
"""
|
||||
Class that manages everything regarding embeddings. Including embedding function, loaders and chunkers.
|
||||
|
||||
Embedding functions and vector dimensions are set based on the child class you choose.
|
||||
To manually overwrite you can use this classes `set_...` methods.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
|
||||
"""
|
||||
Intialize the embedder class.
|
||||
|
||||
:param config: embedder configuration option class, defaults to None
|
||||
:type config: Optional[BaseEmbedderConfig], optional
|
||||
"""
|
||||
if config is None:
|
||||
self.config = BaseEmbedderConfig()
|
||||
else:
|
||||
self.config = config
|
||||
self.vector_dimension: int
|
||||
|
||||
def set_embedding_fn(self, embedding_fn: Callable[[list[str]], list[str]]):
|
||||
"""
|
||||
Set or overwrite the embedding function to be used by the database to store and retrieve documents.
|
||||
|
||||
:param embedding_fn: Function to be used to generate embeddings.
|
||||
:type embedding_fn: Callable[[list[str]], list[str]]
|
||||
:raises ValueError: Embedding function is not callable.
|
||||
"""
|
||||
if not hasattr(embedding_fn, "__call__"):
|
||||
raise ValueError("Embedding function is not a function")
|
||||
self.embedding_fn = embedding_fn
|
||||
|
||||
def set_vector_dimension(self, vector_dimension: int):
|
||||
"""
|
||||
Set or overwrite the vector dimension size
|
||||
|
||||
:param vector_dimension: vector dimension size
|
||||
:type vector_dimension: int
|
||||
"""
|
||||
self.vector_dimension = vector_dimension
|
||||
|
||||
@staticmethod
|
||||
def _langchain_default_concept(embeddings: Any):
|
||||
"""
|
||||
Langchains default function layout for embeddings.
|
||||
|
||||
:param embeddings: Langchain embeddings
|
||||
:type embeddings: Any
|
||||
:return: embedding function
|
||||
:rtype: Callable
|
||||
"""
|
||||
|
||||
def embed_function(texts: Documents) -> Embeddings:
|
||||
return embeddings.embed_documents(texts)
|
||||
|
||||
return embed_function
|
||||
@@ -0,0 +1,21 @@
|
||||
from typing import Optional
|
||||
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
from embedchain.config import BaseEmbedderConfig
|
||||
from embedchain.embedder.base_embedder import BaseEmbedder
|
||||
from embedchain.models import EmbeddingFunctions
|
||||
|
||||
|
||||
class GPT4AllEmbedder(BaseEmbedder):
|
||||
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
|
||||
# Note: We could use langchains GPT4ALL embedding, but it's not available in all versions.
|
||||
super().__init__(config=config)
|
||||
if self.config.model is None:
|
||||
self.config.model = "all-MiniLM-L6-v2"
|
||||
|
||||
embedding_fn = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=self.config.model)
|
||||
self.set_embedding_fn(embedding_fn=embedding_fn)
|
||||
|
||||
vector_dimension = EmbeddingFunctions.GPT4ALL.value
|
||||
self.set_vector_dimension(vector_dimension=vector_dimension)
|
||||
@@ -0,0 +1,19 @@
|
||||
from typing import Optional
|
||||
|
||||
from langchain.embeddings import HuggingFaceEmbeddings
|
||||
|
||||
from embedchain.config import BaseEmbedderConfig
|
||||
from embedchain.embedder.base_embedder import BaseEmbedder
|
||||
from embedchain.models import EmbeddingFunctions
|
||||
|
||||
|
||||
class HuggingFaceEmbedder(BaseEmbedder):
|
||||
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
|
||||
super().__init__(config=config)
|
||||
|
||||
embeddings = HuggingFaceEmbeddings(model_name=self.config.model)
|
||||
embedding_fn = BaseEmbedder._langchain_default_concept(embeddings)
|
||||
self.set_embedding_fn(embedding_fn=embedding_fn)
|
||||
|
||||
vector_dimension = EmbeddingFunctions.HUGGING_FACE.value
|
||||
self.set_vector_dimension(vector_dimension=vector_dimension)
|
||||
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from langchain.embeddings import OpenAIEmbeddings
|
||||
|
||||
from embedchain.config import BaseEmbedderConfig
|
||||
from embedchain.embedder.base_embedder import BaseEmbedder
|
||||
from embedchain.models import EmbeddingFunctions
|
||||
|
||||
try:
|
||||
from chromadb.utils import embedding_functions
|
||||
except RuntimeError:
|
||||
from embedchain.utils import use_pysqlite3
|
||||
|
||||
use_pysqlite3()
|
||||
from chromadb.utils import embedding_functions
|
||||
|
||||
|
||||
class OpenAiEmbedder(BaseEmbedder):
|
||||
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
|
||||
super().__init__(config=config)
|
||||
if self.config.model is None:
|
||||
self.config.model = "text-embedding-ada-002"
|
||||
|
||||
if self.config.deployment_name:
|
||||
embeddings = OpenAIEmbeddings(deployment=self.config.deployment_name)
|
||||
embedding_fn = BaseEmbedder._langchain_default_concept(embeddings)
|
||||
else:
|
||||
if os.getenv("OPENAI_API_KEY") is None and os.getenv("OPENAI_ORGANIZATION") is None:
|
||||
raise ValueError(
|
||||
"OPENAI_API_KEY or OPENAI_ORGANIZATION environment variables not provided"
|
||||
) # noqa:E501
|
||||
embedding_fn = embedding_functions.OpenAIEmbeddingFunction(
|
||||
api_key=os.getenv("OPENAI_API_KEY"),
|
||||
organization_id=os.getenv("OPENAI_ORGANIZATION"),
|
||||
model_name=self.config.model,
|
||||
)
|
||||
|
||||
self.set_embedding_fn(embedding_fn=embedding_fn)
|
||||
self.set_vector_dimension(vector_dimension=EmbeddingFunctions.OPENAI.value)
|
||||
@@ -0,0 +1,19 @@
|
||||
from typing import Optional
|
||||
|
||||
from langchain.embeddings import VertexAIEmbeddings
|
||||
|
||||
from embedchain.config import BaseEmbedderConfig
|
||||
from embedchain.embedder.base_embedder import BaseEmbedder
|
||||
from embedchain.models import EmbeddingFunctions
|
||||
|
||||
|
||||
class VertexAiEmbedder(BaseEmbedder):
|
||||
def __init__(self, config: Optional[BaseEmbedderConfig] = None):
|
||||
super().__init__(config=config)
|
||||
|
||||
embeddings = VertexAIEmbeddings(model_name=config.model)
|
||||
embedding_fn = BaseEmbedder._langchain_default_concept(embeddings)
|
||||
self.set_embedding_fn(embedding_fn=embedding_fn)
|
||||
|
||||
vector_dimension = EmbeddingFunctions.VERTEX_AI.value
|
||||
self.set_vector_dimension(vector_dimension=vector_dimension)
|
||||
@@ -0,0 +1,180 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, Type, TypeVar, Union
|
||||
|
||||
T = TypeVar("T", bound="JSONSerializable")
|
||||
|
||||
# NOTE: Through inheritance, all of our classes should be children of JSONSerializable. (highest level)
|
||||
# NOTE: The @register_deserializable decorator should be added to all user facing child classes. (lowest level)
|
||||
|
||||
|
||||
def register_deserializable(cls: Type[T]) -> Type[T]:
|
||||
"""
|
||||
A class decorator to register a class as deserializable.
|
||||
|
||||
When a class is decorated with @register_deserializable, it becomes
|
||||
a part of the set of classes that the JSONSerializable class can
|
||||
deserialize.
|
||||
|
||||
Deserialization is in essence loading attributes from a json file.
|
||||
This decorator is a security measure put in place to make sure that
|
||||
you don't load attributes that were initially part of another class.
|
||||
|
||||
Example:
|
||||
@register_deserializable
|
||||
class ChildClass(JSONSerializable):
|
||||
def __init__(self, ...):
|
||||
# initialization logic
|
||||
|
||||
Args:
|
||||
cls (Type): The class to be registered.
|
||||
|
||||
Returns:
|
||||
Type: The same class, after registration.
|
||||
"""
|
||||
JSONSerializable.register_class_as_deserializable(cls)
|
||||
return cls
|
||||
|
||||
|
||||
class JSONSerializable:
|
||||
"""
|
||||
A class to represent a JSON serializable object.
|
||||
|
||||
This class provides methods to serialize and deserialize objects,
|
||||
as well as save serialized objects to a file and load them back.
|
||||
"""
|
||||
|
||||
_deserializable_classes = set() # Contains classes that are whitelisted for deserialization.
|
||||
|
||||
def serialize(self) -> str:
|
||||
"""
|
||||
Serialize the object to a JSON-formatted string.
|
||||
|
||||
Returns:
|
||||
str: A JSON string representation of the object.
|
||||
"""
|
||||
try:
|
||||
return json.dumps(self, default=self._auto_encoder, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logging.error(f"Serialization error: {e}")
|
||||
return "{}"
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, json_str: str) -> Any:
|
||||
"""
|
||||
Deserialize a JSON-formatted string to an object.
|
||||
If it fails, a default class is returned instead.
|
||||
Note: This *returns* an instance, it's not automatically loaded on the calling class.
|
||||
|
||||
Example:
|
||||
app = App.deserialize(json_str)
|
||||
|
||||
Args:
|
||||
json_str (str): A JSON string representation of an object.
|
||||
|
||||
Returns:
|
||||
Object: The deserialized object.
|
||||
"""
|
||||
try:
|
||||
return json.loads(json_str, object_hook=cls._auto_decoder)
|
||||
except Exception as e:
|
||||
logging.error(f"Deserialization error: {e}")
|
||||
# Return a default instance in case of failure
|
||||
return cls()
|
||||
|
||||
@staticmethod
|
||||
def _auto_encoder(obj: Any) -> Union[Dict[str, Any], None]:
|
||||
"""
|
||||
Automatically encode an object for JSON serialization.
|
||||
|
||||
Args:
|
||||
obj (Object): The object to be encoded.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary representation of the object.
|
||||
"""
|
||||
if hasattr(obj, "__dict__"):
|
||||
dct = obj.__dict__.copy()
|
||||
for key, value in list(
|
||||
dct.items()
|
||||
): # We use list() to get a copy of items to avoid dictionary size change during iteration.
|
||||
try:
|
||||
# Recursive: If the value is an instance of a subclass of JSONSerializable,
|
||||
# serialize it using the JSONSerializable serialize method.
|
||||
if isinstance(value, JSONSerializable):
|
||||
serialized_value = value.serialize()
|
||||
# The value is stored as a serialized string.
|
||||
dct[key] = json.loads(serialized_value)
|
||||
else:
|
||||
json.dumps(value) # Try to serialize the value.
|
||||
except TypeError:
|
||||
del dct[key] # If it fails, remove the key-value pair from the dictionary.
|
||||
|
||||
dct["__class__"] = obj.__class__.__name__
|
||||
return dct
|
||||
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
||||
|
||||
@classmethod
|
||||
def _auto_decoder(cls, dct: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Automatically decode a dictionary to an object during JSON deserialization.
|
||||
|
||||
Args:
|
||||
dct (dict): The dictionary representation of an object.
|
||||
|
||||
Returns:
|
||||
Object: The decoded object or the original dictionary if decoding is not possible.
|
||||
"""
|
||||
class_name = dct.pop("__class__", None)
|
||||
if class_name:
|
||||
if not hasattr(cls, "_deserializable_classes"): # Additional safety check
|
||||
raise AttributeError(f"`{class_name}` has no registry of allowed deserializations.")
|
||||
if class_name not in {cl.__name__ for cl in cls._deserializable_classes}:
|
||||
raise KeyError(f"Deserialization of class `{class_name}` is not allowed.")
|
||||
target_class = next((cl for cl in cls._deserializable_classes if cl.__name__ == class_name), None)
|
||||
if target_class:
|
||||
obj = target_class.__new__(target_class)
|
||||
for key, value in dct.items():
|
||||
default_value = getattr(target_class, key, None)
|
||||
setattr(obj, key, value or default_value)
|
||||
return obj
|
||||
return dct
|
||||
|
||||
def save_to_file(self, filename: str) -> None:
|
||||
"""
|
||||
Save the serialized object to a file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the file where the object should be saved.
|
||||
"""
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(self.serialize())
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls, filename: str) -> Any:
|
||||
"""
|
||||
Load and deserialize an object from a file.
|
||||
|
||||
Args:
|
||||
filename (str): The path to the file from which the object should be loaded.
|
||||
|
||||
Returns:
|
||||
Object: The deserialized object.
|
||||
"""
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
json_str = f.read()
|
||||
return cls.deserialize(json_str)
|
||||
|
||||
@classmethod
|
||||
def register_class_as_deserializable(cls, target_class: Type[T]) -> None:
|
||||
"""
|
||||
Register a class as deserializable. This is a classmethod and globally shared.
|
||||
|
||||
This method adds the target class to the set of classes that
|
||||
can be deserialized. This is a security measure to ensure only
|
||||
whitelisted classes are deserialized.
|
||||
|
||||
Args:
|
||||
target_class (Type): The class to be registered.
|
||||
"""
|
||||
cls._deserializable_classes.add(target_class)
|
||||
@@ -0,0 +1,28 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config import BaseLlmConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class AntrophicLlm(BaseLlm):
|
||||
def __init__(self, config: Optional[BaseLlmConfig] = None):
|
||||
super().__init__(config=config)
|
||||
|
||||
def get_llm_model_answer(self, prompt):
|
||||
return AntrophicLlm._get_athrophic_answer(prompt=prompt, config=self.config)
|
||||
|
||||
@staticmethod
|
||||
def _get_athrophic_answer(prompt: str, config: BaseLlmConfig) -> str:
|
||||
from langchain.chat_models import ChatAnthropic
|
||||
|
||||
chat = ChatAnthropic(temperature=config.temperature, model=config.model)
|
||||
|
||||
if config.max_tokens and config.max_tokens != 1000:
|
||||
logging.warning("Config option `max_tokens` is not supported by this model.")
|
||||
|
||||
messages = BaseLlm._get_messages(prompt, system_prompt=config.system_prompt)
|
||||
|
||||
return chat(messages).content
|
||||
@@ -0,0 +1,38 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config import BaseLlmConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class AzureOpenAiLlm(BaseLlm):
|
||||
def __init__(self, config: Optional[BaseLlmConfig] = None):
|
||||
super().__init__(config=config)
|
||||
|
||||
def get_llm_model_answer(self, prompt):
|
||||
return AzureOpenAiLlm._get_azure_openai_answer(prompt=prompt, config=self.config)
|
||||
|
||||
@staticmethod
|
||||
def _get_azure_openai_answer(prompt: str, config: BaseLlmConfig) -> 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=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,
|
||||
)
|
||||
|
||||
if config.top_p and config.top_p != 1:
|
||||
logging.warning("Config option `top_p` is not supported by this model.")
|
||||
|
||||
messages = BaseLlm._get_messages(prompt, system_prompt=config.system_prompt)
|
||||
|
||||
return chat(messages).content
|
||||
@@ -0,0 +1,272 @@
|
||||
import logging
|
||||
from typing import Any, Dict, Generator, List, Optional
|
||||
|
||||
from langchain.memory import ConversationBufferMemory
|
||||
from langchain.schema import BaseMessage
|
||||
|
||||
from embedchain.config import BaseLlmConfig
|
||||
from embedchain.config.llm.base_llm_config import (
|
||||
DEFAULT_PROMPT, DEFAULT_PROMPT_WITH_HISTORY_TEMPLATE,
|
||||
DOCS_SITE_PROMPT_TEMPLATE)
|
||||
from embedchain.helper_classes.json_serializable import JSONSerializable
|
||||
|
||||
|
||||
class BaseLlm(JSONSerializable):
|
||||
def __init__(self, config: Optional[BaseLlmConfig] = None):
|
||||
"""Initialize a base LLM class
|
||||
|
||||
:param config: LLM configuration option class, defaults to None
|
||||
:type config: Optional[BaseLlmConfig], optional
|
||||
"""
|
||||
if config is None:
|
||||
self.config = BaseLlmConfig()
|
||||
else:
|
||||
self.config = config
|
||||
|
||||
self.memory = ConversationBufferMemory()
|
||||
self.is_docs_site_instance = False
|
||||
self.online = False
|
||||
self.history: Any = None
|
||||
|
||||
def get_llm_model_answer(self):
|
||||
"""
|
||||
Usually implemented by child class
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_history(self, history: Any):
|
||||
"""
|
||||
Provide your own history.
|
||||
Especially interesting for the query method, which does not internally manage conversation history.
|
||||
|
||||
:param history: History to set
|
||||
:type history: Any
|
||||
"""
|
||||
self.history = history
|
||||
|
||||
def update_history(self):
|
||||
"""Update class history attribute with history in memory (for chat method)"""
|
||||
chat_history = self.memory.load_memory_variables({})["history"]
|
||||
if chat_history:
|
||||
self.set_history(chat_history)
|
||||
|
||||
def generate_prompt(self, input_query: str, contexts: List[str], **kwargs: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Generates a prompt based on the given query and context, ready to be
|
||||
passed to an LLM
|
||||
|
||||
:param input_query: The query to use.
|
||||
:type input_query: str
|
||||
:param contexts: List of similar documents to the query used as context.
|
||||
:type contexts: List[str]
|
||||
:return: The prompt
|
||||
:rtype: str
|
||||
"""
|
||||
context_string = (" | ").join(contexts)
|
||||
web_search_result = kwargs.get("web_search_result", "")
|
||||
if web_search_result:
|
||||
context_string = self._append_search_and_context(context_string, web_search_result)
|
||||
if not self.history:
|
||||
prompt = self.config.template.substitute(context=context_string, query=input_query)
|
||||
else:
|
||||
# check if it's the default template without history
|
||||
if (
|
||||
not self.config._validate_template_history(self.config.template)
|
||||
and self.config.template.template == DEFAULT_PROMPT
|
||||
):
|
||||
# swap in the template with history
|
||||
prompt = DEFAULT_PROMPT_WITH_HISTORY_TEMPLATE.substitute(
|
||||
context=context_string, query=input_query, history=self.history
|
||||
)
|
||||
elif not self.config._validate_template_history(self.config.template):
|
||||
logging.warning("Template does not include `$history` key. History is not included in prompt.")
|
||||
prompt = self.config.template.substitute(context=context_string, query=input_query)
|
||||
else:
|
||||
prompt = self.config.template.substitute(
|
||||
context=context_string, query=input_query, history=self.history
|
||||
)
|
||||
return prompt
|
||||
|
||||
def _append_search_and_context(self, context: str, web_search_result: str) -> str:
|
||||
"""Append web search context to existing context
|
||||
|
||||
:param context: Existing context
|
||||
:type context: str
|
||||
:param web_search_result: Web search result
|
||||
:type web_search_result: str
|
||||
:return: Concatenated web search result
|
||||
:rtype: str
|
||||
"""
|
||||
return f"{context}\nWeb Search Result: {web_search_result}"
|
||||
|
||||
def get_answer_from_llm(self, prompt: str):
|
||||
"""
|
||||
Gets an answer based on the given query and context by passing it
|
||||
to an LLM.
|
||||
|
||||
:param prompt: Gets an answer based on the given query and context by passing it to an LLM.
|
||||
:type prompt: str
|
||||
:return: The answer.
|
||||
:rtype: _type_
|
||||
"""
|
||||
return self.get_llm_model_answer(prompt)
|
||||
|
||||
def access_search_and_get_results(self, input_query: str):
|
||||
"""
|
||||
Search the internet for additional context
|
||||
|
||||
:param input_query: search query
|
||||
:type input_query: str
|
||||
:return: Search results
|
||||
:rtype: Unknown
|
||||
"""
|
||||
from langchain.tools import DuckDuckGoSearchRun
|
||||
|
||||
search = DuckDuckGoSearchRun()
|
||||
logging.info(f"Access search to get answers for {input_query}")
|
||||
return search.run(input_query)
|
||||
|
||||
def _stream_query_response(self, answer: Any) -> Generator[Any, Any, None]:
|
||||
"""Generator to be used as streaming response
|
||||
|
||||
:param answer: Answer chunk from llm
|
||||
:type answer: Any
|
||||
:yield: Answer chunk from llm
|
||||
:rtype: Generator[Any, Any, None]
|
||||
"""
|
||||
streamed_answer = ""
|
||||
for chunk in answer:
|
||||
streamed_answer = streamed_answer + chunk
|
||||
yield chunk
|
||||
logging.info(f"Answer: {streamed_answer}")
|
||||
|
||||
def _stream_chat_response(self, answer: Any) -> Generator[Any, Any, None]:
|
||||
"""Generator to be used as streaming response
|
||||
|
||||
:param answer: Answer chunk from llm
|
||||
:type answer: Any
|
||||
:yield: Answer chunk from llm
|
||||
:rtype: Generator[Any, Any, None]
|
||||
"""
|
||||
streamed_answer = ""
|
||||
for chunk in answer:
|
||||
streamed_answer = streamed_answer + chunk
|
||||
yield chunk
|
||||
self.memory.chat_memory.add_ai_message(streamed_answer)
|
||||
logging.info(f"Answer: {streamed_answer}")
|
||||
|
||||
def query(self, input_query: str, contexts: List[str], config: BaseLlmConfig = None, dry_run=False):
|
||||
"""
|
||||
Queries the vector database based on the given input query.
|
||||
Gets relevant doc based on the query and then passes it to an
|
||||
LLM as context to get the answer.
|
||||
|
||||
:param input_query: The query to use.
|
||||
:type input_query: str
|
||||
:param contexts: Embeddings retrieved from the database to be used as context.
|
||||
:type contexts: List[str]
|
||||
:param config: The `LlmConfig` instance to use as configuration options. This is used for one method call.
|
||||
To persistently use a config, declare it during app init., defaults to None
|
||||
:type config: Optional[BaseLlmConfig], optional
|
||||
:param dry_run: A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response., defaults to False
|
||||
:type dry_run: bool, optional
|
||||
:return: The answer to the query or the dry run result
|
||||
:rtype: str
|
||||
"""
|
||||
query_config = config or self.config
|
||||
|
||||
if self.is_docs_site_instance:
|
||||
query_config.template = DOCS_SITE_PROMPT_TEMPLATE
|
||||
query_config.number_documents = 5
|
||||
k = {}
|
||||
if self.online:
|
||||
k["web_search_result"] = self.access_search_and_get_results(input_query)
|
||||
prompt = self.generate_prompt(input_query, contexts, **k)
|
||||
logging.info(f"Prompt: {prompt}")
|
||||
|
||||
if dry_run:
|
||||
return prompt
|
||||
|
||||
answer = self.get_answer_from_llm(prompt)
|
||||
|
||||
if isinstance(answer, str):
|
||||
logging.info(f"Answer: {answer}")
|
||||
return answer
|
||||
else:
|
||||
return self._stream_query_response(answer)
|
||||
|
||||
def chat(self, input_query: str, contexts: List[str], config: BaseLlmConfig = None, dry_run=False):
|
||||
"""
|
||||
Queries the vector database on the given input query.
|
||||
Gets relevant doc based on the query and then passes it to an
|
||||
LLM as context to get the answer.
|
||||
|
||||
Maintains the whole conversation in memory.
|
||||
|
||||
:param input_query: The query to use.
|
||||
:type input_query: str
|
||||
:param contexts: Embeddings retrieved from the database to be used as context.
|
||||
:type contexts: List[str]
|
||||
:param config: The `LlmConfig` instance to use as configuration options. This is used for one method call.
|
||||
To persistently use a config, declare it during app init., defaults to None
|
||||
:type config: Optional[BaseLlmConfig], optional
|
||||
:param dry_run: A dry run does everything except send the resulting prompt to
|
||||
the LLM. The purpose is to test the prompt, not the response., defaults to False
|
||||
:type dry_run: bool, optional
|
||||
:return: The answer to the query or the dry run result
|
||||
:rtype: str
|
||||
"""
|
||||
query_config = config or self.config
|
||||
|
||||
if self.is_docs_site_instance:
|
||||
query_config.template = DOCS_SITE_PROMPT_TEMPLATE
|
||||
query_config.number_documents = 5
|
||||
k = {}
|
||||
if self.online:
|
||||
k["web_search_result"] = self.access_search_and_get_results(input_query)
|
||||
|
||||
self.update_history()
|
||||
|
||||
prompt = self.generate_prompt(input_query, contexts, **k)
|
||||
logging.info(f"Prompt: {prompt}")
|
||||
|
||||
if dry_run:
|
||||
return prompt
|
||||
|
||||
answer = self.get_answer_from_llm(prompt)
|
||||
|
||||
self.memory.chat_memory.add_user_message(input_query)
|
||||
|
||||
if isinstance(answer, str):
|
||||
self.memory.chat_memory.add_ai_message(answer)
|
||||
logging.info(f"Answer: {answer}")
|
||||
|
||||
# NOTE: Adding to history before and after. This could be seen as redundant.
|
||||
# If we change it, we have to change the tests (no big deal).
|
||||
self.update_history()
|
||||
|
||||
return answer
|
||||
else:
|
||||
# this is a streamed response and needs to be handled differently.
|
||||
return self._stream_chat_response(answer)
|
||||
|
||||
@staticmethod
|
||||
def _get_messages(prompt: str, system_prompt: Optional[str] = None) -> List[BaseMessage]:
|
||||
"""
|
||||
Construct a list of langchain messages
|
||||
|
||||
:param prompt: User prompt
|
||||
:type prompt: str
|
||||
:param system_prompt: System prompt, defaults to None
|
||||
:type system_prompt: Optional[str], optional
|
||||
:return: List of messages
|
||||
:rtype: List[BaseMessage]
|
||||
"""
|
||||
from langchain.schema import HumanMessage, SystemMessage
|
||||
|
||||
messages = []
|
||||
if system_prompt:
|
||||
messages.append(SystemMessage(content=system_prompt))
|
||||
messages.append(HumanMessage(content=prompt))
|
||||
return messages
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import Iterable, Optional, Union
|
||||
|
||||
from embedchain.config import BaseLlmConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class GPT4ALLLlm(BaseLlm):
|
||||
def __init__(self, config: Optional[BaseLlmConfig] = None):
|
||||
super().__init__(config=config)
|
||||
if self.config.model is None:
|
||||
self.config.model = "orca-mini-3b.ggmlv3.q4_0.bin"
|
||||
self.instance = GPT4ALLLlm._get_instance(self.config.model)
|
||||
|
||||
def get_llm_model_answer(self, prompt):
|
||||
return self._get_gpt4all_answer(prompt=prompt, config=self.config)
|
||||
|
||||
@staticmethod
|
||||
def _get_instance(model):
|
||||
try:
|
||||
from gpt4all import GPT4All
|
||||
except ModuleNotFoundError:
|
||||
raise ModuleNotFoundError(
|
||||
"The GPT4All python package is not installed. Please install it with `pip install embedchain[opensource]`" # noqa E501
|
||||
) from None
|
||||
|
||||
return GPT4All(model_name=model)
|
||||
|
||||
def _get_gpt4all_answer(self, prompt: str, config: BaseLlmConfig) -> Union[str, Iterable]:
|
||||
if config.model and config.model != self.config.model:
|
||||
raise RuntimeError(
|
||||
"OpenSourceApp does not support switching models at runtime. Please create a new app instance."
|
||||
)
|
||||
|
||||
if config.system_prompt:
|
||||
raise ValueError("OpenSourceApp does not support `system_prompt`")
|
||||
|
||||
response = self.instance.generate(
|
||||
prompt=prompt,
|
||||
streaming=config.stream,
|
||||
top_p=config.top_p,
|
||||
max_tokens=config.max_tokens,
|
||||
temp=config.temperature,
|
||||
)
|
||||
return response
|
||||
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from langchain.llms import Replicate
|
||||
|
||||
from embedchain.config import BaseLlmConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class Llama2Llm(BaseLlm):
|
||||
def __init__(self, config: Optional[BaseLlmConfig] = None):
|
||||
if "REPLICATE_API_TOKEN" not in os.environ:
|
||||
raise ValueError("Please set the REPLICATE_API_TOKEN environment variable.")
|
||||
super().__init__(config=config)
|
||||
|
||||
def get_llm_model_answer(self, prompt):
|
||||
# TODO: Move the model and other inputs into config
|
||||
if self.config.system_prompt:
|
||||
raise ValueError("Llama2App does not support `system_prompt`")
|
||||
llm = Replicate(
|
||||
model="a16z-infra/llama13b-v2-chat:df7690f1994d94e96ad9d568eac121aecf50684a0b0963b25a41cc40061269e5",
|
||||
input={"temperature": self.config.temperature or 0.75, "max_length": 500, "top_p": self.config.top_p},
|
||||
)
|
||||
return llm(prompt)
|
||||
@@ -0,0 +1,42 @@
|
||||
from typing import Optional
|
||||
|
||||
import openai
|
||||
|
||||
from embedchain.config import BaseLlmConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class OpenAiLlm(BaseLlm):
|
||||
def __init__(self, config: Optional[BaseLlmConfig] = None):
|
||||
super().__init__(config=config)
|
||||
|
||||
# NOTE: This class does not use langchain. One reason is that `top_p` is not supported.
|
||||
|
||||
def get_llm_model_answer(self, prompt):
|
||||
messages = []
|
||||
if self.config.system_prompt:
|
||||
messages.append({"role": "system", "content": self.config.system_prompt})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
response = openai.ChatCompletion.create(
|
||||
model=self.config.model or "gpt-3.5-turbo-0613",
|
||||
messages=messages,
|
||||
temperature=self.config.temperature,
|
||||
max_tokens=self.config.max_tokens,
|
||||
top_p=self.config.top_p,
|
||||
stream=self.config.stream,
|
||||
)
|
||||
|
||||
if self.config.stream:
|
||||
return self._stream_llm_model_response(response)
|
||||
else:
|
||||
return response["choices"][0]["message"]["content"]
|
||||
|
||||
def _stream_llm_model_response(self, response):
|
||||
"""
|
||||
This is a generator for streaming response from the OpenAI completions API
|
||||
"""
|
||||
for line in response:
|
||||
chunk = line["choices"][0].get("delta", {}).get("content", "")
|
||||
yield chunk
|
||||
@@ -0,0 +1,28 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from embedchain.config import BaseLlmConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class VertexAiLlm(BaseLlm):
|
||||
def __init__(self, config: Optional[BaseLlmConfig] = None):
|
||||
super().__init__(config=config)
|
||||
|
||||
def get_llm_model_answer(self, prompt):
|
||||
return VertexAiLlm._get_athrophic_answer(prompt=prompt, config=self.config)
|
||||
|
||||
@staticmethod
|
||||
def _get_athrophic_answer(prompt: str, config: BaseLlmConfig) -> str:
|
||||
from langchain.chat_models import ChatVertexAI
|
||||
|
||||
chat = ChatVertexAI(temperature=config.temperature, model=config.model)
|
||||
|
||||
if config.top_p and config.top_p != 1:
|
||||
logging.warning("Config option `top_p` is not supported by this model.")
|
||||
|
||||
messages = BaseLlm._get_messages(prompt, system_prompt=config.system_prompt)
|
||||
|
||||
return chat(messages).content
|
||||
@@ -1,4 +1,7 @@
|
||||
class BaseLoader:
|
||||
from embedchain.helper_classes.json_serializable import JSONSerializable
|
||||
|
||||
|
||||
class BaseLoader(JSONSerializable):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import csv
|
||||
from io import StringIO
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
|
||||
|
||||
class CsvLoader(BaseLoader):
|
||||
@staticmethod
|
||||
def _detect_delimiter(first_line):
|
||||
delimiters = [",", "\t", ";", "|"]
|
||||
counts = {delimiter: first_line.count(delimiter) for delimiter in delimiters}
|
||||
return max(counts, key=counts.get)
|
||||
|
||||
@staticmethod
|
||||
def _get_file_content(content):
|
||||
url = urlparse(content)
|
||||
if all([url.scheme, url.netloc]) and url.scheme not in ["file", "http", "https"]:
|
||||
raise ValueError("Not a valid URL.")
|
||||
|
||||
if url.scheme in ["http", "https"]:
|
||||
response = requests.get(content)
|
||||
response.raise_for_status()
|
||||
return StringIO(response.text)
|
||||
elif url.scheme == "file":
|
||||
path = url.path
|
||||
return open(path, newline="") # Open the file using the path from the URI
|
||||
else:
|
||||
return open(content, newline="") # Treat content as a regular file path
|
||||
|
||||
@staticmethod
|
||||
def load_data(content):
|
||||
"""Load a csv file with headers. Each line is a document"""
|
||||
result = []
|
||||
|
||||
with CsvLoader._get_file_content(content) as file:
|
||||
first_line = file.readline()
|
||||
delimiter = CsvLoader._detect_delimiter(first_line)
|
||||
file.seek(0) # Reset the file pointer to the start
|
||||
reader = csv.DictReader(file, delimiter=delimiter)
|
||||
for i, row in enumerate(reader):
|
||||
line = ", ".join([f"{field}: {value}" for field, value in row.items()])
|
||||
result.append({"content": line, "meta_data": {"url": content, "row": i + 1}})
|
||||
return result
|
||||
@@ -4,9 +4,11 @@ from urllib.parse import urljoin, urlparse
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class DocsSiteLoader(BaseLoader):
|
||||
def __init__(self):
|
||||
self.visited_links = set()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from langchain.document_loaders import Docx2txtLoader
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class DocxFileLoader(BaseLoader):
|
||||
def load_data(self, url):
|
||||
"""Load data from a .docx file."""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class LocalQnaPairLoader(BaseLoader):
|
||||
def load_data(self, content):
|
||||
"""Load data from a local QnA pair."""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class LocalTextLoader(BaseLoader):
|
||||
def load_data(self, content):
|
||||
"""Load data from a local text file."""
|
||||
|
||||
@@ -7,10 +7,12 @@ except ImportError:
|
||||
raise ImportError("Notion requires extra dependencies. Install with `pip install embedchain[community]`") from None
|
||||
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
from embedchain.utils import clean_string
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class NotionLoader(BaseLoader):
|
||||
def load_data(self, source):
|
||||
"""Load data from a PDF file."""
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from langchain.document_loaders import PyPDFLoader
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
from embedchain.utils import clean_string
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class PdfFileLoader(BaseLoader):
|
||||
def load_data(self, url):
|
||||
"""Load data from a PDF file."""
|
||||
|
||||
@@ -4,11 +4,13 @@ import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from bs4.builder import ParserRejectedMarkup
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
from embedchain.loaders.web_page import WebPageLoader
|
||||
from embedchain.utils import is_readable
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class SitemapLoader(BaseLoader):
|
||||
def load_data(self, sitemap_url):
|
||||
"""
|
||||
|
||||
@@ -3,10 +3,12 @@ import logging
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
from embedchain.utils import clean_string
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class WebPageLoader(BaseLoader):
|
||||
def load_data(self, url):
|
||||
"""Load data from a web page."""
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from langchain.document_loaders import YoutubeLoader
|
||||
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.loaders.base_loader import BaseLoader
|
||||
from embedchain.utils import clean_string
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class YoutubeVideoLoader(BaseLoader):
|
||||
def load_data(self, url):
|
||||
"""Load data from a Youtube video."""
|
||||
|
||||
@@ -11,3 +11,4 @@ class DataType(Enum):
|
||||
TEXT = "text"
|
||||
QNA_PAIR = "qna_pair"
|
||||
NOTION = "notion"
|
||||
CSV = "csv"
|
||||
|
||||
+8
-9
@@ -93,15 +93,6 @@ def use_pysqlite3():
|
||||
"Error:",
|
||||
e,
|
||||
)
|
||||
__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}.",
|
||||
)
|
||||
|
||||
|
||||
def format_source(source: str, limit: int = 20) -> str:
|
||||
@@ -156,6 +147,10 @@ def detect_datatype(source: Any) -> DataType:
|
||||
logging.debug(f"Source of `{formatted_source}` detected as `sitemap`.")
|
||||
return DataType.SITEMAP
|
||||
|
||||
if url.path.endswith(".csv"):
|
||||
logging.debug(f"Source of `{formatted_source}` detected as `csv`.")
|
||||
return DataType.CSV
|
||||
|
||||
if url.path.endswith(".docx"):
|
||||
logging.debug(f"Source of `{formatted_source}` detected as `docx`.")
|
||||
return DataType.DOCX
|
||||
@@ -191,6 +186,10 @@ def detect_datatype(source: Any) -> DataType:
|
||||
logging.debug(f"Source of `{formatted_source}` detected as `docx`.")
|
||||
return DataType.DOCX
|
||||
|
||||
if source.endswith(".csv"):
|
||||
logging.debug(f"Source of `{formatted_source}` detected as `csv`.")
|
||||
return DataType.CSV
|
||||
|
||||
# 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(
|
||||
|
||||
@@ -1,27 +1,77 @@
|
||||
class BaseVectorDB:
|
||||
from embedchain.config.vectordbs.BaseVectorDbConfig import BaseVectorDbConfig
|
||||
from embedchain.embedder.base_embedder import BaseEmbedder
|
||||
from embedchain.helper_classes.json_serializable import JSONSerializable
|
||||
|
||||
|
||||
class BaseVectorDB(JSONSerializable):
|
||||
"""Base class for vector database."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config: BaseVectorDbConfig):
|
||||
"""Initialize the database. Save the config and client as an attribute.
|
||||
|
||||
:param config: Database configuration class instance.
|
||||
:type config: BaseVectorDbConfig
|
||||
"""
|
||||
self.client = self._get_or_create_db()
|
||||
self.config: BaseVectorDbConfig = config
|
||||
|
||||
def _initialize(self):
|
||||
"""
|
||||
This method is needed because `embedder` attribute needs to be set externally before it can be initialized.
|
||||
|
||||
So it's can't be done in __init__ in one step.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_or_create_db(self):
|
||||
"""Get or create the database."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_or_create_collection(self):
|
||||
"""Get or create a named collection."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _set_embedder(self, embedder: BaseEmbedder):
|
||||
"""
|
||||
The database needs to access the embedder sometimes, with this method you can persistently set it.
|
||||
|
||||
:param embedder: Embedder to be set as the embedder for this database.
|
||||
:type embedder: BaseEmbedder
|
||||
"""
|
||||
self.embedder = embedder
|
||||
|
||||
def get(self):
|
||||
"""Get database embeddings by id."""
|
||||
raise NotImplementedError
|
||||
|
||||
def add(self):
|
||||
"""Add to database"""
|
||||
raise NotImplementedError
|
||||
|
||||
def query(self):
|
||||
"""Query contents from vector data base based on vector similarity"""
|
||||
raise NotImplementedError
|
||||
|
||||
def count(self):
|
||||
def count(self) -> int:
|
||||
"""
|
||||
Count number of documents/chunks embedded in the database.
|
||||
|
||||
:return: number of documents
|
||||
:rtype: int
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the database. Deletes all embeddings irreversibly.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_collection_name(self, name: str):
|
||||
"""
|
||||
Set the name of the collection. A collection is an isolated space for vectors.
|
||||
|
||||
:param name: Name of the collection.
|
||||
:type name: str
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -1,62 +1,102 @@
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from chromadb.errors import InvalidDimensionException
|
||||
from chromadb import Collection, QueryResult
|
||||
from langchain.docstore.document import Document
|
||||
|
||||
from embedchain.config import ChromaDbConfig
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
|
||||
try:
|
||||
import chromadb
|
||||
from chromadb.config import Settings
|
||||
from chromadb.errors import InvalidDimensionException
|
||||
except RuntimeError:
|
||||
from embedchain.utils import use_pysqlite3
|
||||
|
||||
use_pysqlite3()
|
||||
import chromadb
|
||||
|
||||
from chromadb.config import Settings
|
||||
|
||||
from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
from chromadb.config import Settings
|
||||
from chromadb.errors import InvalidDimensionException
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class ChromaDB(BaseVectorDB):
|
||||
"""Vector database using ChromaDB."""
|
||||
|
||||
def __init__(self, db_dir=None, embedding_fn=None, host=None, port=None):
|
||||
self.embedding_fn = embedding_fn
|
||||
def __init__(self, config: Optional[ChromaDbConfig] = None):
|
||||
"""Initialize a new ChromaDB instance
|
||||
|
||||
if not hasattr(embedding_fn, "__call__"):
|
||||
raise ValueError("Embedding function is not a function")
|
||||
|
||||
if host and port:
|
||||
logging.info(f"Connecting to ChromaDB server: {host}:{port}")
|
||||
self.settings = Settings(chroma_server_host=host, chroma_server_http_port=port)
|
||||
self.client = chromadb.HttpClient(self.settings)
|
||||
:param config: Configuration options for Chroma, defaults to None
|
||||
:type config: Optional[ChromaDbConfig], optional
|
||||
"""
|
||||
if config:
|
||||
self.config = config
|
||||
else:
|
||||
if db_dir is None:
|
||||
db_dir = "db"
|
||||
self.settings = Settings(anonymized_telemetry=False, allow_reset=True)
|
||||
self.client = chromadb.PersistentClient(
|
||||
path=db_dir,
|
||||
settings=self.settings,
|
||||
)
|
||||
super().__init__()
|
||||
self.config = ChromaDbConfig()
|
||||
|
||||
self.settings = Settings()
|
||||
if self.config.chroma_settings:
|
||||
for key, value in self.config.chroma_settings.items():
|
||||
if hasattr(self.settings, key):
|
||||
setattr(self.settings, key, value)
|
||||
|
||||
if self.config.host and self.config.port:
|
||||
logging.info(f"Connecting to ChromaDB server: {self.config.host}:{self.config.port}")
|
||||
self.settings.chroma_server_host = self.config.host
|
||||
self.settings.chroma_server_http_port = self.config.port
|
||||
self.settings.chroma_api_impl = "chromadb.api.fastapi.FastAPI"
|
||||
else:
|
||||
if self.config.dir is None:
|
||||
self.config.dir = "db"
|
||||
|
||||
self.settings.persist_directory = self.config.dir
|
||||
self.settings.is_persistent = True
|
||||
|
||||
self.client = chromadb.Client(self.settings)
|
||||
super().__init__(config=self.config)
|
||||
|
||||
def _initialize(self):
|
||||
"""
|
||||
This method is needed because `embedder` attribute needs to be set externally before it can be initialized.
|
||||
"""
|
||||
if not self.embedder:
|
||||
raise ValueError("Embedder not set. Please set an embedder with `set_embedder` before initialization.")
|
||||
self._get_or_create_collection(self.config.collection_name)
|
||||
|
||||
def _get_or_create_db(self):
|
||||
"""Get or create the database."""
|
||||
"""Called during initialization"""
|
||||
return self.client
|
||||
|
||||
def _get_or_create_collection(self, name):
|
||||
"""Get or create the collection."""
|
||||
def _get_or_create_collection(self, name: str) -> Collection:
|
||||
"""
|
||||
Get or create a named collection.
|
||||
|
||||
:param name: Name of the collection
|
||||
:type name: str
|
||||
:raises ValueError: No embedder configured.
|
||||
:return: Created collection
|
||||
:rtype: Collection
|
||||
"""
|
||||
if not hasattr(self, "embedder") or not self.embedder:
|
||||
raise ValueError("Cannot create a Chroma database collection without an embedder.")
|
||||
self.collection = self.client.get_or_create_collection(
|
||||
name=name,
|
||||
embedding_function=self.embedding_fn,
|
||||
embedding_function=self.embedder.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 ids: list of doc ids to check for existence
|
||||
:type ids: List[str]
|
||||
:param where: Optional. to filter data
|
||||
:type where: Dict[str, any]
|
||||
:return: Existing documents.
|
||||
:rtype: List[str]
|
||||
"""
|
||||
existing_docs = self.collection.get(
|
||||
ids=ids,
|
||||
@@ -65,16 +105,28 @@ class ChromaDB(BaseVectorDB):
|
||||
|
||||
return set(existing_docs["ids"])
|
||||
|
||||
def add(self, documents: List[str], metadatas: List[object], ids: List[str]) -> Any:
|
||||
def add(self, documents: List[str], metadatas: List[object], ids: List[str]):
|
||||
"""
|
||||
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
|
||||
Add vectors to chroma database
|
||||
|
||||
:param documents: Documents
|
||||
:type documents: List[str]
|
||||
:param metadatas: Metadatas
|
||||
:type metadatas: List[object]
|
||||
:param ids: ids
|
||||
:type ids: List[str]
|
||||
"""
|
||||
self.collection.add(documents=documents, metadatas=metadatas, ids=ids)
|
||||
|
||||
def _format_result(self, results):
|
||||
def _format_result(self, results: QueryResult) -> list[tuple[Document, float]]:
|
||||
"""
|
||||
Format Chroma results
|
||||
|
||||
:param results: ChromaDB query results to format.
|
||||
:type results: QueryResult
|
||||
:return: Formatted results
|
||||
:rtype: list[tuple[Document, float]]
|
||||
"""
|
||||
return [
|
||||
(Document(page_content=result[0], metadata=result[1] or {}), result[2])
|
||||
for result in zip(
|
||||
@@ -86,11 +138,17 @@ class ChromaDB(BaseVectorDB):
|
||||
|
||||
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
|
||||
Query contents from vector data base based on vector similarity
|
||||
|
||||
:param input_query: list of query string
|
||||
:type input_query: List[str]
|
||||
:param n_results: no of similar documents to fetch from database
|
||||
:param where: Optional. to filter data
|
||||
:type n_results: int
|
||||
:param where: to filter data
|
||||
:type where: Dict[str, any]
|
||||
:raises InvalidDimensionException: Dimensions do not match.
|
||||
:return: The content of the document that matched your query.
|
||||
:rtype: List[str]
|
||||
"""
|
||||
try:
|
||||
result = self.collection.query(
|
||||
@@ -110,9 +168,43 @@ class ChromaDB(BaseVectorDB):
|
||||
contents = [result[0].page_content for result in results_formatted]
|
||||
return contents
|
||||
|
||||
def set_collection_name(self, name: str):
|
||||
"""
|
||||
Set the name of the collection. A collection is an isolated space for vectors.
|
||||
|
||||
:param name: Name of the collection.
|
||||
:type name: str
|
||||
"""
|
||||
self.config.collection_name = name
|
||||
self._get_or_create_collection(self.config.collection_name)
|
||||
|
||||
def count(self) -> int:
|
||||
"""
|
||||
Count number of documents/chunks embedded in the database.
|
||||
|
||||
:return: number of documents
|
||||
:rtype: int
|
||||
"""
|
||||
return self.collection.count()
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the database. Deletes all embeddings irreversibly.
|
||||
"""
|
||||
# Delete all data from the database
|
||||
self.client.reset()
|
||||
try:
|
||||
self.client.reset()
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
"For safety reasons, resetting is disabled."
|
||||
'Please enable it by including `chromadb_settings={"allow_reset": True}` in your ChromaDbConfig'
|
||||
) from None
|
||||
# Recreate
|
||||
self._get_or_create_collection(self.config.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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Callable, Dict, List
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
try:
|
||||
from elasticsearch import Elasticsearch
|
||||
@@ -9,62 +9,73 @@ except ImportError:
|
||||
) from None
|
||||
|
||||
from embedchain.config import ElasticsearchDBConfig
|
||||
from embedchain.models.VectorDimensions import VectorDimensions
|
||||
from embedchain.helper_classes.json_serializable import register_deserializable
|
||||
from embedchain.vectordb.base_vector_db import BaseVectorDB
|
||||
|
||||
|
||||
@register_deserializable
|
||||
class ElasticsearchDB(BaseVectorDB):
|
||||
"""
|
||||
Elasticsearch as vector database
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
es_config: ElasticsearchDBConfig = None,
|
||||
embedding_fn: Callable[[list[str]], list[str]] = None,
|
||||
vector_dim: VectorDimensions = None,
|
||||
collection_name: str = None,
|
||||
config: Optional[ElasticsearchDBConfig] = None,
|
||||
es_config: Optional[ElasticsearchDBConfig] = None, # Backwards compatibility
|
||||
):
|
||||
"""Elasticsearch as vector database.
|
||||
|
||||
:param config: Elasticsearch database config, defaults to None
|
||||
:type config: ElasticsearchDBConfig, optional
|
||||
:param es_config: `es_config` is supported as an alias for `config` (for backwards compatibility),
|
||||
defaults to None
|
||||
:type es_config: ElasticsearchDBConfig, optional
|
||||
:raises ValueError: No config provided
|
||||
"""
|
||||
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:
|
||||
if config is None and 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.config = config or es_config
|
||||
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}"
|
||||
|
||||
# Call parent init here because embedder is needed
|
||||
super().__init__(config=self.config)
|
||||
|
||||
def _initialize(self):
|
||||
"""
|
||||
This method is needed because `embedder` attribute needs to be set externally before it can be initialized.
|
||||
"""
|
||||
index_settings = {
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"text": {"type": "text"},
|
||||
"embeddings": {"type": "dense_vector", "index": False, "dims": self.vector_dim},
|
||||
"embeddings": {"type": "dense_vector", "index": False, "dims": self.embedder.vector_dimension},
|
||||
}
|
||||
}
|
||||
}
|
||||
if not self.client.indices.exists(index=self.es_index):
|
||||
es_index = self._get_index()
|
||||
if not self.client.indices.exists(index=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__()
|
||||
print("Creating index", es_index, index_settings)
|
||||
self.client.indices.create(index=es_index, body=index_settings)
|
||||
|
||||
def _get_or_create_db(self):
|
||||
"""Called during initialization"""
|
||||
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]:
|
||||
def get(self, ids: List[str], where: Dict[str, any]) -> Set[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
|
||||
|
||||
:param ids: _list of doc ids to check for existance
|
||||
:type ids: List[str]
|
||||
:param where: to filter data
|
||||
:type where: Dict[str, any]
|
||||
:return: ids
|
||||
:rtype: Set[str]
|
||||
"""
|
||||
query = {"bool": {"must": [{"ids": {"values": ids}}]}}
|
||||
if "app_id" in where:
|
||||
@@ -75,35 +86,44 @@ class ElasticsearchDB(BaseVectorDB):
|
||||
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
|
||||
def add(self, documents: List[str], metadatas: List[object], ids: List[str]):
|
||||
"""add data in vector database
|
||||
|
||||
:param documents: list of texts to add
|
||||
:type documents: List[str]
|
||||
:param metadatas: list of metadata associated with docs
|
||||
:type metadatas: List[object]
|
||||
:param ids: ids of docs
|
||||
:type ids: List[str]
|
||||
"""
|
||||
|
||||
docs = []
|
||||
embeddings = self.embedding_fn(documents)
|
||||
embeddings = self.embedder.embedding_fn(documents)
|
||||
for id, text, metadata, embeddings in zip(ids, documents, metadatas, embeddings):
|
||||
docs.append(
|
||||
{
|
||||
"_index": self.es_index,
|
||||
"_index": self._get_index(),
|
||||
"_id": id,
|
||||
"_source": {"text": text, "metadata": metadata, "embeddings": embeddings},
|
||||
}
|
||||
)
|
||||
bulk(self.client, docs)
|
||||
self.client.indices.refresh(index=self.es_index)
|
||||
return
|
||||
self.client.indices.refresh(index=self._get_index())
|
||||
|
||||
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
|
||||
:type input_query: List[str]
|
||||
:param n_results: no of similar documents to fetch from database
|
||||
:type n_results: int
|
||||
:param where: Optional. to filter data
|
||||
:type where: Dict[str, any]
|
||||
:return: Database contents that are the result of the query
|
||||
:rtype: List[str]
|
||||
"""
|
||||
input_query_vector = self.embedding_fn(input_query)
|
||||
input_query_vector = self.embedder.embedding_fn(input_query)
|
||||
query_vector = input_query_vector[0]
|
||||
query = {
|
||||
"script_score": {
|
||||
@@ -118,19 +138,47 @@ class ElasticsearchDB(BaseVectorDB):
|
||||
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)
|
||||
response = self.client.search(index=self._get_index(), query=query, _source=_source, size=n_results)
|
||||
docs = response["hits"]["hits"]
|
||||
contents = [doc["_source"]["text"] for doc in docs]
|
||||
return contents
|
||||
|
||||
def set_collection_name(self, name: str):
|
||||
"""
|
||||
Set the name of the collection. A collection is an isolated space for vectors.
|
||||
|
||||
:param name: Name of the collection.
|
||||
:type name: str
|
||||
"""
|
||||
self.config.collection_name = name
|
||||
|
||||
def count(self) -> int:
|
||||
"""
|
||||
Count number of documents/chunks embedded in the database.
|
||||
|
||||
:return: number of documents
|
||||
:rtype: int
|
||||
"""
|
||||
query = {"match_all": {}}
|
||||
response = self.client.count(index=self.es_index, query=query)
|
||||
response = self.client.count(index=self._get_index(), query=query)
|
||||
doc_count = response["count"]
|
||||
return doc_count
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Resets the database. Deletes all embeddings irreversibly.
|
||||
"""
|
||||
# Delete all data from the database
|
||||
if self.client.indices.exists(index=self.es_index):
|
||||
if self.client.indices.exists(index=self._get_index()):
|
||||
# delete index in Es
|
||||
self.client.indices.delete(index=self.es_index)
|
||||
self.client.indices.delete(index=self._get_index())
|
||||
|
||||
def _get_index(self) -> str:
|
||||
"""Get the Elasticsearch index for a collection
|
||||
|
||||
:return: Elasticsearch index
|
||||
:rtype: str
|
||||
"""
|
||||
# NOTE: The method is preferred to an attribute, because if collection name changes,
|
||||
# it's always up-to-date.
|
||||
return f"{self.config.collection_name}_{self.embedder.vector_dimension}"
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
__pycache__
|
||||
db
|
||||
database
|
||||
pyenv
|
||||
venv
|
||||
.env
|
||||
trash_files/
|
||||
@@ -1,3 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,5 +0,0 @@
|
||||
flask==2.3.2
|
||||
slackeventsapi==3.0.1
|
||||
slacksdk==3.21.3
|
||||
python-dotenv==1.0.0
|
||||
embedchain
|
||||
@@ -1,58 +0,0 @@
|
||||
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)
|
||||
@@ -1,3 +0,0 @@
|
||||
SLACK_SIGNING_SECRET=""
|
||||
SLACK_BOT_TOKEN=""
|
||||
OPENAI_API_KEY=""
|
||||
+13
-7
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "embedchain"
|
||||
version = "0.0.47"
|
||||
version = "0.0.54"
|
||||
description = "embedchain is a framework to easily create LLM powered bots over any dataset"
|
||||
authors = ["Taranjeet Singh"]
|
||||
license = "Apache License"
|
||||
@@ -82,10 +82,11 @@ color = true
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<3.9.7 || >3.9.7,<4.0"
|
||||
python-dotenv = "^1.0.0"
|
||||
langchain = "^0.0.237"
|
||||
langchain = "^0.0.279"
|
||||
requests = "^2.31.0"
|
||||
openai = "^0.27.5"
|
||||
chromadb ="^0.4.2"
|
||||
tiktoken = "^0.4.0"
|
||||
chromadb ="^0.4.8"
|
||||
youtube-transcript-api = "^0.6.1"
|
||||
beautifulsoup4 = "^4.12.2"
|
||||
pypdf = "^3.11.0"
|
||||
@@ -94,11 +95,14 @@ 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 }
|
||||
gpt4all = { version = "1.0.8", optional = true }
|
||||
# 1.0.9 is not working for some users (https://github.com/nomic-ai/gpt4all/issues/1394)
|
||||
elasticsearch = { version = "^8.9.0", optional = true }
|
||||
flask = "^2.3.3"
|
||||
twilio = "^8.5.0"
|
||||
flask = { version = "^2.3.3", optional = true }
|
||||
twilio = { version = "^8.5.0", optional = true }
|
||||
fastapi-poe = { version = "0.0.16", optional = true }
|
||||
discord = { version = "^2.3.2", optional = true }
|
||||
slack-sdk = { version = "3.21.3", optional = true }
|
||||
|
||||
|
||||
|
||||
@@ -118,10 +122,12 @@ community = ["llama-index"]
|
||||
opensource = ["sentence-transformers", "torch", "gpt4all"]
|
||||
elasticsearch = ["elasticsearch"]
|
||||
poe = ["fastapi-poe"]
|
||||
discord = ["discord"]
|
||||
slack = ["slack-sdk", "flask"]
|
||||
whatsapp = ["twilio", "flask"]
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
|
||||
|
||||
|
||||
[tool.poetry.scripts]
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
def setUp(self):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
self.app = App(config=AppConfig(collect_metrics=False))
|
||||
|
||||
@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_get_answer, mock_retrieve):
|
||||
"""
|
||||
This test checks the functionality of the 'chat' method in the App class with respect to the chat history
|
||||
memory.
|
||||
The 'chat' method is called twice. The first call initializes the chat history memory.
|
||||
The second call is expected to use the chat history from the first call.
|
||||
|
||||
Key assumptions tested:
|
||||
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.
|
||||
"""
|
||||
app = App()
|
||||
first_answer = app.chat("Test query 1")
|
||||
self.assertEqual(first_answer, "Test answer")
|
||||
self.assertEqual(len(app.memory.chat_memory.messages), 2)
|
||||
second_answer = app.chat("Test query 2")
|
||||
self.assertEqual(second_answer, "Test answer")
|
||||
self.assertEqual(len(app.memory.chat_memory.messages), 4)
|
||||
@@ -3,7 +3,7 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig
|
||||
from embedchain.config import AppConfig, ChromaDbConfig
|
||||
|
||||
|
||||
class TestChromaDbHostsLoglevel(unittest.TestCase):
|
||||
@@ -12,8 +12,8 @@ class TestChromaDbHostsLoglevel(unittest.TestCase):
|
||||
@patch("chromadb.api.models.Collection.Collection.add")
|
||||
@patch("chromadb.api.models.Collection.Collection.get")
|
||||
@patch("embedchain.embedchain.EmbedChain.retrieve_from_database")
|
||||
@patch("embedchain.embedchain.EmbedChain.get_answer_from_llm")
|
||||
@patch("embedchain.embedchain.EmbedChain.get_llm_model_answer")
|
||||
@patch("embedchain.llm.base_llm.BaseLlm.get_answer_from_llm")
|
||||
@patch("embedchain.llm.base_llm.BaseLlm.get_llm_model_answer")
|
||||
def test_whole_app(
|
||||
self,
|
||||
_mock_get,
|
||||
@@ -42,13 +42,14 @@ class TestChromaDbHostsLoglevel(unittest.TestCase):
|
||||
"""
|
||||
Test if the `App` instance is correctly reconstructed after a reset.
|
||||
"""
|
||||
app = App()
|
||||
config = AppConfig(log_level="DEBUG", collect_metrics=False)
|
||||
app = App(config=config, chromadb_config=ChromaDbConfig(chroma_settings={"allow_reset": True}))
|
||||
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(
|
||||
app.db.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"},
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig, QueryConfig
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
|
||||
def setUp(self):
|
||||
self.app = App(config=AppConfig(collect_metrics=False))
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_query(self):
|
||||
"""
|
||||
This test checks the functionality of the 'query' method in the App class.
|
||||
It simulates a scenario where the 'retrieve_from_database' method returns a context list and
|
||||
'get_llm_model_answer' returns an expected answer string.
|
||||
|
||||
The 'query' method is expected to call 'retrieve_from_database' and 'get_llm_model_answer' methods
|
||||
appropriately and return the right answer.
|
||||
|
||||
Key assumptions tested:
|
||||
- 'retrieve_from_database' method is called exactly once with arguments: "Test query" and an instance of
|
||||
QueryConfig.
|
||||
- 'get_llm_model_answer' is called exactly once. The specific arguments are not checked in this test.
|
||||
- 'query' method returns the value it received from 'get_llm_model_answer'.
|
||||
|
||||
The test isolates the 'query' method behavior by mocking out 'retrieve_from_database' and
|
||||
'get_llm_model_answer' methods.
|
||||
"""
|
||||
with patch.object(self.app, "retrieve_from_database") as mock_retrieve:
|
||||
mock_retrieve.return_value = ["Test context"]
|
||||
with patch.object(self.app, "get_llm_model_answer") as mock_answer:
|
||||
mock_answer.return_value = "Test answer"
|
||||
answer = self.app.query("Test query")
|
||||
|
||||
self.assertEqual(answer, "Test answer")
|
||||
self.assertEqual(mock_retrieve.call_args[0][0], "Test query")
|
||||
self.assertIsInstance(mock_retrieve.call_args[0][1], QueryConfig)
|
||||
mock_answer.assert_called_once()
|
||||
|
||||
@patch("openai.ChatCompletion.create")
|
||||
def test_query_config_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 systemp_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
|
||||
@@ -0,0 +1,71 @@
|
||||
import random
|
||||
import unittest
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig
|
||||
from embedchain.helper_classes.json_serializable import (
|
||||
JSONSerializable, register_deserializable)
|
||||
|
||||
|
||||
class TestJsonSerializable(unittest.TestCase):
|
||||
"""Test that the datatype detection is working, based on the input."""
|
||||
|
||||
def test_base_function(self):
|
||||
"""Test that the base premise of serialization and deserealization is working"""
|
||||
|
||||
@register_deserializable
|
||||
class TestClass(JSONSerializable):
|
||||
def __init__(self):
|
||||
self.rng = random.random()
|
||||
|
||||
original_class = TestClass()
|
||||
serial = original_class.serialize()
|
||||
|
||||
# Negative test to show that a new class does not have the same random number.
|
||||
negative_test_class = TestClass()
|
||||
self.assertNotEqual(original_class.rng, negative_test_class.rng)
|
||||
|
||||
# Test to show that a deserialized class has the same random number.
|
||||
positive_test_class: TestClass = TestClass().deserialize(serial)
|
||||
self.assertEqual(original_class.rng, positive_test_class.rng)
|
||||
self.assertTrue(isinstance(positive_test_class, TestClass))
|
||||
|
||||
# Test that it works as a static method too.
|
||||
positive_test_class: TestClass = TestClass.deserialize(serial)
|
||||
self.assertEqual(original_class.rng, positive_test_class.rng)
|
||||
|
||||
# TODO: There's no reason it shouldn't work, but serialization to and from file should be tested too.
|
||||
|
||||
def test_registration_required(self):
|
||||
"""Test that registration is required, and that without registration the default class is returned."""
|
||||
|
||||
class SecondTestClass(JSONSerializable):
|
||||
def __init__(self):
|
||||
self.default = True
|
||||
|
||||
app = SecondTestClass()
|
||||
# Make not default
|
||||
app.default = False
|
||||
# Serialize
|
||||
serial = app.serialize()
|
||||
# Deserialize. Due to the way errors are handled, it will not fail but return a default class.
|
||||
app: SecondTestClass = SecondTestClass().deserialize(serial)
|
||||
self.assertTrue(app.default)
|
||||
# If we register and try again with the same serial, it should work
|
||||
SecondTestClass.register_class_as_deserializable(SecondTestClass)
|
||||
app: SecondTestClass = SecondTestClass().deserialize(serial)
|
||||
self.assertFalse(app.default)
|
||||
|
||||
def test_recursive(self):
|
||||
"""Test recursiveness with the real app"""
|
||||
random_id = str(random.random())
|
||||
config = AppConfig(id=random_id, collect_metrics=False)
|
||||
# config class is set under app.config.
|
||||
app = App(config=config)
|
||||
# w/o recursion it would just be <embedchain.config.apps.OpenSourceAppConfig.OpenSourceAppConfig object at x>
|
||||
s = app.serialize()
|
||||
new_app: App = App.deserialize(s)
|
||||
# The id of the new app is the same as the first one.
|
||||
self.assertEqual(random_id, new_app.config.id)
|
||||
# We have proven that a nested class (app.config) can be serialized and deserialized just the same.
|
||||
# TODO: test deeper recursion
|
||||
@@ -0,0 +1,124 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig, BaseLlmConfig
|
||||
from embedchain.llm.base_llm import BaseLlm
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
def setUp(self):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
self.app = App(config=AppConfig(collect_metrics=False))
|
||||
|
||||
@patch.object(App, "retrieve_from_database", return_value=["Test context"])
|
||||
@patch.object(BaseLlm, "get_answer_from_llm", return_value="Test answer")
|
||||
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.
|
||||
The 'chat' method is called twice. The first call initializes the chat history memory.
|
||||
The second call is expected to use the chat history from the first call.
|
||||
|
||||
Key assumptions tested:
|
||||
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.
|
||||
"""
|
||||
config = AppConfig(collect_metrics=False)
|
||||
app = App(config=config)
|
||||
first_answer = app.chat("Test query 1")
|
||||
self.assertEqual(first_answer, "Test answer")
|
||||
self.assertEqual(len(app.llm.memory.chat_memory.messages), 2)
|
||||
self.assertEqual(len(app.llm.history.splitlines()), 2)
|
||||
second_answer = app.chat("Test query 2")
|
||||
self.assertEqual(second_answer, "Test answer")
|
||||
self.assertEqual(len(app.llm.memory.chat_memory.messages), 4)
|
||||
self.assertEqual(len(app.llm.history.splitlines()), 4)
|
||||
|
||||
@patch.object(App, "retrieve_from_database", return_value=["Test context"])
|
||||
@patch.object(BaseLlm, "get_answer_from_llm", return_value="Test answer")
|
||||
def test_template_replacement(self, mock_get_answer, mock_retrieve):
|
||||
"""
|
||||
Tests that if a default template is used and it doesn't contain history,
|
||||
the default template is swapped in.
|
||||
|
||||
Also tests that a dry run does not change the history
|
||||
"""
|
||||
config = AppConfig(collect_metrics=False)
|
||||
app = App(config=config)
|
||||
first_answer = app.chat("Test query 1")
|
||||
self.assertEqual(first_answer, "Test answer")
|
||||
self.assertEqual(len(app.llm.history.splitlines()), 2)
|
||||
history = app.llm.history
|
||||
dry_run = app.chat("Test query 2", dry_run=True)
|
||||
self.assertIn("History:", dry_run)
|
||||
self.assertEqual(history, app.llm.history)
|
||||
self.assertEqual(len(app.llm.history.splitlines()), 2)
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_chat_with_where_in_params(self):
|
||||
"""
|
||||
This test checks the functionality of the 'chat' method in the App class.
|
||||
It simulates a scenario where the 'retrieve_from_database' method returns a context list based on
|
||||
a where filter and 'get_llm_model_answer' returns an expected answer string.
|
||||
|
||||
The 'chat' method is expected to call 'retrieve_from_database' with the where filter and
|
||||
'get_llm_model_answer' methods appropriately and return the right answer.
|
||||
|
||||
Key assumptions tested:
|
||||
- 'retrieve_from_database' method is called exactly once with arguments: "Test query" and an instance of
|
||||
QueryConfig.
|
||||
- 'get_llm_model_answer' is called exactly once. The specific arguments are not checked in this test.
|
||||
- 'chat' method returns the value it received from 'get_llm_model_answer'.
|
||||
|
||||
The test isolates the 'chat' method behavior by mocking out 'retrieve_from_database' and
|
||||
'get_llm_model_answer' methods.
|
||||
"""
|
||||
with patch.object(self.app, "retrieve_from_database") as mock_retrieve:
|
||||
mock_retrieve.return_value = ["Test context"]
|
||||
with patch.object(self.app.llm, "get_llm_model_answer") as mock_answer:
|
||||
mock_answer.return_value = "Test answer"
|
||||
answer = self.app.chat("Test query", where={"attribute": "value"})
|
||||
|
||||
self.assertEqual(answer, "Test answer")
|
||||
_args, kwargs = mock_retrieve.call_args
|
||||
self.assertEqual(kwargs.get("input_query"), "Test query")
|
||||
self.assertEqual(kwargs.get("where"), {"attribute": "value"})
|
||||
mock_answer.assert_called_once()
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_chat_with_where_in_chat_config(self):
|
||||
"""
|
||||
This test checks the functionality of the 'chat' method in the App class.
|
||||
It simulates a scenario where the 'retrieve_from_database' method returns a context list based on
|
||||
a where filter and 'get_llm_model_answer' returns an expected answer string.
|
||||
|
||||
The 'chat' method is expected to call 'retrieve_from_database' with the where filter specified
|
||||
in the QueryConfig and 'get_llm_model_answer' methods appropriately and return the right answer.
|
||||
|
||||
Key assumptions tested:
|
||||
- 'retrieve_from_database' method is called exactly once with arguments: "Test query" and an instance of
|
||||
QueryConfig.
|
||||
- 'get_llm_model_answer' is called exactly once. The specific arguments are not checked in this test.
|
||||
- 'chat' method returns the value it received from 'get_llm_model_answer'.
|
||||
|
||||
The test isolates the 'chat' method behavior by mocking out 'retrieve_from_database' and
|
||||
'get_llm_model_answer' methods.
|
||||
"""
|
||||
with patch.object(self.app.llm, "get_llm_model_answer") as mock_answer:
|
||||
mock_answer.return_value = "Test answer"
|
||||
with patch.object(self.app.db, "query") as mock_database_query:
|
||||
mock_database_query.return_value = ["Test context"]
|
||||
queryConfig = BaseLlmConfig(where={"attribute": "value"})
|
||||
answer = self.app.chat("Test query", queryConfig)
|
||||
|
||||
self.assertEqual(answer, "Test answer")
|
||||
_args, kwargs = mock_database_query.call_args
|
||||
self.assertEqual(kwargs.get("input_query"), "Test query")
|
||||
self.assertEqual(kwargs.get("where"), {"attribute": "value"})
|
||||
mock_answer.assert_called_once()
|
||||
@@ -2,7 +2,7 @@ import unittest
|
||||
from string import Template
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig, QueryConfig
|
||||
from embedchain.config import AppConfig, BaseLlmConfig
|
||||
|
||||
|
||||
class TestGeneratePrompt(unittest.TestCase):
|
||||
@@ -23,10 +23,11 @@ class TestGeneratePrompt(unittest.TestCase):
|
||||
input_query = "Test query"
|
||||
contexts = ["Context 1", "Context 2", "Context 3"]
|
||||
template = "You are a bot. Context: ${context} - Query: ${query} - Helpful answer:"
|
||||
config = QueryConfig(template=Template(template))
|
||||
config = BaseLlmConfig(template=Template(template))
|
||||
self.app.llm.config = config
|
||||
|
||||
# Execute
|
||||
result = self.app.generate_prompt(input_query, contexts, config)
|
||||
result = self.app.llm.generate_prompt(input_query, contexts)
|
||||
|
||||
# Assert
|
||||
expected_result = (
|
||||
@@ -45,10 +46,11 @@ class TestGeneratePrompt(unittest.TestCase):
|
||||
# Setup
|
||||
input_query = "Test query"
|
||||
contexts = ["Context 1", "Context 2", "Context 3"]
|
||||
config = QueryConfig()
|
||||
config = BaseLlmConfig()
|
||||
|
||||
# Execute
|
||||
result = self.app.generate_prompt(input_query, contexts, config)
|
||||
self.app.llm.config = config
|
||||
result = self.app.llm.generate_prompt(input_query, contexts)
|
||||
|
||||
# Assert
|
||||
expected_result = config.template.substitute(context="Context 1 | Context 2 | Context 3", query=input_query)
|
||||
@@ -58,9 +60,11 @@ class TestGeneratePrompt(unittest.TestCase):
|
||||
"""
|
||||
Test the 'generate_prompt' method with QueryConfig containing a history attribute.
|
||||
"""
|
||||
config = QueryConfig(history=["Past context 1", "Past context 2"])
|
||||
config = BaseLlmConfig()
|
||||
config.template = Template("Context: $context | Query: $query | History: $history")
|
||||
prompt = self.app.generate_prompt("Test query", ["Test context"], config)
|
||||
self.app.llm.config = config
|
||||
self.app.llm.set_history(["Past context 1", "Past context 2"])
|
||||
prompt = self.app.llm.generate_prompt("Test query", ["Test context"])
|
||||
|
||||
expected_prompt = "Context: Test context | Query: Test query | History: ['Past context 1', 'Past context 2']"
|
||||
self.assertEqual(prompt, expected_prompt)
|
||||
@@ -0,0 +1,147 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig, BaseLlmConfig
|
||||
|
||||
|
||||
class TestApp(unittest.TestCase):
|
||||
os.environ["OPENAI_API_KEY"] = "test_key"
|
||||
|
||||
def setUp(self):
|
||||
self.app = App(config=AppConfig(collect_metrics=False))
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_query(self):
|
||||
"""
|
||||
This test checks the functionality of the 'query' method in the App class.
|
||||
It simulates a scenario where the 'retrieve_from_database' method returns a context list and
|
||||
'get_llm_model_answer' returns an expected answer string.
|
||||
|
||||
The 'query' method is expected to call 'retrieve_from_database' and 'get_llm_model_answer' methods
|
||||
appropriately and return the right answer.
|
||||
|
||||
Key assumptions tested:
|
||||
- 'retrieve_from_database' method is called exactly once with arguments: "Test query" and an instance of
|
||||
QueryConfig.
|
||||
- 'get_llm_model_answer' is called exactly once. The specific arguments are not checked in this test.
|
||||
- 'query' method returns the value it received from 'get_llm_model_answer'.
|
||||
|
||||
The test isolates the 'query' method behavior by mocking out 'retrieve_from_database' and
|
||||
'get_llm_model_answer' methods.
|
||||
"""
|
||||
with patch.object(self.app, "retrieve_from_database") as mock_retrieve:
|
||||
mock_retrieve.return_value = ["Test context"]
|
||||
with patch.object(self.app.llm, "get_llm_model_answer") as mock_answer:
|
||||
mock_answer.return_value = "Test answer"
|
||||
_answer = self.app.query(input_query="Test query")
|
||||
|
||||
# Ensure retrieve_from_database was called
|
||||
mock_retrieve.assert_called_once()
|
||||
|
||||
# Check the call arguments
|
||||
args, kwargs = mock_retrieve.call_args
|
||||
input_query_arg = kwargs.get("input_query")
|
||||
self.assertEqual(input_query_arg, "Test query")
|
||||
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(collect_metrics=False)
|
||||
chat_config = BaseLlmConfig(system_prompt="Test system prompt")
|
||||
app = App(config=config, llm_config=chat_config)
|
||||
|
||||
app.llm.get_llm_model_answer("Test query")
|
||||
|
||||
# Test system_prompt: Check that the 'create' method was called with the correct 'messages' argument
|
||||
messages_arg = mock_create.call_args.kwargs["messages"]
|
||||
self.assertTrue(messages_arg[0].get("role"), "system")
|
||||
self.assertEqual(messages_arg[0].get("content"), "Test system prompt")
|
||||
self.assertTrue(messages_arg[1].get("role"), "user")
|
||||
self.assertEqual(messages_arg[1].get("content"), "Test query")
|
||||
|
||||
# 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(collect_metrics=False)
|
||||
chat_config = BaseLlmConfig()
|
||||
app = App(config=config, llm_config=chat_config, system_prompt="Test system prompt")
|
||||
|
||||
self.assertEqual(app.llm.config.system_prompt, "Test system prompt")
|
||||
|
||||
app.llm.get_llm_model_answer("Test query")
|
||||
|
||||
# Test system_prompt: Check that the 'create' method was called with the correct 'messages' argument
|
||||
messages_arg = mock_create.call_args.kwargs["messages"]
|
||||
self.assertTrue(messages_arg[0].get("role"), "system")
|
||||
self.assertEqual(messages_arg[0].get("content"), "Test system prompt")
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_query_with_where_in_params(self):
|
||||
"""
|
||||
This test checks the functionality of the 'query' method in the App class.
|
||||
It simulates a scenario where the 'retrieve_from_database' method returns a context list based on
|
||||
a where filter and 'get_llm_model_answer' returns an expected answer string.
|
||||
|
||||
The 'query' method is expected to call 'retrieve_from_database' with the where filter and
|
||||
'get_llm_model_answer' methods appropriately and return the right answer.
|
||||
|
||||
Key assumptions tested:
|
||||
- 'retrieve_from_database' method is called exactly once with arguments: "Test query" and an instance of
|
||||
QueryConfig.
|
||||
- 'get_llm_model_answer' is called exactly once. The specific arguments are not checked in this test.
|
||||
- 'query' method returns the value it received from 'get_llm_model_answer'.
|
||||
|
||||
The test isolates the 'query' method behavior by mocking out 'retrieve_from_database' and
|
||||
'get_llm_model_answer' methods.
|
||||
"""
|
||||
with patch.object(self.app, "retrieve_from_database") as mock_retrieve:
|
||||
mock_retrieve.return_value = ["Test context"]
|
||||
with patch.object(self.app.llm, "get_llm_model_answer") as mock_answer:
|
||||
mock_answer.return_value = "Test answer"
|
||||
answer = self.app.query("Test query", where={"attribute": "value"})
|
||||
|
||||
self.assertEqual(answer, "Test answer")
|
||||
_args, kwargs = mock_retrieve.call_args
|
||||
self.assertEqual(kwargs.get("input_query"), "Test query")
|
||||
self.assertEqual(kwargs.get("where"), {"attribute": "value"})
|
||||
mock_answer.assert_called_once()
|
||||
|
||||
@patch("chromadb.api.models.Collection.Collection.add", MagicMock)
|
||||
def test_query_with_where_in_query_config(self):
|
||||
"""
|
||||
This test checks the functionality of the 'query' method in the App class.
|
||||
It simulates a scenario where the 'retrieve_from_database' method returns a context list based on
|
||||
a where filter and 'get_llm_model_answer' returns an expected answer string.
|
||||
|
||||
The 'query' method is expected to call 'retrieve_from_database' with the where filter and
|
||||
'get_llm_model_answer' methods appropriately and return the right answer.
|
||||
|
||||
Key assumptions tested:
|
||||
- 'retrieve_from_database' method is called exactly once with arguments: "Test query" and an instance of
|
||||
QueryConfig.
|
||||
- 'get_llm_model_answer' is called exactly once. The specific arguments are not checked in this test.
|
||||
- 'query' method returns the value it received from 'get_llm_model_answer'.
|
||||
|
||||
The test isolates the 'query' method behavior by mocking out 'retrieve_from_database' and
|
||||
'get_llm_model_answer' methods.
|
||||
"""
|
||||
|
||||
with patch.object(self.app.llm, "get_llm_model_answer") as mock_answer:
|
||||
mock_answer.return_value = "Test answer"
|
||||
with patch.object(self.app.db, "query") as mock_database_query:
|
||||
mock_database_query.return_value = ["Test context"]
|
||||
queryConfig = BaseLlmConfig(where={"attribute": "value"})
|
||||
answer = self.app.query("Test query", queryConfig)
|
||||
|
||||
self.assertEqual(answer, "Test answer")
|
||||
_args, kwargs = mock_database_query.call_args
|
||||
self.assertEqual(kwargs.get("input_query"), "Test query")
|
||||
self.assertEqual(kwargs.get("where"), {"attribute": "value"})
|
||||
mock_answer.assert_called_once()
|
||||
@@ -0,0 +1,84 @@
|
||||
import csv
|
||||
import os
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from embedchain.loaders.csv import CsvLoader
|
||||
|
||||
|
||||
@pytest.mark.parametrize("delimiter", [",", "\t", ";", "|"])
|
||||
def test_load_data(delimiter):
|
||||
"""
|
||||
Test csv loader
|
||||
|
||||
Tests that file is loaded, metadata is correct and content is correct
|
||||
"""
|
||||
# Creating temporary CSV file
|
||||
with tempfile.NamedTemporaryFile(mode="w+", newline="", delete=False) as tmpfile:
|
||||
writer = csv.writer(tmpfile, delimiter=delimiter)
|
||||
writer.writerow(["Name", "Age", "Occupation"])
|
||||
writer.writerow(["Alice", "28", "Engineer"])
|
||||
writer.writerow(["Bob", "35", "Doctor"])
|
||||
writer.writerow(["Charlie", "22", "Student"])
|
||||
|
||||
tmpfile.seek(0)
|
||||
filename = tmpfile.name
|
||||
|
||||
# Loading CSV using CsvLoader
|
||||
loader = CsvLoader()
|
||||
result = loader.load_data(filename)
|
||||
|
||||
# Assertions
|
||||
assert len(result) == 3
|
||||
assert result[0]["content"] == "Name: Alice, Age: 28, Occupation: Engineer"
|
||||
assert result[0]["meta_data"]["url"] == filename
|
||||
assert result[0]["meta_data"]["row"] == 1
|
||||
assert result[1]["content"] == "Name: Bob, Age: 35, Occupation: Doctor"
|
||||
assert result[1]["meta_data"]["url"] == filename
|
||||
assert result[1]["meta_data"]["row"] == 2
|
||||
assert result[2]["content"] == "Name: Charlie, Age: 22, Occupation: Student"
|
||||
assert result[2]["meta_data"]["url"] == filename
|
||||
assert result[2]["meta_data"]["row"] == 3
|
||||
|
||||
# Cleaning up the temporary file
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("delimiter", [",", "\t", ";", "|"])
|
||||
def test_load_data_with_file_uri(delimiter):
|
||||
"""
|
||||
Test csv loader with file URI
|
||||
|
||||
Tests that file is loaded, metadata is correct and content is correct
|
||||
"""
|
||||
# Creating temporary CSV file
|
||||
with tempfile.NamedTemporaryFile(mode="w+", newline="", delete=False) as tmpfile:
|
||||
writer = csv.writer(tmpfile, delimiter=delimiter)
|
||||
writer.writerow(["Name", "Age", "Occupation"])
|
||||
writer.writerow(["Alice", "28", "Engineer"])
|
||||
writer.writerow(["Bob", "35", "Doctor"])
|
||||
writer.writerow(["Charlie", "22", "Student"])
|
||||
|
||||
tmpfile.seek(0)
|
||||
filename = pathlib.Path(tmpfile.name).as_uri() # Convert path to file URI
|
||||
|
||||
# Loading CSV using CsvLoader
|
||||
loader = CsvLoader()
|
||||
result = loader.load_data(filename)
|
||||
|
||||
# Assertions
|
||||
assert len(result) == 3
|
||||
assert result[0]["content"] == "Name: Alice, Age: 28, Occupation: Engineer"
|
||||
assert result[0]["meta_data"]["url"] == filename
|
||||
assert result[0]["meta_data"]["row"] == 1
|
||||
assert result[1]["content"] == "Name: Bob, Age: 35, Occupation: Doctor"
|
||||
assert result[1]["meta_data"]["url"] == filename
|
||||
assert result[1]["meta_data"]["row"] == 2
|
||||
assert result[2]["content"] == "Name: Charlie, Age: 22, Occupation: Student"
|
||||
assert result[2]["meta_data"]["url"] == filename
|
||||
assert result[2]["meta_data"]["row"] == 3
|
||||
|
||||
# Cleaning up the temporary file
|
||||
os.unlink(tmpfile.name)
|
||||
@@ -6,8 +6,8 @@ from unittest.mock import patch
|
||||
from chromadb.config import Settings
|
||||
|
||||
from embedchain import App
|
||||
from embedchain.config import AppConfig
|
||||
from embedchain.vectordb.chroma_db import ChromaDB, chromadb
|
||||
from embedchain.config import AppConfig, ChromaDbConfig
|
||||
from embedchain.vectordb.chroma_db import ChromaDB
|
||||
|
||||
|
||||
class TestChromaDbHosts(unittest.TestCase):
|
||||
@@ -17,56 +17,74 @@ class TestChromaDbHosts(unittest.TestCase):
|
||||
"""
|
||||
host = "test-host"
|
||||
port = "1234"
|
||||
config = ChromaDbConfig(host=host, port=port)
|
||||
|
||||
with patch.object(chromadb, "HttpClient") as mock_client:
|
||||
_db = ChromaDB(host=host, port=port, embedding_fn=len)
|
||||
db = ChromaDB(config=config)
|
||||
settings = db.client.get_settings()
|
||||
self.assertEqual(settings.chroma_server_host, host)
|
||||
self.assertEqual(settings.chroma_server_http_port, port)
|
||||
|
||||
expected_settings = Settings(
|
||||
chroma_server_host="test-host",
|
||||
chroma_server_http_port="1234",
|
||||
def test_init_with_basic_auth(self):
|
||||
host = "test-host"
|
||||
port = "1234"
|
||||
|
||||
chroma_auth_settings = {
|
||||
"chroma_client_auth_provider": "chromadb.auth.basic.BasicAuthClientProvider",
|
||||
"chroma_client_auth_credentials": "admin:admin",
|
||||
}
|
||||
|
||||
config = ChromaDbConfig(host=host, port=port, chroma_settings=chroma_auth_settings)
|
||||
db = ChromaDB(config=config)
|
||||
settings = db.client.get_settings()
|
||||
self.assertEqual(settings.chroma_server_host, host)
|
||||
self.assertEqual(settings.chroma_server_http_port, port)
|
||||
self.assertEqual(settings.chroma_client_auth_provider, chroma_auth_settings["chroma_client_auth_provider"])
|
||||
self.assertEqual(
|
||||
settings.chroma_client_auth_credentials, chroma_auth_settings["chroma_client_auth_credentials"]
|
||||
)
|
||||
|
||||
mock_client.assert_called_once_with(expected_settings)
|
||||
|
||||
|
||||
# Review this test
|
||||
class TestChromaDbHostsInit(unittest.TestCase):
|
||||
@patch("embedchain.vectordb.chroma_db.chromadb.Client")
|
||||
def test_init_with_host_and_port(self, mock_client):
|
||||
def test_app_init_with_host_and_port(self, mock_client):
|
||||
"""
|
||||
Test if the `App` instance is initialized with the correct host and port values.
|
||||
"""
|
||||
host = "test-host"
|
||||
port = "1234"
|
||||
|
||||
config = AppConfig(host=host, port=port, collect_metrics=False)
|
||||
config = AppConfig(collect_metrics=False)
|
||||
chromadb_config = ChromaDbConfig(host=host, port=port)
|
||||
|
||||
_app = App(config)
|
||||
_app = App(config, chromadb_config=chromadb_config)
|
||||
|
||||
# self.assertEqual(mock_client.call_args[0][0].chroma_server_host, host)
|
||||
# self.assertEqual(mock_client.call_args[0][0].chroma_server_http_port, port)
|
||||
called_settings: Settings = mock_client.call_args[0][0]
|
||||
|
||||
self.assertEqual(called_settings.chroma_server_host, host)
|
||||
self.assertEqual(called_settings.chroma_server_http_port, port)
|
||||
|
||||
|
||||
class TestChromaDbHostsNone(unittest.TestCase):
|
||||
@patch("embedchain.vectordb.chroma_db.chromadb.Client")
|
||||
def test_init_with_host_and_port(self, mock_client):
|
||||
def test_init_with_host_and_port_none(self, mock_client):
|
||||
"""
|
||||
Test if the `App` instance is initialized without default hosts and ports.
|
||||
"""
|
||||
|
||||
_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)
|
||||
called_settings: Settings = mock_client.call_args[0][0]
|
||||
self.assertEqual(called_settings.chroma_server_host, None)
|
||||
self.assertEqual(called_settings.chroma_server_http_port, None)
|
||||
|
||||
|
||||
class TestChromaDbHostsLoglevel(unittest.TestCase):
|
||||
@patch("embedchain.vectordb.chroma_db.chromadb.Client")
|
||||
def test_init_with_host_and_port(self, mock_client):
|
||||
def test_init_with_host_and_port_log_level(self, mock_client):
|
||||
"""
|
||||
Test if the `App` instance is initialized without a config that does not contain default hosts and ports.
|
||||
"""
|
||||
config = AppConfig(log_level="DEBUG")
|
||||
|
||||
_app = App(config=AppConfig(collect_metrics=False))
|
||||
|
||||
@@ -75,16 +93,21 @@ class TestChromaDbHostsLoglevel(unittest.TestCase):
|
||||
|
||||
|
||||
class TestChromaDbDuplicateHandling:
|
||||
chroma_settings = {"allow_reset": True}
|
||||
chroma_config = ChromaDbConfig(chroma_settings=chroma_settings)
|
||||
app_config = AppConfig(collection_name=False, collect_metrics=False)
|
||||
app_with_settings = App(config=app_config, chromadb_config=chroma_config)
|
||||
|
||||
def test_duplicates_throw_warning(self, caplog):
|
||||
"""
|
||||
Test that add duplicates throws an error.
|
||||
"""
|
||||
# Start with a clean app
|
||||
App().reset()
|
||||
self.app_with_settings.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"])
|
||||
app.db.collection.add(embeddings=[[0, 0, 0]], ids=["0"])
|
||||
app.db.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
|
||||
|
||||
@@ -95,67 +118,73 @@ class TestChromaDbDuplicateHandling:
|
||||
# NOTE: Not part of the TestChromaDbCollection because `unittest.TestCase` doesn't have caplog.
|
||||
|
||||
# Start with a clean app
|
||||
App().reset()
|
||||
self.app_with_settings.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"])
|
||||
app.set_collection_name("test_collection_1")
|
||||
app.db.collection.add(embeddings=[[0, 0, 0]], ids=["0"])
|
||||
app.set_collection_name("test_collection_2")
|
||||
app.db.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):
|
||||
chroma_settings = {"allow_reset": True}
|
||||
chroma_config = ChromaDbConfig(chroma_settings=chroma_settings)
|
||||
app_config = AppConfig(collection_name=False, collect_metrics=False)
|
||||
app_with_settings = App(config=app_config, chromadb_config=chroma_config)
|
||||
|
||||
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")
|
||||
self.assertEqual(app.db.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)
|
||||
config = AppConfig(collect_metrics=False)
|
||||
app = App(config=config)
|
||||
app.set_collection_name(name="test_collection")
|
||||
|
||||
self.assertEqual(app.collection.name, "test_collection")
|
||||
self.assertEqual(app.db.collection.name, "test_collection")
|
||||
|
||||
def test_set_collection(self):
|
||||
def test_set_collection_name(self):
|
||||
"""
|
||||
Test if the `App` collection is correctly switched using the `set_collection` method.
|
||||
Test if the `App` collection is correctly switched using the `set_collection_name` method.
|
||||
"""
|
||||
app = App(config=AppConfig(collect_metrics=False))
|
||||
app.set_collection("test_collection")
|
||||
app.set_collection_name("test_collection")
|
||||
|
||||
self.assertEqual(app.collection.name, "test_collection")
|
||||
self.assertEqual(app.db.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()
|
||||
self.app_with_settings.reset()
|
||||
|
||||
app = App(config=AppConfig(collect_metrics=False))
|
||||
app.set_collection("test_collection_1")
|
||||
app.set_collection_name("test_collection_1")
|
||||
# Collection should be empty when created
|
||||
self.assertEqual(app.count(), 0)
|
||||
|
||||
app.collection.add(embeddings=[0, 0, 0], ids=["0"])
|
||||
app.db.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")
|
||||
app.set_collection_name("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")
|
||||
app.db.collection.add(embeddings=[0, 0, 0], ids=["0"])
|
||||
app.set_collection_name("test_collection_1")
|
||||
# Should still be 1, not 2.
|
||||
self.assertEqual(app.count(), 1)
|
||||
|
||||
@@ -164,15 +193,15 @@ class TestChromaDbCollection(unittest.TestCase):
|
||||
Test that a collection can be picked up later.
|
||||
"""
|
||||
# Start with a clean app
|
||||
App().reset()
|
||||
self.app_with_settings.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_name("test_collection_1")
|
||||
app.db.collection.add(embeddings=[[0, 0, 0]], ids=["0"])
|
||||
del app
|
||||
|
||||
app = App(config=AppConfig(collect_metrics=False))
|
||||
app.set_collection("test_collection_1")
|
||||
app.set_collection_name("test_collection_1")
|
||||
self.assertEqual(app.count(), 1)
|
||||
|
||||
def test_parallel_collections(self):
|
||||
@@ -182,25 +211,25 @@ class TestChromaDbCollection(unittest.TestCase):
|
||||
the other app.
|
||||
"""
|
||||
# Start clean
|
||||
App().reset()
|
||||
self.app_with_settings.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)
|
||||
app1.db.collection.add(embeddings=[0, 0, 0], ids=["0"])
|
||||
self.assertEqual(app1.db.count(), 1)
|
||||
self.assertEqual(app2.db.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"])
|
||||
app1.db.collection.add(embeddings=[[0, 0, 0], [1, 1, 1]], ids=["1", "2"])
|
||||
app2.db.collection.add(embeddings=[0, 0, 0], ids=["0"])
|
||||
|
||||
# Swap names and test
|
||||
app1.set_collection("test_collection_2")
|
||||
app1.set_collection_name("test_collection_2")
|
||||
self.assertEqual(app1.count(), 1)
|
||||
app2.set_collection("test_collection_1")
|
||||
app2.set_collection_name("test_collection_1")
|
||||
self.assertEqual(app2.count(), 3)
|
||||
|
||||
def test_ids_share_collections(self):
|
||||
@@ -208,15 +237,17 @@ class TestChromaDbCollection(unittest.TestCase):
|
||||
Different ids should still share collections.
|
||||
"""
|
||||
# Start clean
|
||||
App().reset()
|
||||
self.app_with_settings.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))
|
||||
app1 = App(AppConfig(id="new_app_id_1", collect_metrics=False))
|
||||
app1.set_collection_name("one_collection")
|
||||
app2 = App(AppConfig(id="new_app_id_2", collect_metrics=False))
|
||||
app2.set_collection_name("one_collection")
|
||||
|
||||
# Add data
|
||||
app1.collection.add(embeddings=[[0, 0, 0], [1, 1, 1]], ids=["0", "1"])
|
||||
app2.collection.add(embeddings=[0, 0, 0], ids=["2"])
|
||||
app1.db.collection.add(embeddings=[[0, 0, 0], [1, 1, 1]], ids=["0", "1"])
|
||||
app2.db.collection.add(embeddings=[0, 0, 0], ids=["2"])
|
||||
|
||||
# Both should have the same collection
|
||||
self.assertEqual(app1.count(), 3)
|
||||
@@ -227,20 +258,24 @@ class TestChromaDbCollection(unittest.TestCase):
|
||||
Resetting should hit all collections and ids.
|
||||
"""
|
||||
# Start clean
|
||||
App().reset()
|
||||
self.app_with_settings.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))
|
||||
app1 = App(AppConfig(id="new_app_id_1", collect_metrics=False), chromadb_config=self.chroma_config)
|
||||
app1.set_collection_name("one_collection")
|
||||
app2 = App(AppConfig(id="new_app_id_2", collect_metrics=False))
|
||||
app2.set_collection_name("one_collection")
|
||||
app3 = App(AppConfig(id="new_app_id_1", collect_metrics=False))
|
||||
app3.set_collection_name("three_collection")
|
||||
app4 = App(AppConfig(id="new_app_id_4", collect_metrics=False))
|
||||
app4.set_collection_name("four_collection")
|
||||
|
||||
# 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"])
|
||||
app1.db.collection.add(embeddings=[0, 0, 0], ids=["1"])
|
||||
app2.db.collection.add(embeddings=[0, 0, 0], ids=["2"])
|
||||
app3.db.collection.add(embeddings=[0, 0, 0], ids=["3"])
|
||||
app4.db.collection.add(embeddings=[0, 0, 0], ids=["4"])
|
||||
|
||||
# Resetting the first one should reset them all.
|
||||
app1.reset()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from embedchain.config import ElasticsearchDBConfig
|
||||
from embedchain.embedder.base_embedder import BaseEmbedder
|
||||
from embedchain.vectordb.elasticsearch_db import ElasticsearchDB
|
||||
|
||||
|
||||
@@ -10,24 +10,20 @@ class TestEsDB(unittest.TestCase):
|
||||
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)
|
||||
ElasticsearchDB(es_config=None)
|
||||
|
||||
def test_init_with_invalid_vector_dim(self):
|
||||
# Test if an exception is raised when an invalid vector_dim is provided
|
||||
embedder = BaseEmbedder()
|
||||
embedder.set_vector_dimension(None)
|
||||
with self.assertRaises(ValueError):
|
||||
ElasticsearchDB(embedding_fn=Mock(), es_config=self.es_config, vector_dim=None)
|
||||
ElasticsearchDB(es_config=self.es_config)
|
||||
|
||||
def test_init_with_invalid_collection_name(self):
|
||||
# Test if an exception is raised when an invalid collection_name is provided
|
||||
self.es_config.collection_name = None
|
||||
with self.assertRaises(ValueError):
|
||||
ElasticsearchDB(
|
||||
embedding_fn=Mock(), es_config=self.es_config, vector_dim=self.vector_dim, collection_name=None
|
||||
)
|
||||
ElasticsearchDB(es_config=self.es_config)
|
||||
|
||||
Reference in New Issue
Block a user