feat: 🎸 marketplace (#1657)

added explore button and removed unused feature openai key

# Description

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

## Checklist before requesting a review

Please delete options that are not relevant.

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

## Screenshots (if appropriate):
This commit is contained in:
Stan Girard 2023-11-19 18:46:12 +01:00 committed by GitHub
parent b062573f00
commit d955e31f50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 77 additions and 516 deletions

View File

@ -6,12 +6,10 @@ from repository.files.upload_file import DocumentSerializable
@shared_task
def create_embedding_for_document(
brain_id, doc_with_metadata, user_openai_api_key, file_sha1
):
def create_embedding_for_document(brain_id, doc_with_metadata, file_sha1):
neurons = Neurons()
doc = DocumentSerializable.from_json(doc_with_metadata)
created_vector = neurons.create_vector(doc, user_openai_api_key)
created_vector = neurons.create_vector(doc)
database = get_supabase_db()
database.set_file_sha_from_metadata(file_sha1)

View File

@ -56,9 +56,7 @@ else:
def process_file_and_notify(
file_name: str,
file_original_name: str,
enable_summarization,
brain_id,
openai_api_key,
notification_id=None,
):
supabase_client = get_supabase_client()
@ -81,9 +79,7 @@ def process_file_and_notify(
message = loop.run_until_complete(
filter_file(
file=file_instance,
enable_summarization=enable_summarization,
brain_id=brain_id,
openai_api_key=openai_api_key,
original_file_name=file_original_name,
)
)
@ -112,9 +108,7 @@ def process_file_and_notify(
@celery.task(name="process_crawl_and_notify")
def process_crawl_and_notify(
crawl_website_url,
enable_summarization,
brain_id,
openai_api_key,
notification_id=None,
):
crawl_website = CrawlWebsite(url=crawl_website_url)
@ -136,9 +130,7 @@ def process_crawl_and_notify(
message = loop.run_until_complete(
filter_file(
file=file_instance,
enable_summarization=enable_summarization,
brain_id=brain_id,
openai_api_key=openai_api_key,
original_file_name=crawl_website_url,
)
)
@ -147,9 +139,7 @@ def process_crawl_and_notify(
message = loop.run_until_complete(
process_github(
repo=crawl_website.url,
enable_summarization="false",
brain_id=brain_id,
user_openai_api_key=openai_api_key,
)
)

View File

@ -63,7 +63,6 @@ class QABaseBrainPicking(BaseModel):
chat_id: str = None # pyright: ignore reportPrivateUsage=none
brain_id: str = None # pyright: ignore reportPrivateUsage=none
max_tokens: int = 256
user_openai_api_key: str = None # pyright: ignore reportPrivateUsage=none
streaming: bool = False
openai_api_key: str = None # pyright: ignore reportPrivateUsage=none
@ -71,13 +70,6 @@ class QABaseBrainPicking(BaseModel):
AsyncIteratorCallbackHandler
] = None # pyright: ignore reportPrivateUsage=none
def _determine_api_key(self, openai_api_key, user_openai_api_key):
"""If user provided an API key, use it."""
if user_openai_api_key is not None:
return user_openai_api_key
else:
return openai_api_key
def _determine_streaming(self, model: str, streaming: bool) -> bool:
"""If the model name allows for streaming and streaming is declared, set streaming to True."""
return streaming

View File

@ -32,20 +32,12 @@ class HeadlessQA(BaseModel):
model: str
temperature: float = 0.0
max_tokens: int = 2000
user_openai_api_key: Optional[str] = None
openai_api_key: Optional[str] = None
streaming: bool = False
chat_id: str
callbacks: Optional[List[AsyncIteratorCallbackHandler]] = None
prompt_id: Optional[UUID] = None
def _determine_api_key(self, openai_api_key, user_openai_api_key):
"""If user provided an API key, use it."""
if user_openai_api_key is not None:
return user_openai_api_key
else:
return openai_api_key
def _determine_streaming(self, streaming: bool) -> bool:
"""If the model name allows for streaming and streaming is declared, set streaming to True."""
return streaming
@ -61,11 +53,6 @@ class HeadlessQA(BaseModel):
def __init__(self, **data):
super().__init__(**data)
print("in HeadlessQA")
self.openai_api_key = self._determine_api_key(
self.openai_api_key, self.user_openai_api_key
)
self.streaming = self._determine_streaming(self.streaming)
self.callbacks = self._determine_callback_array(self.streaming)

View File

@ -20,7 +20,6 @@ class BrainEntity(BaseModel):
temperature: Optional[float]
model: Optional[str]
max_tokens: Optional[int]
openai_api_key: Optional[str]
status: Optional[str]
prompt_id: Optional[UUID]
last_update: str

View File

@ -19,7 +19,6 @@ class Brain(BaseModel):
model: Optional[str] = None
temperature: Optional[float] = 0.0
max_tokens: Optional[int] = 256
openai_api_key: Optional[str] = None
files: List[Any] = []
prompt_id: Optional[UUID] = None

View File

@ -25,7 +25,6 @@ class CreateBrainProperties(BaseModel, extra=Extra.forbid):
model: Optional[str]
temperature: Optional[float] = 0.0
max_tokens: Optional[int] = 256
openai_api_key: Optional[str] = None
prompt_id: Optional[UUID] = None
brain_type: Optional[BrainType] = BrainType.DOC
brain_definition: Optional[CreateApiBrainDefinition]
@ -44,7 +43,6 @@ class BrainUpdatableProperties(BaseModel):
temperature: Optional[float]
model: Optional[str]
max_tokens: Optional[int]
openai_api_key: Optional[str]
status: Optional[str]
prompt_id: Optional[UUID]

View File

@ -11,24 +11,27 @@ class BrainRateLimiting(BaseSettings):
class BrainSettings(BaseSettings):
openai_api_key: str
anthropic_api_key: str
supabase_url: str
supabase_service_key: str
pg_database_url: str = "not implemented"
resend_api_key: str = "null"
resend_email_address: str = "brain@mail.quivr.app"
class ContactsSettings(BaseSettings):
resend_contact_sales_from: str = "null"
resend_contact_sales_to: str = "null"
class LLMSettings(BaseSettings):
private: bool = False
model_path: str = "./local_models/ggml-gpt4all-j-v1.3-groovy.bin"
class ResendSettings(BaseSettings):
resend_api_key: str = "null"
def get_supabase_client() -> Client:
settings = BrainSettings() # pyright: ignore reportPrivateUsage=none
supabase_client: Client = create_client(

View File

@ -33,7 +33,6 @@ async def get_user_endpoint(
user_daily_usage = UserUsage(
id=current_user.id,
email=current_user.email,
openai_api_key=current_user.openai_api_key,
)
user_settings = user_daily_usage.get_user_settings()
max_brain_size = user_settings.get("max_brain_size", 1000000000)

View File

@ -7,4 +7,3 @@ from pydantic import BaseModel
class UserIdentity(BaseModel):
id: UUID
email: Optional[str] = None
openai_api_key: Optional[str] = None

View File

@ -2,7 +2,6 @@ from concurrent.futures import ThreadPoolExecutor
from typing import List
from uuid import UUID
from langchain.embeddings.openai import OpenAIEmbeddings
from logger import get_logger
from models.settings import get_documents_vector_store, get_embeddings, get_supabase_db
from pydantic import BaseModel
@ -12,14 +11,11 @@ logger = get_logger(__name__)
# TODO: Create interface for embeddings and implement it for Supabase and OpenAI (current Quivr)
class Neurons(BaseModel):
def create_vector(self, doc, user_openai_api_key=None):
def create_vector(self, doc):
documents_vector_store = get_documents_vector_store()
logger.info("Creating vector for document")
logger.info(f"Document: {doc}")
if user_openai_api_key:
documents_vector_store._embedding = OpenAIEmbeddings(
openai_api_key=user_openai_api_key
) # pyright: ignore reportPrivateUsage=none
try:
sids = documents_vector_store.add_documents([doc])
if sids and len(sids) > 0:

View File

@ -11,9 +11,7 @@ from packages.files.file import compute_sha1_from_content
async def process_audio(
file: File,
enable_summarization: bool,
user,
user_openai_api_key,
):
temp_filename = None
file_sha = ""
@ -21,11 +19,6 @@ async def process_audio(
file_meta_name = f"audiotranscript_{dateshort}.txt"
documents_vector_store = get_documents_vector_store()
# use this for whisper
os.environ.get("OPENAI_API_KEY")
if user_openai_api_key:
pass
try:
upload_file = file.file
with tempfile.NamedTemporaryFile(

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
async def process_python(file: File, enable_summarization, brain_id, user_openai_api_key):
async def process_python(file: File, brain_id):
return await process_file(
file=file,
loader_class=PythonLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -8,9 +8,7 @@ from repository.files.upload_file import DocumentSerializable
async def process_file(
file: File,
loader_class,
enable_summarization,
brain_id,
user_openai_api_key,
):
dateshort = time.strftime("%Y%m%d")
@ -24,14 +22,13 @@ async def process_file(
"chunk_size": file.chunk_size,
"chunk_overlap": file.chunk_overlap,
"date": dateshort,
"summarization": "true" if enable_summarization else "false",
}
doc_with_metadata = DocumentSerializable(
page_content=doc.page_content, metadata=metadata
)
create_embedding_for_document.delay(
brain_id, doc_with_metadata.to_json(), user_openai_api_key, file.file_sha1
brain_id, doc_with_metadata.to_json(), file.file_sha1
)
return "Hello World!"

View File

@ -6,14 +6,10 @@ from .common import process_file
def process_csv(
file: File,
enable_summarization,
brain_id,
user_openai_api_key,
):
return process_file(
file=file,
loader_class=CSVLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
def process_docx(file: File, enable_summarization, brain_id, user_openai_api_key):
def process_docx(file: File, brain_id):
return process_file(
file=file,
loader_class=Docx2txtLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
def process_epub(file: File, enable_summarization, brain_id, user_openai_api_key):
def process_epub(file: File, brain_id):
return process_file(
file=file,
loader_class=UnstructuredEPubLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -11,9 +11,7 @@ from packages.files.file import compute_sha1_from_content
async def process_github(
repo,
enable_summarization,
brain_id,
user_openai_api_key,
):
random_dir_name = os.urandom(16).hex()
dateshort = time.strftime("%Y%m%d")
@ -54,7 +52,6 @@ async def process_github(
"chunk_size": chunk_size,
"chunk_overlap": chunk_overlap,
"date": dateshort,
"summarization": "true" if enable_summarization else "false",
}
doc_with_metadata = Document(page_content=doc.page_content, metadata=metadata)
@ -66,9 +63,7 @@ async def process_github(
if not file_exists:
neurons = Neurons()
created_vector = neurons.create_vector(
doc_with_metadata, user_openai_api_key
)
created_vector = neurons.create_vector(doc_with_metadata)
file_exists_in_brain = file.file_already_exists_in_brain(brain_id)

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
def process_html(file: File, enable_summarization, brain_id, user_openai_api_key):
def process_html(file: File, brain_id):
return process_file(
file=file,
loader_class=UnstructuredHTMLLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
def process_markdown(file: File, enable_summarization, brain_id, user_openai_api_key):
def process_markdown(file: File, brain_id):
return process_file(
file=file,
loader_class=UnstructuredMarkdownLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
def process_ipnyb(file: File, enable_summarization, brain_id, user_openai_api_key):
def process_ipnyb(file: File, brain_id):
return process_file(
file=file,
loader_class=NotebookLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
def process_odt(file: File, enable_summarization, brain_id, user_openai_api_key):
def process_odt(file: File, brain_id):
return process_file(
file=file,
loader_class=UnstructuredPDFLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
def process_pdf(file: File, enable_summarization, brain_id, user_openai_api_key):
def process_pdf(file: File, brain_id):
return process_file(
file=file,
loader_class=UnstructuredPDFLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -4,11 +4,9 @@ from models import File
from .common import process_file
def process_powerpoint(file: File, enable_summarization, brain_id, user_openai_api_key):
def process_powerpoint(file: File, brain_id):
return process_file(
file=file,
loader_class=UnstructuredPowerPointLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -6,14 +6,10 @@ from .common import process_file
def process_telegram(
file: File,
enable_summarization,
brain_id,
user_openai_api_key,
):
return process_file(
file=file,
loader_class=TelegramChatFileLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -4,11 +4,12 @@ from models import File
from .common import process_file
async def process_txt(file: File, enable_summarization, brain_id, user_openai_api_key):
async def process_txt(
file: File,
brain_id,
):
return await process_file(
file=file,
loader_class=TextLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -6,14 +6,10 @@ from .common import process_file
def process_xlsx(
file: File,
enable_summarization,
brain_id,
user_openai_api_key,
):
return process_file(
file=file,
loader_class=UnstructuredExcelLoader,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
)

View File

@ -49,9 +49,7 @@ def create_response(message, type):
# TODO: Move filter_file to a file service to avoid circular imports from models/files.py for File class
async def filter_file(
file,
enable_summarization: bool,
brain_id,
openai_api_key,
original_file_name=None,
):
await file.compute_file_sha1()
@ -85,9 +83,7 @@ async def filter_file(
try:
await file_processors[file.file_extension](
file=file,
enable_summarization=enable_summarization,
brain_id=brain_id,
user_openai_api_key=openai_api_key,
)
return create_response(
f"{using_file_name} has been uploaded to brain {brain.name}.", # pyright: ignore reportPrivateUsage=none

View File

@ -89,7 +89,6 @@ async def create_new_brain(
user_usage = UserUsage(
id=current_user.id,
email=current_user.email,
openai_api_key=current_user.openai_api_key,
)
user_settings = user_usage.get_user_settings()

View File

@ -39,7 +39,6 @@ class BrainfulChat(ChatInterface):
model,
max_tokens,
temperature,
user_openai_api_key,
streaming,
prompt_id,
user_id,
@ -59,7 +58,6 @@ class BrainfulChat(ChatInterface):
max_tokens=max_tokens,
temperature=temperature,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
streaming=streaming,
prompt_id=prompt_id,
)
@ -70,7 +68,6 @@ class BrainfulChat(ChatInterface):
max_tokens=max_tokens,
temperature=temperature,
brain_id=brain_id,
user_openai_api_key=user_openai_api_key,
streaming=streaming,
prompt_id=prompt_id,
user_id=user_id,

View File

@ -20,7 +20,6 @@ class BrainlessChat(ChatInterface):
model,
max_tokens,
temperature,
user_openai_api_key,
streaming,
prompt_id,
user_id,
@ -30,7 +29,6 @@ class BrainlessChat(ChatInterface):
model=model,
max_tokens=max_tokens,
temperature=temperature,
user_openai_api_key=user_openai_api_key,
streaming=streaming,
prompt_id=prompt_id,
)

View File

@ -18,7 +18,6 @@ class ChatInterface(ABC):
model,
max_tokens,
temperature,
user_openai_api_key,
streaming,
prompt_id,
user_id,

View File

@ -38,21 +38,18 @@ def delete_chat_from_db(supabase_db: SupabaseDB, chat_id):
def check_user_requests_limit(
user: UserIdentity,
):
userDailyUsage = UserUsage(
id=user.id, email=user.email, openai_api_key=user.openai_api_key
)
userDailyUsage = UserUsage(id=user.id, email=user.email)
userSettings = userDailyUsage.get_user_settings()
date = time.strftime("%Y%m%d")
userDailyUsage.handle_increment_user_request_count(date)
if user.openai_api_key is None:
daily_chat_credit = userSettings.get("daily_chat_credit", 0)
if int(userDailyUsage.daily_requests_count) >= int(daily_chat_credit):
raise HTTPException(
status_code=429, # pyright: ignore reportPrivateUsage=none
detail="You have reached the maximum number of requests for today.", # pyright: ignore reportPrivateUsage=none
)
daily_chat_credit = userSettings.get("daily_chat_credit", 0)
if int(userDailyUsage.daily_requests_count) >= int(daily_chat_credit):
raise HTTPException(
status_code=429, # pyright: ignore reportPrivateUsage=none
detail="You have reached the maximum number of requests for today.", # pyright: ignore reportPrivateUsage=none
)
else:
pass

View File

@ -133,22 +133,16 @@ async def create_question_handler(
chat_instance.validate_authorization(user_id=current_user.id, brain_id=brain_id)
current_user.openai_api_key = request.headers.get("Openai-Api-Key")
brain = Brain(id=brain_id)
brain_details: BrainEntity | None = None
userDailyUsage = UserUsage(
id=current_user.id,
email=current_user.email,
openai_api_key=current_user.openai_api_key,
)
userSettings = userDailyUsage.get_user_settings()
is_model_ok = (brain_details or chat_question).model in userSettings.get("models", ["gpt-3.5-turbo"]) # type: ignore
if not current_user.openai_api_key:
current_user.openai_api_key = chat_instance.get_openai_api_key(
brain_id=brain_id, user_id=current_user.id
)
# Retrieve chat model (temperature, max_tokens, model)
if (
not chat_question.model
@ -171,7 +165,6 @@ async def create_question_handler(
max_tokens=chat_question.max_tokens,
temperature=chat_question.temperature,
brain_id=str(brain_id),
user_openai_api_key=current_user.openai_api_key, # pyright: ignore reportPrivateUsage=none
streaming=False,
prompt_id=chat_question.prompt_id,
user_id=current_user.id,
@ -207,22 +200,15 @@ async def create_stream_question_handler(
chat_instance.validate_authorization(user_id=current_user.id, brain_id=brain_id)
# Retrieve user's OpenAI API key
current_user.openai_api_key = request.headers.get("Openai-Api-Key")
brain = Brain(id=brain_id)
brain_details: BrainEntity | None = None
userDailyUsage = UserUsage(
id=current_user.id,
email=current_user.email,
openai_api_key=current_user.openai_api_key,
)
userSettings = userDailyUsage.get_user_settings()
if not current_user.openai_api_key:
current_user.openai_api_key = chat_instance.get_openai_api_key(
brain_id=brain_id, user_id=current_user.id
)
# Retrieve chat model (temperature, max_tokens, model)
if (
not chat_question.model
@ -247,7 +233,6 @@ async def create_stream_question_handler(
model=(brain_details or chat_question).model if is_model_ok else "gpt-3.5-turbo", # type: ignore
max_tokens=(brain_details or chat_question).max_tokens, # type: ignore
temperature=(brain_details or chat_question).temperature, # type: ignore
user_openai_api_key=current_user.openai_api_key, # pyright: ignore reportPrivateUsage=none
streaming=True,
prompt_id=chat_question.prompt_id,
brain_id=str(brain_id),

View File

@ -30,7 +30,6 @@ async def crawl_endpoint(
crawl_website: CrawlWebsite,
brain_id: UUID = Query(..., description="The ID of the brain"),
chat_id: Optional[UUID] = Query(None, description="The ID of the chat"),
enable_summarization: bool = False,
current_user: UserIdentity = Depends(get_current_user),
):
"""
@ -43,7 +42,6 @@ async def crawl_endpoint(
userDailyUsage = UserUsage(
id=current_user.id,
email=current_user.email,
openai_api_key=current_user.openai_api_key,
)
userSettings = userDailyUsage.get_user_settings()
@ -81,9 +79,7 @@ async def crawl_endpoint(
process_crawl_and_notify.delay(
crawl_website_url=crawl_website.url,
enable_summarization=enable_summarization,
brain_id=brain_id,
openai_api_key=request.headers.get("Openai-Api-Key", None),
notification_id=crawl_notification.id,
)

View File

@ -11,9 +11,7 @@ from models.databases.supabase.knowledge import CreateKnowledgeProperties
from models.databases.supabase.notifications import CreateNotificationProperties
from models.notifications import NotificationsStatusEnum
from modules.user.entity.user_identity import UserIdentity
from modules.user.repository import get_user_identity
from packages.files.file import convert_bytes, get_file_size
from repository.brain import get_brain_details
from repository.files.upload_file import upload_file_storage
from repository.knowledge.add_knowledge import add_knowledge
from repository.notification.add_notification import add_notification
@ -37,7 +35,6 @@ async def upload_file(
uploadFile: UploadFile,
brain_id: UUID = Query(..., description="The ID of the brain"),
chat_id: Optional[UUID] = Query(None, description="The ID of the chat"),
enable_summarization: bool = False,
current_user: UserIdentity = Depends(get_current_user),
):
validate_brain_authorization(
@ -47,7 +44,6 @@ async def upload_file(
userDailyUsage = UserUsage(
id=current_user.id,
email=current_user.email,
openai_api_key=current_user.openai_api_key,
)
userSettings = userDailyUsage.get_user_settings()
@ -72,13 +68,6 @@ async def upload_file(
status=NotificationsStatusEnum.Pending,
)
)
openai_api_key = request.headers.get("Openai-Api-Key", None)
if openai_api_key is None:
brain_details = get_brain_details(brain_id)
if brain_details:
openai_api_key = brain_details.openai_api_key
if openai_api_key is None:
openai_api_key = get_user_identity(current_user.id).openai_api_key
file_content = await uploadFile.read()
filename_with_brain_id = str(brain_id) + "/" + str(uploadFile.filename)
@ -112,9 +101,7 @@ async def upload_file(
process_file_and_notify.delay(
file_name=filename_with_brain_id,
file_original_name=uploadFile.filename,
enable_summarization=enable_summarization,
brain_id=brain_id,
openai_api_key=openai_api_key,
notification_id=upload_notification.id if upload_notification else None,
)
return {"message": "File processing has started."}

View File

@ -32,4 +32,3 @@ def test_get_user_identity(client, api_key):
print(user_identity)
assert "id" in user_identity
assert "email" in user_identity
assert "openai_api_key" in user_identity

View File

@ -2,6 +2,7 @@ version: "3"
services:
frontend:
image: frontend-dev
env_file:
- ./frontend/.env
build:

View File

@ -1,7 +1,6 @@
import { UUID } from "crypto";
import { useTranslation } from "react-i18next";
import Field from "@/lib/components/ui/Field";
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
import { SaveButton } from "@/shared/SaveButton";
@ -15,20 +14,13 @@ type ModelSelectionProps = {
};
export const ModelSelection = (props: ModelSelectionProps): JSX.Element => {
const { model, maxTokens, temperature, register } = useBrainFormState();
const { model, maxTokens, register } = useBrainFormState();
const { t } = useTranslation(["translation", "brain", "config"]);
const { handleSubmit, hasEditRights, accessibleModels } = props;
return (
<>
<Field
label={t("openAiKeyLabel", { ns: "config" })}
placeholder={t("openAiKeyPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={!hasEditRights}
{...register("openAiKey")}
/>
<fieldset className="w-full flex flex-col mt-2">
<label className="flex-1 text-sm" htmlFor="model">
{t("modelLabel", { ns: "config" })}
@ -50,21 +42,6 @@ export const ModelSelection = (props: ModelSelectionProps): JSX.Element => {
))}
</select>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="temp">
{t("temperature", { ns: "config" })}: {temperature}
</label>
<input
id="temp"
type="range"
min="0"
max="1"
step="0.01"
value={temperature}
disabled={!hasEditRights}
{...register("temperature")}
/>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="tokens">
{t("maxTokens", { ns: "config" })}: {maxTokens}

View File

@ -54,10 +54,7 @@ export const useBrainFormState = () => {
continue;
}
if (brainKey === "openai_api_key") {
setValue("openAiKey", brain["openai_api_key"] ?? "");
continue;
}
// @ts-expect-error bad type inference from typescript
// eslint-disable-next-line

View File

@ -105,7 +105,6 @@ export const usePrompt = (props: UsePromptProps) => {
const {
prompt,
maxTokens: max_tokens,
openAiKey: openai_api_key,
...otherConfigs
} = getValues();
@ -138,7 +137,6 @@ export const usePrompt = (props: UsePromptProps) => {
await updateBrain(brainId, {
...otherConfigs,
max_tokens,
openai_api_key,
});
refetchBrain();
} else {
@ -146,7 +144,6 @@ export const usePrompt = (props: UsePromptProps) => {
updateBrain(brainId, {
...otherConfigs,
max_tokens,
openai_api_key,
}),
promptHandler(),
]);

View File

@ -13,7 +13,6 @@ import { useUserData } from "@/lib/hooks/useUserData";
import { useBrainFormState } from "./useBrainFormState";
import { checkBrainName } from "../utils/checkBrainName";
import { checkOpenAiKey } from "../utils/checkOpenAiKey";
type UseSettingsTabProps = {
brainId: UUID;
@ -99,10 +98,9 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
if (!hasChanges && checkDirty) {
return;
}
const { name, openAiKey: openai_api_key } = getValues();
const { name} = getValues();
checkBrainName(name, publish, t);
await checkOpenAiKey(openai_api_key, publish, t);
try {
setIsUpdating(true);
@ -111,7 +109,6 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
await updateBrain(brainId, {
...otherConfigs,
max_tokens,
openai_api_key,
prompt_id:
otherConfigs["prompt_id"] !== ""
? otherConfigs["prompt_id"]

View File

@ -1,26 +0,0 @@
import { TFunction } from "i18next";
import { ToastData } from "@/lib/components/ui/Toast/domain/types";
import { validateOpenAIKey } from "./validateOpenAIKey";
export const checkOpenAiKey = async (
openai_api_key: string | undefined,
publish: (toast: ToastData) => void,
t: TFunction<["translation", "brain", "config"]>
): Promise<void> => {
if (
openai_api_key !== undefined &&
openai_api_key !== "" &&
!(await validateOpenAIKey(
openai_api_key,
{
badApiKeyError: t("incorrectApiKey", { ns: "config" }),
invalidApiKeyError: t("invalidApiKeyError", { ns: "config" }),
},
publish
))
) {
return;
}
};

View File

@ -1,71 +0,0 @@
import axios from "axios";
import { ToastData } from "@/lib/components/ui/Toast/domain/types";
import { getAxiosErrorParams } from "@/lib/helpers/getAxiosErrorParams";
export const getOpenAIKeyValidationStatusCode = async (
key: string
): Promise<number> => {
const url = "https://api.openai.com/v1/chat/completions";
const headers = {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
};
const data = JSON.stringify({
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Hello!",
},
],
});
try {
await axios.post(url, data, { headers });
return 200;
} catch (error) {
return getAxiosErrorParams(error)?.status ?? 400;
}
};
type ErrorMessages = {
badApiKeyError: string;
invalidApiKeyError: string;
};
export const validateOpenAIKey = async (
openai_api_key: string | undefined,
errorMessages: ErrorMessages,
publish: (toast: ToastData) => void
): Promise<boolean> => {
if (openai_api_key !== undefined) {
const keyValidationStatusCode = await getOpenAIKeyValidationStatusCode(
openai_api_key
);
if (keyValidationStatusCode !== 200) {
if (keyValidationStatusCode === 401) {
publish({
variant: "danger",
text: errorMessages.badApiKeyError,
});
}
if (keyValidationStatusCode === 429) {
publish({
variant: "danger",
text: errorMessages.invalidApiKeyError,
});
}
return false;
}
return true;
}
return false;
};

View File

@ -13,7 +13,6 @@ export const ConfigModal = (): JSX.Element => {
isConfigModalOpen,
setIsConfigModalOpen,
register,
temperature,
maxTokens,
model,
accessibleModels,
@ -53,21 +52,6 @@ export const ConfigModal = (): JSX.Element => {
))}
</select>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="temp">
Temperature: {temperature}
</label>
<input
id="temp"
type="range"
min="0"
max="1"
step="0.01"
value={temperature}
{...register("temperature")}
/>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="tokens">
Max tokens: {maxTokens}

View File

@ -8,7 +8,7 @@ import {
getChatsConfigFromLocalStorage,
saveChatsConfigInLocalStorage,
} from "@/lib/api/chat/chat.local";
import { USER_DATA_KEY, USER_IDENTITY_DATA_KEY } from "@/lib/api/user/config";
import { USER_DATA_KEY } from "@/lib/api/user/config";
import { useUserApi } from "@/lib/api/user/useUserApi";
import { defaultBrainConfig } from "@/lib/config/defaultBrainConfig";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
@ -22,16 +22,13 @@ export const useConfigModal = () => {
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
const { getBrain } = useBrainApi();
const { currentBrainId } = useBrainContext();
const { getUser, getUserIdentity } = useUserApi();
const { getUser } = useUserApi();
const { data: userData } = useQuery({
queryKey: [USER_DATA_KEY],
queryFn: getUser,
});
const { data: userIdentityData } = useQuery({
queryKey: [USER_IDENTITY_DATA_KEY],
queryFn: getUserIdentity,
});
const { register, watch, setValue } = useForm<ChatConfig>({
defaultValues: {
@ -46,7 +43,6 @@ export const useConfigModal = () => {
const maxTokens = watch("maxTokens");
const accessibleModels = getAccessibleModels({
openAiKey: userIdentityData?.openai_api_key,
userData,
});

View File

@ -15,7 +15,7 @@ export const ChatsList = (): JSX.Element => {
const { shouldDisplayWelcomeChat } = useOnboarding();
return (
<Sidebar showButtons={["myBrains", "upgradeToPlus", "user"]}>
<Sidebar showButtons={["myBrains","marketplace", "upgradeToPlus", "user"]}>
<div className="flex flex-col flex-1 h-full" data-testid="chats-list">
<div className="pt-2">
<NewChatButton />

View File

@ -2,11 +2,10 @@
"use client";
import { useTranslation } from "react-i18next";
import { FaCopy, FaInfoCircle } from "react-icons/fa";
import { FaCopy } from "react-icons/fa";
import Button from "@/lib/components/ui/Button";
import Field from "@/lib/components/ui/Field";
import copyToClipboard from "@/lib/helpers/copyToClipboard";
import { useApiKeyConfig } from "./hooks/useApiKeyConfig";
@ -15,13 +14,7 @@ export const ApiKeyConfig = (): JSX.Element => {
apiKey,
handleCopyClick,
handleCreateClick,
openAiApiKey,
setOpenAiApiKey,
changeOpenAiApiKey,
changeOpenAiApiKeyRequestPending,
userIdentity,
removeOpenAiApiKey,
hasOpenAiApiKey,
} = useApiKeyConfig();
const { t } = useTranslation(["config"]);
@ -47,71 +40,7 @@ export const ApiKeyConfig = (): JSX.Element => {
</div>
)}
</div>
<hr className="my-8" />
<div>
<h3 className="font-semibold mb-2">OpenAI {t("apiKey")}</h3>
<form
className="mb-4"
onSubmit={(event) => {
event.preventDefault();
void changeOpenAiApiKey();
}}
>
<div className="flex items-center space-x-2">
<Field
name="openAiApiKey"
placeholder="Open AI Key"
className="w-full"
value={openAiApiKey ?? ""}
data-testid="open-ai-api-key-input"
onChange={(e) => setOpenAiApiKey(e.target.value)}
/>
<button
hidden={!hasOpenAiApiKey}
data-testid="copy-openai-api-key-button"
onClick={() => void copyToClipboard(openAiApiKey)}
type="button"
>
<FaCopy />
</button>
</div>
<div className="mt-4 flex flex-row justify-between">
{hasOpenAiApiKey && (
<Button
isLoading={changeOpenAiApiKeyRequestPending}
variant="secondary"
onClick={() => void removeOpenAiApiKey()}
>
Remove Key
</Button>
)}
<Button
data-testid="save-open-ai-api-key"
isLoading={changeOpenAiApiKeyRequestPending}
disabled={openAiApiKey === userIdentity?.openai_api_key}
>
Save Key
</Button>
</div>
</form>
<div className="flex space-x-2 bg-sky-100 dark:bg-gray-900 border border-sky-200 dark:border-gray-700 px-4 py-3 rounded relative max-w-md">
<div className="text-xl font-semibold text-sky-600">
<FaInfoCircle />
</div>
<div>
<p>
We will store your OpenAI API key, but we will not use it for any
other purpose. However,{" "}
<strong>we have not implemented any encryption logic yet</strong>
</p>
</div>
</div>
</div>
</>
);
};

View File

@ -24,8 +24,6 @@ describe("ApiKeyConfig", () => {
it("should render ApiConfig Component", () => {
const { getByTestId } = render(<ApiKeyConfig />);
expect(getByTestId("create-new-key")).toBeDefined();
expect(getByTestId("open-ai-api-key-input")).toBeDefined();
expect(getByTestId("save-open-ai-api-key")).toBeDefined();
});
it("renders 'Create New Key' button when apiKey is empty", () => {

View File

@ -1,9 +1,7 @@
/* eslint-disable max-lines */
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { validateOpenAIKey } from "@/app/brains-management/[brainId]/components/BrainManagementTabs/components/SettingsTab/utils/validateOpenAIKey";
import { useAuthApi } from "@/lib/api/auth/useAuthApi";
import { USER_IDENTITY_DATA_KEY } from "@/lib/api/user/config";
import { useUserApi } from "@/lib/api/user/useUserApi";
@ -26,7 +24,6 @@ export const useApiKeyConfig = () => {
const { createApiKey } = useAuthApi();
const { publish } = useToast();
const [userIdentity, setUserIdentity] = useState<UserIdentity>();
const { t } = useTranslation(["config"]);
const queryClient = useQueryClient();
const { data: userData } = useQuery({
queryKey: [USER_IDENTITY_DATA_KEY],
@ -66,25 +63,9 @@ export const useApiKeyConfig = () => {
try {
setChangeOpenAiApiKeyRequestPending(true);
if (
openAiApiKey !== undefined &&
openAiApiKey !== null &&
!(await validateOpenAIKey(
openAiApiKey,
{
badApiKeyError: t("incorrectApiKey", { ns: "config" }),
invalidApiKeyError: t("invalidApiKeyError", { ns: "config" }),
},
publish
))
) {
setChangeOpenAiApiKeyRequestPending(false);
return;
}
await updateUserIdentity({
openai_api_key: openAiApiKey,
});
void queryClient.invalidateQueries({
queryKey: [USER_IDENTITY_DATA_KEY],
@ -105,7 +86,6 @@ export const useApiKeyConfig = () => {
try {
setChangeOpenAiApiKeyRequestPending(true);
await updateUserIdentity({
openai_api_key: null,
});
publish({
@ -123,16 +103,7 @@ export const useApiKeyConfig = () => {
}
};
useEffect(() => {
if (userIdentity?.openai_api_key !== undefined) {
setOpenAiApiKey(userIdentity.openai_api_key);
}
}, [userIdentity]);
const hasOpenAiApiKey =
userIdentity?.openai_api_key !== null &&
userIdentity?.openai_api_key !== undefined &&
userIdentity.openai_api_key !== "";
return {
handleCreateClick,
@ -144,6 +115,5 @@ export const useApiKeyConfig = () => {
changeOpenAiApiKeyRequestPending,
userIdentity,
removeOpenAiApiKey,
hasOpenAiApiKey,
};
};

View File

@ -69,7 +69,6 @@ describe("useBrainApi", () => {
model: "gpt-3.5-turbo",
temperature: 0.0,
max_tokens: 256,
openai_api_key: "123",
};
await createBrain(brain);
@ -220,7 +219,6 @@ describe("useBrainApi", () => {
model: "gpt-3.5-turbo",
temperature: 0.0,
max_tokens: 256,
openai_api_key: "123",
};
await updateBrain(brainId, brain);
expect(axiosPutMock).toHaveBeenCalledTimes(1);

View File

@ -42,7 +42,6 @@ export type CreateBrainInput = {
model?: Model;
temperature?: number;
max_tokens?: number;
openai_api_key?: string;
prompt_id?: string | null;
brain_type?: BrainType;
brain_definition?: ApiBrainDefinition;

View File

@ -1,62 +0,0 @@
import { renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useUserApi } from "../useUserApi";
import { UserIdentityUpdatableProperties } from "../user";
const axiosPutMock = vi.fn(() => ({}));
const axiosGetMock = vi.fn(() => ({}));
vi.mock("@/lib/hooks", () => ({
useAxios: () => ({
axiosInstance: {
put: axiosPutMock,
get: axiosGetMock,
},
}),
}));
describe("useUserApi", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("should call updateUserIdentity with the correct parameters", async () => {
const {
result: {
current: { updateUserIdentity },
},
} = renderHook(() => useUserApi());
const userUpdatableProperties: UserIdentityUpdatableProperties = {
openai_api_key: "sk-xxx",
};
await updateUserIdentity(userUpdatableProperties);
expect(axiosPutMock).toHaveBeenCalledTimes(1);
expect(axiosPutMock).toHaveBeenCalledWith(
`/user/identity`,
userUpdatableProperties
);
});
it("should call getUserIdentity with the correct parameters", async () => {
const {
result: {
current: { getUserIdentity },
},
} = renderHook(() => useUserApi());
await getUserIdentity();
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/user/identity`);
});
it("should call getUser with the correct parameters", async () => {
const {
result: {
current: { getUser },
},
} = renderHook(() => useUserApi());
await getUser();
expect(axiosGetMock).toHaveBeenCalledTimes(1);
expect(axiosGetMock).toHaveBeenCalledWith(`/user`);
});
});

View File

@ -8,7 +8,6 @@ export type UserIdentityUpdatableProperties = {
};
export type UserIdentity = {
openai_api_key?: string | null;
user_id: UUID;
};

View File

@ -115,13 +115,7 @@ export const AddBrainConfig = ({
<ApiRequestDefinition />
</>
)}
<Field
label={t("openAiKeyLabel", { ns: "config" })}
placeholder={t("openAiKeyPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
{...register("openai_api_key")}
/>
<fieldset className="w-full flex flex-col">
<label className="flex-1 text-sm" htmlFor="model">

View File

@ -47,7 +47,6 @@ export const useAddBrainConfig = () => {
formState: { dirtyFields },
} = useFormContext<CreateBrainProps>();
const openAiKey = watch("openai_api_key");
const model = watch("model");
const temperature = watch("temperature");
const maxTokens = watch("max_tokens");
@ -55,7 +54,6 @@ export const useAddBrainConfig = () => {
const brainType = watch("brain_type");
const accessibleModels = getAccessibleModels({
openAiKey,
userData,
});
@ -109,7 +107,6 @@ export const useAddBrainConfig = () => {
description,
max_tokens: maxTokens,
model,
openai_api_key: openAiKey,
temperature,
prompt_id,
status,

View File

@ -2,10 +2,11 @@ import { Fragment } from "react";
import { BrainManagementButton } from "@/lib/components/Sidebar/components/SidebarFooter/components/BrainManagementButton";
import { MarketPlaceButton } from "./components/MarketplaceButton";
import { UpgradeToPlus } from "./components/UpgradeToPlus";
import { UserButton } from "./components/UserButton";
export type SidebarFooterButtons = "myBrains" | "user" | "upgradeToPlus";
export type SidebarFooterButtons = "myBrains" | "user" | "upgradeToPlus" | "marketplace";
type SidebarFooterProps = {
showButtons: SidebarFooterButtons[];
@ -16,6 +17,7 @@ export const SidebarFooter = ({
}: SidebarFooterProps): JSX.Element => {
const buttons = {
myBrains: <BrainManagementButton />,
marketplace: <MarketPlaceButton />,
upgradeToPlus: <UpgradeToPlus />,
user: <UserButton />,
};

View File

@ -0,0 +1,18 @@
import { useTranslation } from "react-i18next";
import { PiDotsNineBold } from "react-icons/pi";
import { SidebarFooterButton } from "./SidebarFooterButton";
export const MarketPlaceButton = (): JSX.Element => {
const { t } = useTranslation("brain");
return (
<SidebarFooterButton
href={`/brains-management/library`}
icon={<PiDotsNineBold className="w-8 h-8" />}
label={t("brain_library_button_label")}
data-testid="brain_library_button_label"
/>
);
};

View File

@ -5,7 +5,6 @@ export const addBrainDefaultValues: CreateBrainInput = {
model: "gpt-3.5-turbo",
temperature: 0,
max_tokens: 500,
openai_api_key: undefined,
prompt_id: undefined,
status: "private",
name: "",

View File

@ -17,7 +17,6 @@ export type Brain = {
model?: Model | null;
max_tokens?: number;
temperature?: number;
openai_api_key?: string | null;
description?: string;
prompt_id?: string | null;
brain_type?: BrainType;

View File

@ -34,7 +34,7 @@
"set_brain_status_to_public_modal_description": "Every Quivr user will be able to:<br/>- Subscribe to your brain in the 'brains library'.<br/>- Use this brain and check the prompt and model configurations.<br/><br/>They won't have access to your uploaded files and people section.",
"confirm_set_brain_status_to_public": "Yes, set as public",
"cancel_set_brain_status_to_public": "No, keep it private",
"brain_library_button_label": "Brains library",
"brain_library_button_label": "Explore",
"brain_management_button_label": "Brains management",
"public_brains_search_bar_placeholder": "Search public brains",
"public_brain_subscribe_button_label": "Subscribe",
@ -48,9 +48,9 @@
"empty_brain_description": "No description",
"myBrains": "My Brains",
"knowledge_source_doc": "Documents",
"knowledge_source_api": "API",
"knowledge_source_api": "App (Through API)",
"knowledge_source_label": "Knowledge source",
"api_brain":{
"api_brain": {
"name": "Name",
"description": "Description",
"type": "Type",
@ -58,4 +58,4 @@
"addRow": "Add row",
"value": "Value"
}
}
}

View File

@ -8,10 +8,10 @@
"openAiKeyLabel": "Open AI Key",
"openAiKeyPlaceholder": "sk-xxx",
"modelLabel": "Model",
"anthropicKeyPlaceholder":"Anthropic API Key",
"anthropicKeyLabel":"Anthropic API Key",
"anthropicKeyPlaceholder": "Anthropic API Key",
"anthropicKeyLabel": "Anthropic API Key",
"temperature": "Temperature",
"maxTokens": "Max tokens",
"maxTokens": "Maximum Words per Response",
"accountSection": "Your Account",
"signedInAs": "Signed In as",
"backendSection": "Backend config",
@ -33,7 +33,7 @@
"removePrompt": "Remove prompt",
"newAPIKey": "Create New Key",
"configSaved": "Config saved",
"configReset": "Config reseted",
"configReset": "Config reseted",
"invalidOpenAiKey": "Invalid OpenAI Key",
"error.createApiKey": "Error creating API Key",
"error.copy": "Failed to copy",
@ -50,4 +50,4 @@
"incorrectApiKey": "Incorrect API Key",
"invalidApiKeyError": "Invalid API Key",
"apiKey": "API Key"
}
}

View File

@ -34,7 +34,7 @@
"set_brain_status_to_public_modal_description": "Chaque utilisateur de Quivr pourra :<br/>- S'abonner à votre cerveau dans la 'bibliothèque des cerveaux'.<br/>- Utiliser ce cerveau et vérifier les configurations de prompts et de modèles.<br/><br/>Ils n'auront pas accès à vos fichiers téléchargés et à la section des personnes.",
"confirm_set_brain_status_to_public": "Oui, définir comme public",
"cancel_set_brain_status_to_public": "Non, le garder privé",
"brain_library_button_label": "Bibliothèque des cerveaux",
"brain_library_button_label": "Explore",
"public_brains_search_bar_placeholder": "Rechercher des cerveaux publics",
"public_brain_subscribe_button_label": "S'abonner",
"public_brain_subscription_success_message": "Vous vous êtes abonné avec succès au cerveau",
@ -50,7 +50,7 @@
"knowledge_source_doc": "Documents",
"knowledge_source_api": "API",
"knowledge_source_label": "Source de connaissance",
"api_brain":{
"api_brain": {
"name": "Nom",
"description": "Description",
"type": "Type",
@ -58,4 +58,4 @@
"addRow": "Ajouter une ligne",
"value": "Valeur"
}
}
}