From 3d71d28e0daca00179c4f8b10f91d1c8415a7218 Mon Sep 17 00:00:00 2001 From: Philippe Rolet Date: Fri, 15 Dec 2023 14:31:36 +0100 Subject: [PATCH] [Ahuna] Users can create private assistants & change scope in builder (#2870) * [Ahuna] Users can create private assistants & change scope in builder * fix flow * Working personal assistants * fix creation published * fix home --------- Co-authored-by: Stanislas Polu --- .../components/assistant/AssistantActions.tsx | 11 +- .../components/assistant/AssistantPreview.tsx | 48 +-- .../assistant_builder/AssistantBuilder.tsx | 51 +++- .../assistant_builder/TeamSharingSection.tsx | 275 ++++++++++++++++++ front/lib/api/assistant/configuration.ts | 19 +- front/pages/w/[wId]/assistant/assistants.tsx | 227 ++++++++++----- front/pages/w/[wId]/assistant/gallery.tsx | 10 +- .../[wId]/builder/assistants/[aId]/index.tsx | 22 +- .../w/[wId]/builder/assistants/index.tsx | 35 ++- .../pages/w/[wId]/builder/assistants/new.tsx | 17 +- 10 files changed, 590 insertions(+), 125 deletions(-) create mode 100644 front/components/assistant_builder/TeamSharingSection.tsx diff --git a/front/components/assistant/AssistantActions.tsx b/front/components/assistant/AssistantActions.tsx index b77d03ab478b..b1b4769d7dfc 100644 --- a/front/components/assistant/AssistantActions.tsx +++ b/front/components/assistant/AssistantActions.tsx @@ -15,12 +15,14 @@ export function DeleteAssistantDialog({ show, onClose, onDelete, + isPrivateAssistant, }: { owner: WorkspaceType; agentConfigurationId: string; show: boolean; onClose: () => void; onDelete: () => void; + isPrivateAssistant?: boolean; }) { const sendNotification = useContext(SendNotificationsContext); @@ -29,7 +31,7 @@ export function DeleteAssistantDialog({ isOpen={show} title={`Deleting assistant`} onCancel={onClose} - validateLabel="Delete for Everyone" + validateLabel={isPrivateAssistant ? "Delete" : "Delete for Everyone"} validateVariant="primaryWarning" onValidate={async () => { try { @@ -68,8 +70,9 @@ export function DeleteAssistantDialog({
Are you sure you want to delete?
- This will be permanent and delete the assistant - for everyone. + {isPrivateAssistant + ? "This will delete your personal assistant permanently." + : "This will be permanent and delete the assistant for everyone."}
@@ -94,7 +97,7 @@ export function RemoveAssistantFromListDialog({ return ( - ); - break; - case "workspace": - galleryChip = ["workspace", "global"].includes( - agentConfiguration.scope - ) && ( - - ); - break; - default: - assertNever(flow); + if (variant === "gallery") { + switch (flow) { + case "personal": + galleryChip = agentConfiguration.userListStatus === "in-list" && ( + + ); + break; + case "workspace": + galleryChip = ["workspace", "global"].includes( + agentConfiguration.scope + ) && ( + + ); + break; + default: + assertNever(flow); + } } // Define button groups with JSX elements, including default buttons diff --git a/front/components/assistant_builder/AssistantBuilder.tsx b/front/components/assistant_builder/AssistantBuilder.tsx index 169cb23e4ff5..f236ca3431c2 100644 --- a/front/components/assistant_builder/AssistantBuilder.tsx +++ b/front/components/assistant_builder/AssistantBuilder.tsx @@ -15,7 +15,11 @@ import { SlackLogo, TrashIcon, } from "@dust-tt/sparkle"; -import { ConnectorProvider, DataSourceType } from "@dust-tt/types"; +import { + AgentConfigurationScope, + ConnectorProvider, + DataSourceType, +} from "@dust-tt/types"; import { UserType, WorkspaceType } from "@dust-tt/types"; import { CLAUDE_DEFAULT_MODEL_CONFIG, @@ -51,6 +55,7 @@ import { SPIRIT_AVATARS_BASE_PATH, TIME_FRAME_UNIT_TO_LABEL, } from "@app/components/assistant_builder/shared"; +import { TeamSharingSection } from "@app/components/assistant_builder/TeamSharingSection"; import DataSourceResourceSelectorTree from "@app/components/DataSourceResourceSelectorTree"; import AppLayout from "@app/components/sparkle/AppLayout"; import { @@ -148,6 +153,7 @@ type AssistantBuilderState = { databaseQueryConfiguration: AssistantBuilderDatabaseQueryConfiguration | null; handle: string | null; description: string | null; + scope: Exclude; instructions: string | null; avatarUrl: string | null; generationSettings: { @@ -170,6 +176,7 @@ export type AssistantBuilderInitialState = { databaseQueryConfiguration: AssistantBuilderState["databaseQueryConfiguration"]; handle: string; description: string; + scope: Exclude; instructions: string; avatarUrl: string; generationSettings: { @@ -178,6 +185,8 @@ export type AssistantBuilderInitialState = { } | null; }; +export const BUILDER_FLOWS = ["workspace_assistants", "my_assistants"] as const; +export type BuilderFlow = (typeof BUILDER_FLOWS)[number]; type AssistantBuilderProps = { user: UserType; owner: WorkspaceType; @@ -188,6 +197,7 @@ type AssistantBuilderProps = { dustApps: AppType[]; initialBuilderState: AssistantBuilderInitialState | null; agentConfigurationId: string | null; + flow: BuilderFlow; }; const DEFAULT_ASSISTANT_STATE: AssistantBuilderState = { @@ -200,6 +210,7 @@ const DEFAULT_ASSISTANT_STATE: AssistantBuilderState = { dustAppConfiguration: null, databaseQueryConfiguration: null, handle: null, + scope: "private", description: null, instructions: null, avatarUrl: null, @@ -235,15 +246,19 @@ export default function AssistantBuilder({ dustApps, initialBuilderState, agentConfigurationId, + flow, }: AssistantBuilderProps) { const router = useRouter(); const sendNotification = React.useContext(SendNotificationsContext); const slackDataSource = dataSources.find( (ds) => ds.connectorProvider === "slack" ); + const defaultScope = + flow === "workspace_assistants" ? "workspace" : "private"; const [builderState, setBuilderState] = useState({ ...DEFAULT_ASSISTANT_STATE, + scope: initialBuilderState?.scope ?? defaultScope, generationSettings: { ...DEFAULT_ASSISTANT_STATE.generationSettings, modelSettings: @@ -358,6 +373,7 @@ export default function AssistantBuilder({ initialBuilderState.databaseQueryConfiguration, handle: initialBuilderState.handle, description: initialBuilderState.description, + scope: initialBuilderState.scope ?? "private", instructions: initialBuilderState.instructions, avatarUrl: initialBuilderState.avatarUrl, generationSettings: initialBuilderState.generationSettings ?? { @@ -615,7 +631,7 @@ export default function AssistantBuilder({ pictureUrl: builderState.avatarUrl, description: builderState.description.trim(), status: "active", - scope: "workspace", + scope: builderState.scope, action: actionParam, generation: { prompt: builderState.instructions.trim(), @@ -785,14 +801,18 @@ export default function AssistantBuilder({ { - await router.push(`/w/${owner.sId}/builder/assistants`); + if (flow === "workspace_assistants") + await router.push(`/w/${owner.sId}/builder/assistants`); + else await router.push(`/w/${owner.sId}/assistant/assistants`); }} /> ) : ( { - await router.push(`/w/${owner.sId}/builder/assistants`); + if (flow === "workspace_assistants") + await router.push(`/w/${owner.sId}/builder/assistants`); + else await router.push(`/w/${owner.sId}/assistant/assistants`); }} onSave={ submitEnabled @@ -800,9 +820,14 @@ export default function AssistantBuilder({ setIsSavingOrDeleting(true); submitForm() .then(async () => { - await router.push( - `/w/${owner.sId}/builder/assistants` - ); + if (flow === "workspace_assistants") + await router.push( + `/w/${owner.sId}/builder/assistants` + ); + else + await router.push( + `/w/${owner.sId}/assistant/assistants` + ); setIsSavingOrDeleting(false); }) .catch((e) => { @@ -895,7 +920,17 @@ export default function AssistantBuilder({ - + + ) => { + setEdited(true); + setBuilderState((state) => ({ ...state, scope })); + }} + />
diff --git a/front/components/assistant_builder/TeamSharingSection.tsx b/front/components/assistant_builder/TeamSharingSection.tsx new file mode 100644 index 000000000000..84d2e7a8273b --- /dev/null +++ b/front/components/assistant_builder/TeamSharingSection.tsx @@ -0,0 +1,275 @@ +import { + Button, + CheckCircleIcon, + Dialog, + Icon, + ImageIcon, + Tooltip, +} from "@dust-tt/sparkle"; +import { AgentConfigurationScope, WorkspaceType } from "@dust-tt/types"; +import { useState } from "react"; + +export function TeamSharingSection({ + owner, + initialScope, + newScope, + setNewScope, +}: { + owner: WorkspaceType; + initialScope: AgentConfigurationScope; + newScope: Exclude; + setNewScope: (scope: Exclude) => void; +}) { + const [showPublishDialog, setShowPublishDialog] = useState(false); + const [showWorkspaceRemoveDialog, setShowWorkspaceRemoveDialog] = + useState(false); + const [showCreateDuplicateDialog, setShowCreateDuplicateDialog] = + useState(false); + + if (initialScope !== "private" && newScope === "private") { + throw new Error("Cannot change scope back to private"); + } + if (initialScope === "global") { + return null; + } + + if (initialScope === "private" && newScope === "private") + return ( +
+
Team sharing
+
+ setShowPublishDialog(false)} + setPublished={() => setNewScope("published")} + /> +
+
+
+ Make the assistant available to your team in the{" "} + Assistant Gallery.
Your + team will be allowed to{" "} + use and modify the assistant. +
+
+
+ ); + + if (initialScope === "private" && newScope === "published") { + return ( +
+
Team sharing
+
+
+ +
+
+ Make the assistant available to your team in the{" "} + Assistant Gallery.
Your + team will be allowed to{" "} + use and modify the assistant. +
+
+
+
+
+ ); + } + + if (initialScope === "published" && newScope === "published") { + return ( +
+ setShowCreateDuplicateDialog(false)} + createDuplicate={function (): void { + throw new Error("Function not implemented."); + }} + /> +
Team sharing
+
+
+ This assistant can be discovered by everyone in the{" "} + Assistant Gallery. Any edits will + apply to everyone. You can create a duplicate to tweak your own, + private version. +
+
+ +
+
+
+ ); + } + + if (initialScope === "workspace" && newScope === "workspace") { + return ( +
+ setShowWorkspaceRemoveDialog(false)} + setPublished={() => setNewScope("published")} + /> +
Team sharing
+
+
+ The assistant is in the workspace list. Only admins and builders can{" "} + modify and delete the assistant. + It is included by default in every member's list. +
+ {(owner.role === "admin" || owner.role === "builder") && ( +
+
+ ); + } + + if (initialScope === "workspace" && newScope === "published") { + return ( +
+
Team sharing
+
+
+ +
+
+ The assistant will be removed from the workspace list. It will be + discoverable by members, but it won't be in their list by default. + Any workspace member can modify the assistant. +
+
+
+
+
+ ); + } +} + +function PublishDialog({ + show, + onClose, + setPublished, +}: { + show: boolean; + onClose: () => void; + setPublished: () => void; +}) { + return ( + { + setPublished(); + onClose(); + }} + > +
+ Once published, the assistant will be visible and editable by members of + your workspace. +
+
+ ); +} + +function WorkspaceRemoveDialog({ + show, + onClose, + setPublished, +}: { + show: boolean; + onClose: () => void; + setPublished: () => void; +}) { + return ( + { + setPublished(); + onClose(); + }} + > +
+ Removing from the workspace leaves the assistant discoverable by + members, but it won't be in their list by default. Any workspace member + can modify the assistant. +
+
+ ); +} + +function CreateDuplicateDialog({ + show, + onClose, + createDuplicate, +}: { + show: boolean; + onClose: () => void; + createDuplicate: () => void; +}) { + return ( + { + createDuplicate(); + onClose(); + }} + > +
+ An exact copy of the assistant will be created for you only. You will + pick a new name and Avatar. +
+
+ ); +} diff --git a/front/lib/api/assistant/configuration.ts b/front/lib/api/assistant/configuration.ts index 3c8202471fd7..237e6736c11e 100644 --- a/front/lib/api/assistant/configuration.ts +++ b/front/lib/api/assistant/configuration.ts @@ -677,11 +677,28 @@ export async function createAgentConfiguration( } ); } + const sId = agentConfigurationId || generateModelSId(); + + // If creating a new private or published agent, it should be in the user's list, so it + // appears in their 'assistants' page (at creation for assistants created published, or at + // publication once a private assistant gets published). + if (["private", "published"].includes(scope) && !agentConfigurationId) { + listStatusOverride = "in-list"; + await AgentUserRelation.create( + { + workspaceId: owner.id, + agentConfiguration: sId, + userId: user.id, + listStatusOverride: "in-list", + }, + { transaction: t } + ); + } // Create Agent config. return AgentConfiguration.create( { - sId: agentConfigurationId || generateModelSId(), + sId, version, status, scope, diff --git a/front/pages/w/[wId]/assistant/assistants.tsx b/front/pages/w/[wId]/assistant/assistants.tsx index 790e529af7bd..779f75baba17 100644 --- a/front/pages/w/[wId]/assistant/assistants.tsx +++ b/front/pages/w/[wId]/assistant/assistants.tsx @@ -4,9 +4,11 @@ import { Button, ContextItem, Page, + PencilSquareIcon, PlusIcon, RobotIcon, Searchbar, + Tab, Tooltip, XMarkIcon, } from "@dust-tt/sparkle"; @@ -20,7 +22,10 @@ import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import Link from "next/link"; import { useState } from "react"; -import { RemoveAssistantFromListDialog } from "@app/components/assistant/AssistantActions"; +import { + DeleteAssistantDialog, + RemoveAssistantFromListDialog, +} from "@app/components/assistant/AssistantActions"; import { AssistantSidebarMenu } from "@app/components/assistant/conversation/SidebarMenu"; import AppLayout from "@app/components/sparkle/AppLayout"; import { @@ -29,14 +34,18 @@ import { } from "@app/components/sparkle/navigation"; import { Authenticator, getSession, getUserFromSession } from "@app/lib/auth"; import { useAgentConfigurations } from "@app/lib/swr"; -import { subFilter } from "@app/lib/utils"; +import { classNames, subFilter } from "@app/lib/utils"; const { GA_TRACKING_ID = "" } = process.env; +const PERSONAL_ASSISTANTS_VIEWS = ["personal", "workspace"] as const; +export type PersonalAssitsantsView = (typeof PERSONAL_ASSISTANTS_VIEWS)[number]; + export const getServerSideProps: GetServerSideProps<{ user: UserType; owner: WorkspaceType; subscription: SubscriptionType; + view: PersonalAssitsantsView; gaTrackingId: string; }> = async (context) => { const session = await getSession(context.req, context.res); @@ -55,11 +64,18 @@ export const getServerSideProps: GetServerSideProps<{ }; } + const view = PERSONAL_ASSISTANTS_VIEWS.includes( + context.query.view as PersonalAssitsantsView + ) + ? (context.query.view as PersonalAssitsantsView) + : "personal"; + return { props: { user, owner, subscription, + view, gaTrackingId: GA_TRACKING_ID, }, }; @@ -69,6 +85,7 @@ export default function PersonalAssistants({ user, owner, subscription, + view, gaTrackingId, }: InferGetServerSidePropsType) { const { agentConfigurations, mutateAgentConfigurations } = @@ -79,14 +96,36 @@ export default function PersonalAssistants({ const [assistantSearch, setAssistantSearch] = useState(""); - const filtered = agentConfigurations.filter((a) => { + const viewAssistants = agentConfigurations.filter((a) => { + if (view === "personal") { + return a.scope === "private" || a.scope === "published"; + } + if (view === "workspace") { + return a.scope === "workspace" || a.scope === "global"; + } + }); + + const filtered = viewAssistants.filter((a) => { return subFilter(assistantSearch.toLowerCase(), a.name.toLowerCase()); }); const [showRemovalModal, setShowRemovalModal] = useState(null); + const [showDeletionModal, setShowDeletionModal] = + useState(null); - // const isBuilder = owner.role === "builder" || owner.role === "admin"; + const tabs = [ + { + label: "Personal", + href: `/w/${owner.sId}/assistant/assistants?view=personal`, + current: view === "personal", + }, + { + label: "From Workspace", + href: `/w/${owner.sId}/assistant/assistants?view=workspace`, + current: view === "workspace", + }, + ]; return ( )} + {showDeletionModal && ( + setShowDeletionModal(null)} + onDelete={() => { + void mutateAgentConfigurations(); + }} + isPrivateAssistant={true} + /> + )} + -
-
-
-
- { - setAssistantSearch(s); - }} - /> + + + +
+
+
+ { + setAssistantSearch(s); + }} + /> +
+ + +
- - -
- - {filtered.map((agent) => ( - } size={"sm"} /> - } - action={ - agent.scope !== "global" && ( -
+ ) + } + > + +
+ {agent.description} +
+
+ + ))} + + ) : ( +
- -
{agent.description}
-
- - ))} - -
+ +
+ )} + + ); diff --git a/front/pages/w/[wId]/assistant/gallery.tsx b/front/pages/w/[wId]/assistant/gallery.tsx index 405ef565010e..28059441590a 100644 --- a/front/pages/w/[wId]/assistant/gallery.tsx +++ b/front/pages/w/[wId]/assistant/gallery.tsx @@ -110,16 +110,16 @@ export default function AssistantsGallery({ href: `/w/${owner.sId}/assistant/gallery?view=all&flow=` + flow, current: agentsGetView === "all", }, + { + label: "Published", + href: `/w/${owner.sId}/assistant/gallery?view=published&flow=` + flow, + current: agentsGetView === "published", + }, { label: "From Workspace", href: `/w/${owner.sId}/assistant/gallery?view=workspace&flow=` + flow, current: agentsGetView === "workspace", }, - { - label: "From Teammates", - href: `/w/${owner.sId}/assistant/gallery?view=published&flow=` + flow, - current: agentsGetView === "published", - }, { label: "From Dust", href: `/w/${owner.sId}/assistant/gallery?view=global&flow=` + flow, diff --git a/front/pages/w/[wId]/builder/assistants/[aId]/index.tsx b/front/pages/w/[wId]/builder/assistants/[aId]/index.tsx index 0369193c12cc..267ad708c523 100644 --- a/front/pages/w/[wId]/builder/assistants/[aId]/index.tsx +++ b/front/pages/w/[wId]/builder/assistants/[aId]/index.tsx @@ -15,6 +15,8 @@ import { GetServerSideProps, InferGetServerSidePropsType } from "next"; import AssistantBuilder, { AssistantBuilderInitialState, + BUILDER_FLOWS, + BuilderFlow, } from "@app/components/assistant_builder/AssistantBuilder"; import { getApps } from "@app/lib/api/app"; import { getAgentConfiguration } from "@app/lib/api/assistant/configuration"; @@ -40,6 +42,7 @@ export const getServerSideProps: GetServerSideProps<{ dustAppConfiguration: AssistantBuilderInitialState["dustAppConfiguration"]; databaseQueryConfiguration: AssistantBuilderInitialState["databaseQueryConfiguration"]; agentConfiguration: AgentConfigurationType; + flow: BuilderFlow; }> = async (context) => { const session = await getSession(context.req, context.res); const user = await getUserFromSession(session); @@ -56,7 +59,7 @@ export const getServerSideProps: GetServerSideProps<{ !plan || !user || !subscription || - !auth.isBuilder() || + !auth.isUser() || !context.params?.aId ) { return { @@ -75,6 +78,11 @@ export const getServerSideProps: GetServerSideProps<{ auth, context.params?.aId as string ); + if (config?.scope === "workspace" && !auth.isBuilder()) { + return { + notFound: true, + }; + } if (!config) { return { @@ -177,6 +185,11 @@ export const getServerSideProps: GetServerSideProps<{ }; } } + const flow: BuilderFlow = BUILDER_FLOWS.includes( + context.query.flow as BuilderFlow + ) + ? (context.query.flow as BuilderFlow) + : "my_assistants"; return { props: { @@ -191,6 +204,7 @@ export const getServerSideProps: GetServerSideProps<{ dustAppConfiguration, databaseQueryConfiguration: databaseQueryConfiguration, agentConfiguration: config, + flow, }, }; }; @@ -207,6 +221,7 @@ export default function EditAssistant({ dustAppConfiguration, databaseQueryConfiguration, agentConfiguration, + flow, }: InferGetServerSidePropsType) { let actionMode: AssistantBuilderInitialState["actionMode"] = "GENERIC"; @@ -241,6 +256,9 @@ export default function EditAssistant({ if (isDatabaseQueryConfiguration(agentConfiguration.action)) { actionMode = "DATABASE_QUERY"; } + if (agentConfiguration.scope === "global") { + throw new Error("Cannot edit global assistant"); + } return ( -
+
@@ -208,7 +208,9 @@ export default function WorkspaceAssistants({ /> {workspaceAgents.length > 0 && ( - +
)} -
+
= async (context) => { const session = await getSession(context.req, context.res); const user = await getUserFromSession(session); @@ -30,7 +34,7 @@ export const getServerSideProps: GetServerSideProps<{ const owner = auth.workspace(); const plan = auth.plan(); const subscription = auth.subscription(); - if (!owner || !plan || !user || !auth.isBuilder() || !subscription) { + if (!owner || !plan || !user || !auth.isUser() || !subscription) { return { notFound: true, }; @@ -39,6 +43,12 @@ export const getServerSideProps: GetServerSideProps<{ const allDataSources = await getDataSources(auth); const allDustApps = await getApps(auth); + const flow: BuilderFlow = BUILDER_FLOWS.includes( + context.query.flow as BuilderFlow + ) + ? (context.query.flow as BuilderFlow) + : "my_assistants"; + return { props: { user, @@ -48,6 +58,7 @@ export const getServerSideProps: GetServerSideProps<{ gaTrackingId: GA_TRACKING_ID, dataSources: allDataSources, dustApps: allDustApps, + flow, }, }; }; @@ -60,6 +71,7 @@ export default function CreateAssistant({ gaTrackingId, dataSources, dustApps, + flow, }: InferGetServerSidePropsType) { return ( );