mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-26 17:51:54 +01:00
Allow users to share assistants (#2434)
* enable assistant sharing * functional * remove logs * revert ports * remove accidental update * minor updates to copy * update formatting * update for merge queue
This commit is contained in:
parent
7ba829a585
commit
7f7559e3d2
@ -0,0 +1,38 @@
|
||||
"""server default chosen assistants
|
||||
|
||||
Revision ID: 35e6853a51d5
|
||||
Revises: c99d76fcd298
|
||||
Create Date: 2024-09-13 13:20:32.885317
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "35e6853a51d5"
|
||||
down_revision = "c99d76fcd298"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
DEFAULT_ASSISTANTS = [-2, -1, 0]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.alter_column(
|
||||
"user",
|
||||
"chosen_assistants",
|
||||
type_=postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=False,
|
||||
server_default=sa.text(f"'{DEFAULT_ASSISTANTS}'::jsonb"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.alter_column(
|
||||
"user",
|
||||
"chosen_assistants",
|
||||
type_=postgresql.JSONB(astext_type=sa.Text()),
|
||||
nullable=True,
|
||||
server_default=None,
|
||||
)
|
@ -122,7 +122,7 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
# if specified, controls the assistants that are shown to the user + their order
|
||||
# if not specified, all assistants are shown
|
||||
chosen_assistants: Mapped[list[int]] = mapped_column(
|
||||
postgresql.JSONB(), nullable=True
|
||||
postgresql.JSONB(), nullable=False, default=[-2, -1, 0]
|
||||
)
|
||||
|
||||
oidc_expiry: Mapped[datetime.datetime] = mapped_column(
|
||||
|
@ -210,6 +210,22 @@ def update_persona_shared_users(
|
||||
)
|
||||
|
||||
|
||||
def update_persona_public_status(
|
||||
persona_id: int,
|
||||
is_public: bool,
|
||||
db_session: Session,
|
||||
user: User | None,
|
||||
) -> None:
|
||||
persona = fetch_persona_by_id(
|
||||
db_session=db_session, persona_id=persona_id, user=user, get_editable=True
|
||||
)
|
||||
if user and user.role != UserRole.ADMIN and persona.user_id != user.id:
|
||||
raise ValueError("You don't have permission to modify this persona")
|
||||
|
||||
persona.is_public = is_public
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def get_prompts(
|
||||
user_id: UUID | None,
|
||||
db_session: Session,
|
||||
@ -551,6 +567,7 @@ def update_persona_visibility(
|
||||
persona = fetch_persona_by_id(
|
||||
db_session=db_session, persona_id=persona_id, user=user, get_editable=True
|
||||
)
|
||||
|
||||
persona.is_visible = is_visible
|
||||
db_session.commit()
|
||||
|
||||
|
@ -3,6 +3,7 @@ from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Query
|
||||
from fastapi import UploadFile
|
||||
from pydantic import BaseModel
|
||||
@ -20,6 +21,7 @@ from danswer.db.persona import get_personas
|
||||
from danswer.db.persona import mark_persona_as_deleted
|
||||
from danswer.db.persona import mark_persona_as_not_deleted
|
||||
from danswer.db.persona import update_all_personas_display_priority
|
||||
from danswer.db.persona import update_persona_public_status
|
||||
from danswer.db.persona import update_persona_shared_users
|
||||
from danswer.db.persona import update_persona_visibility
|
||||
from danswer.file_store.file_store import get_default_file_store
|
||||
@ -43,6 +45,10 @@ class IsVisibleRequest(BaseModel):
|
||||
is_visible: bool
|
||||
|
||||
|
||||
class IsPublicRequest(BaseModel):
|
||||
is_public: bool
|
||||
|
||||
|
||||
@admin_router.patch("/{persona_id}/visible")
|
||||
def patch_persona_visibility(
|
||||
persona_id: int,
|
||||
@ -58,6 +64,25 @@ def patch_persona_visibility(
|
||||
)
|
||||
|
||||
|
||||
@basic_router.patch("/{persona_id}/public")
|
||||
def patch_user_presona_public_status(
|
||||
persona_id: int,
|
||||
is_public_request: IsPublicRequest,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
try:
|
||||
update_persona_public_status(
|
||||
persona_id=persona_id,
|
||||
is_public=is_public_request.is_public,
|
||||
db_session=db_session,
|
||||
user=user,
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.exception("Failed to update persona public status")
|
||||
raise HTTPException(status_code=403, detail=str(e))
|
||||
|
||||
|
||||
@admin_router.put("/display-priority")
|
||||
def patch_persona_display_priority(
|
||||
display_priority_request: DisplayPriorityRequest,
|
||||
|
@ -12,6 +12,7 @@ command=python danswer/background/update.py
|
||||
redirect_stderr=true
|
||||
autorestart=true
|
||||
|
||||
|
||||
# Background jobs that must be run async due to long time to completion
|
||||
# NOTE: due to an issue with Celery + SQLAlchemy
|
||||
# (https://github.com/celery/celery/issues/7007#issuecomment-1740139367)
|
||||
@ -37,11 +38,9 @@ autorestart=true
|
||||
# Job scheduler for periodic tasks
|
||||
[program:celery_beat]
|
||||
command=celery -A danswer.background.celery.celery_run:celery_app beat
|
||||
--loglevel=INFO
|
||||
--logfile=/var/log/celery_beat_supervisor.log
|
||||
environment=LOG_FILE_NAME=celery_beat
|
||||
redirect_stderr=true
|
||||
autorestart=true
|
||||
|
||||
# Listens for Slack messages and responds with answers
|
||||
# for all channels that the DanswerBot has been added to.
|
||||
@ -68,4 +67,4 @@ command=tail -qF
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
autorestart=true
|
||||
autorestart=true
|
@ -27,4 +27,4 @@ spec:
|
||||
key: redis_password
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: env-configmap
|
||||
name: env-configmap
|
@ -8,7 +8,11 @@ import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { UniqueIdentifier } from "@dnd-kit/core";
|
||||
import { DraggableTable } from "@/components/table/DraggableTable";
|
||||
import { deletePersona, personaComparator } from "./lib";
|
||||
import {
|
||||
deletePersona,
|
||||
personaComparator,
|
||||
togglePersonaVisibility,
|
||||
} from "./lib";
|
||||
import { FiEdit2 } from "react-icons/fi";
|
||||
import { TrashIcon } from "@/components/icons/icons";
|
||||
import { getCurrentUser } from "@/lib/user";
|
||||
@ -31,22 +35,6 @@ function PersonaTypeDisplay({ persona }: { persona: Persona }) {
|
||||
return <Text>Personal {persona.owner && <>({persona.owner.email})</>}</Text>;
|
||||
}
|
||||
|
||||
const togglePersonaVisibility = async (
|
||||
personaId: number,
|
||||
isVisible: boolean
|
||||
) => {
|
||||
const response = await fetch(`/api/admin/persona/${personaId}/visible`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
is_visible: !isVisible,
|
||||
}),
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
export function PersonasTable({
|
||||
allPersonas,
|
||||
editablePersonas,
|
||||
|
@ -320,6 +320,38 @@ export function personaComparator(a: Persona, b: Persona) {
|
||||
return closerToZeroNegativesFirstComparator(a.id, b.id);
|
||||
}
|
||||
|
||||
export const togglePersonaVisibility = async (
|
||||
personaId: number,
|
||||
isVisible: boolean
|
||||
) => {
|
||||
const response = await fetch(`/api/persona/${personaId}/visible`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
is_visible: !isVisible,
|
||||
}),
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
export const togglePersonaPublicStatus = async (
|
||||
personaId: number,
|
||||
isPublic: boolean
|
||||
) => {
|
||||
const response = await fetch(`/api/persona/${personaId}/public`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
is_public: isPublic,
|
||||
}),
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
export function checkPersonaRequiresImageGeneration(persona: Persona) {
|
||||
for (const tool of persona.tools) {
|
||||
if (tool.name === "ImageGenerationTool") {
|
||||
|
@ -1,14 +1,7 @@
|
||||
import { User } from "@/lib/types";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
|
||||
import {
|
||||
FiImage,
|
||||
FiLock,
|
||||
FiMoreHorizontal,
|
||||
FiSearch,
|
||||
FiUnlock,
|
||||
} from "react-icons/fi";
|
||||
import { CustomTooltip } from "@/components/tooltip/CustomTooltip";
|
||||
import { FiLock, FiUnlock } from "react-icons/fi";
|
||||
|
||||
export function AssistantSharedStatusDisplay({
|
||||
assistant,
|
||||
@ -56,20 +49,6 @@ export function AssistantSharedStatusDisplay({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="relative mt-4 text-xs flex text-subtle">
|
||||
<span className="font-medium">Powers:</span>{" "}
|
||||
{assistant.tools.length == 0 ? (
|
||||
<p className="ml-2">None</p>
|
||||
) : (
|
||||
assistant.tools.map((tool, ind) => {
|
||||
if (tool.name === "SearchTool") {
|
||||
return <FiSearch key={ind} className="ml-1 h-3 w-3 my-auto" />;
|
||||
} else if (tool.name === "ImageGenerationTool") {
|
||||
return <FiImage key={ind} className="ml-1 h-3 w-3 my-auto" />;
|
||||
}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -6,11 +6,15 @@ import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { Divider, Text } from "@tremor/react";
|
||||
import {
|
||||
FiEdit2,
|
||||
FiFigma,
|
||||
FiMenu,
|
||||
FiMinus,
|
||||
FiMoreHorizontal,
|
||||
FiPlus,
|
||||
FiSearch,
|
||||
FiShare,
|
||||
FiShare2,
|
||||
FiToggleLeft,
|
||||
FiTrash,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
@ -50,11 +54,14 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { DragHandle } from "@/components/table/DragHandle";
|
||||
import { deletePersona } from "@/app/admin/assistants/lib";
|
||||
import {
|
||||
deletePersona,
|
||||
togglePersonaPublicStatus,
|
||||
} from "@/app/admin/assistants/lib";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { MakePublicAssistantModal } from "@/app/chat/modal/MakePublicAssistantModal";
|
||||
|
||||
function DraggableAssistantListItem(props: any) {
|
||||
const {
|
||||
@ -81,7 +88,7 @@ function DraggableAssistantListItem(props: any) {
|
||||
<DragHandle />
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<AssistantListItem del {...props} />
|
||||
<AssistantListItem {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -95,6 +102,7 @@ function AssistantListItem({
|
||||
isVisible,
|
||||
setPopup,
|
||||
deleteAssistant,
|
||||
shareAssistant,
|
||||
}: {
|
||||
assistant: Persona;
|
||||
user: User | null;
|
||||
@ -102,7 +110,7 @@ function AssistantListItem({
|
||||
allAssistantIds: string[];
|
||||
isVisible: boolean;
|
||||
deleteAssistant: Dispatch<SetStateAction<Persona | null>>;
|
||||
|
||||
shareAssistant: Dispatch<SetStateAction<Persona | null>>;
|
||||
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
@ -258,6 +266,18 @@ function AssistantListItem({
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
isOwnedByUser ? (
|
||||
<div
|
||||
key="delete"
|
||||
className="flex items-center gap-x-2"
|
||||
onClick={() => shareAssistant(assistant)}
|
||||
>
|
||||
{assistant.is_public ? <FiMinus /> : <FiPlus />} Make{" "}
|
||||
{assistant.is_public ? "Private" : "Public"}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
]}
|
||||
</DefaultPopover>
|
||||
</div>
|
||||
@ -290,6 +310,9 @@ export function AssistantsList({
|
||||
assistant.id.toString()
|
||||
);
|
||||
const [deletingPersona, setDeletingPersona] = useState<Persona | null>(null);
|
||||
const [makePublicPersona, setMakePublicPersona] = useState<Persona | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
const router = useRouter();
|
||||
@ -307,7 +330,7 @@ export function AssistantsList({
|
||||
|
||||
async function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
|
||||
filteredAssistants;
|
||||
if (over && active.id !== over.id) {
|
||||
setFilteredAssistants((assistants) => {
|
||||
const oldIndex = assistants.findIndex(
|
||||
@ -351,6 +374,20 @@ export function AssistantsList({
|
||||
/>
|
||||
)}
|
||||
|
||||
{makePublicPersona && (
|
||||
<MakePublicAssistantModal
|
||||
isPublic={makePublicPersona.is_public}
|
||||
onClose={() => setMakePublicPersona(null)}
|
||||
onShare={async (newPublicStatus: boolean) => {
|
||||
await togglePersonaPublicStatus(
|
||||
makePublicPersona.id,
|
||||
newPublicStatus
|
||||
);
|
||||
router.refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mx-auto mobile:w-[90%] desktop:w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>My Assistants</AssistantsPageTitle>
|
||||
|
||||
@ -403,6 +440,7 @@ export function AssistantsList({
|
||||
{filteredAssistants.map((assistant, index) => (
|
||||
<DraggableAssistantListItem
|
||||
deleteAssistant={setDeletingPersona}
|
||||
shareAssistant={setMakePublicPersona}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
@ -431,6 +469,7 @@ export function AssistantsList({
|
||||
{ownedButHiddenAssistants.map((assistant, index) => (
|
||||
<AssistantListItem
|
||||
deleteAssistant={setDeletingPersona}
|
||||
shareAssistant={setMakePublicPersona}
|
||||
key={assistant.id}
|
||||
assistant={assistant}
|
||||
user={user}
|
||||
|
72
web/src/app/chat/modal/MakePublicAssistantModal.tsx
Normal file
72
web/src/app/chat/modal/MakePublicAssistantModal.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { ModalWrapper } from "@/components/modals/ModalWrapper";
|
||||
import { Button, Divider, Text } from "@tremor/react";
|
||||
|
||||
export function MakePublicAssistantModal({
|
||||
isPublic,
|
||||
onShare,
|
||||
onClose,
|
||||
}: {
|
||||
isPublic: boolean;
|
||||
onShare: (shared: boolean) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ModalWrapper onClose={onClose} modalClassName="max-w-3xl">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold text-emphasis">
|
||||
{isPublic ? "Public Assistant" : "Make Assistant Public"}
|
||||
</h2>
|
||||
|
||||
<Text>
|
||||
This assistant is currently{" "}
|
||||
<span className="font-semibold">
|
||||
{isPublic ? "public" : "private"}
|
||||
</span>
|
||||
.
|
||||
{isPublic
|
||||
? " Anyone can currently access this assistant."
|
||||
: " Only you can access this assistant."}
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
{isPublic ? (
|
||||
<div className="space-y-4">
|
||||
<Text>
|
||||
To restrict access to this assistant, you can make it private
|
||||
again.
|
||||
</Text>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await onShare?.(false);
|
||||
onClose();
|
||||
}}
|
||||
size="sm"
|
||||
color="red"
|
||||
>
|
||||
Make Assistant Private
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Text>
|
||||
Making this assistant public will allow anyone with the link to
|
||||
view and use it. Ensure that all content and capabilities of the
|
||||
assistant are safe to share.
|
||||
</Text>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await onShare?.(true);
|
||||
onClose();
|
||||
}}
|
||||
size="sm"
|
||||
color="green"
|
||||
>
|
||||
Make Assistant Public
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user