diff --git a/backend/api/quivr_api/main.py b/backend/api/quivr_api/main.py index d7d1d65af..821054b0c 100644 --- a/backend/api/quivr_api/main.py +++ b/backend/api/quivr_api/main.py @@ -14,7 +14,6 @@ from quivr_api.modules.api_key.controller import api_key_router from quivr_api.modules.assistant.controller import assistant_router from quivr_api.modules.brain.controller import brain_router from quivr_api.modules.chat.controller import chat_router -from quivr_api.modules.contact_support.controller import contact_router from quivr_api.modules.knowledge.controller import knowledge_router from quivr_api.modules.misc.controller import misc_router from quivr_api.modules.onboarding.controller import onboarding_router @@ -86,7 +85,6 @@ app.include_router(api_key_router) app.include_router(subscription_router) app.include_router(prompt_router) app.include_router(knowledge_router) -app.include_router(contact_router) PROFILING = os.getenv("PROFILING", "false").lower() == "true" diff --git a/backend/api/quivr_api/models/settings.py b/backend/api/quivr_api/models/settings.py index d4242ed38..5939c0ebb 100644 --- a/backend/api/quivr_api/models/settings.py +++ b/backend/api/quivr_api/models/settings.py @@ -20,6 +20,12 @@ class BrainRateLimiting(BaseSettings): max_brain_per_user: int = 5 +class SendEmailSettings(BaseSettings): + model_config = SettingsConfigDict(validate_default=False) + resend_contact_sales_from: str = "null" + resend_contact_sales_to: str = "null" + + # The `PostHogSettings` class is used to initialize and interact with the PostHog analytics service. class PostHogSettings(BaseSettings): model_config = SettingsConfigDict(validate_default=False) diff --git a/backend/api/quivr_api/modules/assistant/controller/assistant_routes.py b/backend/api/quivr_api/modules/assistant/controller/assistant_routes.py index d6cc19a7f..3f5605a14 100644 --- a/backend/api/quivr_api/modules/assistant/controller/assistant_routes.py +++ b/backend/api/quivr_api/modules/assistant/controller/assistant_routes.py @@ -29,7 +29,6 @@ async def list_assistants( summary = summary_inputs() # difference = difference_inputs() # crawler = crawler_inputs() - # audio_transcript = audio_transcript_inputs() return [summary] diff --git a/backend/api/quivr_api/modules/assistant/ito/audio_transcript.py b/backend/api/quivr_api/modules/assistant/ito/audio_transcript.py deleted file mode 100644 index c3bff9e8a..000000000 --- a/backend/api/quivr_api/modules/assistant/ito/audio_transcript.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -from tempfile import NamedTemporaryFile - -from openai import OpenAI -from quivr_api.logger import get_logger -from quivr_api.modules.assistant.dto.outputs import ( - AssistantOutput, - InputFile, - Inputs, - OutputBrain, - OutputEmail, - Outputs, -) -from quivr_api.modules.assistant.ito.ito import ITO - -logger = get_logger(__name__) - - -class AudioTranscriptAssistant(ITO): - - def __init__( - self, - **kwargs, - ): - super().__init__( - **kwargs, - ) - - async def process_assistant(self): - client = OpenAI() - - logger.info(f"Processing audio file {self.uploadFile.filename}") - - # Extract the original filename and create a temporary file with the same name - filename = os.path.basename(self.uploadFile.filename) - temp_file = NamedTemporaryFile(delete=False, suffix=filename) - - # Write the uploaded file's data to the temporary file - data = await self.uploadFile.read() - temp_file.write(data) - temp_file.close() - - # Open the temporary file and pass it to the OpenAI API - with open(temp_file.name, "rb") as file: - transcription = client.audio.transcriptions.create( - model="whisper-1", file=file, response_format="text" - ) - logger.info(f"Transcription: {transcription}") - - # Delete the temporary file - os.remove(temp_file.name) - - return await self.create_and_upload_processed_file( - transcription, self.uploadFile.filename, "Audio Transcript" - ) - - -def audio_transcript_inputs(): - output = AssistantOutput( - name="Audio Transcript", - description="Transcribes an audio file", - tags=["new"], - input_description="One audio file to transcribe", - output_description="Transcription of the audio file", - inputs=Inputs( - files=[ - InputFile( - key="audio_file", - allowed_extensions=["mp3", "wav", "ogg", "m4a"], - required=True, - description="The audio file to transcribe", - ) - ] - ), - outputs=Outputs( - brain=OutputBrain( - required=True, - description="The brain to which to upload the document", - type="uuid", - ), - email=OutputEmail( - required=True, - description="Send the document by email", - type="str", - ), - ), - ) - return output diff --git a/backend/api/quivr_api/modules/assistant/ito/ito.py b/backend/api/quivr_api/modules/assistant/ito/ito.py index 50779f1c8..72cdda6f9 100644 --- a/backend/api/quivr_api/modules/assistant/ito/ito.py +++ b/backend/api/quivr_api/modules/assistant/ito/ito.py @@ -10,10 +10,10 @@ from typing import List, Optional from fastapi import UploadFile from pydantic import BaseModel from quivr_api.logger import get_logger +from quivr_api.models.settings import SendEmailSettings from quivr_api.modules.assistant.dto.inputs import InputAssistant from quivr_api.modules.assistant.ito.utils.pdf_generator import PDFGenerator, PDFModel from quivr_api.modules.chat.controller.chat.utils import update_user_usage -from quivr_api.modules.contact_support.controller.settings import ContactsSettings from quivr_api.modules.upload.controller.upload_routes import upload_file from quivr_api.modules.user.entity.user_identity import UserIdentity from quivr_api.modules.user.service.user_usage import UserUsage @@ -80,7 +80,7 @@ class ITO(BaseModel): custom_message: str, brain_id: str = None, ): - settings = ContactsSettings() + settings = SendEmailSettings() file = await self.uploadfile_to_file(file) domain_quivr = os.getenv("QUIVR_DOMAIN", "https://chat.quivr.app/") diff --git a/backend/api/quivr_api/modules/brain/api_brain_qa.py b/backend/api/quivr_api/modules/brain/api_brain_qa.py deleted file mode 100644 index 3c7c41448..000000000 --- a/backend/api/quivr_api/modules/brain/api_brain_qa.py +++ /dev/null @@ -1,499 +0,0 @@ -import json -from typing import Optional -from uuid import UUID - -import jq -import requests -from fastapi import HTTPException -from litellm import completion -from quivr_api.logger import get_logger -from quivr_api.modules.brain.knowledge_brain_qa import KnowledgeBrainQA -from quivr_api.modules.brain.qa_interface import QAInterface -from quivr_api.modules.brain.service.brain_service import BrainService -from quivr_api.modules.brain.service.call_brain_api import call_brain_api -from quivr_api.modules.brain.service.get_api_brain_definition_as_json_schema import ( - get_api_brain_definition_as_json_schema, -) -from quivr_api.modules.chat.dto.chats import ChatQuestion -from quivr_api.modules.chat.dto.inputs import CreateChatHistory -from quivr_api.modules.chat.dto.outputs import GetChatHistoryOutput -from quivr_api.modules.chat.service.chat_service import ChatService -from quivr_api.modules.dependencies import get_service - -brain_service = BrainService() -chat_service = get_service(ChatService)() - -logger = get_logger(__name__) - - -class UUIDEncoder(json.JSONEncoder): - def default(self, obj): - if isinstance(obj, UUID): - # if the object is uuid, we simply return the value of uuid - return str(obj) - return super().default(obj) - - -class APIBrainQA(KnowledgeBrainQA, QAInterface): - user_id: UUID - raw: bool = False - jq_instructions: Optional[str] = None - - def __init__( - self, - model: str, - brain_id: str, - chat_id: str, - streaming: bool = False, - prompt_id: Optional[UUID] = None, - raw: bool = False, - jq_instructions: Optional[str] = None, - **kwargs, - ): - user_id = kwargs.get("user_id") - if not user_id: - raise HTTPException(status_code=400, detail="Cannot find user id") - - super().__init__( - model=model, - brain_id=brain_id, - chat_id=chat_id, - streaming=streaming, - prompt_id=prompt_id, - **kwargs, - ) - self.user_id = user_id - self.raw = raw - self.jq_instructions = jq_instructions - - def get_api_call_response_as_text( - self, method, api_url, params, search_params, secrets - ) -> str: - headers = {} - - api_url_with_search_params = api_url - if search_params: - api_url_with_search_params += "?" - for search_param in search_params: - api_url_with_search_params += ( - f"{search_param}={search_params[search_param]}&" - ) - - for secret in secrets: - headers[secret] = secrets[secret] - - try: - if method in ["GET", "DELETE"]: - response = requests.request( - method, - url=api_url_with_search_params, - params=params or None, - headers=headers or None, - ) - elif method in ["POST", "PUT", "PATCH"]: - response = requests.request( - method, - url=api_url_with_search_params, - json=params or None, - headers=headers or None, - ) - else: - raise ValueError(f"Invalid method: {method}") - - return response.text - - except Exception as e: - logger.error(f"Error calling API: {e}") - return None - - def log_steps(self, message: str, type: str): - if "api" not in self.metadata: - self.metadata["api"] = {} - if "steps" not in self.metadata["api"]: - self.metadata["api"]["steps"] = [] - self.metadata["api"]["steps"].append( - { - "number": len(self.metadata["api"]["steps"]), - "type": type, - "message": message, - } - ) - - async def make_completion( - self, - messages, - functions, - brain_id: UUID, - recursive_count=0, - should_log_steps=True, - ) -> str | None: - if recursive_count > 5: - self.log_steps( - "The assistant is having issues and took more than 5 calls to the API. Please try again later or an other instruction.", - "error", - ) - return - - if "api" not in self.metadata: - self.metadata["api"] = {} - if "raw" not in self.metadata["api"]: - self.metadata["api"]["raw_enabled"] = self.raw - - response = completion( - model=self.model, - temperature=self.temperature, - max_tokens=self.max_tokens, - messages=messages, - functions=functions, - stream=True, - function_call="auto", - ) - - function_call = { - "name": None, - "arguments": "", - } - for chunk in response: - finish_reason = chunk.choices[0].finish_reason - if finish_reason == "stop": - self.log_steps("Quivr has finished", "info") - break - if ( - "function_call" in chunk.choices[0].delta - and chunk.choices[0].delta["function_call"] - ): - if chunk.choices[0].delta["function_call"].name: - function_call["name"] = chunk.choices[0].delta["function_call"].name - if chunk.choices[0].delta["function_call"].arguments: - function_call["arguments"] += ( - chunk.choices[0].delta["function_call"].arguments - ) - - elif finish_reason == "function_call": - try: - arguments = json.loads(function_call["arguments"]) - - except Exception: - self.log_steps(f"Issues with {arguments}", "error") - arguments = {} - - self.log_steps(f"Calling {brain_id} with arguments {arguments}", "info") - - try: - api_call_response = call_brain_api( - brain_id=brain_id, - user_id=self.user_id, - arguments=arguments, - ) - except Exception as e: - logger.info(f"Error while calling API: {e}") - api_call_response = f"Error while calling API: {e}" - function_name = function_call["name"] - self.log_steps("Quivr has called the API", "info") - messages.append( - { - "role": "function", - "name": function_call["name"], - "content": f"The function {function_name} was called and gave The following answer:(data from function) {api_call_response} (end of data from function). Don't call this function again unless there was an error or extremely necessary and asked specifically by the user. If an error, display it to the user in raw.", - } - ) - - self.metadata["api"]["raw_response"] = json.loads(api_call_response) - if self.raw: - # Yield the raw response in a format that can then be catched by the generate_stream function - response_to_yield = f"````raw_response: {api_call_response}````" - - yield response_to_yield - return - - async for value in self.make_completion( - messages=messages, - functions=functions, - brain_id=brain_id, - recursive_count=recursive_count + 1, - should_log_steps=should_log_steps, - ): - yield value - - else: - if ( - hasattr(chunk.choices[0], "delta") - and chunk.choices[0].delta - and hasattr(chunk.choices[0].delta, "content") - ): - content = chunk.choices[0].delta.content - yield content - else: # pragma: no cover - yield "**...**" - break - - async def generate_stream( - self, - chat_id: UUID, - question: ChatQuestion, - save_answer: bool = True, - should_log_steps: Optional[bool] = True, - ): - brain = brain_service.get_brain_by_id(self.brain_id) - - if not brain: - raise HTTPException(status_code=404, detail="Brain not found") - - prompt_content = "You are a helpful assistant that can access functions to help answer questions. If there are information missing in the question, you can ask follow up questions to get more information to the user. Once all the information is available, you can call the function to get the answer." - - if self.prompt_to_use: - prompt_content += self.prompt_to_use.content - - messages = [{"role": "system", "content": prompt_content}] - - history = chat_service.get_chat_history(self.chat_id) - - for message in history: - formatted_message = [ - {"role": "user", "content": message.user_message}, - {"role": "assistant", "content": message.assistant}, - ] - messages.extend(formatted_message) - - messages.append({"role": "user", "content": question.question}) - - if save_answer: - streamed_chat_history = chat_service.update_chat_history( - CreateChatHistory( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": "", - "brain_id": self.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(self.brain_id), - "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(self.brain_id), - "metadata": self.metadata, - } - ) - response_tokens = [] - async for value in self.make_completion( - messages=messages, - functions=[get_api_brain_definition_as_json_schema(brain)], - brain_id=self.brain_id, - should_log_steps=should_log_steps, - ): - # Look if the value is a raw response - if value.startswith("````raw_response:"): - raw_value_cleaned = value.replace("````raw_response: ", "").replace( - "````", "" - ) - logger.info(f"Raw response: {raw_value_cleaned}") - if self.jq_instructions: - json_raw_value_cleaned = json.loads(raw_value_cleaned) - raw_value_cleaned = ( - jq.compile(self.jq_instructions) - .input_value(json_raw_value_cleaned) - .first() - ) - streamed_chat_history.assistant = raw_value_cleaned - response_tokens.append(raw_value_cleaned) - yield f"data: {json.dumps(streamed_chat_history.dict())}" - else: - streamed_chat_history.assistant = value - response_tokens.append(value) - yield f"data: {json.dumps(streamed_chat_history.dict())}" - - if save_answer: - chat_service.update_message_by_id( - message_id=str(streamed_chat_history.message_id), - user_message=question.question, - assistant="".join(str(token) for token in response_tokens), - metadata=self.metadata, - ) - - def make_completion_without_streaming( - self, - messages, - functions, - brain_id: UUID, - recursive_count=0, - should_log_steps=False, - ): - if recursive_count > 5: - print( - "The assistant is having issues and took more than 5 calls to the API. Please try again later or an other instruction." - ) - return - - if should_log_steps: - print("🧠🧠") - - response = completion( - model=self.model, - temperature=self.temperature, - max_tokens=self.max_tokens, - messages=messages, - functions=functions, - stream=False, - function_call="auto", - ) - - response_message = response.choices[0].message - finish_reason = response.choices[0].finish_reason - - if finish_reason == "function_call": - function_call = response_message.function_call - try: - arguments = json.loads(function_call.arguments) - - except Exception: - arguments = {} - - if should_log_steps: - self.log_steps(f"Calling {brain_id} with arguments {arguments}", "info") - - try: - api_call_response = call_brain_api( - brain_id=brain_id, - user_id=self.user_id, - arguments=arguments, - ) - except Exception as e: - raise HTTPException( - status_code=400, - detail=f"Error while calling API: {e}", - ) - - function_name = function_call.name - messages.append( - { - "role": "function", - "name": function_call.name, - "content": f"The function {function_name} was called and gave The following answer:(data from function) {api_call_response} (end of data from function). Don't call this function again unless there was an error or extremely necessary and asked specifically by the user.", - } - ) - - return self.make_completion_without_streaming( - messages=messages, - functions=functions, - brain_id=brain_id, - recursive_count=recursive_count + 1, - should_log_steps=should_log_steps, - ) - - if finish_reason == "stop": - return response_message - - else: - print("Never ending completion") - - def generate_answer( - self, - chat_id: UUID, - question: ChatQuestion, - save_answer: bool = True, - raw: bool = True, - ): - if not self.brain_id: - raise HTTPException( - status_code=400, detail="No brain id provided in the question" - ) - - brain = brain_service.get_brain_by_id(self.brain_id) - - if not brain: - raise HTTPException(status_code=404, detail="Brain not found") - - prompt_content = "You are a helpful assistant that can access functions to help answer questions. If there are information missing in the question, you can ask follow up questions to get more information to the user. Once all the information is available, you can call the function to get the answer." - - if self.prompt_to_use: - prompt_content += self.prompt_to_use.content - - messages = [{"role": "system", "content": prompt_content}] - - history = chat_service.get_chat_history(self.chat_id) - - for message in history: - formatted_message = [ - {"role": "user", "content": message.user_message}, - {"role": "assistant", "content": message.assistant}, - ] - messages.extend(formatted_message) - - messages.append({"role": "user", "content": question.question}) - - response = self.make_completion_without_streaming( - messages=messages, - functions=[get_api_brain_definition_as_json_schema(brain)], - brain_id=self.brain_id, - should_log_steps=False, - raw=raw, - ) - - answer = response.content - if save_answer: - new_chat = chat_service.update_chat_history( - CreateChatHistory( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "brain_id": self.brain_id, - "prompt_id": self.prompt_to_use_id, - } - ) - ) - - return GetChatHistoryOutput( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "message_time": new_chat.message_time, - "prompt_title": ( - self.prompt_to_use.title if self.prompt_to_use else None - ), - "brain_name": brain.name if brain else None, - "message_id": new_chat.message_id, - "metadata": self.metadata, - "brain_id": str(self.brain_id), - } - ) - return GetChatHistoryOutput( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "message_time": "123", - "prompt_title": None, - "brain_name": brain.name, - "message_id": None, - "metadata": self.metadata, - "brain_id": str(self.brain_id), - } - ) diff --git a/backend/api/quivr_api/modules/brain/composite_brain_qa.py b/backend/api/quivr_api/modules/brain/composite_brain_qa.py deleted file mode 100644 index 467d5dcde..000000000 --- a/backend/api/quivr_api/modules/brain/composite_brain_qa.py +++ /dev/null @@ -1,592 +0,0 @@ -import json -from typing import Optional -from uuid import UUID - -from fastapi import HTTPException -from litellm import completion -from quivr_api.logger import get_logger -from quivr_api.modules.brain.api_brain_qa import APIBrainQA -from quivr_api.modules.brain.entity.brain_entity import BrainEntity, BrainType -from quivr_api.modules.brain.knowledge_brain_qa import KnowledgeBrainQA -from quivr_api.modules.brain.qa_headless import HeadlessQA -from quivr_api.modules.brain.service.brain_service import BrainService -from quivr_api.modules.chat.dto.chats import ChatQuestion -from quivr_api.modules.chat.dto.inputs import CreateChatHistory -from quivr_api.modules.chat.dto.outputs import ( - BrainCompletionOutput, - CompletionMessage, - CompletionResponse, - GetChatHistoryOutput, -) -from quivr_api.modules.chat.service.chat_service import ChatService -from quivr_api.modules.dependencies import get_service - -brain_service = BrainService() -chat_service = get_service(ChatService)() - -logger = get_logger(__name__) - - -def format_brain_to_tool(brain): - return { - "type": "function", - "function": { - "name": str(brain.id), - "description": brain.description, - "parameters": { - "type": "object", - "properties": { - "question": { - "type": "string", - "description": "Question to ask the brain", - }, - }, - "required": ["question"], - }, - }, - } - - -class CompositeBrainQA( - KnowledgeBrainQA, -): - user_id: UUID - - def __init__( - self, - model: str, - brain_id: str, - chat_id: str, - streaming: bool = False, - prompt_id: Optional[UUID] = None, - **kwargs, - ): - user_id = kwargs.get("user_id") - if not user_id: - raise HTTPException(status_code=400, detail="Cannot find user id") - - super().__init__( - model=model, - brain_id=brain_id, - chat_id=chat_id, - streaming=streaming, - prompt_id=prompt_id, - **kwargs, - ) - self.user_id = user_id - - def get_answer_generator_from_brain_type(self, brain: BrainEntity): - if brain.brain_type == BrainType.composite: - return self.generate_answer - elif brain.brain_type == BrainType.api: - return APIBrainQA( - brain_id=str(brain.id), - chat_id=self.chat_id, - model=self.model, - max_tokens=self.max_tokens, - temperature=self.temperature, - streaming=self.streaming, - prompt_id=self.prompt_id, - user_id=str(self.user_id), - raw=brain.raw, - jq_instructions=brain.jq_instructions, - ).generate_answer - elif brain.brain_type == BrainType.doc: - return KnowledgeBrainQA( - brain_id=str(brain.id), - chat_id=self.chat_id, - max_tokens=self.max_tokens, - temperature=self.temperature, - streaming=self.streaming, - prompt_id=self.prompt_id, - ).generate_answer - - def generate_answer( - self, chat_id: UUID, question: ChatQuestion, save_answer: bool - ) -> str: - brain = brain_service.get_brain_by_id(question.brain_id) - - connected_brains = brain_service.get_connected_brains(self.brain_id) - - if not connected_brains: - response = HeadlessQA( - chat_id=chat_id, - model=self.model, - max_tokens=self.max_tokens, - temperature=self.temperature, - streaming=self.streaming, - prompt_id=self.prompt_id, - ).generate_answer(chat_id, question, save_answer=False) - if save_answer: - new_chat = chat_service.update_chat_history( - CreateChatHistory( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": response.assistant, - "brain_id": question.brain_id, - "prompt_id": self.prompt_to_use_id, - } - ) - ) - return GetChatHistoryOutput( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": response.assistant, - "message_time": new_chat.message_time, - "prompt_title": ( - self.prompt_to_use.title if self.prompt_to_use else None - ), - "brain_name": brain.name, - "message_id": new_chat.message_id, - "brain_id": str(brain.id), - } - ) - return GetChatHistoryOutput( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": response.assistant, - "message_time": None, - "prompt_title": ( - self.prompt_to_use.title if self.prompt_to_use else None - ), - "brain_name": brain.name, - "message_id": None, - "brain_id": str(brain.id), - } - ) - - tools = [] - available_functions = {} - - connected_brains_details = {} - for connected_brain_id in connected_brains: - connected_brain = brain_service.get_brain_by_id(connected_brain_id) - if connected_brain is None: - continue - - tools.append(format_brain_to_tool(connected_brain)) - - available_functions[connected_brain_id] = ( - self.get_answer_generator_from_brain_type(connected_brain) - ) - - connected_brains_details[str(connected_brain.id)] = connected_brain - - CHOOSE_BRAIN_FROM_TOOLS_PROMPT = ( - "Based on the provided user content, find the most appropriate tools to answer" - + "If you can't find any tool to answer and only then, and if you can answer without using any tool. In that case, let the user know that you are not using any particular brain (i.e tool) " - ) - - messages = [{"role": "system", "content": CHOOSE_BRAIN_FROM_TOOLS_PROMPT}] - - history = chat_service.get_chat_history(self.chat_id) - - for message in history: - formatted_message = [ - {"role": "user", "content": message.user_message}, - {"role": "assistant", "content": message.assistant}, - ] - messages.extend(formatted_message) - - messages.append({"role": "user", "content": question.question}) - - response = completion( - model="gpt-3.5-turbo-0125", - messages=messages, - tools=tools, - tool_choice="auto", - ) - - brain_completion_output = self.make_recursive_tool_calls( - messages, - question, - chat_id, - tools, - available_functions, - recursive_count=0, - last_completion_response=response.choices[0], - ) - - if brain_completion_output: - answer = brain_completion_output.response.message.content - new_chat = None - if save_answer: - new_chat = chat_service.update_chat_history( - CreateChatHistory( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "brain_id": question.brain_id, - "prompt_id": self.prompt_to_use_id, - } - ) - ) - return GetChatHistoryOutput( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": brain_completion_output.response.message.content, - "message_time": new_chat.message_time if new_chat else None, - "prompt_title": ( - self.prompt_to_use.title if self.prompt_to_use else None - ), - "brain_name": brain.name if brain else None, - "message_id": new_chat.message_id if new_chat else None, - "brain_id": str(brain.id) if brain else None, - } - ) - - def make_recursive_tool_calls( - self, - messages, - question, - chat_id, - tools=[], - available_functions={}, - recursive_count=0, - last_completion_response: CompletionResponse = None, - ): - if recursive_count > 5: - print( - "The assistant is having issues and took more than 5 calls to the tools. Please try again later or an other instruction." - ) - return None - - finish_reason = last_completion_response.finish_reason - if finish_reason == "stop": - messages.append(last_completion_response.message) - return BrainCompletionOutput( - **{ - "messages": messages, - "question": question.question, - "response": last_completion_response, - } - ) - - if finish_reason == "tool_calls": - response_message: CompletionMessage = last_completion_response.message - tool_calls = response_message.tool_calls - - messages.append(response_message) - - if ( - len(tool_calls) == 0 - or tool_calls is None - or len(available_functions) == 0 - ): - return - - for tool_call in tool_calls: - function_name = tool_call.function.name - function_to_call = available_functions[function_name] - function_args = json.loads(tool_call.function.arguments) - question = ChatQuestion( - question=function_args["question"], brain_id=function_name - ) - - # TODO: extract chat_id from generate_answer function of XBrainQA - function_response = function_to_call( - chat_id=chat_id, - question=question, - save_answer=False, - ) - messages.append( - { - "tool_call_id": tool_call.id, - "role": "tool", - "name": function_name, - "content": function_response.assistant, - } - ) - - PROMPT_2 = "If initial question can be answered by our conversation messages, then give an answer and end the conversation." - - messages.append({"role": "system", "content": PROMPT_2}) - - for idx, msg in enumerate(messages): - logger.info( - f"Message {idx}: Role - {msg['role']}, Content - {msg['content']}" - ) - - response_after_tools_answers = completion( - model="gpt-3.5-turbo-0125", - messages=messages, - tools=tools, - tool_choice="auto", - ) - - return self.make_recursive_tool_calls( - messages, - question, - chat_id, - tools, - available_functions, - recursive_count=recursive_count + 1, - last_completion_response=response_after_tools_answers.choices[0], - ) - - async def generate_stream( - self, - chat_id: UUID, - question: ChatQuestion, - save_answer: bool, - should_log_steps: Optional[bool] = True, - ): - brain = brain_service.get_brain_by_id(question.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": question.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.id) if brain else None, - } - ) - 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.id) if brain else None, - } - ) - - connected_brains = brain_service.get_connected_brains(self.brain_id) - - if not connected_brains: - headlesss_answer = HeadlessQA( - chat_id=chat_id, - model=self.model, - max_tokens=self.max_tokens, - temperature=self.temperature, - streaming=self.streaming, - prompt_id=self.prompt_id, - ).generate_stream(chat_id, question) - - response_tokens = [] - async for value in headlesss_answer: - streamed_chat_history.assistant = value - response_tokens.append(value) - yield f"data: {json.dumps(streamed_chat_history.dict())}" - - if save_answer: - chat_service.update_message_by_id( - message_id=str(streamed_chat_history.message_id), - user_message=question.question, - assistant="".join(response_tokens), - ) - - tools = [] - available_functions = {} - - connected_brains_details = {} - for brain_id in connected_brains: - brain = brain_service.get_brain_by_id(brain_id) - if brain == None: - continue - - tools.append(format_brain_to_tool(brain)) - - available_functions[brain_id] = self.get_answer_generator_from_brain_type( - brain - ) - - connected_brains_details[str(brain.id)] = brain - - CHOOSE_BRAIN_FROM_TOOLS_PROMPT = ( - "Based on the provided user content, find the most appropriate tools to answer" - + "If you can't find any tool to answer and only then, and if you can answer without using any tool. In that case, let the user know that you are not using any particular brain (i.e tool) " - ) - - messages = [{"role": "system", "content": CHOOSE_BRAIN_FROM_TOOLS_PROMPT}] - - history = chat_service.get_chat_history(self.chat_id) - - for message in history: - formatted_message = [ - {"role": "user", "content": message.user_message}, - {"role": "assistant", "content": message.assistant}, - ] - if message.assistant is None: - print(message) - messages.extend(formatted_message) - - messages.append({"role": "user", "content": question.question}) - - initial_response = completion( - model="gpt-3.5-turbo-0125", - stream=True, - messages=messages, - tools=tools, - tool_choice="auto", - ) - - response_tokens = [] - tool_calls_aggregate = [] - for chunk in initial_response: - content = chunk.choices[0].delta.content - if content is not None: - # Need to store it ? - streamed_chat_history.assistant = content - response_tokens.append(chunk.choices[0].delta.content) - - if save_answer: - yield f"data: {json.dumps(streamed_chat_history.dict())}" - else: - yield f"🧠<' {chunk.choices[0].delta.content}" - - if ( - "tool_calls" in chunk.choices[0].delta - and chunk.choices[0].delta.tool_calls is not None - ): - tool_calls = chunk.choices[0].delta.tool_calls - for tool_call in tool_calls: - id = tool_call.id - name = tool_call.function.name - if id and name: - tool_calls_aggregate += [ - { - "id": tool_call.id, - "function": { - "arguments": tool_call.function.arguments, - "name": tool_call.function.name, - }, - "type": "function", - } - ] - - else: - try: - tool_calls_aggregate[tool_call.index]["function"][ - "arguments" - ] += tool_call.function.arguments - except IndexError: - print("TOOL_CALL_INDEX error", tool_call.index) - print("TOOL_CALLS_AGGREGATE error", tool_calls_aggregate) - - finish_reason = chunk.choices[0].finish_reason - - if finish_reason == "stop": - if save_answer: - chat_service.update_message_by_id( - message_id=str(streamed_chat_history.message_id), - user_message=question.question, - assistant="".join( - [ - token - for token in response_tokens - if not token.startswith("🧠<") - ] - ), - ) - break - - if finish_reason == "tool_calls": - messages.append( - { - "role": "assistant", - "tool_calls": tool_calls_aggregate, - "content": None, - } - ) - for tool_call in tool_calls_aggregate: - function_name = tool_call["function"]["name"] - queried_brain = connected_brains_details[function_name] - function_to_call = available_functions[function_name] - function_args = json.loads(tool_call["function"]["arguments"]) - print("function_args", function_args["question"]) - question = ChatQuestion( - question=function_args["question"], brain_id=queried_brain.id - ) - - # yield f"🧠< Querying the brain {queried_brain.name} with the following arguments: {function_args} >🧠", - - print( - f"🧠< Querying the brain {queried_brain.name} with the following arguments: {function_args}", - ) - function_response = function_to_call( - chat_id=chat_id, - question=question, - save_answer=False, - ) - - messages.append( - { - "tool_call_id": tool_call["id"], - "role": "tool", - "name": function_name, - "content": function_response.assistant, - } - ) - - print("messages", messages) - - PROMPT_2 = "If the last user's question can be answered by our conversation messages since then, then give an answer and end the conversation. If you need to ask question to the user to gather more information and give a more accurate answer, then ask the question and wait for the user's answer." - # Otherwise, ask a new question to the assistant and choose brains you would like to ask questions." - - messages.append({"role": "system", "content": PROMPT_2}) - - response_after_tools_answers = completion( - model="gpt-3.5-turbo-0125", - messages=messages, - tools=tools, - tool_choice="auto", - stream=True, - ) - - response_tokens = [] - for chunk in response_after_tools_answers: - print("chunk_response_after_tools_answers", chunk) - content = chunk.choices[0].delta.content - if content: - streamed_chat_history.assistant = content - response_tokens.append(chunk.choices[0].delta.content) - yield f"data: {json.dumps(streamed_chat_history.dict())}" - - finish_reason = chunk.choices[0].finish_reason - - if finish_reason == "stop": - chat_service.update_message_by_id( - message_id=str(streamed_chat_history.message_id), - user_message=question.question, - assistant="".join( - [ - token - for token in response_tokens - if not token.startswith("🧠<") - ] - ), - ) - break - elif finish_reason is not None: - # TODO: recursively call with tools (update prompt + create intermediary function ) - print("NO STOP") - print(chunk.choices[0]) diff --git a/backend/api/quivr_api/modules/brain/controller/brain_routes.py b/backend/api/quivr_api/modules/brain/controller/brain_routes.py index ff519a50c..05add57e3 100644 --- a/backend/api/quivr_api/modules/brain/controller/brain_routes.py +++ b/backend/api/quivr_api/modules/brain/controller/brain_routes.py @@ -1,4 +1,3 @@ -from typing import Dict from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Request @@ -9,7 +8,7 @@ from quivr_api.modules.brain.dto.inputs import ( BrainUpdatableProperties, CreateBrainProperties, ) -from quivr_api.modules.brain.entity.brain_entity import PublicBrain, RoleEnum +from quivr_api.modules.brain.entity.brain_entity import RoleEnum from quivr_api.modules.brain.entity.integration_brain import ( IntegrationDescriptionEntity, ) @@ -44,7 +43,8 @@ integration_brain_description_service = IntegrationBrainDescriptionService() ) async def get_integration_brain_description() -> list[IntegrationDescriptionEntity]: """Retrieve the integration brain description.""" - return integration_brain_description_service.get_all_integration_descriptions() + # TODO: Deprecated, remove this endpoint + return [] @brain_router.get("/brains/", dependencies=[Depends(AuthBearer())], tags=["Brain"]) @@ -56,14 +56,6 @@ async def retrieve_all_brains_for_user( return {"brains": brains} -@brain_router.get( - "/brains/public", dependencies=[Depends(AuthBearer())], tags=["Brain"] -) -async def retrieve_public_brains() -> list[PublicBrain]: - """Retrieve all Quivr public brains.""" - return brain_service.get_public_brains() - - @brain_router.get( "/brains/{brain_id}/", dependencies=[ @@ -156,67 +148,6 @@ async def update_existing_brain( return {"message": f"Brain {brain_id} has been updated."} -@brain_router.put( - "/brains/{brain_id}/secrets-values", - dependencies=[ - Depends(AuthBearer()), - ], - tags=["Brain"], -) -async def update_existing_brain_secrets( - brain_id: UUID, - secrets: Dict[str, str], - current_user: UserIdentity = Depends(get_current_user), -): - """Update an existing brain's secrets.""" - - existing_brain = brain_service.get_brain_details(brain_id, None) - - if existing_brain is None: - raise HTTPException(status_code=404, detail="Brain not found") - - if ( - existing_brain.brain_definition is None - or existing_brain.brain_definition.secrets is None - ): - raise HTTPException( - status_code=400, - detail="This brain does not support secrets.", - ) - - is_brain_user = ( - brain_user_service.get_brain_for_user( - user_id=current_user.id, - brain_id=brain_id, - ) - is not None - ) - - if not is_brain_user: - raise HTTPException( - status_code=403, - detail="You are not authorized to update this brain.", - ) - - secrets_names = [secret.name for secret in existing_brain.brain_definition.secrets] - - for key, value in secrets.items(): - if key not in secrets_names: - raise HTTPException( - status_code=400, - detail=f"Secret {key} is not a valid secret.", - ) - if value: - brain_service.update_secret_value( - user_id=current_user.id, - brain_id=brain_id, - secret_name=key, - secret_value=value, - ) - - return {"message": f"Brain {brain_id} has been updated."} - - @brain_router.post( "/brains/{brain_id}/documents", dependencies=[Depends(AuthBearer()), Depends(has_brain_authorization())], diff --git a/backend/api/quivr_api/modules/brain/dto/inputs.py b/backend/api/quivr_api/modules/brain/dto/inputs.py index 08b126234..bab8ef292 100644 --- a/backend/api/quivr_api/modules/brain/dto/inputs.py +++ b/backend/api/quivr_api/modules/brain/dto/inputs.py @@ -3,28 +3,12 @@ from uuid import UUID from pydantic import BaseModel from quivr_api.logger import get_logger -from quivr_api.modules.brain.entity.api_brain_definition_entity import ( - ApiBrainAllowedMethods, - ApiBrainDefinitionEntity, - ApiBrainDefinitionSchema, - ApiBrainDefinitionSecret, -) from quivr_api.modules.brain.entity.brain_entity import BrainType from quivr_api.modules.brain.entity.integration_brain import IntegrationType logger = get_logger(__name__) -class CreateApiBrainDefinition(BaseModel, extra="ignore"): - method: ApiBrainAllowedMethods - url: str - params: Optional[ApiBrainDefinitionSchema] = ApiBrainDefinitionSchema() - search_params: ApiBrainDefinitionSchema = ApiBrainDefinitionSchema() - secrets: Optional[list[ApiBrainDefinitionSecret]] = [] - raw: Optional[bool] = False - jq_instructions: Optional[str] = None - - class CreateIntegrationBrain(BaseModel, extra="ignore"): integration_name: str integration_logo_url: str @@ -52,9 +36,6 @@ class CreateBrainProperties(BaseModel, extra="ignore"): max_tokens: Optional[int] = 2000 prompt_id: Optional[UUID] = None brain_type: Optional[BrainType] = BrainType.doc - brain_definition: Optional[CreateApiBrainDefinition] = None - brain_secrets_values: Optional[dict] = {} - connected_brains_ids: Optional[list[UUID]] = [] integration: Optional[BrainIntegrationSettings] = None def dict(self, *args, **kwargs): @@ -72,8 +53,6 @@ class BrainUpdatableProperties(BaseModel, extra="ignore"): 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 def dict(self, *args, **kwargs): diff --git a/backend/api/quivr_api/modules/brain/entity/__init__.py b/backend/api/quivr_api/modules/brain/entity/__init__.py index ba083445c..e69de29bb 100644 --- a/backend/api/quivr_api/modules/brain/entity/__init__.py +++ b/backend/api/quivr_api/modules/brain/entity/__init__.py @@ -1 +0,0 @@ -from .api_brain_definition_entity import ApiBrainDefinitionEntity diff --git a/backend/api/quivr_api/modules/brain/entity/api_brain_definition_entity.py b/backend/api/quivr_api/modules/brain/entity/api_brain_definition_entity.py deleted file mode 100644 index d327d44ac..000000000 --- a/backend/api/quivr_api/modules/brain/entity/api_brain_definition_entity.py +++ /dev/null @@ -1,47 +0,0 @@ -from enum import Enum -from typing import Optional -from uuid import UUID - -from pydantic import BaseModel, Extra - - -class ApiBrainDefinitionSchemaProperty(BaseModel, extra=Extra.forbid): - type: str - description: str - enum: Optional[list] = None - name: str - - def dict(self, **kwargs): - result = super().dict(**kwargs) - if "enum" in result and result["enum"] is None: - del result["enum"] - return result - - -class ApiBrainDefinitionSchema(BaseModel, extra=Extra.forbid): - properties: list[ApiBrainDefinitionSchemaProperty] = [] - required: list[str] = [] - - -class ApiBrainDefinitionSecret(BaseModel, extra=Extra.forbid): - name: str - type: str - description: Optional[str] = None - - -class ApiBrainAllowedMethods(str, Enum): - GET = "GET" - POST = "POST" - PUT = "PUT" - DELETE = "DELETE" - - -class ApiBrainDefinitionEntity(BaseModel, extra=Extra.forbid): - brain_id: UUID - method: ApiBrainAllowedMethods - url: str - params: ApiBrainDefinitionSchema - search_params: ApiBrainDefinitionSchema - secrets: list[ApiBrainDefinitionSecret] - raw: bool = False - jq_instructions: Optional[str] = None diff --git a/backend/api/quivr_api/modules/brain/entity/brain_entity.py b/backend/api/quivr_api/modules/brain/entity/brain_entity.py index 8bd9e85fd..91799a648 100644 --- a/backend/api/quivr_api/modules/brain/entity/brain_entity.py +++ b/backend/api/quivr_api/modules/brain/entity/brain_entity.py @@ -4,9 +4,6 @@ from typing import List, Optional from uuid import UUID from pydantic import BaseModel -from quivr_api.modules.brain.entity.api_brain_definition_entity import ( - ApiBrainDefinitionEntity, -) from quivr_api.modules.brain.entity.integration_brain import ( IntegrationDescriptionEntity, IntegrationEntity, @@ -82,10 +79,6 @@ class BrainEntity(BaseModel): prompt_id: Optional[UUID] = None last_update: datetime brain_type: BrainType - brain_definition: Optional[ApiBrainDefinitionEntity] = None - connected_brains_ids: Optional[List[UUID]] = None - raw: Optional[bool] = None - jq_instructions: Optional[str] = None integration: Optional[IntegrationEntity] = None integration_description: Optional[IntegrationDescriptionEntity] = None @@ -101,16 +94,6 @@ class BrainEntity(BaseModel): return data -class PublicBrain(BaseModel): - id: UUID - name: str - description: Optional[str] = None - number_of_subscribers: int = 0 - last_update: str - brain_type: BrainType - brain_definition: Optional[ApiBrainDefinitionEntity] = None - - class RoleEnum(str, Enum): Viewer = "Viewer" Editor = "Editor" diff --git a/backend/api/quivr_api/modules/brain/entity/composite_brain_connection_entity.py b/backend/api/quivr_api/modules/brain/entity/composite_brain_connection_entity.py deleted file mode 100644 index bb976112e..000000000 --- a/backend/api/quivr_api/modules/brain/entity/composite_brain_connection_entity.py +++ /dev/null @@ -1,8 +0,0 @@ -from uuid import UUID - -from pydantic import BaseModel - - -class CompositeBrainConnectionEntity(BaseModel): - composite_brain_id: UUID - connected_brain_id: UUID diff --git a/backend/api/quivr_api/modules/brain/integrations/Multi_Contract/Brain.py b/backend/api/quivr_api/modules/brain/integrations/Multi_Contract/Brain.py index 1e664f39e..3f196c176 100644 --- a/backend/api/quivr_api/modules/brain/integrations/Multi_Contract/Brain.py +++ b/backend/api/quivr_api/modules/brain/integrations/Multi_Contract/Brain.py @@ -19,12 +19,6 @@ logger = get_logger(__name__) class cited_answer(BaseModelV1): """Answer the user question based only on the given sources, and cite the sources used.""" - thoughts: str = FieldV1( - ..., - description="""Description of the thought process, based only on the given sources. - Cite the text as much as possible and give the document name it appears in. In the format : 'Doc_name states : cited_text'. Be the most - procedural as possible.""", - ) answer: str = FieldV1( ..., description="The answer to the user question, which is based only on the given sources.", @@ -34,10 +28,6 @@ class cited_answer(BaseModelV1): description="The integer IDs of the SPECIFIC sources which justify the answer.", ) - thoughts: str = FieldV1( - ..., - description="Explain shortly what you did to find the answer and what you used by citing the sources by their name.", - ) followup_questions: List[str] = FieldV1( ..., description="Generate up to 3 follow-up questions that could be asked based on the answer given or context provided.", diff --git a/backend/api/quivr_api/modules/brain/knowledge_brain_qa.py b/backend/api/quivr_api/modules/brain/knowledge_brain_qa.py deleted file mode 100644 index c2b934ed7..000000000 --- a/backend/api/quivr_api/modules/brain/knowledge_brain_qa.py +++ /dev/null @@ -1,514 +0,0 @@ -import json -from typing import AsyncIterable, List, Optional -from uuid import UUID - -from langchain.callbacks.streaming_aiter import AsyncIteratorCallbackHandler -from pydantic import BaseModel, ConfigDict -from pydantic_settings import BaseSettings -from quivr_api.logger import get_logger -from quivr_api.models.settings import BrainSettings -from quivr_api.modules.brain.entity.brain_entity import BrainEntity -from quivr_api.modules.brain.qa_interface import ( - QAInterface, - model_compatible_with_function_calling, -) -from quivr_api.modules.brain.rags.quivr_rag import QuivrRAG -from quivr_api.modules.brain.rags.rag_interface import RAGInterface -from quivr_api.modules.brain.service.brain_service import BrainService -from quivr_api.modules.brain.service.utils.format_chat_history import ( - format_chat_history, -) -from quivr_api.modules.brain.service.utils.get_prompt_to_use_id import ( - get_prompt_to_use_id, -) -from quivr_api.modules.chat.controller.chat.utils import ( - find_model_and_generate_metadata, - update_user_usage, -) -from quivr_api.modules.chat.dto.chats import ChatQuestion, Sources -from quivr_api.modules.chat.dto.inputs import CreateChatHistory -from quivr_api.modules.chat.dto.outputs import GetChatHistoryOutput -from quivr_api.modules.chat.service.chat_service import ChatService -from quivr_api.modules.prompt.service.get_prompt_to_use import get_prompt_to_use -from quivr_api.modules.upload.service.generate_file_signed_url import ( - generate_file_signed_url, -) -from quivr_api.modules.user.service.user_usage import UserUsage - -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." - -brain_service = BrainService() - - -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 - - -def generate_source( - source_documents, - brain_id: UUID, - citations: List[int] | None = None, -): - """ - Generate the sources list for the answer - It takes in a list of sources documents and citations that points to the docs index that was used in the answer - """ - # Initialize an empty list for sources - sources_list: List[Sources] = [] - - # Initialize a dictionary for storing generated URLs - generated_urls = {} - - # remove duplicate sources with same name and create a list of unique sources - sources_url_cache = {} - - # Get source documents from the result, default to an empty list if not found - - # If source documents exist - if source_documents: - logger.info(f"Citations {citations}") - # Iterate over each document - for doc, index in zip(source_documents, range(len(source_documents))): - logger.info(f"Processing source document {doc.metadata['file_name']}") - if citations is not None: - if index not in citations: - logger.info(f"Skipping source document {doc.metadata['file_name']}") - continue - # Check if 'url' is in the document metadata - is_url = ( - "original_file_name" in doc.metadata - and doc.metadata["original_file_name"] is not None - and doc.metadata["original_file_name"].startswith("http") - ) - - # Determine the name based on whether it's a URL or a file - name = ( - doc.metadata["original_file_name"] - if is_url - else doc.metadata["file_name"] - ) - - # Determine the type based on whether it's a URL or a file - type_ = "url" if is_url else "file" - - # Determine the source URL based on whether it's a URL or a file - if is_url: - source_url = doc.metadata["original_file_name"] - else: - file_path = f"{brain_id}/{doc.metadata['file_name']}" - # Check if the URL has already been generated - if file_path in generated_urls: - source_url = generated_urls[file_path] - else: - # Generate the URL - if file_path in sources_url_cache: - source_url = sources_url_cache[file_path] - else: - generated_url = generate_file_signed_url(file_path) - if generated_url is not None: - source_url = generated_url.get("signedURL", "") - else: - source_url = "" - # Store the generated URL - generated_urls[file_path] = source_url - - # Append a new Sources object to the list - sources_list.append( - Sources( - name=name, - type=type_, - source_url=source_url, - original_file_name=name, - citation=doc.page_content, - ) - ) - else: - logger.info("No source documents found or source_documents is not a list.") - return sources_list - - -class KnowledgeBrainQA(BaseModel, QAInterface): - """ - Main class for the Brain Picking functionality. - It allows to initialize a Chat model, generate questions and retrieve answers using ConversationalRetrievalChain. - It has two main methods: `generate_question` and `generate_stream`. - One is for generating questions in a single request, the other is for generating questions in a streaming fashion. - Both are the same, except that the streaming version streams the last message as a stream. - Each have the same prompt template, which is defined in the `prompt_template` property. - """ - - model_config = ConfigDict(arbitrary_types_allowed=True) - - # Instantiate settings - brain_settings: BaseSettings = BrainSettings() - - # TODO: remove this !!!!! Only added for compatibility - chat_service: ChatService - - # Default class attributes - model: str = "gpt-3.5-turbo-0125" # 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 - max_input: int = 2000 - streaming: bool = False - knowledge_qa: Optional[RAGInterface] = None - brain: Optional[BrainEntity] = None - user_id: str = None - user_email: str = None - user_usage: Optional[UserUsage] = None - user_settings: Optional[dict] = None - models_settings: Optional[List[dict]] = None - metadata: Optional[dict] = None - - callbacks: List[AsyncIteratorCallbackHandler] = ( - None # pyright: ignore reportPrivateUsage=none - ) - - prompt_id: Optional[UUID] = None - - def __init__( - self, - brain_id: str, - chat_id: str, - chat_service: ChatService, - user_id: str = None, - user_email: str = None, - streaming: bool = False, - prompt_id: Optional[UUID] = None, - metadata: Optional[dict] = None, - cost: int = 100, - **kwargs, - ): - super().__init__( - brain_id=brain_id, - chat_id=chat_id, - chat_service=chat_service, - streaming=streaming, - **kwargs, - ) - self.chat_service = chat_service - self.prompt_id = prompt_id - self.user_id = user_id - self.user_email = user_email - self.user_usage = UserUsage(id=user_id, email=user_email) - # TODO: we already have a brain before !!! - self.brain = brain_service.get_brain_by_id(brain_id) - self.user_settings = self.user_usage.get_user_settings() - - # Get Model settings for the user - self.models_settings = self.user_usage.get_models() - self.increase_usage_user() - self.knowledge_qa = QuivrRAG( - model=self.brain.model if self.brain.model else self.model, - brain_id=brain_id, - chat_id=chat_id, - streaming=streaming, - max_input=self.max_input, - max_tokens=self.max_tokens, - **kwargs, - ) # type: ignore - - @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 - - @property - def prompt_to_use_id(self) -> Optional[UUID]: - # TODO: move to prompt service or instruction or something - if self.brain_id and is_valid_uuid(self.brain_id): - return get_prompt_to_use_id(UUID(self.brain_id), self.prompt_id) - else: - return None - - def filter_history( - self, chat_history, max_history: int = 10, max_tokens: int = 2000 - ): - """ - Filter out the chat history to only include the messages that are relevant to the current question - - Takes in a chat_history= [HumanMessage(content='Qui est Chloé ? '), AIMessage(content="Chloé est une salariée travaillant pour l'entreprise Quivr en tant qu'AI Engineer, sous la direction de son supérieur hiérarchique, Stanislas Girard."), HumanMessage(content='Dis moi en plus sur elle'), AIMessage(content=''), HumanMessage(content='Dis moi en plus sur elle'), AIMessage(content="Désolé, je n'ai pas d'autres informations sur Chloé à partir des fichiers fournis.")] - Returns a filtered chat_history with in priority: first max_tokens, then max_history where a Human message and an AI message count as one pair - a token is 4 characters - """ - chat_history = chat_history[::-1] - total_tokens = 0 - total_pairs = 0 - filtered_chat_history = [] - for i in range(0, len(chat_history), 2): - if i + 1 < len(chat_history): - human_message = chat_history[i] - ai_message = chat_history[i + 1] - message_tokens = ( - len(human_message.content) + len(ai_message.content) - ) // 4 - if ( - total_tokens + message_tokens > max_tokens - or total_pairs >= max_history - ): - break - filtered_chat_history.append(human_message) - filtered_chat_history.append(ai_message) - total_tokens += message_tokens - total_pairs += 1 - chat_history = filtered_chat_history[::-1] - - return chat_history - - def increase_usage_user(self): - # Raises an error if the user has consumed all of of his credits - - update_user_usage( - usage=self.user_usage, - user_settings=self.user_settings, - cost=self.calculate_pricing(), - ) - - def calculate_pricing(self): - model_to_use = find_model_and_generate_metadata( - self.brain.model, - self.user_settings, - self.models_settings, - ) - self.model = model_to_use.name - self.max_input = model_to_use.max_input - self.max_tokens = model_to_use.max_output - user_choosen_model_price = 1000 - - for model_setting in self.models_settings: - if model_setting["name"] == self.model: - user_choosen_model_price = model_setting["price"] - - return user_choosen_model_price - - # TODO: deprecated - async def generate_answer( - self, chat_id: UUID, question: ChatQuestion, save_answer: bool = True - ) -> GetChatHistoryOutput: - conversational_qa_chain = self.knowledge_qa.get_chain() - transformed_history, _ = await self.initialize_streamed_chat_history( - chat_id, question - ) - metadata = self.metadata or {} - citations = None - answer = "" - config = {"metadata": {"conversation_id": str(chat_id)}} - - model_response = conversational_qa_chain.invoke( - { - "question": question.question, - "chat_history": transformed_history, - "custom_personality": ( - self.prompt_to_use.content if self.prompt_to_use else None - ), - }, - config=config, - ) - - if model_compatible_with_function_calling(model=self.model): - if model_response["answer"].tool_calls: - citations = model_response["answer"].tool_calls[-1]["args"]["citations"] - followup_questions = model_response["answer"].tool_calls[-1]["args"][ - "followup_questions" - ] - thoughts = model_response["answer"].tool_calls[-1]["args"]["thoughts"] - if citations: - citations = citations - if followup_questions: - metadata["followup_questions"] = followup_questions - if thoughts: - metadata["thoughts"] = thoughts - answer = model_response["answer"].tool_calls[-1]["args"]["answer"] - else: - answer = model_response["answer"].content - - sources = model_response["docs"] or [] - - if len(sources) > 0: - sources_list = generate_source(sources, self.brain_id, citations=citations) - serialized_sources_list = [source.dict() for source in sources_list] - metadata["sources"] = serialized_sources_list - - return self.save_non_streaming_answer( - chat_id=chat_id, question=question, answer=answer, metadata=metadata - ) - - async def generate_stream( - self, chat_id: UUID, question: ChatQuestion, save_answer: bool = True - ) -> AsyncIterable: - if hasattr(self, "get_chain") and callable(self.get_chain): - conversational_qa_chain = self.get_chain() - else: - conversational_qa_chain = self.knowledge_qa.get_chain() - ( - transformed_history, - streamed_chat_history, - ) = await self.initialize_streamed_chat_history(chat_id, question) - response_tokens = "" - sources = [] - citations = [] - first = True - config = {"metadata": {"conversation_id": str(chat_id)}} - - 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 - ), - }, - config=config, - ): - if not streamed_chat_history.metadata: - streamed_chat_history.metadata = {} - if model_compatible_with_function_calling(model=self.model): - if chunk.get("answer"): - if first: - gathered = chunk["answer"] - first = False - else: - gathered = gathered + chunk["answer"] - if ( - gathered.tool_calls - and gathered.tool_calls[-1].get("args") - and "answer" in gathered.tool_calls[-1]["args"] - ): - # Only send the difference between answer and response_tokens which was the previous answer - answer = gathered.tool_calls[-1]["args"]["answer"] - difference = answer[len(response_tokens) :] - streamed_chat_history.assistant = difference - response_tokens = answer - - yield f"data: {json.dumps(streamed_chat_history.dict())}" - if ( - gathered.tool_calls - and gathered.tool_calls[-1].get("args") - and "citations" in gathered.tool_calls[-1]["args"] - ): - citations = gathered.tool_calls[-1]["args"]["citations"] - if ( - gathered.tool_calls - and gathered.tool_calls[-1].get("args") - and "followup_questions" in gathered.tool_calls[-1]["args"] - ): - followup_questions = gathered.tool_calls[-1]["args"][ - "followup_questions" - ] - streamed_chat_history.metadata["followup_questions"] = ( - followup_questions - ) - if ( - gathered.tool_calls - and gathered.tool_calls[-1].get("args") - and "thoughts" in gathered.tool_calls[-1]["args"] - ): - thoughts = gathered.tool_calls[-1]["args"]["thoughts"] - streamed_chat_history.metadata["thoughts"] = thoughts - else: - if chunk.get("answer"): - response_tokens += chunk["answer"].content - streamed_chat_history.assistant = chunk["answer"].content - yield f"data: {streamed_chat_history.model_dump_json()}" - - if chunk.get("docs"): - sources = chunk["docs"] - - sources_list = generate_source(sources, self.brain_id, citations) - - # 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: {streamed_chat_history.model_dump_json()}" - self.save_answer(question, response_tokens, streamed_chat_history, save_answer) - - async def initialize_streamed_chat_history(self, chat_id, question): - history = await self.chat_service.get_chat_history(self.chat_id) - transformed_history = format_chat_history(history) - brain = brain_service.get_brain_by_id(self.brain_id) - - streamed_chat_history = self.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, - } - ) - - return transformed_history, streamed_chat_history - - def save_answer( - self, question, response_tokens, streamed_chat_history, save_answer - ): - assistant = "".join(response_tokens) - - try: - if save_answer: - self.chat_service.update_message_by_id( - message_id=str(streamed_chat_history.message_id), - user_message=question.question, - assistant=assistant, - metadata=streamed_chat_history.metadata, - ) - except Exception as e: - logger.error("Error updating message by ID: %s", e) - - def save_non_streaming_answer(self, chat_id, question, answer, metadata): - new_chat = self.chat_service.update_chat_history( - CreateChatHistory( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "brain_id": self.brain.brain_id, - "prompt_id": self.prompt_to_use_id, - "metadata": metadata, - } - ) - ) - - return GetChatHistoryOutput( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "message_time": new_chat.message_time, - "prompt_title": ( - self.prompt_to_use.title if self.prompt_to_use else None - ), - "brain_name": self.brain.name if self.brain else None, - "message_id": new_chat.message_id, - "brain_id": str(self.brain.brain_id) if self.brain else None, - "metadata": metadata, - } - ) diff --git a/backend/api/quivr_api/modules/brain/qa_headless.py b/backend/api/quivr_api/modules/brain/qa_headless.py deleted file mode 100644 index 08c8ecded..000000000 --- a/backend/api/quivr_api/modules/brain/qa_headless.py +++ /dev/null @@ -1,267 +0,0 @@ -import asyncio -import json -from typing import AsyncIterable, Awaitable, List, Optional -from uuid import UUID - -from langchain.callbacks.streaming_aiter import AsyncIteratorCallbackHandler -from langchain.chains import LLMChain -from langchain.chat_models.base import BaseChatModel -from langchain.prompts.chat import ChatPromptTemplate, HumanMessagePromptTemplate -from langchain_community.chat_models import ChatLiteLLM -from pydantic import BaseModel, ConfigDict -from quivr_api.logger import get_logger -from quivr_api.models.settings import ( - BrainSettings, -) # Importing settings related to the 'brain' -from quivr_api.modules.brain.qa_interface import QAInterface -from quivr_api.modules.brain.service.utils.format_chat_history import ( - format_chat_history, - format_history_to_openai_mesages, -) -from quivr_api.modules.brain.service.utils.get_prompt_to_use_id import ( - get_prompt_to_use_id, -) -from quivr_api.modules.chat.dto.chats import ChatQuestion -from quivr_api.modules.chat.dto.inputs import CreateChatHistory -from quivr_api.modules.chat.dto.outputs import GetChatHistoryOutput -from quivr_api.modules.chat.service.chat_service import ChatService -from quivr_api.modules.dependencies import get_service -from quivr_api.modules.prompt.service.get_prompt_to_use import get_prompt_to_use - -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." -chat_service = get_service(ChatService)() - - -class HeadlessQA(BaseModel, QAInterface): - brain_settings = BrainSettings() - model: str - temperature: float = 0.0 - max_tokens: int = 2000 - streaming: bool = False - chat_id: str - callbacks: Optional[List[AsyncIteratorCallbackHandler]] = None - prompt_id: Optional[UUID] = None - - def _determine_streaming(self, streaming: bool) -> bool: - """If the model name allows for streaming and streaming is declared, set streaming to True.""" - return streaming - - def _determine_callback_array( - self, streaming - ) -> List[AsyncIteratorCallbackHandler]: - """If streaming is set, set the AsyncIteratorCallbackHandler as the only callback.""" - if streaming: - return [AsyncIteratorCallbackHandler()] - else: - return [] - - def __init__(self, **data): - super().__init__(**data) - self.streaming = self._determine_streaming(self.streaming) - self.callbacks = self._determine_callback_array(self.streaming) - - @property - def prompt_to_use(self) -> str: - return get_prompt_to_use(None, self.prompt_id) - - @property - def prompt_to_use_id(self) -> Optional[UUID]: - return get_prompt_to_use_id(None, self.prompt_id) - - def _create_llm( - self, - model, - temperature=0, - streaming=False, - callbacks=None, - ) -> BaseChatModel: - """ - Determine the language model to be used. - :param model: Language model name to be used. - :param streaming: Whether to enable streaming of the model - :param callbacks: Callbacks to be used for streaming - :return: Language model instance - """ - 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, - model=model, - streaming=streaming, - verbose=True, - callbacks=callbacks, - max_tokens=self.max_tokens, - api_base=api_base, - ) - - def _create_prompt_template(self): - messages = [ - HumanMessagePromptTemplate.from_template("{question}"), - ] - CHAT_PROMPT = ChatPromptTemplate.from_messages(messages) - return CHAT_PROMPT - - def generate_answer( - self, chat_id: UUID, question: ChatQuestion, save_answer: bool = True - ) -> GetChatHistoryOutput: - # Move format_chat_history to chat service ? - transformed_history = format_chat_history( - chat_service.get_chat_history(self.chat_id) - ) - prompt_content = ( - self.prompt_to_use.content if self.prompt_to_use else SYSTEM_MESSAGE - ) - - messages = format_history_to_openai_mesages( - transformed_history, prompt_content, question.question - ) - answering_llm = self._create_llm( - model=self.model, - streaming=False, - callbacks=self.callbacks, - ) - model_prediction = answering_llm.predict_messages(messages) - answer = model_prediction.content - if save_answer: - new_chat = chat_service.update_chat_history( - CreateChatHistory( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "brain_id": None, - "prompt_id": self.prompt_to_use_id, - } - ) - ) - - return GetChatHistoryOutput( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "message_time": new_chat.message_time, - "prompt_title": ( - self.prompt_to_use.title if self.prompt_to_use else None - ), - "brain_name": None, - "message_id": new_chat.message_id, - } - ) - else: - return GetChatHistoryOutput( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": answer, - "message_time": None, - "prompt_title": ( - self.prompt_to_use.title if self.prompt_to_use else None - ), - "brain_name": None, - "message_id": None, - } - ) - - async def generate_stream( - self, chat_id: UUID, question: ChatQuestion, save_answer: bool = True - ) -> AsyncIterable: - callback = AsyncIteratorCallbackHandler() - self.callbacks = [callback] - - transformed_history = format_chat_history( - chat_service.get_chat_history(self.chat_id) - ) - prompt_content = ( - self.prompt_to_use.content if self.prompt_to_use else SYSTEM_MESSAGE - ) - - messages = format_history_to_openai_mesages( - transformed_history, prompt_content, question.question - ) - answering_llm = self._create_llm( - model=self.model, - streaming=True, - callbacks=self.callbacks, - ) - - CHAT_PROMPT = ChatPromptTemplate.from_messages(messages) - headlessChain = LLMChain(llm=answering_llm, prompt=CHAT_PROMPT) - - response_tokens = [] - - async def wrap_done(fn: Awaitable, event: asyncio.Event): - try: - await fn - except Exception as e: - logger.error(f"Caught exception: {e}") - finally: - event.set() - - run = asyncio.create_task( - wrap_done( - headlessChain.acall({}), - callback.done, - ), - ) - - if save_answer: - streamed_chat_history = chat_service.update_chat_history( - CreateChatHistory( - **{ - "chat_id": chat_id, - "user_message": question.question, - "assistant": "", - "brain_id": None, - "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": None, - } - ) - 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": None, - } - ) - - async for token in callback.aiter(): - response_tokens.append(token) - streamed_chat_history.assistant = token - yield f"data: {json.dumps(streamed_chat_history.dict())}" - - await run - assistant = "".join(response_tokens) - - if save_answer: - chat_service.update_message_by_id( - message_id=str(streamed_chat_history.message_id), - user_message=question.question, - assistant=assistant, - ) - - model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/backend/api/quivr_api/modules/brain/qa_interface.py b/backend/api/quivr_api/modules/brain/qa_interface.py deleted file mode 100644 index 660799de0..000000000 --- a/backend/api/quivr_api/modules/brain/qa_interface.py +++ /dev/null @@ -1,58 +0,0 @@ -from abc import ABC, abstractmethod -from uuid import UUID - -from quivr_api.modules.chat.dto.chats import ChatQuestion - - -def model_compatible_with_function_calling(model: str): - return model in [ - "gpt-4o", - "gpt-4-turbo", - "gpt-4-turbo-2024-04-09", - "gpt-4-turbo-preview", - "gpt-4-0125-preview", - "gpt-4-1106-preview", - "gpt-4", - "gpt-4-0613", - "gpt-3.5-turbo", - "gpt-3.5-turbo-0125", - "gpt-3.5-turbo-1106", - "gpt-3.5-turbo-0613", - ] - - -class QAInterface(ABC): - """ - Abstract class for all QA interfaces. - This can be used to implement custom answer generation logic. - """ - - @abstractmethod - def calculate_pricing(self): - raise NotImplementedError( - "calculate_pricing is an abstract method and must be implemented" - ) - - @abstractmethod - def generate_answer( - self, - chat_id: UUID, - question: ChatQuestion, - save_answer: bool, - *custom_params: tuple, - ): - raise NotImplementedError( - "generate_answer is an abstract method and must be implemented" - ) - - @abstractmethod - async def generate_stream( - self, - chat_id: UUID, - question: ChatQuestion, - save_answer: bool, - *custom_params: tuple, - ): - raise NotImplementedError( - "generate_stream is an abstract method and must be implemented" - ) diff --git a/backend/api/quivr_api/modules/brain/rags/quivr_rag.py b/backend/api/quivr_api/modules/brain/rags/quivr_rag.py index 904f16dce..644d5999c 100644 --- a/backend/api/quivr_api/modules/brain/rags/quivr_rag.py +++ b/backend/api/quivr_api/modules/brain/rags/quivr_rag.py @@ -41,12 +41,6 @@ logger = get_logger(__name__) class cited_answer(BaseModelV1): """Answer the user question based only on the given sources, and cite the sources used.""" - thoughts: str = FieldV1( - ..., - description="""Description of the thought process, based only on the given sources. - Cite the text as much as possible and give the document name it appears in. In the format : 'Doc_name states : cited_text'. Be the most - procedural as possible. Write all the steps needed to find the answer until you find it.""", - ) answer: str = FieldV1( ..., description="The answer to the user question, which is based only on the given sources.", @@ -56,10 +50,6 @@ class cited_answer(BaseModelV1): description="The integer IDs of the SPECIFIC sources which justify the answer.", ) - thoughts: str = FieldV1( - ..., - description="Explain shortly what you did to find the answer and what you used by citing the sources by their name.", - ) followup_questions: List[str] = FieldV1( ..., description="Generate up to 3 follow-up questions that could be asked based on the answer given or context provided.", diff --git a/backend/api/quivr_api/modules/brain/repository/api_brain_definitions.py b/backend/api/quivr_api/modules/brain/repository/api_brain_definitions.py deleted file mode 100644 index 90ef99f3e..000000000 --- a/backend/api/quivr_api/modules/brain/repository/api_brain_definitions.py +++ /dev/null @@ -1,58 +0,0 @@ -from typing import Optional -from uuid import UUID - -from quivr_api.models.settings import get_supabase_client -from quivr_api.modules.brain.dto.inputs import CreateApiBrainDefinition -from quivr_api.modules.brain.entity.api_brain_definition_entity import ( - ApiBrainDefinitionEntity, -) -from quivr_api.modules.brain.repository.interfaces import ApiBrainDefinitionsInterface - - -class ApiBrainDefinitions(ApiBrainDefinitionsInterface): - def __init__(self): - self.db = get_supabase_client() - - def get_api_brain_definition( - self, brain_id: UUID - ) -> Optional[ApiBrainDefinitionEntity]: - response = ( - self.db.table("api_brain_definition") - .select("*") - .filter("brain_id", "eq", brain_id) - .execute() - ) - if len(response.data) == 0: - return None - - return ApiBrainDefinitionEntity(**response.data[0]) - - def add_api_brain_definition( - self, brain_id: UUID, api_brain_definition: CreateApiBrainDefinition - ) -> Optional[ApiBrainDefinitionEntity]: - response = ( - self.db.table("api_brain_definition") - .insert([{"brain_id": str(brain_id), **api_brain_definition.dict()}]) - .execute() - ) - if len(response.data) == 0: - return None - return ApiBrainDefinitionEntity(**response.data[0]) - - def update_api_brain_definition( - self, brain_id: UUID, api_brain_definition: ApiBrainDefinitionEntity - ) -> Optional[ApiBrainDefinitionEntity]: - response = ( - self.db.table("api_brain_definition") - .update(api_brain_definition.dict(exclude={"brain_id"})) - .filter("brain_id", "eq", str(brain_id)) - .execute() - ) - if len(response.data) == 0: - return None - return ApiBrainDefinitionEntity(**response.data[0]) - - def delete_api_brain_definition(self, brain_id: UUID) -> None: - self.db.table("api_brain_definition").delete().filter( - "brain_id", "eq", str(brain_id) - ).execute() diff --git a/backend/api/quivr_api/modules/brain/repository/brains.py b/backend/api/quivr_api/modules/brain/repository/brains.py index eb328a62e..d207dee5e 100644 --- a/backend/api/quivr_api/modules/brain/repository/brains.py +++ b/backend/api/quivr_api/modules/brain/repository/brains.py @@ -7,7 +7,7 @@ from quivr_api.models.settings import ( get_supabase_client, ) from quivr_api.modules.brain.dto.inputs import BrainUpdatableProperties -from quivr_api.modules.brain.entity.brain_entity import BrainEntity, PublicBrain +from quivr_api.modules.brain.entity.brain_entity import BrainEntity from quivr_api.modules.brain.repository.interfaces.brains_interface import ( BrainsInterface, ) @@ -29,9 +29,6 @@ class Brains(BrainsInterface): brain_meaning = embeddings.embed_query(string_to_embed) brain_dict = brain.dict( exclude={ - "brain_definition", - "brain_secrets_values", - "connected_brains_ids", "integration", } ) @@ -40,27 +37,6 @@ class Brains(BrainsInterface): return BrainEntity(**response.data[0]) - def get_public_brains(self): - response = ( - self.db.from_("brains") - .select( - "id:brain_id, name, description, last_update, brain_type, brain_definition: api_brain_definition(*), number_of_subscribers:brains_users(count)" - ) - .filter("status", "eq", "public") - .execute() - ) - public_brains: list[PublicBrain] = [] - - for item in response.data: - item["number_of_subscribers"] = item["number_of_subscribers"][0]["count"] - if not item["brain_definition"]: - del item["brain_definition"] - else: - item["brain_definition"]["secrets"] = [] - - public_brains.append(PublicBrain(**item)) - return public_brains - def update_brain_last_update_time(self, brain_id): try: with self.pg_engine.begin() as connection: diff --git a/backend/api/quivr_api/modules/brain/repository/composite_brains_connections.py b/backend/api/quivr_api/modules/brain/repository/composite_brains_connections.py deleted file mode 100644 index 1d33f3f91..000000000 --- a/backend/api/quivr_api/modules/brain/repository/composite_brains_connections.py +++ /dev/null @@ -1,63 +0,0 @@ -from uuid import UUID - -from quivr_api.logger import get_logger -from quivr_api.models.settings import get_supabase_client -from quivr_api.modules.brain.entity.composite_brain_connection_entity import ( - CompositeBrainConnectionEntity, -) -from quivr_api.modules.brain.repository.interfaces import ( - CompositeBrainsConnectionsInterface, -) - -logger = get_logger(__name__) - - -class CompositeBrainsConnections(CompositeBrainsConnectionsInterface): - def __init__(self): - self.db = get_supabase_client() - - def connect_brain( - self, composite_brain_id: UUID, connected_brain_id: UUID - ) -> CompositeBrainConnectionEntity: - response = ( - self.db.table("composite_brain_connections") - .insert( - { - "composite_brain_id": str(composite_brain_id), - "connected_brain_id": str(connected_brain_id), - } - ) - .execute() - ) - - return CompositeBrainConnectionEntity(**response.data[0]) - - def get_connected_brains(self, composite_brain_id: UUID) -> list[UUID]: - response = ( - self.db.from_("composite_brain_connections") - .select("connected_brain_id") - .filter("composite_brain_id", "eq", str(composite_brain_id)) - .execute() - ) - - return [item["connected_brain_id"] for item in response.data] - - def disconnect_brain( - self, composite_brain_id: UUID, connected_brain_id: UUID - ) -> None: - self.db.table("composite_brain_connections").delete().match( - { - "composite_brain_id": composite_brain_id, - "connected_brain_id": connected_brain_id, - } - ).execute() - - def is_connected_brain(self, brain_id: UUID) -> bool: - response = ( - self.db.from_("composite_brain_connections") - .select("connected_brain_id") - .filter("connected_brain_id", "eq", str(brain_id)) - .execute() - ) - - return len(response.data) > 0 diff --git a/backend/api/quivr_api/modules/brain/repository/external_api_secrets.py b/backend/api/quivr_api/modules/brain/repository/external_api_secrets.py deleted file mode 100644 index 7552bd4ea..000000000 --- a/backend/api/quivr_api/modules/brain/repository/external_api_secrets.py +++ /dev/null @@ -1,60 +0,0 @@ -from uuid import UUID - -from quivr_api.models.settings import get_supabase_client -from quivr_api.modules.brain.repository.interfaces.external_api_secrets_interface import ( - ExternalApiSecretsInterface, -) - - -def build_secret_unique_name(user_id: UUID, brain_id: UUID, secret_name: str): - return f"{user_id}-{brain_id}-{secret_name}" - - -class ExternalApiSecrets(ExternalApiSecretsInterface): - def __init__(self): - supabase_client = get_supabase_client() - self.db = supabase_client - - def create_secret( - self, user_id: UUID, brain_id: UUID, secret_name: str, secret_value - ) -> UUID | None: - response = self.db.rpc( - "insert_secret", - { - "name": build_secret_unique_name( - user_id=user_id, brain_id=brain_id, secret_name=secret_name - ), - "secret": secret_value, - }, - ).execute() - - return response.data - - def read_secret( - self, - user_id: UUID, - brain_id: UUID, - secret_name: str, - ) -> UUID | None: - response = self.db.rpc( - "read_secret", - { - "secret_name": build_secret_unique_name( - user_id=user_id, brain_id=brain_id, secret_name=secret_name - ), - }, - ).execute() - - return response.data - - def delete_secret(self, user_id: UUID, brain_id: UUID, secret_name: str) -> bool: - response = self.db.rpc( - "delete_secret", - { - "secret_name": build_secret_unique_name( - user_id=user_id, brain_id=brain_id, secret_name=secret_name - ), - }, - ).execute() - - return response.data diff --git a/backend/api/quivr_api/modules/brain/repository/interfaces/__init__.py b/backend/api/quivr_api/modules/brain/repository/interfaces/__init__.py index 0c5d0cee9..aab7d31bb 100644 --- a/backend/api/quivr_api/modules/brain/repository/interfaces/__init__.py +++ b/backend/api/quivr_api/modules/brain/repository/interfaces/__init__.py @@ -1,9 +1,6 @@ -from .api_brain_definitions_interface import ApiBrainDefinitionsInterface +# noqa from .brains_interface import BrainsInterface from .brains_users_interface import BrainsUsersInterface from .brains_vectors_interface import BrainsVectorsInterface -from .composite_brains_connections_interface import \ - CompositeBrainsConnectionsInterface -from .external_api_secrets_interface import ExternalApiSecretsInterface from .integration_brains_interface import (IntegrationBrainInterface, - IntegrationDescriptionInterface) + IntegrationDescriptionInterface) diff --git a/backend/api/quivr_api/modules/brain/repository/interfaces/api_brain_definitions_interface.py b/backend/api/quivr_api/modules/brain/repository/interfaces/api_brain_definitions_interface.py deleted file mode 100644 index a5200d0d9..000000000 --- a/backend/api/quivr_api/modules/brain/repository/interfaces/api_brain_definitions_interface.py +++ /dev/null @@ -1,38 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Optional -from uuid import UUID - -from quivr_api.modules.brain.dto.inputs import CreateApiBrainDefinition -from quivr_api.modules.brain.entity.api_brain_definition_entity import ( - ApiBrainDefinitionEntity, -) - - -class ApiBrainDefinitionsInterface(ABC): - @abstractmethod - def get_api_brain_definition( - self, brain_id: UUID - ) -> Optional[ApiBrainDefinitionEntity]: - pass - - @abstractmethod - def add_api_brain_definition( - self, brain_id: UUID, api_brain_definition: CreateApiBrainDefinition - ) -> Optional[ApiBrainDefinitionEntity]: - pass - - @abstractmethod - def update_api_brain_definition( - self, brain_id: UUID, api_brain_definition: ApiBrainDefinitionEntity - ) -> Optional[ApiBrainDefinitionEntity]: - """ - Get all public brains - """ - pass - - @abstractmethod - def delete_api_brain_definition(self, brain_id: UUID) -> None: - """ - Update the last update time of the brain - """ - pass diff --git a/backend/api/quivr_api/modules/brain/repository/interfaces/brains_interface.py b/backend/api/quivr_api/modules/brain/repository/interfaces/brains_interface.py index bc30908f8..714807649 100644 --- a/backend/api/quivr_api/modules/brain/repository/interfaces/brains_interface.py +++ b/backend/api/quivr_api/modules/brain/repository/interfaces/brains_interface.py @@ -5,7 +5,7 @@ from quivr_api.modules.brain.dto.inputs import ( BrainUpdatableProperties, CreateBrainProperties, ) -from quivr_api.modules.brain.entity.brain_entity import BrainEntity, PublicBrain +from quivr_api.modules.brain.entity.brain_entity import BrainEntity class BrainsInterface(ABC): @@ -16,13 +16,6 @@ class BrainsInterface(ABC): """ pass - @abstractmethod - def get_public_brains(self) -> list[PublicBrain]: - """ - Get all public brains - """ - pass - @abstractmethod def get_brain_details(self, brain_id: UUID, user_id: UUID) -> BrainEntity | None: """ diff --git a/backend/api/quivr_api/modules/brain/repository/interfaces/composite_brains_connections_interface.py b/backend/api/quivr_api/modules/brain/repository/interfaces/composite_brains_connections_interface.py deleted file mode 100644 index a8e7e2c98..000000000 --- a/backend/api/quivr_api/modules/brain/repository/interfaces/composite_brains_connections_interface.py +++ /dev/null @@ -1,40 +0,0 @@ -from abc import ABC, abstractmethod -from uuid import UUID - -from quivr_api.modules.brain.entity.composite_brain_connection_entity import ( - CompositeBrainConnectionEntity, -) - - -class CompositeBrainsConnectionsInterface(ABC): - @abstractmethod - def connect_brain( - self, composite_brain_id: UUID, connected_brain_id: UUID - ) -> CompositeBrainConnectionEntity: - """ - Connect a brain to a composite brain in the composite_brain_connections table - """ - pass - - @abstractmethod - def get_connected_brains(self, composite_brain_id: UUID) -> list[UUID]: - """ - Get all brains connected to a composite brain - """ - pass - - @abstractmethod - def disconnect_brain( - self, composite_brain_id: UUID, connected_brain_id: UUID - ) -> None: - """ - Disconnect a brain from a composite brain - """ - pass - - @abstractmethod - def is_connected_brain(self, brain_id: UUID) -> bool: - """ - Check if a brain is connected to any composite brain - """ - pass diff --git a/backend/api/quivr_api/modules/brain/repository/interfaces/external_api_secrets_interface.py b/backend/api/quivr_api/modules/brain/repository/interfaces/external_api_secrets_interface.py deleted file mode 100644 index b2f2439d1..000000000 --- a/backend/api/quivr_api/modules/brain/repository/interfaces/external_api_secrets_interface.py +++ /dev/null @@ -1,29 +0,0 @@ -from abc import ABC, abstractmethod -from uuid import UUID - - -class ExternalApiSecretsInterface(ABC): - @abstractmethod - def create_secret( - self, user_id: UUID, brain_id: UUID, secret_name: str, secret_value - ) -> UUID | None: - """ - Create a new secret for the API Request in given brain - """ - pass - - @abstractmethod - def read_secret( - self, user_id: UUID, brain_id: UUID, secret_name: str - ) -> UUID | None: - """ - Read a secret for the API Request in given brain - """ - pass - - @abstractmethod - def delete_secret(self, user_id: UUID, brain_id: UUID, secret_name: str) -> bool: - """ - Delete a secret from a brain - """ - pass diff --git a/backend/api/quivr_api/modules/brain/service/api_brain_definition_service.py b/backend/api/quivr_api/modules/brain/service/api_brain_definition_service.py deleted file mode 100644 index cff6f5b3e..000000000 --- a/backend/api/quivr_api/modules/brain/service/api_brain_definition_service.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import Optional -from uuid import UUID - -from quivr_api.modules.brain.dto.inputs import CreateApiBrainDefinition -from quivr_api.modules.brain.entity.api_brain_definition_entity import ( - ApiBrainDefinitionEntity, -) -from quivr_api.modules.brain.repository.api_brain_definitions import ApiBrainDefinitions -from quivr_api.modules.brain.repository.interfaces import ApiBrainDefinitionsInterface - - -class ApiBrainDefinitionService: - repository: ApiBrainDefinitionsInterface - - def __init__(self): - self.repository = ApiBrainDefinitions() - - def add_api_brain_definition( - self, brain_id: UUID, api_brain_definition: CreateApiBrainDefinition - ) -> None: - self.repository.add_api_brain_definition(brain_id, api_brain_definition) - - def delete_api_brain_definition(self, brain_id: UUID) -> None: - self.repository.delete_api_brain_definition(brain_id) - - def get_api_brain_definition( - self, brain_id: UUID - ) -> Optional[ApiBrainDefinitionEntity]: - return self.repository.get_api_brain_definition(brain_id) - - def update_api_brain_definition( - self, brain_id: UUID, api_brain_definition: ApiBrainDefinitionEntity - ) -> Optional[ApiBrainDefinitionEntity]: - return self.repository.update_api_brain_definition( - brain_id, api_brain_definition - ) diff --git a/backend/api/quivr_api/modules/brain/service/brain_service.py b/backend/api/quivr_api/modules/brain/service/brain_service.py index 11c1c5d63..5f1db6e81 100644 --- a/backend/api/quivr_api/modules/brain/service/brain_service.py +++ b/backend/api/quivr_api/modules/brain/service/brain_service.py @@ -8,11 +8,7 @@ from quivr_api.modules.brain.dto.inputs import ( BrainUpdatableProperties, CreateBrainProperties, ) -from quivr_api.modules.brain.entity.brain_entity import ( - BrainEntity, - BrainType, - PublicBrain, -) +from quivr_api.modules.brain.entity.brain_entity import BrainEntity, BrainType from quivr_api.modules.brain.entity.integration_brain import IntegrationEntity from quivr_api.modules.brain.repository import ( Brains, @@ -21,24 +17,18 @@ from quivr_api.modules.brain.repository import ( IntegrationBrain, IntegrationDescription, ) -from quivr_api.modules.brain.service.api_brain_definition_service import ( - ApiBrainDefinitionService, -) -from quivr_api.modules.brain.service.utils.validate_brain import validate_api_brain from quivr_api.modules.knowledge.service.knowledge_service import KnowledgeService from quivr_api.vectorstore.supabase import CustomSupabaseVectorStore logger = get_logger(__name__) knowledge_service = KnowledgeService() -api_brain_definition_service = ApiBrainDefinitionService() class BrainService: # brain_repository: BrainsInterface # brain_user_repository: BrainsUsersInterface # brain_vector_repository: BrainsVectorsInterface - # external_api_secrets_repository: ExternalApiSecretsInterface # integration_brains_repository: IntegrationBrainInterface # integration_description_repository: IntegrationDescriptionInterface @@ -123,14 +113,7 @@ class BrainService: brain: Optional[CreateBrainProperties], ) -> BrainEntity: if brain is None: - brain = CreateBrainProperties() # type: ignore model and brain_definition - - if brain.brain_type == BrainType.api: - validate_api_brain(brain) - return self.create_brain_api(user_id, brain) - - if brain.brain_type == BrainType.composite: - return self.create_brain_composite(brain) + brain = CreateBrainProperties() if brain.brain_type == BrainType.integration: return self.create_brain_integration(user_id, brain) @@ -138,46 +121,6 @@ class BrainService: created_brain = self.brain_repository.create_brain(brain) return created_brain - def create_brain_api( - self, - user_id: UUID, - brain: CreateBrainProperties, - ) -> BrainEntity: - created_brain = self.brain_repository.create_brain(brain) - - if brain.brain_definition is not None: - api_brain_definition_service.add_api_brain_definition( - brain_id=created_brain.brain_id, - api_brain_definition=brain.brain_definition, - ) - - secrets_values = brain.brain_secrets_values - - for secret_name in secrets_values: - self.external_api_secrets_repository.create_secret( - user_id=user_id, - brain_id=created_brain.brain_id, - secret_name=secret_name, - secret_value=secrets_values[secret_name], - ) - - return created_brain - - def create_brain_composite( - self, - brain: CreateBrainProperties, - ) -> BrainEntity: - created_brain = self.brain_repository.create_brain(brain) - - if brain.connected_brains_ids is not None: - for connected_brain_id in brain.connected_brains_ids: - self.composite_brains_connections_repository.connect_brain( - composite_brain_id=created_brain.brain_id, - connected_brain_id=connected_brain_id, - ) - - return created_brain - def create_brain_integration( self, user_id: UUID, @@ -203,37 +146,11 @@ class BrainService: ) return created_brain - def delete_brain_secrets_values(self, brain_id: UUID) -> None: - brain_definition = api_brain_definition_service.get_api_brain_definition( - brain_id=brain_id - ) - - if brain_definition is None: - raise HTTPException(status_code=404, detail="Brain definition not found.") - - secrets = brain_definition.secrets - - if len(secrets) > 0: - brain_users = self.brain_user_repository.get_brain_users(brain_id=brain_id) - for user in brain_users: - for secret in secrets: - self.external_api_secrets_repository.delete_secret( - user_id=user.user_id, - brain_id=brain_id, - secret_name=secret.name, - ) - def delete_brain(self, brain_id: UUID) -> dict[str, str]: brain_to_delete = self.get_brain_by_id(brain_id=brain_id) if brain_to_delete is None: raise HTTPException(status_code=404, detail="Brain not found.") - if brain_to_delete.brain_type == BrainType.api: - self.delete_brain_secrets_values( - brain_id=brain_id, - ) - api_brain_definition_service.delete_api_brain_definition(brain_id=brain_id) - else: knowledge_service.remove_brain_all_knowledge(brain_id) self.brain_vector.delete_brain_vector(str(brain_id)) @@ -263,9 +180,7 @@ class BrainService: brain_update_answer = self.brain_repository.update_brain_by_id( brain_id, brain=BrainUpdatableProperties( - **brain_new_values.dict( - exclude={"brain_definition", "connected_brains_ids", "integration"} - ) + **brain_new_values.dict(exclude={"integration"}) ), ) @@ -275,35 +190,6 @@ class BrainService: detail=f"Brain with id {brain_id} not found", ) - if ( - brain_update_answer.brain_type == BrainType.api - and brain_new_values.brain_definition - ): - existing_brain_secrets_definition = ( - existing_brain.brain_definition.secrets - if existing_brain.brain_definition - else None - ) - brain_new_values_secrets_definition = ( - brain_new_values.brain_definition.secrets - if brain_new_values.brain_definition - else None - ) - should_remove_existing_secrets_values = ( - existing_brain_secrets_definition - and brain_new_values_secrets_definition - and existing_brain_secrets_definition - != brain_new_values_secrets_definition - ) - - if should_remove_existing_secrets_values: - self.delete_brain_secrets_values(brain_id=brain_id) - - api_brain_definition_service.update_api_brain_definition( - brain_id, - api_brain_definition=brain_new_values.brain_definition, - ) - if brain_update_answer is None: raise HTTPException( status_code=404, @@ -345,9 +231,6 @@ class BrainService: brain_id ) - def get_public_brains(self) -> list[PublicBrain]: - return self.brain_repository.get_public_brains() - def update_secret_value( self, user_id: UUID, diff --git a/backend/api/quivr_api/modules/brain/service/brain_user_service.py b/backend/api/quivr_api/modules/brain/service/brain_user_service.py index 33632ad58..b1bf15038 100644 --- a/backend/api/quivr_api/modules/brain/service/brain_user_service.py +++ b/backend/api/quivr_api/modules/brain/service/brain_user_service.py @@ -5,43 +5,32 @@ from fastapi import HTTPException from quivr_api.logger import get_logger from quivr_api.modules.brain.entity.brain_entity import ( BrainEntity, - BrainType, BrainUser, MinimalUserBrainEntity, RoleEnum, ) from quivr_api.modules.brain.repository.brains import Brains from quivr_api.modules.brain.repository.brains_users import BrainsUsers -from quivr_api.modules.brain.repository.external_api_secrets import ExternalApiSecrets from quivr_api.modules.brain.repository.interfaces.brains_interface import ( BrainsInterface, ) from quivr_api.modules.brain.repository.interfaces.brains_users_interface import ( BrainsUsersInterface, ) -from quivr_api.modules.brain.repository.interfaces.external_api_secrets_interface import ( - ExternalApiSecretsInterface, -) -from quivr_api.modules.brain.service.api_brain_definition_service import ( - ApiBrainDefinitionService, -) from quivr_api.modules.brain.service.brain_service import BrainService logger = get_logger(__name__) brain_service = BrainService() -api_brain_definition_service = ApiBrainDefinitionService() class BrainUserService: brain_repository: BrainsInterface brain_user_repository: BrainsUsersInterface - external_api_secrets_repository: ExternalApiSecretsInterface def __init__(self): self.brain_repository = Brains() self.brain_user_repository = BrainsUsers() - self.external_api_secrets_repository = ExternalApiSecrets() def get_user_default_brain(self, user_id: UUID) -> BrainEntity | None: brain_id = self.brain_user_repository.get_user_default_brain_id(user_id) @@ -56,22 +45,6 @@ class BrainUserService: if brain_to_delete_user_from is None: raise HTTPException(status_code=404, detail="Brain not found.") - if brain_to_delete_user_from.brain_type == BrainType.api: - brain_definition = api_brain_definition_service.get_api_brain_definition( - brain_id=brain_id - ) - if brain_definition is None: - raise HTTPException( - status_code=404, detail="Brain definition not found." - ) - secrets = brain_definition.secrets - for secret in secrets: - self.external_api_secrets_repository.delete_secret( - user_id=user_id, - brain_id=brain_id, - secret_name=secret.name, - ) - self.brain_user_repository.delete_brain_user_by_id( user_id=user_id, brain_id=brain_id, diff --git a/backend/api/quivr_api/modules/brain/service/call_brain_api.py b/backend/api/quivr_api/modules/brain/service/call_brain_api.py deleted file mode 100644 index 57ed42a7a..000000000 --- a/backend/api/quivr_api/modules/brain/service/call_brain_api.py +++ /dev/null @@ -1,116 +0,0 @@ -from uuid import UUID - -import requests -from quivr_api.logger import get_logger - -logger = get_logger(__name__) - -from fastapi import HTTPException -from quivr_api.modules.brain.entity.api_brain_definition_entity import ( - ApiBrainDefinitionSchema, -) -from quivr_api.modules.brain.service.api_brain_definition_service import ( - ApiBrainDefinitionService, -) -from quivr_api.modules.brain.service.brain_service import BrainService - -brain_service = BrainService() -api_brain_definition_service = ApiBrainDefinitionService() - - -def get_api_call_response_as_text( - method, api_url, params, search_params, secrets -) -> str: - headers = {} - - api_url_with_search_params = api_url - if search_params: - api_url_with_search_params += "?" - for search_param in search_params: - api_url_with_search_params += ( - f"{search_param}={search_params[search_param]}&" - ) - - for secret in secrets: - headers[secret] = secrets[secret] - - try: - if method in ["GET", "DELETE"]: - response = requests.request( - method, - url=api_url_with_search_params, - params=params or None, - headers=headers or None, - ) - elif method in ["POST", "PUT", "PATCH"]: - response = requests.request( - method, - url=api_url_with_search_params, - json=params or None, - headers=headers or None, - ) - else: - raise ValueError(f"Invalid method: {method}") - - return response.text - - except Exception as e: - logger.error(f"Error calling API: {e}") - return None - - -def extract_api_brain_definition_values_from_llm_output( - brain_schema: ApiBrainDefinitionSchema, arguments: dict -) -> dict: - params_values = {} - properties = brain_schema.properties - required_values = brain_schema.required - for property in properties: - if property.name in arguments: - if property.type == "number": - params_values[property.name] = float(arguments[property.name]) - else: - params_values[property.name] = arguments[property.name] - continue - - if property.name in required_values: - raise HTTPException( - status_code=400, - detail=f"Required parameter {property.name} not found in arguments", - ) - - return params_values - - -def call_brain_api(brain_id: UUID, user_id: UUID, arguments: dict) -> str: - brain_definition = api_brain_definition_service.get_api_brain_definition(brain_id) - - if brain_definition is None: - raise HTTPException( - status_code=404, detail=f"Brain definition {brain_id} not found" - ) - - brain_params_values = extract_api_brain_definition_values_from_llm_output( - brain_definition.params, arguments - ) - - brain_search_params_values = extract_api_brain_definition_values_from_llm_output( - brain_definition.search_params, arguments - ) - - secrets = brain_definition.secrets - secrets_values = {} - - for secret in secrets: - secret_value = brain_service.external_api_secrets_repository.read_secret( - user_id=user_id, brain_id=brain_id, secret_name=secret.name - ) - secrets_values[secret.name] = secret_value - - return get_api_call_response_as_text( - api_url=brain_definition.url, - params=brain_params_values, - search_params=brain_search_params_values, - secrets=secrets_values, - method=brain_definition.method, - ) diff --git a/backend/api/quivr_api/modules/brain/service/get_api_brain_definition_as_json_schema.py b/backend/api/quivr_api/modules/brain/service/get_api_brain_definition_as_json_schema.py deleted file mode 100644 index b8fcbeb5b..000000000 --- a/backend/api/quivr_api/modules/brain/service/get_api_brain_definition_as_json_schema.py +++ /dev/null @@ -1,64 +0,0 @@ -import re - -from fastapi import HTTPException -from quivr_api.modules.brain.entity.api_brain_definition_entity import ( - ApiBrainDefinitionSchemaProperty, -) -from quivr_api.modules.brain.entity.brain_entity import BrainEntity -from quivr_api.modules.brain.service.api_brain_definition_service import ( - ApiBrainDefinitionService, -) - -api_brain_definition_service = ApiBrainDefinitionService() - - -def sanitize_function_name(string): - sanitized_string = re.sub(r"[^a-zA-Z0-9_-]", "", string) - - return sanitized_string - - -def format_api_brain_property(property: ApiBrainDefinitionSchemaProperty): - property_data: dict = { - "type": property.type, - "description": property.description, - } - if property.enum: - property_data["enum"] = property.enum - return property_data - - -def get_api_brain_definition_as_json_schema(brain: BrainEntity): - api_brain_definition = api_brain_definition_service.get_api_brain_definition( - brain.id - ) - if not api_brain_definition: - raise HTTPException( - status_code=404, detail=f"Brain definition {brain.id} not found" - ) - - required = [] - required.extend(api_brain_definition.params.required) - required.extend(api_brain_definition.search_params.required) - properties = {} - - api_properties = ( - api_brain_definition.params.properties - + api_brain_definition.search_params.properties - ) - - for property in api_properties: - properties[property.name] = format_api_brain_property(property) - - parameters = { - "type": "object", - "properties": properties, - "required": required, - } - schema = { - "name": sanitize_function_name(brain.name), - "description": brain.description, - "parameters": parameters, - } - - return schema diff --git a/backend/api/quivr_api/modules/brain/service/utils/__init__.py b/backend/api/quivr_api/modules/brain/service/utils/__init__.py index 1b5b3a524..e69de29bb 100644 --- a/backend/api/quivr_api/modules/brain/service/utils/__init__.py +++ b/backend/api/quivr_api/modules/brain/service/utils/__init__.py @@ -1 +0,0 @@ -from .validate_brain import validate_api_brain diff --git a/backend/api/quivr_api/modules/brain/service/utils/validate_brain.py b/backend/api/quivr_api/modules/brain/service/utils/validate_brain.py index b1c9bc58e..43ec8e025 100644 --- a/backend/api/quivr_api/modules/brain/service/utils/validate_brain.py +++ b/backend/api/quivr_api/modules/brain/service/utils/validate_brain.py @@ -1,13 +1,2 @@ from fastapi import HTTPException from quivr_api.modules.brain.dto.inputs import CreateBrainProperties - - -def validate_api_brain(brain: CreateBrainProperties): - if brain.brain_definition is None: - raise HTTPException(status_code=404, detail="Brain definition not found") - - if brain.brain_definition.url is None: - raise HTTPException(status_code=404, detail="Brain url not found") - - if brain.brain_definition.method is None: - raise HTTPException(status_code=404, detail="Brain method not found") diff --git a/backend/api/quivr_api/modules/chat/controller/chat/brainful_chat.py b/backend/api/quivr_api/modules/chat/controller/chat/brainful_chat.py deleted file mode 100644 index fa5570896..000000000 --- a/backend/api/quivr_api/modules/chat/controller/chat/brainful_chat.py +++ /dev/null @@ -1,111 +0,0 @@ -from quivr_api.logger import get_logger -from quivr_api.modules.brain.entity.brain_entity import BrainType, RoleEnum -from quivr_api.modules.brain.integrations.Big.Brain import BigBrain -from quivr_api.modules.brain.integrations.GPT4.Brain import GPT4Brain -from quivr_api.modules.brain.integrations.Multi_Contract.Brain import MultiContractBrain -from quivr_api.modules.brain.integrations.Notion.Brain import NotionBrain -from quivr_api.modules.brain.integrations.Proxy.Brain import ProxyBrain -from quivr_api.modules.brain.integrations.Self.Brain import SelfBrain -from quivr_api.modules.brain.integrations.SQL.Brain import SQLBrain -from quivr_api.modules.brain.knowledge_brain_qa import KnowledgeBrainQA -from quivr_api.modules.brain.service.api_brain_definition_service import ( - ApiBrainDefinitionService, -) -from quivr_api.modules.brain.service.brain_authorization_service import ( - validate_brain_authorization, -) -from quivr_api.modules.brain.service.brain_service import BrainService -from quivr_api.modules.brain.service.integration_brain_service import ( - IntegrationBrainDescriptionService, -) -from quivr_api.modules.chat.controller.chat.interface import ChatInterface -from quivr_api.modules.chat.service.chat_service import ChatService -from quivr_api.modules.dependencies import get_service - -chat_service = get_service(ChatService)() -api_brain_definition_service = ApiBrainDefinitionService() -integration_brain_description_service = IntegrationBrainDescriptionService() - -logger = get_logger(__name__) - -models_supporting_function_calls = [ - "gpt-4", - "gpt-4-1106-preview", - "gpt-4-0613", - "gpt-3.5-turbo-0125", - "gpt-3.5-turbo-1106", - "gpt-3.5-turbo-0613", - "gpt-4-0125-preview", - "gpt-3.5-turbo", - "gpt-4-turbo", - "gpt-4o", -] - - -integration_list = { - "notion": NotionBrain, - "gpt4": GPT4Brain, - "sql": SQLBrain, - "big": BigBrain, - "doc": KnowledgeBrainQA, - "proxy": ProxyBrain, - "self": SelfBrain, - "multi-contract": MultiContractBrain, -} - -brain_service = BrainService() - - -def validate_authorization(user_id, brain_id): - if brain_id: - validate_brain_authorization( - brain_id=brain_id, - user_id=user_id, - required_roles=[RoleEnum.Viewer, RoleEnum.Editor, RoleEnum.Owner], - ) - - -# TODO: redo this -class BrainfulChat(ChatInterface): - def get_answer_generator( - self, - brain, - chat_id, - chat_service, - model, - temperature, - streaming, - prompt_id, - user_id, - user_email, - ): - if brain and brain.brain_type == BrainType.doc: - return KnowledgeBrainQA( - chat_service=chat_service, - chat_id=chat_id, - brain_id=str(brain.brain_id), - streaming=streaming, - prompt_id=prompt_id, - user_id=user_id, - user_email=user_email, - ) - - if brain.brain_type == BrainType.integration: - integration_brain = integration_brain_description_service.get_integration_description_by_user_brain_id( - brain.brain_id, user_id - ) - - integration_class = integration_list.get( - integration_brain.integration_name.lower() - ) - if integration_class: - return integration_class( - chat_service=chat_service, - chat_id=chat_id, - temperature=temperature, - brain_id=str(brain.brain_id), - streaming=streaming, - prompt_id=prompt_id, - user_id=user_id, - user_email=user_email, - ) diff --git a/backend/api/quivr_api/modules/chat/controller/chat/brainless_chat.py b/backend/api/quivr_api/modules/chat/controller/chat/brainless_chat.py deleted file mode 100644 index 80b2382b5..000000000 --- a/backend/api/quivr_api/modules/chat/controller/chat/brainless_chat.py +++ /dev/null @@ -1,26 +0,0 @@ -from llm.qa_headless import HeadlessQA -from quivr_api.modules.chat.controller.chat.interface import ChatInterface - - -class BrainlessChat(ChatInterface): - def validate_authorization(self, user_id, brain_id): - pass - - def get_answer_generator( - self, - chat_id, - model, - max_tokens, - temperature, - streaming, - prompt_id, - user_id, - ): - return HeadlessQA( - chat_id=chat_id, - model=model, - max_tokens=max_tokens, - temperature=temperature, - streaming=streaming, - prompt_id=prompt_id, - ) diff --git a/backend/api/quivr_api/modules/chat/controller/chat/factory.py b/backend/api/quivr_api/modules/chat/controller/chat/factory.py deleted file mode 100644 index 792328fee..000000000 --- a/backend/api/quivr_api/modules/chat/controller/chat/factory.py +++ /dev/null @@ -1,11 +0,0 @@ -from uuid import UUID - -from .brainful_chat import BrainfulChat -from .brainless_chat import BrainlessChat - - -def get_chat_strategy(brain_id: UUID | None = None): - if brain_id: - return BrainfulChat() - else: - return BrainlessChat() diff --git a/backend/api/quivr_api/modules/chat/controller/chat_routes.py b/backend/api/quivr_api/modules/chat/controller/chat_routes.py index c7c62f986..fafe94f59 100644 --- a/backend/api/quivr_api/modules/chat/controller/chat_routes.py +++ b/backend/api/quivr_api/modules/chat/controller/chat_routes.py @@ -5,25 +5,26 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import StreamingResponse from quivr_api.logger import get_logger from quivr_api.middlewares.auth import AuthBearer, get_current_user -from quivr_api.models.settings import get_embedding_client, get_supabase_client +from quivr_api.modules.brain.entity.brain_entity import RoleEnum +from quivr_api.modules.brain.service.brain_authorization_service import ( + validate_brain_authorization, +) from quivr_api.modules.brain.service.brain_service import BrainService -from quivr_api.modules.chat.controller.chat.brainful_chat import ( - BrainfulChat, validate_authorization) from quivr_api.modules.chat.dto.chats import ChatItem, ChatQuestion -from quivr_api.modules.chat.dto.inputs import (ChatMessageProperties, - ChatUpdatableProperties, - CreateChatProperties, - QuestionAndAnswer) +from quivr_api.modules.chat.dto.inputs import ( + ChatMessageProperties, + ChatUpdatableProperties, + CreateChatProperties, + QuestionAndAnswer, +) from quivr_api.modules.chat.entity.chat import Chat from quivr_api.modules.chat.service.chat_service import ChatService from quivr_api.modules.dependencies import get_service -from quivr_api.modules.knowledge.repository.knowledges import \ - KnowledgeRepository +from quivr_api.modules.knowledge.repository.knowledges import KnowledgeRepository from quivr_api.modules.prompt.service.prompt_service import PromptService from quivr_api.modules.rag_service import RAGService from quivr_api.modules.user.entity.user_identity import UserIdentity from quivr_api.packages.utils.telemetry import maybe_send_telemetry -from quivr_api.vectorstore.supabase import CustomSupabaseVectorStore logger = get_logger(__name__) @@ -37,52 +38,13 @@ ChatServiceDep = Annotated[ChatService, Depends(get_service(ChatService))] UserIdentityDep = Annotated[UserIdentity, Depends(get_current_user)] -def init_vector_store(user_id: UUID) -> CustomSupabaseVectorStore: - """ - Initialize the vector store - """ - supabase_client = get_supabase_client() - embedding_service = get_embedding_client() - vector_store = CustomSupabaseVectorStore( - supabase_client, embedding_service, table_name="vectors", user_id=user_id - ) - - return vector_store - - -async def get_answer_generator( - chat_id: UUID, - chat_question: ChatQuestion, - chat_service: ChatService, - brain_id: UUID | None, - current_user: UserIdentity, -): - chat_instance = BrainfulChat() - vector_store = init_vector_store(user_id=current_user.id) - - # Get History only if needed - if not brain_id: - history = await chat_service.get_chat_history(chat_id) - else: - history = [] - - # TODO(@aminediro) : NOT USED anymore - brain, metadata_brain = brain_service.find_brain_from_question( - brain_id, chat_question.question, current_user, chat_id, history, vector_store - ) - gpt_answer_generator = chat_instance.get_answer_generator( - brain=brain, - chat_id=str(chat_id), - chat_service=chat_service, - model=brain.model, - temperature=0.1, - streaming=True, - prompt_id=chat_question.prompt_id, - user_id=current_user.id, - user_email=current_user.email, - ) - - return gpt_answer_generator +def validate_authorization(user_id, brain_id): + if brain_id: + validate_brain_authorization( + brain_id=brain_id, + user_id=user_id, + required_roles=[RoleEnum.Viewer, RoleEnum.Editor, RoleEnum.Owner], + ) @chat_router.get("/chat/healthz", tags=["Health"]) diff --git a/backend/api/quivr_api/modules/chat/dto/chats.py b/backend/api/quivr_api/modules/chat/dto/chats.py index e7182e00d..e900602d1 100644 --- a/backend/api/quivr_api/modules/chat/dto/chats.py +++ b/backend/api/quivr_api/modules/chat/dto/chats.py @@ -14,7 +14,6 @@ class ChatMessage(BaseModel): history: List[Tuple[str, str]] temperature: float = 0.0 max_tokens: int = 256 - use_summarization: bool = False chat_id: Optional[UUID] = None chat_name: Optional[str] = None diff --git a/backend/api/quivr_api/modules/contact_support/__init__.py b/backend/api/quivr_api/modules/contact_support/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/api/quivr_api/modules/contact_support/controller/__init__.py b/backend/api/quivr_api/modules/contact_support/controller/__init__.py deleted file mode 100644 index 8d66e49ff..000000000 --- a/backend/api/quivr_api/modules/contact_support/controller/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .contact_routes import contact_router diff --git a/backend/api/quivr_api/modules/contact_support/controller/contact_routes.py b/backend/api/quivr_api/modules/contact_support/controller/contact_routes.py deleted file mode 100644 index 0a01f2451..000000000 --- a/backend/api/quivr_api/modules/contact_support/controller/contact_routes.py +++ /dev/null @@ -1,42 +0,0 @@ -from fastapi import APIRouter -from pydantic import BaseModel -from quivr_api.logger import get_logger -from quivr_api.modules.contact_support.controller.settings import ContactsSettings -from quivr_api.packages.emails.send_email import send_email - - -class ContactMessage(BaseModel): - customer_email: str - content: str - - -contact_router = APIRouter() -logger = get_logger(__name__) - - -def resend_contact_sales_email(customer_email: str, content: str): - settings = ContactsSettings() - mail_from = settings.resend_contact_sales_from - mail_to = settings.resend_contact_sales_to - body = f""" -

