Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: activate public brain subscription #1241

Merged
merged 7 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/celery_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
from models.notifications import NotificationsStatusEnum
from models.settings import get_supabase_client
from parsers.github import process_github
from repository.brain.update_brain_last_update_time import (
update_brain_last_update_time,
)
from repository.notification.update_notification import update_notification_by_id
from utils.processors import filter_file

Expand Down Expand Up @@ -98,6 +101,8 @@ def process_file_and_notify(
message=str(notification_message),
),
)
update_brain_last_update_time(brain_id)

return True


Expand Down Expand Up @@ -158,4 +163,5 @@ def process_crawl_and_notify(
message=str(notification_message),
),
)
update_brain_last_update_time(brain_id)
return True
2 changes: 2 additions & 0 deletions backend/models/brain_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class BrainEntity(BaseModel):
openai_api_key: Optional[str]
status: Optional[str]
prompt_id: Optional[UUID]
last_update: str

@property
def id(self) -> UUID:
Expand All @@ -40,3 +41,4 @@ class PublicBrain(BaseModel):
name: str
description: Optional[str]
number_of_subscribers: int = 0
last_update: str
8 changes: 7 additions & 1 deletion backend/models/databases/supabase/brains.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def get_user_brains(self, user_id) -> list[MinimalBrainEntity]:
def get_public_brains(self) -> list[PublicBrain]:
response = (
self.db.from_("brains")
.select("id:brain_id, name, description")
.select("id:brain_id, name, description, last_update")
.filter("status", "eq", "public")
.execute()
)
Expand All @@ -88,11 +88,17 @@ def get_public_brains(self) -> list[PublicBrain]:
id=item["id"],
name=item["name"],
description=item["description"],
last_update=item["last_update"],
)
brain.number_of_subscribers = self.get_brain_subscribers_count(brain.id)
public_brains.append(brain)
return public_brains

def update_brain_last_update_time(self, brain_id: UUID) -> None:
self.db.table("brains").update({"last_update": "now()"}).match(
{"brain_id": brain_id}
).execute()

