feat(lcel): migrated to lcel and pydantic (#2185)

# Description

Please include a summary of the changes and the related issue. Please
also include relevant motivation and context.

## Checklist before requesting a review

Please delete options that are not relevant.

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):

---------

Co-authored-by: Antoine Dewez <44063631+Zewed@users.noreply.github.com>
This commit is contained in:
Stan Girard 2024-02-14 14:01:35 -08:00 committed by GitHub
parent 2ba3bc1f07
commit 08e015af6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 974 additions and 718 deletions

29
Pipfile
View File

@ -6,46 +6,49 @@ name = "pypi"
[packages]
langchain = "*"
litellm = "*"
openai = "==1.1.1"
openai = "*"
gitpython = "==3.1.36"
pdf2image = "==1.16.3"
nest-asyncio = "==1.5.6"
pypdf = "==3.9.0"
supabase = "==1.1.0"
tiktoken = "==0.4.0"
fastapi = "==0.95.2"
supabase = "*"
tiktoken = "*"
fastapi = "*"
python-multipart = "==0.0.6"
uvicorn = "==0.22.0"
pypandoc = "==1.11"
docx2txt = "==0.8"
python-jose = "==3.3.0"
asyncpg = "==0.27.0"
flake8 = "==6.0.0"
flake8-black = "==0.3.6"
flake8 = "*"
flake8-black = "*"
sentry-sdk = {extras = ["fastapi"] }
pyright = "==1.1.316"
resend = "==0.5.1"
resend = "*"
html5lib = "==1.1"
beautifulsoup4 = "*"
newspaper3k = "*"
xlrd = "==1.0.0"
redis = "==4.5.4"
xlrd = "*"
redis = "*"
flower = "*"
boto3 = "==1.33.7"
botocore = "==1.33.7"
boto3 = "*"
botocore = "*"
celery = {extras = ["sqs"] }
python-dotenv = "*"
pytest-mock = "*"
pytest-celery = "*"
pytesseract = "==0.3.10"
pytesseract = "*"
async-generator = "*"
posthog = "==3.1.0"
posthog = "*"
jq = "==1.6.0"
pytest = "*"
ddtrace = "*"
watchdog = "*"
langchain-community = "*"
langchain-openai = "*"
pydantic-settings = "*"
unstructured = {extras = ["all-docs"], version = "*"}
langfuse = "*"
[dev-packages]
black = "*"

803
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -40,9 +40,11 @@ RUN pip install --no-cache-dir -r requirements.txt --timeout 200
RUN if [ "$DEV_MODE" = "true" ]; then pip install --no-cache debugpy --timeout 200; fi
WORKDIR /code
# Copy the rest of the application
COPY . .
EXPOSE 5050
CMD ["uvicorn", "main:app","--reload", "--host", "0.0.0.0", "--port", "5050", "--workers", "6"]

View File