Customer email: {customer_email}

-

{content}

- """ - params = { - "from": mail_from, - "to": mail_to, - "subject": "Contact sales", - "reply_to": customer_email, - "html": body, - } - - return send_email(params) - - -@contact_router.post("/contact") -def post_contact(message: ContactMessage): - try: - resend_contact_sales_email(message.customer_email, message.content) - except Exception as e: - logger.error(e) - return {"error": "There was an error sending the email"} diff --git a/backend/api/quivr_api/modules/contact_support/controller/settings.py b/backend/api/quivr_api/modules/contact_support/controller/settings.py deleted file mode 100644 index b1b84cb52..000000000 --- a/backend/api/quivr_api/modules/contact_support/controller/settings.py +++ /dev/null @@ -1,7 +0,0 @@ -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" diff --git a/backend/api/quivr_api/modules/onboarding/__init__.py b/backend/api/quivr_api/modules/onboarding/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/api/quivr_api/modules/onboarding/dto/__init__.py b/backend/api/quivr_api/modules/onboarding/dto/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/api/quivr_api/modules/onboarding/entity/__init__.py b/backend/api/quivr_api/modules/onboarding/entity/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/api/quivr_api/modules/onboarding/repository/__init__.py b/backend/api/quivr_api/modules/onboarding/repository/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/api/quivr_api/modules/onboarding/service/__init__.py b/backend/api/quivr_api/modules/onboarding/service/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/api/quivr_api/modules/tools/email_sender.py b/backend/api/quivr_api/modules/tools/email_sender.py index a061e6764..766c18a11 100644 --- a/backend/api/quivr_api/modules/tools/email_sender.py +++ b/backend/api/quivr_api/modules/tools/email_sender.py @@ -11,8 +11,7 @@ from langchain_community.document_loaders import PlaywrightURLLoader from langchain_core.tools import BaseTool from pydantic import BaseModel from quivr_api.logger import get_logger -from quivr_api.models.settings import BrainSettings -from quivr_api.modules.contact_support.controller.settings import ContactsSettings +from quivr_api.models.settings import BrainSettings, SendEmailSettings from quivr_api.packages.emails.send_email import send_email logger = get_logger(__name__) @@ -32,7 +31,7 @@ class EmailSenderTool(BaseTool): description = "useful for when you need to send an email." args_schema: Type[BaseModel] = EmailInput brain_settings: BrainSettings = BrainSettings() - contact_settings: ContactsSettings = ContactsSettings() + contact_settings: SendEmailSettings = SendEmailSettings() def _run( self, text: str, run_manager: Optional[CallbackManagerForToolRun] = None diff --git a/backend/api/quivr_api/modules/upload/controller/upload_routes.py b/backend/api/quivr_api/modules/upload/controller/upload_routes.py index 0baf8835a..c466b4e5b 100644 --- a/backend/api/quivr_api/modules/upload/controller/upload_routes.py +++ b/backend/api/quivr_api/modules/upload/controller/upload_routes.py @@ -33,11 +33,6 @@ notification_service = NotificationService() knowledge_service = KnowledgeService() -@upload_router.get("/upload/healthz", tags=["Health"]) -async def healthz(): - return {"status": "ok"} - - @upload_router.post("/upload", dependencies=[Depends(AuthBearer())], tags=["Upload"]) async def upload_file( uploadFile: UploadFile, diff --git a/backend/api/quivr_api/modules/user/repository/users.py b/backend/api/quivr_api/modules/user/repository/users.py index b557e3319..500260f90 100644 --- a/backend/api/quivr_api/modules/user/repository/users.py +++ b/backend/api/quivr_api/modules/user/repository/users.py @@ -41,7 +41,6 @@ class Users(UsersInterface): user_identity = response.data[0] - print("USER_IDENTITY", user_identity) return UserIdentity(id=user_id) def get_user_identity(self, user_id): diff --git a/backend/api/quivr_api/playground/auth-azure.py b/backend/api/quivr_api/playground/auth-azure.py deleted file mode 100644 index 8b73669fe..000000000 --- a/backend/api/quivr_api/playground/auth-azure.py +++ /dev/null @@ -1,202 +0,0 @@ -import json -import os - -import msal -import requests -from fastapi import Depends, FastAPI, HTTPException, Request -from fastapi.responses import JSONResponse, StreamingResponse - -app = FastAPI() - -CLIENT_ID = "511dce23-02f3-4724-8684-05da226df5f3" -AUTHORITY = "https://login.microsoftonline.com/common" -REDIRECT_URI = "http://localhost:8000/oauth2callback" -SCOPE = [ - "https://graph.microsoft.com/Files.Read", - "https://graph.microsoft.com/User.Read", - "https://graph.microsoft.com/Sites.Read.All", -] - -client = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY) - - -def get_token_data(): - if not os.path.exists("azure_token.json"): - raise HTTPException(status_code=401, detail="User not authenticated") - with open("azure_token.json", "r") as token_file: - token_data = json.load(token_file) - if "access_token" not in token_data: - raise HTTPException(status_code=401, detail="Invalid token data") - return token_data - - -def refresh_token(): - if not os.path.exists("azure_token.json"): - raise HTTPException(status_code=401, detail="User not authenticated") - with open("azure_token.json", "r") as token_file: - token_data = json.load(token_file) - if "refresh_token" not in token_data: - raise HTTPException(status_code=401, detail="No refresh token available") - - result = client.acquire_token_by_refresh_token( - token_data["refresh_token"], scopes=SCOPE - ) - if "access_token" not in result: - raise HTTPException(status_code=400, detail="Failed to refresh token") - - with open("azure_token.json", "w") as token: - json.dump(result, token) - - return result - - -def get_headers(token_data): - return { - "Authorization": f"Bearer {token_data['access_token']}", - "Accept": "application/json", - } - - -@app.get("/authorize") -def authorize(): - authorization_url = client.get_authorization_request_url( - scopes=SCOPE, redirect_uri=REDIRECT_URI - ) - return JSONResponse(content={"authorization_url": authorization_url}) - - -@app.get("/oauth2callback") -def oauth2callback(request: Request): - code = request.query_params.get("code") - if not code: - raise HTTPException(status_code=400, detail="Authorization code not found") - - result = client.acquire_token_by_authorization_code( - code, scopes=SCOPE, redirect_uri=REDIRECT_URI - ) - if "access_token" not in result: - print(f"Token acquisition failed: {result}") - raise HTTPException(status_code=400, detail="Failed to acquire token") - - with open("azure_token.json", "w") as token: - json.dump(result, token) - - return JSONResponse(content={"message": "Authentication successful"}) - - -@app.get("/list_sites") -def list_sites(token_data: dict = Depends(get_token_data)): - headers = get_headers(token_data) - endpoint = "https://graph.microsoft.com/v1.0/sites?search=*" - response = requests.get(endpoint, headers=headers) - if response.status_code == 401: - token_data = refresh_token() - headers = get_headers(token_data) - response = requests.get(endpoint, headers=headers) - if response.status_code != 200: - raise HTTPException(status_code=response.status_code, detail=response.text) - sites = response.json().get("value", []) - return JSONResponse(content={"sites": sites}) - - -def extract_files_and_folders(items, headers, page_size): - result = [] - for item in items: - entry = { - "name": item.get("name"), - "id": item.get("id"), - "parentReference": item.get("parentReference"), - "lastModifiedDateTime": item.get("lastModifiedDateTime"), - "webUrl": item.get("webUrl"), - "size": item.get("size"), - "fileSystemInfo": item.get("fileSystemInfo"), - "folder": item.get("folder"), - "file": item.get("file"), - } - if "folder" in item: - folder_endpoint = f"https://graph.microsoft.com/v1.0/me/drive/items/{item['id']}/children?$top={page_size}" - children = [] - while folder_endpoint: - folder_response = requests.get(folder_endpoint, headers=headers) - if folder_response.status_code == 200: - children_page = folder_response.json().get("value", []) - children.extend(children_page) - folder_endpoint = folder_response.json().get( - "@odata.nextLink", None - ) - else: - break - entry["children"] = extract_files_and_folders(children, headers, page_size) - result.append(entry) - return result - - -def fetch_all_files(headers, page_size): - endpoint = ( - f"https://graph.microsoft.com/v1.0/me/drive/root/children?$top={page_size}" - ) - all_files = [] - while endpoint: - response = requests.get(endpoint, headers=headers) - if response.status_code == 401: - token_data = refresh_token() - headers = get_headers(token_data) - response = requests.get(endpoint, headers=headers) - if response.status_code != 200: - raise HTTPException(status_code=response.status_code, detail=response.text) - files = response.json().get("value", []) - all_files.extend(files) - endpoint = response.json().get("@odata.nextLink", None) - return all_files - - -@app.get("/list_files") -def list_files(page_size: int = 1, token_data: dict = Depends(get_token_data)): - headers = get_headers(token_data) - all_files = fetch_all_files(headers, page_size) - structured_files = extract_files_and_folders(all_files, headers, page_size) - return JSONResponse(content={"files": structured_files}) - - -@app.get("/download_file/{file_id}") -def download_file(file_id: str, token_data: dict = Depends(get_token_data)): - headers = get_headers(token_data) - metadata_endpoint = f"https://graph.microsoft.com/v1.0/me/drive/items/{file_id}" - metadata_response = requests.get(metadata_endpoint, headers=headers) - if metadata_response.status_code == 401: - token_data = refresh_token() - headers = get_headers(token_data) - metadata_response = requests.get(metadata_endpoint, headers=headers) - if metadata_response.status_code != 200: - raise HTTPException( - status_code=metadata_response.status_code, detail=metadata_response.text - ) - metadata = metadata_response.json() - if "folder" in metadata: - raise HTTPException( - status_code=400, detail="The specified ID is a folder, not a file" - ) - download_endpoint = ( - f"https://graph.microsoft.com/v1.0/me/drive/items/{file_id}/content" - ) - download_response = requests.get(download_endpoint, headers=headers, stream=True) - if download_response.status_code == 401: - token_data = refresh_token() - headers = get_headers(token_data) - download_response = requests.get( - download_endpoint, headers=headers, stream=True - ) - if download_response.status_code != 200: - raise HTTPException( - status_code=download_response.status_code, detail=download_response.text - ) - return StreamingResponse( - download_response.iter_content(chunk_size=1024), - headers={"Content-Disposition": f"attachment; filename={metadata.get('name')}"}, - ) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/api/quivr_api/playground/auth.py b/backend/api/quivr_api/playground/auth.py deleted file mode 100644 index bd7717219..000000000 --- a/backend/api/quivr_api/playground/auth.py +++ /dev/null @@ -1,91 +0,0 @@ -import json -import os - -from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import JSONResponse -from google.auth.transport.requests import Request as GoogleRequest -from google.oauth2.credentials import Credentials -from google_auth_oauthlib.flow import Flow -from googleapiclient.discovery import build -from googleapiclient.errors import HttpError - -app = FastAPI() - -SCOPES = ["https://www.googleapis.com/auth/drive.metadata.readonly"] -CLIENT_SECRETS_FILE = "credentials.json" -REDIRECT_URI = "http://localhost:8000/oauth2callback" - -# Disable OAuthlib's HTTPS verification when running locally. -os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" - - -@app.get("/authorize") -def authorize(): - flow = Flow.from_client_secrets_file( - CLIENT_SECRETS_FILE, scopes=SCOPES, redirect_uri=REDIRECT_URI - ) - authorization_url, state = flow.authorization_url( - access_type="offline", include_granted_scopes="true" - ) - # Store the state in session to validate the callback later - with open("state.json", "w") as state_file: - json.dump({"state": state}, state_file) - return JSONResponse(content={"authorization_url": authorization_url}) - - -@app.get("/oauth2callback") -def oauth2callback(request: Request): - state = request.query_params.get("state") - with open("state.json", "r") as state_file: - saved_state = json.load(state_file)["state"] - - if state != saved_state: - raise HTTPException(status_code=400, detail="Invalid state parameter") - - flow = Flow.from_client_secrets_file( - CLIENT_SECRETS_FILE, scopes=SCOPES, state=state, redirect_uri=REDIRECT_URI - ) - flow.fetch_token(authorization_response=str(request.url)) - creds = flow.credentials - - # Save the credentials for future use - with open("token.json", "w") as token: - token.write(creds.to_json()) - - return JSONResponse(content={"message": "Authentication successful"}) - - -@app.get("/list_files") -def list_files(): - creds = None - if os.path.exists("token.json"): - creds = Credentials.from_authorized_user_file("token.json", SCOPES) - - if not creds or not creds.valid: - if creds and creds.expired and creds.refresh_token: - creds.refresh(GoogleRequest()) - else: - raise HTTPException(status_code=401, detail="Credentials are not valid") - - try: - service = build("drive", "v3", credentials=creds) - results = ( - service.files() - .list(pageSize=10, fields="nextPageToken, files(id, name)") - .execute() - ) - items = results.get("files", []) - - if not items: - return JSONResponse(content={"files": "No files found."}) - - files = [{"name": item["name"], "id": item["id"]} for item in items] - return JSONResponse(content={"files": files}) - except HttpError as error: - raise HTTPException(status_code=500, detail=f"An error occurred: {error}") - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/api/quivr_api/routes/subscription_routes.py b/backend/api/quivr_api/routes/subscription_routes.py index a78733cf2..ce1ee92d6 100644 --- a/backend/api/quivr_api/routes/subscription_routes.py +++ b/backend/api/quivr_api/routes/subscription_routes.py @@ -8,9 +8,6 @@ from quivr_api.middlewares.auth.auth_bearer import AuthBearer, get_current_user from quivr_api.models.brains_subscription_invitations import BrainSubscription from quivr_api.modules.brain.entity.brain_entity import RoleEnum from quivr_api.modules.brain.repository import IntegrationBrain -from quivr_api.modules.brain.service.api_brain_definition_service import ( - ApiBrainDefinitionService, -) from quivr_api.modules.brain.service.brain_authorization_service import ( has_brain_authorization, validate_brain_authorization, @@ -35,7 +32,6 @@ user_service = UserService() prompt_service = PromptService() brain_user_service = BrainUserService() brain_service = BrainService() -api_brain_definition_service = ApiBrainDefinitionService() integration_brains_repository = IntegrationBrain() @@ -425,26 +421,6 @@ async def subscribe_to_brain_handler( status_code=403, detail="You are already subscribed to this brain", ) - if brain.brain_type == "api": - brain_definition = api_brain_definition_service.get_api_brain_definition( - brain_id - ) - brain_secrets = brain_definition.secrets if brain_definition != None else [] - - for secret in brain_secrets: - if not secrets[secret.name]: - raise HTTPException( - status_code=400, - detail=f"Please provide the secret {secret}", - ) - - for secret in brain_secrets: - brain_service.external_api_secrets_repository.create_secret( - user_id=current_user.id, - brain_id=brain_id, - secret_name=secret.name, - secret_value=secrets[secret.name], - ) try: brain_user_service.create_brain_user( diff --git a/backend/api/tests/ragas_evaluation/run_evaluation.py b/backend/api/tests/ragas_evaluation/run_evaluation.py index 26e97af14..e9fb45737 100644 --- a/backend/api/tests/ragas_evaluation/run_evaluation.py +++ b/backend/api/tests/ragas_evaluation/run_evaluation.py @@ -142,7 +142,6 @@ def generate_replies( contexts = [] test_questions = test_data.question.tolist() test_groundtruths = test_data.ground_truth.tolist() - thoughts = [] for question in test_questions: response = brain_chain.invoke({"question": question, "chat_history": []}) @@ -151,14 +150,12 @@ def generate_replies( ]["arguments"] cited_answer_obj = json.loads(cited_answer_data) answers.append(cited_answer_obj["answer"]) - thoughts.append(cited_answer_obj["thoughts"]) contexts.append([context.page_content for context in response["docs"]]) return Dataset.from_dict( { "question": test_questions, "answer": answers, - "thoughs": thoughts, "contexts": contexts, "ground_truth": test_groundtruths, } diff --git a/backend/core/quivr_core/models.py b/backend/core/quivr_core/models.py index 999f0a6f3..3993e56e2 100644 --- a/backend/core/quivr_core/models.py +++ b/backend/core/quivr_core/models.py @@ -17,12 +17,6 @@ class cited_answer(BaseModelV1): ..., description="The answer to the user question, which is based only on the given sources.", ) - thoughts: str = FieldV1( - ..., - description="""Description of the thought process, based only on the given sources. - Cite the text as much as possible and give the document name it appears in. In the format : 'Doc_name states : cited_text'. Be the most - procedural as possible. Write all the steps needed to find the answer until you find it.""", - ) citations: list[int] = FieldV1( ..., description="The integer IDs of the SPECIFIC sources which justify the answer.", @@ -63,7 +57,6 @@ class RawRAGResponse(TypedDict): class RAGResponseMetadata(BaseModel): citations: list[int] | None = None - thoughts: str | list[str] | None = None followup_questions: list[str] | None = None sources: list[Any] | None = None diff --git a/backend/core/quivr_core/utils.py b/backend/core/quivr_core/utils.py index 9d260cf05..63736b654 100644 --- a/backend/core/quivr_core/utils.py +++ b/backend/core/quivr_core/utils.py @@ -4,7 +4,6 @@ from typing import Any, List, Tuple, no_type_check from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage from langchain_core.messages.ai import AIMessageChunk from langchain_core.prompts import format_document - from quivr_core.models import ( ParsedRAGResponse, QuivrKnowledge, @@ -71,10 +70,6 @@ def get_chunk_metadata( followup_questions = gathered_args["followup_questions"] metadata["followup_questions"] = followup_questions - if "thoughts" in gathered_args: - thoughts = gathered_args["thoughts"] - metadata["thoughts"] = thoughts - return RAGResponseMetadata(**metadata) @@ -127,12 +122,8 @@ def parse_response(raw_response: RawRAGResponse, model_name: str) -> ParsedRAGRe followup_questions = raw_response["answer"].tool_calls[-1]["args"][ "followup_questions" ] - thoughts = raw_response["answer"].tool_calls[-1]["args"]["thoughts"] if followup_questions: metadata["followup_questions"] = followup_questions - if thoughts: - metadata["thoughts"] = thoughts - answer = raw_response["answer"].tool_calls[-1]["args"]["answer"] parsed_response = ParsedRAGResponse( answer=answer, metadata=RAGResponseMetadata(**metadata) diff --git a/backend/core/tests/test_quivr_rag.py b/backend/core/tests/test_quivr_rag.py index 822191466..bc4f2f84b 100644 --- a/backend/core/tests/test_quivr_rag.py +++ b/backend/core/tests/test_quivr_rag.py @@ -1,7 +1,6 @@ from uuid import uuid4 import pytest - from quivr_core.chat import ChatHistory from quivr_core.config import LLMEndpointConfig, RAGConfig from quivr_core.llm import LLMEndpoint @@ -63,7 +62,6 @@ async def test_quivrqarag( # TODO(@aminediro) : test responses with sources assert last_response.metadata.sources == [] assert last_response.metadata.citations == [] - assert last_response.metadata.thoughts and len(last_response.metadata.thoughts) > 0 # Assert whole response makes sense assert "".join([r.answer for r in stream_responses]) == full_response