def get_brain_for_user(self, user_id, brain_id) -> MinimalBrainEntity | None:
response = (
self.db.from_("brains_users")
Expand Down
9 changes: 8 additions & 1 deletion backend/repository/brain/update_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
from models import BrainEntity, get_supabase_db
from models.databases.supabase.brains import BrainUpdatableProperties

from repository.brain.update_brain_last_update_time import update_brain_last_update_time


def update_brain_by_id(brain_id: UUID, brain: BrainUpdatableProperties) -> BrainEntity:
"""Update a prompt by id"""
supabase_db = get_supabase_db()

return supabase_db.update_brain_by_id(brain_id, brain) # type: ignore
brain_update_answer = supabase_db.update_brain_by_id(brain_id, brain)
if brain_update_answer is None:
raise Exception("Brain not found")

update_brain_last_update_time(brain_id)
return brain_update_answer
8 changes: 8 additions & 0 deletions backend/repository/brain/update_brain_last_update_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from uuid import UUID

from models.settings import get_supabase_db


def update_brain_last_update_time(brain_id: UUID):
supabase_db = get_supabase_db()
supabase_db.update_brain_last_update_time(brain_id)
7 changes: 7 additions & 0 deletions backend/routes/authorizations/brain_authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from fastapi import Depends, HTTPException, status
from models import UserIdentity
from repository.brain import get_brain_for_user
from repository.brain.get_brain_details import get_brain_details

from routes.authorizations.types import RoleEnum


Expand Down Expand Up @@ -43,6 +45,11 @@ def validate_brain_authorization(
return: None
"""

brain = get_brain_details(brain_id)

if brain and brain.status == "public":
return

if required_roles is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
Expand Down
42 changes: 42 additions & 0 deletions backend/routes/subscription_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,45 @@ def update_brain_subscription(
update_brain_user_rights(brain_id, user_id, subscription.rights)

return {"message": "Brain subscription updated successfully"}


@subscription_router.post(
"/brains/{brain_id}/subscribe",
tags=["Subscription"],
)
async def subscribe_to_brain_handler(
brain_id: UUID, current_user: UserIdentity = Depends(get_current_user)
):
"""
Subscribe to a public brain
"""
if not current_user.email:
raise HTTPException(status_code=400, detail="UserIdentity email is not defined")

brain = get_brain_by_id(brain_id)

if brain is None:
raise HTTPException(status_code=404, detail="Brain not found")
if brain.status != "public":
raise HTTPException(
status_code=403,
detail="You cannot subscribe to this brain without invitation",
)
# check if user is already subscribed to brain
user_brain = get_brain_for_user(current_user.id, brain_id)
if user_brain is not None:
raise HTTPException(
status_code=403,
detail="You are already subscribed to this brain",
)
try:
create_brain_user(
user_id=current_user.id,
brain_id=brain_id,
rights=RoleEnum.Viewer,
is_default_brain=False,
)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}")

return {"message": "You have successfully subscribed to the brain"}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/* eslint-disable max-lines */
import { Content, List, Root } from "@radix-ui/react-tabs";
import { useTranslation } from "react-i18next";

import Button from "@/lib/components/ui/Button";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";

import { BrainTabTrigger, KnowledgeTab, PeopleTab } from "./components";
import ConfirmationDeleteModal from "./components/Modals/ConfirmationDeleteModal";
import { SettingsTab } from "./components/SettingsTab/SettingsTab";
import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs";
import { isUserBrainOwner } from "./utils/isUserBrainOwner";

export const BrainManagementTabs = (): JSX.Element => {
const { t } = useTranslation(["translation", "config", "delete_brain"]);
Expand All @@ -19,13 +22,21 @@ export const BrainManagementTabs = (): JSX.Element => {
setIsDeleteModalOpen,
brain,
} = useBrainManagementTabs();

const isPubliclyAccessible = brain?.status === "public";
const { allBrains } = useBrainContext();

if (brainId === undefined) {
return <div />;
}

const isCurrentUserBrainOwner = isUserBrainOwner({
brainId,
userAccessibleBrains: allBrains,
});

const isPublicBrain = brain?.status === "public";

const hasEditRights = !isPublicBrain || isCurrentUserBrainOwner;

return (
<Root
className="flex flex-col w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden bg-white dark:bg-black border border-black/10 dark:border-white/25 p-4 md:p-10"
Expand All @@ -41,7 +52,7 @@ export const BrainManagementTabs = (): JSX.Element => {
value="settings"
onChange={setSelectedTab}
/>
{!isPubliclyAccessible && (
{hasEditRights && (
<>
<BrainTabTrigger
selected={selectedTab === "people"}
Expand Down Expand Up @@ -73,7 +84,7 @@ export const BrainManagementTabs = (): JSX.Element => {

<div className="flex justify-center mt-4">
<Button
disabled={isPubliclyAccessible}
disabled={!isCurrentUserBrainOwner}
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
onClick={() => setIsDeleteModalOpen(true)}
>
Expand All @@ -89,5 +100,3 @@ export const BrainManagementTabs = (): JSX.Element => {
</Root>
);
};

export default BrainManagementTabs;
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import { Chip } from "@/lib/components/ui/Chip";
import { Divider } from "@/lib/components/ui/Divider";
import Field from "@/lib/components/ui/Field";
import { TextArea } from "@/lib/components/ui/TextArea";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
import { SaveButton } from "@/shared/SaveButton";

import { PublicPrompts } from "./components/PublicPrompts/PublicPrompts";
import { useSettingsTab } from "./hooks/useSettingsTab";
import { isUserBrainOwner } from "../../utils/isUserBrainOwner";

type SettingsTabProps = {
brainId: UUID;
};

// eslint-disable-next-line complexity
export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const {
Expand All @@ -38,8 +41,16 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
accessibleModels,
brain,
} = useSettingsTab({ brainId });
const { allBrains } = useBrainContext();

const isPubliclyAccessible = brain?.status === "public";
const isCurrentUserBrainOwner = isUserBrainOwner({
brainId,
userAccessibleBrains: allBrains,
});

const isPublicBrain = brain?.status === "public";

const hasEditRights = !isPublicBrain || isCurrentUserBrainOwner;

return (
<form
Expand All @@ -58,14 +69,14 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
autoComplete="off"
className="flex-1"
required
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("name")}
/>
</div>

<div className="mt-4">
<div className="flex flex-1 items-center flex-col">
{isPubliclyAccessible && (
{isPublicBrain && (
<Chip className="mb-3 bg-purple-600 text-white w-full">
{t("brain:public_brain_label")}
</Chip>
Expand All @@ -80,7 +91,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
isLoading={isSettingAsDefault}
onClick={() => void setAsDefaultBrainHandler()}
type="button"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
>
{t("setDefaultBrain", { ns: "brain" })}
</Button>
Expand All @@ -93,7 +104,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })}
autoComplete="off"
className="flex-1 m-3"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("description")}
/>
<Divider text={t("modelSection", { ns: "config" })} />
Expand All @@ -102,7 +113,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
placeholder={t("openAiKeyPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("openAiKey")}
/>
<fieldset className="w-full flex flex-col mt-2">
Expand All @@ -111,7 +122,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
</label>
<select
id="model"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("model")}
className="px-5 py-2 dark:bg-gray-700 bg-gray-200 rounded-md"
onChange={() => {
Expand All @@ -136,7 +147,7 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
max="1"
step="0.01"
value={temperature}
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("temperature")}
/>
</fieldset>
Expand All @@ -149,43 +160,37 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
min="10"
max={defineMaxTokens(model)}
value={maxTokens}
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("maxTokens")}
/>
</fieldset>
<div className="flex w-full justify-end py-4">
<SaveButton
disabled={isPubliclyAccessible}
handleSubmit={handleSubmit}
/>
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
</div>
<Divider text={t("customPromptSection", { ns: "config" })} />
{!isPubliclyAccessible && <PublicPrompts onSelect={pickPublicPrompt} />}
{hasEditRights && <PublicPrompts onSelect={pickPublicPrompt} />}
<Field
label={t("promptName", { ns: "config" })}
placeholder={t("promptNamePlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("prompt.title")}
/>
<TextArea
label={t("promptContent", { ns: "config" })}
placeholder={t("promptContentPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={isPubliclyAccessible}
disabled={!hasEditRights}
{...register("prompt.content")}
/>
<div className="flex w-full justify-end py-4">
<SaveButton
disabled={isPubliclyAccessible}
handleSubmit={handleSubmit}
/>
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
</div>
{promptId !== "" && (
<Button
disabled={isUpdating || isPubliclyAccessible}
disabled={isUpdating || !hasEditRights}
onClick={() => void removeBrainPrompt()}
>
{t("removePrompt", { ns: "config" })}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { UUID } from "crypto";

import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";

type IsUserBrainOwnerProps = {
userAccessibleBrains: MinimalBrainForUser[];
brainId?: UUID;
};
export const isUserBrainOwner = ({
brainId,
userAccessibleBrains,
}: IsUserBrainOwnerProps): boolean => {
const brain = userAccessibleBrains.find(({ id }) => id === brainId);
if (brain === undefined) {
return false;
}

return brain.role === "Owner";
};
Loading
Loading