@ -1,12 +1,18 @@
from typing import List, Tuple
from typing import Dict, List, Tuple
from langchain.schema import AIMessage, BaseMessage, HumanMessage, SystemMessage
from modules.chat.dto.outputs import GetChatHistoryOutput
def format_chat_history(history) -> List[Tuple[str, str]]:
"""Format the chat history into a list of tuples (human, ai)"""
return [(chat.user_message, chat.assistant) for chat in history]
def format_chat_history(
history: List[GetChatHistoryOutput],
) -> List[Dict[str, str]]:
"""Format the chat history into a list of HumanMessage and AIMessage"""
formatted_history = []
for chat in history:
formatted_history.append(HumanMessage(chat.user_message))
formatted_history.append(AIMessage(chat.assistant))
return formatted_history
def format_history_to_openai_mesages(

View File

@ -2,15 +2,12 @@ from typing import Optional
from uuid import UUID
from llm.utils.get_prompt_to_use_id import get_prompt_to_use_id
from modules.prompt.entity.prompt import Prompt
from modules.prompt.service import PromptService
promptService = PromptService()
def get_prompt_to_use(
brain_id: Optional[UUID], prompt_id: Optional[UUID]
) -> Optional[Prompt]:
def get_prompt_to_use(brain_id: Optional[UUID], prompt_id: Optional[UUID]) -> str:
prompt_to_use_id = get_prompt_to_use_id(brain_id, prompt_id)
if prompt_to_use_id is None:
return None

View File

@ -1,7 +1,7 @@
from uuid import UUID
from logger import get_logger
from pydantic import BaseModel
from pydantic import ConfigDict, BaseModel
logger = get_logger(__name__)
@ -10,6 +10,4 @@ class BrainSubscription(BaseModel):
brain_id: UUID
email: str
rights: str = "Viewer"
class Config:
arbitrary_types_allowed = True
model_config = ConfigDict(arbitrary_types_allowed=True)

View File

@ -18,7 +18,7 @@ logger = get_logger(__name__)
class File(BaseModel):
id: Optional[UUID] = None
file: Optional[UploadFile]
file: Optional[UploadFile] = None
file_name: Optional[str] = ""
file_size: Optional[int] = None
file_sha1: Optional[str] = ""

View File

@ -5,7 +5,7 @@ from langchain.embeddings.openai import OpenAIEmbeddings
from logger import get_logger
from models.databases.supabase.supabase import SupabaseDB
from posthog import Posthog
from pydantic import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
from supabase.client import Client, create_client
from vectorstore.supabase import SupabaseVectorStore
@ -13,11 +13,13 @@ logger = get_logger(__name__)
class BrainRateLimiting(BaseSettings):
model_config = SettingsConfigDict(validate_default=False)
max_brain_per_user: int = 5
# The `PostHogSettings` class is used to initialize and interact with the PostHog analytics service.
class PostHogSettings(BaseSettings):
model_config = SettingsConfigDict(validate_default=False)
posthog_api_key: str = None
posthog_api_url: str = None
posthog: Posthog = None
@ -102,15 +104,19 @@ class PostHogSettings(BaseSettings):
class BrainSettings(BaseSettings):
openai_api_key: str
supabase_url: str
supabase_service_key: str
model_config = SettingsConfigDict(validate_default=False)
openai_api_key: str = ""
supabase_url: str = ""
supabase_service_key: str = ""
resend_api_key: str = "null"
resend_email_address: str = "brain@mail.quivr.app"
ollama_api_base_url: str = None
langfuse_public_key: str = None
langfuse_secret_key: str = None
class ResendSettings(BaseSettings):
model_config = SettingsConfigDict(validate_default=False)
resend_api_key: str = "null"

View File

@ -6,7 +6,6 @@ from modules.api_key.repository.api_key_interface import ApiKeysInterface
from modules.api_key.repository.api_keys import ApiKeys
from modules.user.entity.user_identity import UserIdentity
from modules.user.service.user_service import UserService
from pydantic import DateError
logger = get_logger(__name__)
@ -37,7 +36,8 @@ class ApiKeyService:
if api_key_creation_date.year == current_date.year:
return True
return False
except DateError:
except Exception as e:
logger.error(f"Error verifying API key: {e}")
return False
async def get_user_from_api_key(

View File

@ -43,12 +43,12 @@ class CreateBrainProperties(BaseModel, extra=Extra.forbid):
name: Optional[str] = "Default brain"
description: str = "This is a description"
status: Optional[str] = "private"
model: Optional[str]
model: Optional[str] = None
temperature: Optional[float] = 0.0
max_tokens: Optional[int] = 2000
prompt_id: Optional[UUID] = None
brain_type: Optional[BrainType] = BrainType.DOC
brain_definition: Optional[CreateApiBrainDefinition]
brain_definition: Optional[CreateApiBrainDefinition] = None
brain_secrets_values: Optional[dict] = {}
connected_brains_ids: Optional[list[UUID]] = []
integration: Optional[BrainIntegrationSettings] = None
@ -61,14 +61,14 @@ class CreateBrainProperties(BaseModel, extra=Extra.forbid):
class BrainUpdatableProperties(BaseModel):
name: Optional[str]
description: Optional[str]
temperature: Optional[float]
model: Optional[str]
max_tokens: Optional[int]
status: Optional[str]
prompt_id: Optional[UUID]
brain_definition: Optional[ApiBrainDefinitionEntity]
name: Optional[str] = None
description: Optional[str] = None
temperature: Optional[float] = None
model: Optional[str] = None
max_tokens: Optional[int] = None
status: Optional[str] = None
prompt_id: Optional[UUID] = None
brain_definition: Optional[ApiBrainDefinitionEntity] = None
connected_brains_ids: Optional[list[UUID]] = []
integration: Optional[BrainIntegrationUpdateSettings] = None

View File

@ -8,7 +8,7 @@ from pydantic import BaseModel, Extra
class ApiBrainDefinitionSchemaProperty(BaseModel, extra=Extra.forbid):
type: str
description: str
enum: Optional[list]
enum: Optional[list] = None
name: str
def dict(self, **kwargs):
@ -26,7 +26,7 @@ class ApiBrainDefinitionSchema(BaseModel, extra=Extra.forbid):
class ApiBrainDefinitionSecret(BaseModel, extra=Extra.forbid):
name: str
type: str
description: Optional[str]
description: Optional[str] = None
class ApiBrainAllowedMethods(str, Enum):

View File

@ -16,18 +16,18 @@ class BrainType(str, Enum):
class BrainEntity(BaseModel):
brain_id: UUID
name: str
description: Optional[str]
temperature: Optional[float]
model: Optional[str]
max_tokens: Optional[int]
status: Optional[str]
prompt_id: Optional[UUID]
description: Optional[str] = None
temperature: Optional[float] = None
model: Optional[str] = None
max_tokens: Optional[int] = None
status: Optional[str] = None
prompt_id: Optional[UUID] = None
last_update: str
brain_type: BrainType
brain_definition: Optional[ApiBrainDefinitionEntity]
connected_brains_ids: Optional[List[UUID]]
raw: Optional[bool]
jq_instructions: Optional[str]
brain_definition: Optional[ApiBrainDefinitionEntity] = None
connected_brains_ids: Optional[List[UUID]] = None
raw: Optional[bool] = None
jq_instructions: Optional[str] = None
@property
def id(self) -> UUID:
@ -44,11 +44,11 @@ class BrainEntity(BaseModel):
class PublicBrain(BaseModel):
id: UUID
name: str
description: Optional[str]
description: Optional[str] = None
number_of_subscribers: int = 0
last_update: str
brain_type: BrainType
brain_definition: Optional[ApiBrainDefinitionEntity]
brain_definition: Optional[ApiBrainDefinitionEntity] = None
class RoleEnum(str, Enum):

View File

@ -7,8 +7,8 @@ from pydantic import BaseModel
class IntegrationDescriptionEntity(BaseModel):
id: UUID
integration_name: str
integration_logo_url: Optional[str]
connection_settings: Optional[dict]
integration_logo_url: Optional[str] = None
connection_settings: Optional[dict] = None
class IntegrationEntity(BaseModel):
@ -16,5 +16,5 @@ class IntegrationEntity(BaseModel):
user_id: str
brain_id: str
integration_id: str
settings: Optional[dict]
credentials: Optional[dict]
settings: Optional[dict] = None
credentials: Optional[dict] = None

View File

@ -33,7 +33,7 @@ class NotionSearchResponse(BaseModel):
"""Represents the response from the Notion Search API"""
results: list[dict[str, Any]]
next_cursor: Optional[str]
next_cursor: Optional[str] = None
has_more: bool = False

View File

@ -1,6 +1,5 @@
import asyncio
import json
from typing import AsyncIterable, Awaitable, List, Optional
from typing import AsyncIterable, List, Optional
from uuid import UUID
from langchain.callbacks.streaming_aiter import AsyncIteratorCallbackHandler
@ -18,7 +17,8 @@ from modules.chat.dto.chats import ChatQuestion, Sources
from modules.chat.dto.inputs import CreateChatHistory
from modules.chat.dto.outputs import GetChatHistoryOutput
from modules.chat.service.chat_service import ChatService
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from pydantic_settings import BaseSettings
from repository.files.generate_file_signed_url import generate_file_signed_url
logger = get_logger(__name__)
@ -111,30 +111,27 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
Each have the same prompt template, which is defined in the `prompt_template` property.
"""
class Config:
"""Configuration of the Pydantic Object"""
arbitrary_types_allowed = True
model_config = ConfigDict(arbitrary_types_allowed=True)
# Instantiate settings
brain_settings = BrainSettings() # type: ignore other parameters are optional
brain_settings: BaseSettings = BrainSettings()
# Default class attributes
model: str = None # pyright: ignore reportPrivateUsage=none
temperature: float = 0.1
chat_id: str = None # pyright: ignore reportPrivateUsage=none
brain_id: str # pyright: ignore reportPrivateUsage=none
brain_id: str = None # pyright: ignore reportPrivateUsage=none
max_tokens: int = 2000
max_input: int = 2000
streaming: bool = False
knowledge_qa: Optional[RAGInterface]
knowledge_qa: Optional[RAGInterface] = None
metadata: Optional[dict] = None
callbacks: List[AsyncIteratorCallbackHandler] = (
None # pyright: ignore reportPrivateUsage=none
)
prompt_id: Optional[UUID]
prompt_id: Optional[UUID] = None
def __init__(
self,
@ -263,123 +260,61 @@ class KnowledgeBrainQA(BaseModel, QAInterface):
self, chat_id: UUID, question: ChatQuestion, save_answer: bool = True
) -> AsyncIterable:
history = chat_service.get_chat_history(self.chat_id)
callback = AsyncIteratorCallbackHandler()
self.callbacks = [callback]
# The Chain that combines the question and answer
qa = ConversationalRetrievalChain(
retriever=self.knowledge_qa.get_retriever(),
combine_docs_chain=self.knowledge_qa.get_doc_chain(
callbacks=self.callbacks,
streaming=True,
),
question_generator=self.knowledge_qa.get_question_generation_llm(),
verbose=False,
rephrase_question=False,
return_source_documents=True,
)
conversational_qa_chain = self.knowledge_qa.get_chain()
transformed_history = format_chat_history(history)
response_tokens = []
async def wrap_done(fn: Awaitable, event: asyncio.Event):
try:
return await fn
except Exception as e:
logger.error(f"Caught exception: {e}")
return None # Or some sentinel value that indicates failure
finally:
event.set()
brain = brain_service.get_brain_by_id(self.brain_id)
prompt_content = self.prompt_to_use.content if self.prompt_to_use else None
run = asyncio.create_task(
wrap_done(
qa.acall(
{
"question": question.question,
"chat_history": transformed_history,
"custom_personality": prompt_content,
}
),
callback.done,
streamed_chat_history = chat_service.update_chat_history(
CreateChatHistory(
**{
"chat_id": chat_id,
"user_message": question.question,
"assistant": "",
"brain_id": brain.brain_id,
"prompt_id": self.prompt_to_use_id,
}
)
)
brain = brain_service.get_brain_by_id(self.brain_id)
if save_answer:
streamed_chat_history = chat_service.update_chat_history(
CreateChatHistory(
**{
"chat_id": chat_id,
"user_message": question.question,
"assistant": "",
"brain_id": brain.brain_id,
"prompt_id": self.prompt_to_use_id,
}
)
)
streamed_chat_history = GetChatHistoryOutput(
**{
"chat_id": str(chat_id),
"message_id": streamed_chat_history.message_id,
"message_time": streamed_chat_history.message_time,
"user_message": question.question,
"assistant": "",
"prompt_title": (
self.prompt_to_use.title if self.prompt_to_use else None
),
"brain_name": brain.name if brain else None,
"brain_id": str(brain.brain_id) if brain else None,
"metadata": self.metadata,
}
)
else:
streamed_chat_history = GetChatHistoryOutput(
**{
"chat_id": str(chat_id),
"message_id": None,
"message_time": None,
"user_message": question.question,
"assistant": "",
"prompt_title": (
self.prompt_to_use.title if self.prompt_to_use else None
),
"brain_name": brain.name if brain else None,
"brain_id": str(brain.brain_id) if brain else None,
"metadata": self.metadata,
}
)
streamed_chat_history = GetChatHistoryOutput(
**{
"chat_id": str(chat_id),
"message_id": streamed_chat_history.message_id,
"message_time": streamed_chat_history.message_time,
"user_message": question.question,
"assistant": "",
"prompt_title": (
self.prompt_to_use.title if self.prompt_to_use else None
),
"brain_name": brain.name if brain else None,
"brain_id": str(brain.brain_id) if brain else None,
"metadata": self.metadata,
}
)
try:
async for token in callback.aiter():
logger.debug("Token: %s", token)
response_tokens.append(token)
streamed_chat_history.assistant = token
async for chunk in conversational_qa_chain.astream(
{
"question": question.question,
"chat_history": transformed_history,
"custom_personality": (
self.prompt_to_use.content if self.prompt_to_use else None
),
}
):
response_tokens.append(chunk.content)
streamed_chat_history.assistant = chunk.content
yield f"data: {json.dumps(streamed_chat_history.dict())}"
except Exception as e:
logger.error("Error during streaming tokens: %s", e)
try:
# Python
# Await the run
result = await run
sources_list = generate_source(result, brain)
# Create metadata if it doesn't exist
if not streamed_chat_history.metadata:
streamed_chat_history.metadata = {}
# Serialize the sources list
serialized_sources_list = [source.dict() for source in sources_list]
streamed_chat_history.metadata["sources"] = serialized_sources_list
yield f"data: {json.dumps(streamed_chat_history.dict())}"
except Exception as e:
logger.error("Error processing source documents: %s", e)
logger.error("Error generating stream: %s", e)
# Combine all response tokens to form the final assistant message
assistant = "".join(response_tokens)
try:

View File

@ -21,8 +21,7 @@ from modules.chat.dto.chats import ChatQuestion
from modules.chat.dto.inputs import CreateChatHistory
from modules.chat.dto.outputs import GetChatHistoryOutput
from modules.chat.service.chat_service import ChatService
from modules.prompt.entity.prompt import Prompt
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
logger = get_logger(__name__)
SYSTEM_MESSAGE = "Your name is Quivr. You're a helpful assistant. If you don't know the answer, just say that you don't know, don't try to make up an answer.When answering use markdown or any other techniques to display the content in a nice and aerated way."
@ -58,7 +57,7 @@ class HeadlessQA(BaseModel, QAInterface):
self.callbacks = self._determine_callback_array(self.streaming)
@property
def prompt_to_use(self) -> Optional[Prompt]:
def prompt_to_use(self) -> str:
return get_prompt_to_use(None, self.prompt_id)
@property
@ -260,5 +259,4 @@ class HeadlessQA(BaseModel, QAInterface):
assistant=assistant,
)
class Config:
arbitrary_types_allowed = True
model_config = ConfigDict(arbitrary_types_allowed=True)

View File

@ -0,0 +1,210 @@
from operator import itemgetter
from typing import Optional
from uuid import UUID
from langchain.chains import ConversationalRetrievalChain
from langchain.embeddings.ollama import OllamaEmbeddings
from langchain.llms.base import BaseLLM
from langchain.schema import format_document
from langchain_community.chat_models import ChatLiteLLM
from langchain_core.messages import get_buffer_string
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from llm.utils.get_prompt_to_use import get_prompt_to_use
from logger import get_logger
from models import BrainSettings # Importing settings related to the 'brain'
from modules.brain.service.brain_service import BrainService
from modules.chat.service.chat_service import ChatService
from pydantic import BaseModel, ConfigDict
from supabase.client import Client, create_client
from vectorstore.supabase import CustomSupabaseVectorStore
logger = get_logger(__name__)
# First step is to create the Rephrasing Prompt
_template = """Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.
Chat History:
{chat_history}
Follow Up Input: {question}
Standalone question:"""
CONDENSE_QUESTION_PROMPT = PromptTemplate.from_template(_template)
# Next is the answering prompt
template = """Answer the question based only on the following context from files:
{context}
Question: {question}
"""
ANSWER_PROMPT = ChatPromptTemplate.from_template(template)
# How we format documents
DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(
template="File {file_name}: {page_content}"
)
def is_valid_uuid(uuid_to_test, version=4):
try:
uuid_obj = UUID(uuid_to_test, version=version)
except ValueError:
return False
return str(uuid_obj) == uuid_to_test
brain_service = BrainService()
chat_service = ChatService()
class QuivrRAG(BaseModel):
"""
Quivr implementation of the RAGInterface.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
# Instantiate settings
brain_settings = BrainSettings() # type: ignore other parameters are optional
# Default class attributes
model: str = None # pyright: ignore reportPrivateUsage=none
temperature: float = 0.1
chat_id: str = None # pyright: ignore reportPrivateUsage=none
brain_id: str = None # pyright: ignore reportPrivateUsage=none
max_tokens: int = 2000 # Output length
max_input: int = 2000
streaming: bool = False
@property
def embeddings(self):
if self.brain_settings.ollama_api_base_url:
return OllamaEmbeddings(
base_url=self.brain_settings.ollama_api_base_url
) # pyright: ignore reportPrivateUsage=none
else:
return OpenAIEmbeddings()
@property
def prompt_to_use(self):
if self.brain_id and is_valid_uuid(self.brain_id):
return get_prompt_to_use(UUID(self.brain_id), self.prompt_id)
else:
return None
supabase_client: Optional[Client] = None
vector_store: Optional[CustomSupabaseVectorStore] = None
qa: Optional[ConversationalRetrievalChain] = None
prompt_id: Optional[UUID]
def __init__(
self,
model: str,
brain_id: str,
chat_id: str,
streaming: bool = False,
prompt_id: Optional[UUID] = None,
max_tokens: int = 2000,
max_input: int = 2000,
**kwargs,
):
super().__init__(
model=model,
brain_id=brain_id,
chat_id=chat_id,
streaming=streaming,
max_tokens=max_tokens,
max_input=max_input,
**kwargs,
)
self.supabase_client = self._create_supabase_client()
self.vector_store = self._create_vector_store()
self.prompt_id = prompt_id
self.max_tokens = max_tokens
self.max_input = max_input
self.model = model
self.brain_id = brain_id
self.chat_id = chat_id
self.streaming = streaming
logger.info(f"QuivrRAG initialized with model {model} and brain {brain_id}")
logger.info("Max input length: " + str(self.max_input))
def _create_supabase_client(self) -> Client:
return create_client(
self.brain_settings.supabase_url, self.brain_settings.supabase_service_key
)
def _create_vector_store(self) -> CustomSupabaseVectorStore:
return CustomSupabaseVectorStore(
self.supabase_client,
self.embeddings,
table_name="vectors",
brain_id=self.brain_id,
max_input=self.max_input,
)
def _create_llm(
self,
callbacks,
model,
streaming=False,
temperature=0,
) -> BaseLLM:
"""
Create a LLM with the given parameters
"""
if streaming and callbacks is None:
raise ValueError(
"Callbacks must be provided when using streaming language models"
)
api_base = None
if self.brain_settings.ollama_api_base_url and model.startswith("ollama"):
api_base = self.brain_settings.ollama_api_base_url
return ChatLiteLLM(
temperature=temperature,
max_tokens=self.max_tokens,
model=model,
streaming=streaming,
verbose=False,
callbacks=callbacks,
api_base=api_base,
)
def _combine_documents(
docs, document_prompt=DEFAULT_DOCUMENT_PROMPT, document_separator="\n\n"
):
doc_strings = [format_document(doc, document_prompt) for doc in docs]
return document_separator.join(doc_strings)
def get_retriever(self):
return self.vector_store.as_retriever()
def get_chain(self):
retriever = self.get_retriever()
_inputs = RunnableParallel(
standalone_question=RunnablePassthrough.assign(
chat_history=lambda x: get_buffer_string(x["chat_history"])
)
| CONDENSE_QUESTION_PROMPT
| ChatOpenAI(temperature=0)
| StrOutputParser(),
)
_context = {
"context": itemgetter("standalone_question")
| retriever
| self._combine_documents,
"question": lambda x: x["standalone_question"],
}
conversational_qa_chain = _inputs | _context | ANSWER_PROMPT | ChatOpenAI()
return conversational_qa_chain

View File

@ -1,30 +1,69 @@
from operator import itemgetter
from typing import Optional
from uuid import UUID
from langchain.chains import ConversationalRetrievalChain, LLMChain
from langchain.chains.question_answering import load_qa_chain
from langchain.chains import ConversationalRetrievalChain
from langchain.embeddings.ollama import OllamaEmbeddings
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.llms.base import BaseLLM
from langchain.prompts.chat import (
ChatPromptTemplate,
HumanMessagePromptTemplate,
SystemMessagePromptTemplate,
)
from langchain.prompts import HumanMessagePromptTemplate
from langchain.schema import format_document
from langchain_community.chat_models import ChatLiteLLM
from llm.prompts.CONDENSE_PROMPT import CONDENSE_QUESTION_PROMPT
from langchain_core.messages import SystemMessage, get_buffer_string
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from llm.utils.get_prompt_to_use import get_prompt_to_use
from logger import get_logger
from models import BrainSettings # Importing settings related to the 'brain'
from modules.brain.rags.rag_interface import RAGInterface
from modules.brain.service.brain_service import BrainService
from modules.chat.service.chat_service import ChatService
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from pydantic_settings import BaseSettings
from supabase.client import Client, create_client
from vectorstore.supabase import CustomSupabaseVectorStore
logger = get_logger(__name__)
QUIVR_DEFAULT_PROMPT = "Your name is Quivr. You're a helpful assistant. If you don't know the answer, just say that you don't know, don't try to make up an answer."
# First step is to create the Rephrasing Prompt
_template = """Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.
Chat History:
{chat_history}
Follow Up Input: {question}
Standalone question:"""
CONDENSE_QUESTION_PROMPT = PromptTemplate.from_template(_template)
# Next is the answering prompt
template_answer = """
Context:
{context}
User Instructions to follow when answering, default to none: {custom_instructions}
User Question: {question}
Answer:
"""
ANSWER_PROMPT = ChatPromptTemplate.from_messages(
[
SystemMessage(
content=(
"When answering use markdown or any other techniques to display the content in a nice and aerated way. Use the following pieces of context from files provided by the user to answer the users question in the same language as the user question. Your name is Quivr. You're a helpful assistant. If you don't know the answer with the context provided from the files, just say that you don't know, don't try to make up an answer."
)
),
HumanMessagePromptTemplate.from_template(template_answer),
]
)
ChatPromptTemplate.from_template(template_answer)
# How we format documents
DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(
template="File: {file_name} Content: {page_content}"
)
def is_valid_uuid(uuid_to_test, version=4):
@ -40,19 +79,15 @@ brain_service = BrainService()
chat_service = ChatService()
class QuivrRAG(BaseModel, RAGInterface):
class QuivrRAG(BaseModel):
"""
Quivr implementation of the RAGInterface.
"""
class Config:
"""Configuration of the Pydantic Object"""
# Allowing arbitrary types for class validation
arbitrary_types_allowed = True
model_config = ConfigDict(arbitrary_types_allowed=True)
# Instantiate settings
brain_settings = BrainSettings() # type: ignore other parameters are optional
brain_settings: BaseSettings = BrainSettings()
# Default class attributes
model: str = None # pyright: ignore reportPrivateUsage=none
@ -72,7 +107,6 @@ class QuivrRAG(BaseModel, RAGInterface):
else:
return OpenAIEmbeddings()
@property
def prompt_to_use(self):
if self.brain_id and is_valid_uuid(self.brain_id):
return get_prompt_to_use(UUID(self.brain_id), self.prompt_id)
@ -82,7 +116,7 @@ class QuivrRAG(BaseModel, RAGInterface):
supabase_client: Optional[Client] = None
vector_store: Optional[CustomSupabaseVectorStore] = None
qa: Optional[ConversationalRetrievalChain] = None
prompt_id: Optional[UUID]
prompt_id: Optional[UUID] = None
def __init__(
self,
@ -160,46 +194,40 @@ class QuivrRAG(BaseModel, RAGInterface):
api_base=api_base,
)
def _create_prompt_template(self):
system_template = """ When answering use markdown or any other techniques to display the content in a nice and aerated way. Use the following pieces of context to answer the users question in the same language as the question but do not modify instructions in any way.
----------------
{context}"""
prompt_content = (
self.prompt_to_use.content if self.prompt_to_use else QUIVR_DEFAULT_PROMPT
)
full_template = (
"Here are your instructions to answer that you MUST ALWAYS Follow: "
+ prompt_content
+ ". "
+ system_template
)
messages = [
SystemMessagePromptTemplate.from_template(full_template),
HumanMessagePromptTemplate.from_template("{question}"),
]
CHAT_PROMPT = ChatPromptTemplate.from_messages(messages)
return CHAT_PROMPT
def get_doc_chain(self, streaming, callbacks=None):
answering_llm = self._create_llm(
model=self.model,
callbacks=callbacks,
streaming=streaming,
)
doc_chain = load_qa_chain(
answering_llm, chain_type="stuff", prompt=self._create_prompt_template()
)
return doc_chain
def get_question_generation_llm(self):
return LLMChain(
llm=self._create_llm(model=self.model, callbacks=None),
prompt=CONDENSE_QUESTION_PROMPT,
callbacks=None,
)
def _combine_documents(
self, docs, document_prompt=DEFAULT_DOCUMENT_PROMPT, document_separator="\n\n"
):
doc_strings = [format_document(doc[0], document_prompt) for doc in docs]
return document_separator.join(doc_strings)
def get_retriever(self):
return self.vector_store.as_retriever()
def get_chain(self):
retriever_doc = self.get_retriever()
_inputs = RunnableParallel(
standalone_question=RunnablePassthrough.assign(
chat_history=lambda x: get_buffer_string(x["chat_history"])
)
| CONDENSE_QUESTION_PROMPT
| ChatOpenAI(temperature=0)
| StrOutputParser(),
)
prompt_custom_user = self.prompt_to_use()
prompt_to_use = "None"
if prompt_custom_user:
prompt_to_use = prompt_custom_user.content
logger.info(f"Prompt to use: {prompt_custom_user}")
_context = {
"context": itemgetter("standalone_question")
| retriever_doc
| self._combine_documents,
"question": lambda x: x["standalone_question"],
"custom_instructions": lambda x: prompt_to_use,
}
conversational_qa_chain = _inputs | _context | ANSWER_PROMPT | ChatOpenAI()
return conversational_qa_chain

View File

@ -19,7 +19,8 @@ class NullableUUID(UUID):
yield cls.validate
@classmethod
def validate(cls, v) -> UUID | None:
def validate(v, values, **kwargs):
logger.info(f"Validating UUID: {v}")
if v == "":
return None
try:

View File

@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Annotated, List, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Request
@ -12,7 +12,6 @@ from models.user_usage import UserUsage
from modules.brain.service.brain_service import BrainService
from modules.chat.controller.chat.brainful_chat import BrainfulChat
from modules.chat.controller.chat.utils import (
NullableUUID,
check_user_requests_limit,
find_model_and_generate_metadata,
)
@ -88,6 +87,8 @@ def get_answer_generator(
brain_id, chat_question.question, current_user, chat_id, history, vector_store
)
logger.info(f"Brain: {brain}")
model_to_use, metadata = find_model_and_generate_metadata(
chat_id,
brain,
@ -208,12 +209,13 @@ async def create_question_handler(
request: Request,
chat_question: ChatQuestion,
chat_id: UUID,
brain_id: NullableUUID | UUID | None = Query(
..., description="The ID of the brain"
),
brain_id: Annotated[UUID | None, Query()] = None,
current_user: UserIdentity = Depends(get_current_user),
):
try:
logger.info(
f"Creating question for chat {chat_id} with brain {brain_id} of type {type(brain_id)}"
)
gpt_answer_generator = get_answer_generator(
chat_id, chat_question, brain_id, current_user
)
@ -241,11 +243,10 @@ async def create_stream_question_handler(
request: Request,
chat_question: ChatQuestion,
chat_id: UUID,
brain_id: NullableUUID | UUID | None = Query(
..., description="The ID of the brain"
),
brain_id: Annotated[UUID | None, Query()] = None,
current_user: UserIdentity = Depends(get_current_user),
) -> StreamingResponse:
chat_instance = BrainfulChat()
chat_instance.validate_authorization(user_id=current_user.id, brain_id=brain_id)
@ -253,10 +254,17 @@ async def create_stream_question_handler(
id=current_user.id,
email=current_user.email,
)
logger.info(
f"Creating question for chat {chat_id} with brain {brain_id} of type {type(brain_id)}"
)
gpt_answer_generator = get_answer_generator(
chat_id, chat_question, brain_id, current_user
)
logger.info(gpt_answer_generator)
try:
return StreamingResponse(
gpt_answer_generator.generate_stream(

View File

@ -4,7 +4,7 @@ from uuid import UUID
from modules.chat.dto.outputs import GetChatHistoryOutput
from modules.notification.entity.notification import Notification
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
class ChatMessage(BaseModel):
@ -21,11 +21,11 @@ class ChatMessage(BaseModel):
class ChatQuestion(BaseModel):
question: str
model: Optional[str]
temperature: Optional[float]
max_tokens: Optional[int]
brain_id: Optional[UUID]
prompt_id: Optional[UUID]
model: Optional[str] = None
temperature: Optional[float] = None
max_tokens: Optional[int] = None
brain_id: Optional[UUID] = None
prompt_id: Optional[UUID] = None
class Sources(BaseModel):
@ -34,12 +34,6 @@ class Sources(BaseModel):
type: str
original_file_name: str
class Config:
json_encoders = {
**BaseModel.Config.json_encoders,
UUID: lambda v: str(v),
}
class ChatItemType(Enum):
MESSAGE = "MESSAGE"

View File

@ -9,8 +9,8 @@ class CreateChatHistory(BaseModel):
chat_id: UUID
user_message: str
assistant: str
prompt_id: Optional[UUID]
brain_id: Optional[UUID]
prompt_id: Optional[UUID] = None
brain_id: Optional[UUID] = None
class QuestionAndAnswer(BaseModel):

View File

@ -9,11 +9,13 @@ class GetChatHistoryOutput(BaseModel):
message_id: Optional[UUID] | str
user_message: str
assistant: str
message_time: Optional[str]
prompt_title: Optional[str] | None
brain_name: Optional[str] | None
brain_id: Optional[str] | None # string because UUID is not JSON serializable
metadata: Optional[dict] | None
message_time: Optional[str] = None
prompt_title: Optional[str] | None = None
brain_name: Optional[str] | None = None
brain_id: Optional[str] | None = (
None # string because UUID is not JSON serializable
)
metadata: Optional[dict] | None = None
def dict(self, *args, **kwargs):
chat_history = super().dict(*args, **kwargs)
@ -37,8 +39,8 @@ class ChatCompletionMessageToolCall(BaseModel):
class CompletionMessage(BaseModel):
# = "assistant" | "user" | "system" | "tool"
role: str
content: str | None
tool_calls: Optional[List[ChatCompletionMessageToolCall]]
content: str | None = None
tool_calls: Optional[List[ChatCompletionMessageToolCall]] = None
class CompletionResponse(BaseModel):

View File

@ -1,6 +1,7 @@
from pydantic import BaseSettings
from pydantic_settings import BaseSettings, SettingsConfigDict
class ContactsSettings(BaseSettings):
model_config = SettingsConfigDict(validate_default=False)
resend_contact_sales_from: str = "null"
resend_contact_sales_to: str = "null"

View File

@ -23,5 +23,5 @@ class CreateNotificationProperties(BaseModel):
class NotificationUpdatableProperties(BaseModel):
"""Properties that can be received on notification update"""
message: Optional[str]
message: Optional[str] = None
status: Optional[NotificationsStatusEnum] = NotificationsStatusEnum.Done

View File

@ -13,8 +13,8 @@ class NotificationsStatusEnum(str, Enum):
class Notification(BaseModel):
id: UUID
datetime: str
chat_id: Optional[UUID]
message: Optional[str]
chat_id: Optional[UUID] = None
message: Optional[str] = None
action: str
status: NotificationsStatusEnum

View File

@ -1,16 +1,14 @@
from typing import Optional
from pydantic import BaseModel
from pydantic import ConfigDict, BaseModel
class OnboardingUpdatableProperties(BaseModel):
"""Properties that can be received on onboarding update"""
onboarding_a: Optional[bool]
onboarding_b1: Optional[bool]
onboarding_b2: Optional[bool]
onboarding_b3: Optional[bool]
class Config:
extra = "forbid"
onboarding_a: Optional[bool] = None
onboarding_b1: Optional[bool] = None
onboarding_b2: Optional[bool] = None
onboarding_b3: Optional[bool] = None
model_config = ConfigDict(extra="forbid")

View File

@ -28,9 +28,9 @@ class CreatePromptProperties(BaseModel):
class PromptUpdatableProperties(BaseModel):
"""Properties that can be received on prompt update"""
title: Optional[str]
content: Optional[str]
status: Optional[PromptStatusEnum]
title: Optional[str] = None
content: Optional[str] = None
status: Optional[PromptStatusEnum] = None
class DeletePromptResponse(BaseModel):

View File

@ -2,18 +2,18 @@
aiohttp==3.9.3; python_version >= '3.8'
aiosignal==1.3.1; python_version >= '3.7'
amqp==5.2.0; python_version >= '3.6'
annotated-types==0.6.0; python_version >= '3.8'
antlr4-python3-runtime==4.9.3
anyio==3.7.1; python_version >= '3.7'
anyio==4.2.0; python_version >= '3.8'
async-generator==1.10; python_version >= '3.5'
async-timeout==4.0.3; python_full_version <= '3.11.2'
asyncpg==0.27.0; python_full_version >= '3.7.0'
attrs==23.2.0; python_version >= '3.7'
backoff==2.2.1; python_version >= '3.7' and python_version < '4.0'
beautifulsoup4==4.12.3; python_full_version >= '3.6.0'
billiard==4.2.0; python_version >= '3.7'
black==24.2.0; python_version >= '3.8'
boto3==1.33.7; python_version >= '3.7'
botocore==1.33.7; python_version >= '3.7'
boto3==1.34.41; python_version >= '3.8'
botocore==1.34.41; python_version >= '3.8'
bytecode==0.15.1; python_version >= '3.8'
cattrs==23.2.3; python_version >= '3.8'
celery[sqs]==5.3.6; python_version >= '3.8'
@ -21,6 +21,7 @@ certifi==2024.2.2; python_version >= '3.6'
cffi==1.16.0; platform_python_implementation != 'PyPy'
chardet==5.2.0; python_version >= '3.7'
charset-normalizer==3.3.2; python_full_version >= '3.7.0'
chevron==0.14.0
click==8.1.7; python_version >= '3.7'
click-didyoumean==0.3.0; python_full_version >= '3.6.2' and python_full_version < '4.0.0'
click-plugins==1.1.1
@ -33,22 +34,22 @@ cycler==0.12.1; python_version >= '3.8'
dataclasses-json==0.6.4; python_version >= '3.7' and python_version < '4.0'
dataclasses-json-speakeasy==0.5.11; python_version >= '3.7' and python_version < '4.0'
ddsketch==2.0.4; python_version >= '2.7'
ddtrace==2.6.0; python_version >= '3.7'
ddtrace==2.6.1; python_version >= '3.7'
deprecated==1.2.14; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
deprecation==2.1.0
distro==1.9.0; python_version >= '3.6'
docx2txt==0.8
ecdsa==0.18.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
ecdsa==0.18.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
effdet==0.4.1
emoji==2.10.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
envier==0.5.1; python_version >= '3.7'
et-xmlfile==1.1.0; python_version >= '3.6'
fastapi==0.95.2; python_version >= '3.7'
fastapi==0.109.2; python_version >= '3.8'
feedfinder2==0.0.4
feedparser==6.0.11; python_version >= '3.6'
filelock==3.13.1; python_version >= '3.8'
filetype==1.2.0
flake8==6.0.0; python_full_version >= '3.8.1'
flake8==7.0.0; python_full_version >= '3.8.1'
flake8-black==0.3.6; python_version >= '3.7'
flatbuffers==23.5.26
flower==2.0.1; python_version >= '3.7'
@ -57,11 +58,11 @@ frozenlist==1.4.1; python_version >= '3.8'
fsspec==2024.2.0; python_version >= '3.8'
gitdb==4.0.11; python_version >= '3.7'
gitpython==3.1.36; python_version >= '3.7'
gotrue==1.3.1; python_version >= '3.8' and python_version < '4.0'
gotrue==2.1.0; python_version >= '3.8' and python_version < '4.0'
h11==0.14.0; python_version >= '3.7'
html5lib==1.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
httpcore==0.17.3; python_version >= '3.7'
httpx==0.24.1; python_version >= '3.7'
httpcore==1.0.2; python_version >= '3.8'
httpx==0.25.2; python_version >= '3.8'
huggingface-hub==0.20.3; python_full_version >= '3.8.0'
humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
humanize==4.9.0; python_version >= '3.8'
@ -79,22 +80,24 @@ jsonpath-python==1.0.6; python_version >= '3.6'
jsonpointer==2.4; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
kiwisolver==1.4.5; python_version >= '3.7'
kombu[sqs]==5.3.5; python_version >= '3.8'
langchain==0.1.6; python_version < '4.0' and python_full_version >= '3.8.1'
langchain-community==0.0.19; python_version < '4.0' and python_full_version >= '3.8.1'
langchain-core==0.1.22; python_version < '4.0' and python_full_version >= '3.8.1'
langchain==0.1.7; python_full_version >= '3.8.1' and python_version < '4.0'
langchain-community==0.0.20; python_full_version >= '3.8.1' and python_version < '4.0'
langchain-core==0.1.23; python_full_version >= '3.8.1' and python_version < '4.0'
langchain-openai==0.0.6; python_full_version >= '3.8.1' and python_version < '4.0'
langdetect==1.0.9
langsmith==0.0.87; python_version < '4.0' and python_full_version >= '3.8.1'
langfuse==2.13.2; python_full_version >= '3.8.1' and python_version < '4.0'
langsmith==0.0.87; python_full_version >= '3.8.1' and python_version < '4.0'
layoutparser[layoutmodels,tesseract]==0.3.4; python_version >= '3.6'
litellm==1.23.10; python_version not in '2.7, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7' and python_version >= '3.8'
litellm==1.23.14; python_version not in '2.7, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7' and python_version >= '3.8'
lxml==5.1.0; python_version >= '3.6'
markdown==3.5.2; python_version >= '3.8'
markdown==3.5.2
markupsafe==2.1.5; python_version >= '3.7'
marshmallow==3.20.2; python_version >= '3.8'
matplotlib==3.8.2; python_version >= '3.9'
mccabe==0.7.0; python_version >= '3.6'
monotonic==1.6
mpmath==1.3.0
msg-parser==1.2.0; python_version >= '3.4'
msg-parser==1.2.0
multidict==6.0.5; python_version >= '3.7'
mypy-extensions==1.0.0; python_version >= '3.5'
nest-asyncio==1.5.6; python_version >= '3.5'
@ -107,15 +110,15 @@ olefile==0.47; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2,
omegaconf==2.3.0; python_version >= '3.6'
onnx==1.15.0
onnxruntime==1.15.1
openai==1.1.1; python_full_version >= '3.7.1'
openai==1.12.0; python_full_version >= '3.7.1'
opencv-python==4.9.0.80; python_version >= '3.6'
openpyxl==3.1.2; python_version >= '3.6'
openpyxl==3.1.2
opentelemetry-api==1.22.0; python_version >= '3.7'
packaging==23.2; python_version >= '3.7'
pandas==2.2.0; python_version >= '3.8'
pandas==2.2.0
pathspec==0.12.1; python_version >= '3.8'
pdf2image==1.16.3
pdfminer.six==20221105; python_version >= '3.6'
pdfminer.six==20221105
pdfplumber==0.10.4; python_version >= '3.8'
pikepdf==8.12.0
pillow==10.2.0; python_version >= '3.8'
@ -123,18 +126,20 @@ pillow-heif==0.15.0
platformdirs==4.2.0; python_version >= '3.8'
pluggy==1.4.0; python_version >= '3.8'
portalocker==2.8.2; python_version >= '3.8'
postgrest==0.11.0; python_version >= '3.8' and python_version < '4.0'
posthog==3.1.0
postgrest==0.15.0; python_version >= '3.8' and python_version < '4.0'
posthog==3.4.1
prometheus-client==0.19.0; python_version >= '3.8'
prompt-toolkit==3.0.43; python_full_version >= '3.7.0'
protobuf==4.25.2; python_version >= '3.8'
pyasn1==0.5.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
pycocotools==2.0.7; python_version >= '3.5'
pycodestyle==2.10.0; python_version >= '3.6'
pycodestyle==2.11.1; python_version >= '3.8'
pycparser==2.21
pycurl==7.45.2
pydantic==1.10.14; python_version >= '3.7'
pyflakes==3.0.1; python_version >= '3.6'
pydantic==2.6.1; python_version >= '3.8'
pydantic-core==2.16.2; python_version >= '3.8'
pydantic-settings==2.1.0; python_version >= '3.8'
pyflakes==3.2.0; python_version >= '3.8'
pypandoc==1.11; python_version >= '3.6'
pyparsing==3.1.1; python_full_version >= '3.6.8'
pypdf==3.9.0; python_version >= '3.6'
@ -144,8 +149,8 @@ pytesseract==0.3.10; python_version >= '3.7'
pytest==8.0.0; python_version >= '3.8'
pytest-celery==0.0.0
pytest-mock==3.12.0; python_version >= '3.8'
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-docx==1.1.0; python_version >= '3.7'
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
python-docx==1.1.0
python-dotenv==1.0.1; python_version >= '3.8'
python-iso639==2024.2.7; python_version >= '3.8'
python-jose==3.3.0
@ -156,32 +161,32 @@ pytz==2024.1
pyyaml==6.0.1; python_version >= '3.6'
rapidfuzz==3.6.1; python_version >= '3.8'
realtime==1.0.2; python_version >= '3.8' and python_version < '4.0'
redis==4.5.4; python_version >= '3.7'
redis==5.0.1; python_version >= '3.7'
regex==2023.12.25; python_version >= '3.7'
requests==2.31.0; python_version >= '3.7'
requests-file==2.0.0
resend==0.5.1; python_version >= '3.7'
resend==0.7.2; python_version >= '3.7'
rsa==4.9; python_version >= '3.6' and python_version < '4'
s3transfer==0.8.2; python_version >= '3.7'
s3transfer==0.10.0; python_version >= '3.8'
safetensors==0.4.2; python_version >= '3.7'
scipy==1.12.0; python_version >= '3.9'
sentry-sdk[fastapi]==1.40.3
sentry-sdk[fastapi]==1.40.4
setuptools==69.1.0; python_version >= '3.8'
sgmllib3k==1.0.0
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
smmap==5.0.1; python_version >= '3.7'
sniffio==1.3.0; python_version >= '3.7'
soupsieve==2.5; python_version >= '3.8'
sqlalchemy==2.0.26; python_version >= '3.7'
starlette==0.27.0; python_version >= '3.7'
storage3==0.6.1; python_version >= '3.8' and python_version < '4.0'
sqlalchemy==2.0.27; python_version >= '3.7'
starlette==0.36.3; python_version >= '3.8'
storage3==0.7.0; python_version >= '3.8' and python_version < '4.0'
strenum==0.4.15
supabase==1.1.0; python_version >= '3.8' and python_version < '4.0'
supafunc==0.2.3; python_version >= '3.8' and python_version < '4.0'
supabase==2.3.4; python_version >= '3.8' and python_version < '4.0'
supafunc==0.3.3; python_version >= '3.8' and python_version < '4.0'
sympy==1.12; python_version >= '3.8'
tabulate==0.9.0; python_version >= '3.7'
tenacity==8.2.3; python_version >= '3.7'
tiktoken==0.4.0; python_version >= '3.8'
tiktoken==0.6.0; python_version >= '3.8'
timm==0.9.12; python_version >= '3.7'
tinysegmenter==0.3
tldextract==5.1.1; python_version >= '3.8'
@ -205,8 +210,8 @@ watchdog==4.0.0; python_version >= '3.8'
wcwidth==0.2.13
webencodings==0.5.1
websockets==11.0.3; python_version >= '3.7'
wrapt==1.16.0; python_version >= '3.6'
xlrd==1.0.0
wrapt==1.14.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
xlrd==2.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
xlsxwriter==3.1.9; python_version >= '3.6'
xmltodict==0.13.0; python_version >= '3.4'
yarl==1.9.4; python_version >= '3.7'

View File

@ -292,7 +292,7 @@ async def decline_invitation(
class BrainSubscriptionUpdatableProperties(BaseModel):
rights: str | None
rights: str | None = None
email: str

View File

@ -100,16 +100,4 @@ class CustomSupabaseVectorStore(SupabaseVectorStore):
if search.get("content")
]
documents = [doc for doc, _ in match_result]
max_tokens_input = self.max_input
documents_to_return = []
# Limits to max_tokens_input with metadata chunk_size
for doc in documents:
if doc.metadata["chunk_size"] <= max_tokens_input:
documents_to_return.append(doc)
max_tokens_input -= doc.metadata["chunk_size"]
else:
break
return documents_to_return
return match_result

View File

@ -44,14 +44,11 @@ services:
- .env
build:
context: backend
dockerfile: Dockerfile
dockerfile: Dockerfile.dev
container_name: worker
# volumes:
# - ./backend/:/code/
command: >
/bin/sh -c "
watchmedo auto-restart -d . -p '*.py' --recursive -- celery -A celery_worker worker -l info
"
command: celery -A celery_worker worker -l info
restart: always
depends_on:
- redis
@ -63,13 +60,10 @@ services:
- .env
build:
context: backend
dockerfile: Dockerfile
dockerfile: Dockerfile.dev
container_name: beat
command: >
/bin/sh -c "
watchmedo auto-restart -d . -p '*.py' --recursive -- celery -A celery_worker beat -l info
"
command: celery -A celery_worker beat -l info
restart: always
depends_on:
- redis
@ -81,7 +75,7 @@ services:
- .env
build:
context: backend
dockerfile: Dockerfile
dockerfile: Dockerfile.dev
container_name: flower
command: celery -A celery_worker flower -l info --port=5555

View File

@ -60,11 +60,12 @@ export const useQuestion = (): UseChatService => {
const body = JSON.stringify(chatQuestion);
try {
const response = await fetchInstance.post(
`/chat/${chatId}/question/stream?brain_id=${currentBrain?.id ?? ""}`,
body,
headers
);
let url = `/chat/${chatId}/question/stream`;
console.log("currentBrain", currentBrain);
if (currentBrain?.id) {
url += `?brain_id=${currentBrain.id}`;
}
const response = await fetchInstance.post(url, body, headers);
if (!response.ok) {
void handleFetchError(response);