diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json index e21fc3764..4cc516706 100644 --- a/frontend/jsconfig.json +++ b/frontend/jsconfig.json @@ -4,7 +4,9 @@ "target": "esnext", "jsx": "react", "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } } -} +} \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b29e6eea9..627b8341e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -21,6 +21,7 @@ const AdminInvites = lazy(() => import("@/pages/Admin/Invitations")); const AdminWorkspaces = lazy(() => import("@/pages/Admin/Workspaces")); const AdminSystem = lazy(() => import("@/pages/Admin/System")); const AdminLogs = lazy(() => import("@/pages/Admin/Logging")); +const AdminAgents = lazy(() => import("@/pages/Admin/Agents")); const GeneralChats = lazy(() => import("@/pages/GeneralSettings/Chats")); const GeneralAppearance = lazy( () => import("@/pages/GeneralSettings/Appearance") @@ -106,6 +107,10 @@ export default function App() { path="/settings/vector-database" element={<AdminRoute Component={GeneralVectorDatabase} />} /> + <Route + path="/settings/agents" + element={<AdminRoute Component={AdminAgents} />} + /> <Route path="/settings/event-logs" element={<AdminRoute Component={AdminLogs} />} diff --git a/frontend/src/components/ContextualSaveBar/index.jsx b/frontend/src/components/ContextualSaveBar/index.jsx new file mode 100644 index 000000000..056bf739b --- /dev/null +++ b/frontend/src/components/ContextualSaveBar/index.jsx @@ -0,0 +1,32 @@ +import { Warning } from "@phosphor-icons/react"; + +export default function ContextualSaveBar({ + showing = false, + onSave, + onCancel, +}) { + if (!showing) return null; + + return ( + <div className="fixed top-0 left-0 right-0 h-14 bg-[#18181B] flex items-center justify-end px-4 z-[9999]"> + <div className="absolute left-1/2 transform -translate-x-1/2 flex items-center gap-x-2"> + <Warning size={18} className="text-white" /> + <p className="text-white font-medium text-xs">Unsaved Changes</p> + </div> + <div className="flex items-center gap-x-2"> + <button + className="border-none text-white font-medium text-sm px-[10px] py-[6px] rounded-md bg-white/5 hover:bg-white/10" + onClick={onCancel} + > + Cancel + </button> + <button + className="border-none text-[#222628] font-medium text-sm px-[10px] py-[6px] rounded-md bg-[#46C8FF] hover:bg-[#3DB5E8]" + onClick={onSave} + > + Save + </button> + </div> + </div> + ); +} diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 41fa60ea4..6049d83fb 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -22,6 +22,7 @@ import { EyeSlash, SplitVertical, Microphone, + Robot, } from "@phosphor-icons/react"; import useUser from "@/hooks/useUser"; import { USER_BACKGROUND_COLOR } from "@/utils/constants"; @@ -258,6 +259,15 @@ const SidebarOptions = ({ user = null }) => ( flex={true} allowedRole={["admin", "manager"]} /> + + <Option + href={paths.settings.agentSkills()} + btnText="Agent Skills" + icon={<Robot className="h-5 w-5 flex-shrink-0" />} + user={user} + flex={true} + allowedRole={["admin", "manager"]} + /> <Option href={paths.settings.appearance()} btnText="Appearance" diff --git a/frontend/src/media/agents/generate-charts.png b/frontend/src/media/agents/generate-charts.png new file mode 100644 index 000000000..ec18323b7 Binary files /dev/null and b/frontend/src/media/agents/generate-charts.png differ diff --git a/frontend/src/media/agents/generate-save-files.png b/frontend/src/media/agents/generate-save-files.png new file mode 100644 index 000000000..0f81b14ce Binary files /dev/null and b/frontend/src/media/agents/generate-save-files.png differ diff --git a/frontend/src/media/agents/rag-memory.png b/frontend/src/media/agents/rag-memory.png new file mode 100644 index 000000000..3dd95be52 Binary files /dev/null and b/frontend/src/media/agents/rag-memory.png differ diff --git a/frontend/src/media/agents/scrape-websites.png b/frontend/src/media/agents/scrape-websites.png new file mode 100644 index 000000000..e88af6f5c Binary files /dev/null and b/frontend/src/media/agents/scrape-websites.png differ diff --git a/frontend/src/media/agents/sql-agent.png b/frontend/src/media/agents/sql-agent.png new file mode 100644 index 000000000..e27bf9b07 Binary files /dev/null and b/frontend/src/media/agents/sql-agent.png differ diff --git a/frontend/src/media/agents/view-summarize.png b/frontend/src/media/agents/view-summarize.png new file mode 100644 index 000000000..587a369b4 Binary files /dev/null and b/frontend/src/media/agents/view-summarize.png differ diff --git a/frontend/src/pages/Admin/Agents/Badges/default.jsx b/frontend/src/pages/Admin/Agents/Badges/default.jsx new file mode 100644 index 000000000..238155508 --- /dev/null +++ b/frontend/src/pages/Admin/Agents/Badges/default.jsx @@ -0,0 +1,25 @@ +import { Tooltip } from "react-tooltip"; + +export function DefaultBadge({ title }) { + return ( + <> + <span + className="w-fit" + data-tooltip-id={`default-skill-${title}`} + data-tooltip-content="This skill is enabled by default and cannot be turned off." + > + <div className="flex items-center gap-x-1 w-fit rounded-full bg-[#F4FFD0]/10 px-2.5 py-0.5 text-sm font-medium text-sky-400 shadow-sm cursor-pointer"> + <div className="text-[#F4FFD0] text-[12px] leading-[15px]"> + Default + </div> + </div> + </span> + <Tooltip + id={`default-skill-${title}`} + place="bottom" + delayShow={300} + className="tooltip !text-xs" + /> + </> + ); +} diff --git a/frontend/src/pages/Admin/Agents/DefaultSkillPanel/index.jsx b/frontend/src/pages/Admin/Agents/DefaultSkillPanel/index.jsx new file mode 100644 index 000000000..fdda731c0 --- /dev/null +++ b/frontend/src/pages/Admin/Agents/DefaultSkillPanel/index.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import { DefaultBadge } from "../Badges/default"; + +export default function DefaultSkillPanel({ title, description, image, icon }) { + return ( + <div className="p-2"> + <div className="flex flex-col gap-y-[18px] max-w-[500px]"> + <div className="flex w-full justify-between items-center"> + <div className="flex items-center gap-x-2"> + {icon && + React.createElement(icon, { + size: 24, + color: "white", + weight: "bold", + })} + <label htmlFor="name" className="text-white text-md font-bold"> + {title} + </label> + <DefaultBadge title={title} /> + </div> + </div> + <img src={image} alt={title} className="w-full rounded-md" /> + <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> + {description} + </p> + </div> + </div> + ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/GenericSkill/index.jsx b/frontend/src/pages/Admin/Agents/GenericSkillPanel/index.jsx similarity index 69% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/GenericSkill/index.jsx rename to frontend/src/pages/Admin/Agents/GenericSkillPanel/index.jsx index 6b100bdd2..ed8b8a4f0 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/GenericSkill/index.jsx +++ b/frontend/src/pages/Admin/Agents/GenericSkillPanel/index.jsx @@ -1,21 +1,30 @@ import React from "react"; -export default function GenericSkill({ + +export default function GenericSkillPanel({ title, description, skill, toggleSkill, enabled = false, disabled = false, + image, + icon, }) { return ( - <div className="border-b border-white/40 pb-4"> - <div className="flex flex-col"> - <div className="flex w-full justify-between items-center"> - <label htmlFor="name" className="block input-label"> + <div className="p-2"> + <div className="flex flex-col gap-y-[18px] max-w-[500px]"> + <div className="flex items-center gap-x-2"> + {icon && + React.createElement(icon, { + size: 24, + color: "white", + weight: "bold", + })} + <label htmlFor="name" className="text-white text-md font-bold"> {title} </label> <label - className={`border-none relative inline-flex items-center mt-2 ${ + className={`border-none relative inline-flex items-center ml-auto ${ disabled ? "cursor-not-allowed" : "cursor-pointer" }`} > @@ -24,12 +33,13 @@ export default function GenericSkill({ disabled={disabled} className="peer sr-only" checked={enabled} - onClick={() => toggleSkill(skill)} + onChange={() => toggleSkill(skill)} /> <div className="peer-disabled:opacity-50 pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> </label> </div> + <img src={image} alt={title} className="w-full rounded-md" /> <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> {description} </p> diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/DBConnection.jsx b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/DBConnection.jsx rename to frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/NewConnectionModal.jsx b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/NewConnectionModal.jsx similarity index 98% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/NewConnectionModal.jsx rename to frontend/src/pages/Admin/Agents/SQLConnectorSelection/NewConnectionModal.jsx index e76b27cce..62f4722c2 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/NewConnectionModal.jsx +++ b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/NewConnectionModal.jsx @@ -74,8 +74,8 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { // to the parent container form so we don't have nested forms. return createPortal( <ModalWrapper isOpen={isOpen}> - <div className="relative w-1/3 max-h-full "> - <div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)] max-h-[90vh] overflow-y-scroll no-scroll"> + <div className="relative w-1/3 max-h-full mt-8"> + <div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)] max-h-[85vh] overflow-y-scroll no-scroll"> <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> <h3 className="text-xl font-semibold text-white"> New SQL Connection diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/icons/mssql.png b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/mssql.png similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/icons/mssql.png rename to frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/mssql.png diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/icons/mysql.png b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/mysql.png similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/icons/mysql.png rename to frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/mysql.png diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/icons/postgresql.png b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/postgresql.png similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/icons/postgresql.png rename to frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/postgresql.png diff --git a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/index.jsx b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/index.jsx new file mode 100644 index 000000000..d7eaa23c1 --- /dev/null +++ b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/index.jsx @@ -0,0 +1,117 @@ +import React, { useState } from "react"; +import DBConnection from "./DBConnection"; +import { Plus, Database } from "@phosphor-icons/react"; +import NewSQLConnection from "./NewConnectionModal"; +import { useModal } from "@/hooks/useModal"; +import SQLAgentImage from "@/media/agents/sql-agent.png"; + +export default function AgentSQLConnectorSelection({ + skill, + settings, + toggleSkill, + enabled = false, + setHasChanges, +}) { + const { isOpen, openModal, closeModal } = useModal(); + const [connections, setConnections] = useState( + settings?.preferences?.agent_sql_connections || [] + ); + return ( + <> + <div className="p-2"> + <div className="flex flex-col gap-y-[18px] max-w-[500px]"> + <div className="flex items-center gap-x-2"> + <Database size={24} color="white" weight="bold" /> + <label htmlFor="name" className="text-white text-md font-bold"> + SQL Agent + </label> + <label className="border-none relative inline-flex cursor-pointer items-center ml-auto"> + <input + type="checkbox" + className="peer sr-only" + checked={enabled} + onChange={() => toggleSkill(skill)} + /> + <div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> + <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> + </label> + </div> + <img + src={SQLAgentImage} + alt="SQL Agent" + className="w-full rounded-md" + /> + <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> + Enable your agent to be able to leverage SQL to answer you questions + by connecting to various SQL database providers. + </p> + {enabled && ( + <> + <input + name="system::agent_sql_connections" + type="hidden" + value={JSON.stringify(connections)} + /> + <input + type="hidden" + value={JSON.stringify( + connections.filter((conn) => conn.action !== "remove") + )} + /> + <div className="flex flex-col mt-2 gap-y-2"> + <p className="text-white font-semibold text-sm"> + Your database connections + </p> + <div className="flex flex-col gap-y-3"> + {connections + .filter((connection) => connection.action !== "remove") + .map((connection) => ( + <DBConnection + key={connection.database_id} + connection={connection} + onRemove={(databaseId) => { + setHasChanges(true); + setConnections((prev) => + prev.map((conn) => { + if (conn.database_id === databaseId) + return { ...conn, action: "remove" }; + return conn; + }) + ); + }} + /> + ))} + <button + type="button" + onClick={openModal} + className="w-fit relative flex h-[40px] items-center border-none hover:bg-slate-600/20 rounded-lg" + > + <div className="flex w-full gap-x-2 items-center p-4"> + <div className="bg-zinc-600 p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center"> + <Plus + weight="bold" + size={14} + className="shrink-0 text-slate-100" + /> + </div> + <p className="text-left text-slate-100 text-sm"> + New SQL connection + </p> + </div> + </button> + </div> + </div> + </> + )} + </div> + </div> + <NewSQLConnection + isOpen={isOpen} + closeModal={closeModal} + onSubmit={(newDb) => + setConnections((prev) => [...prev, { action: "add", ...newDb }]) + } + /> + </> + ); +} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderItem/index.jsx b/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderItem/index.jsx similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderItem/index.jsx rename to frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderItem/index.jsx diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderOptions/index.jsx b/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/SearchProviderOptions/index.jsx rename to frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/bing.png b/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/bing.png similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/bing.png rename to frontend/src/pages/Admin/Agents/WebSearchSelection/icons/bing.png diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/google.png b/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/google.png similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/google.png rename to frontend/src/pages/Admin/Agents/WebSearchSelection/icons/google.png diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/serper.png b/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/serper.png similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/serper.png rename to frontend/src/pages/Admin/Agents/WebSearchSelection/icons/serper.png diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/serply.png b/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/serply.png similarity index 100% rename from frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/icons/serply.png rename to frontend/src/pages/Admin/Agents/WebSearchSelection/icons/serply.png diff --git a/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx b/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx new file mode 100644 index 000000000..0c46c9ce0 --- /dev/null +++ b/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx @@ -0,0 +1,227 @@ +import React, { useEffect, useRef, useState } from "react"; +import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; +import GoogleSearchIcon from "./icons/google.png"; +import SerperDotDevIcon from "./icons/serper.png"; +import BingSearchIcon from "./icons/bing.png"; +import SerplySearchIcon from "./icons/serply.png"; +import { + CaretUpDown, + MagnifyingGlass, + X, + ListMagnifyingGlass, +} from "@phosphor-icons/react"; +import SearchProviderItem from "./SearchProviderItem"; +import WebSearchImage from "@/media/agents/scrape-websites.png"; +import { + SerperDotDevOptions, + GoogleSearchOptions, + BingSearchOptions, + SerplySearchOptions, +} from "./SearchProviderOptions"; + +const SEARCH_PROVIDERS = [ + { + name: "Please make a selection", + value: "none", + logo: AnythingLLMIcon, + options: () => <React.Fragment />, + description: + "Web search will be disabled until a provider and keys are provided.", + }, + { + name: "Google Search Engine", + value: "google-search-engine", + logo: GoogleSearchIcon, + options: (settings) => <GoogleSearchOptions settings={settings} />, + description: + "Web search powered by a custom Google Search Engine. Free for 100 queries per day.", + }, + { + name: "Serper.dev", + value: "serper-dot-dev", + logo: SerperDotDevIcon, + options: (settings) => <SerperDotDevOptions settings={settings} />, + description: + "Serper.dev web-search. Free account with a 2,500 calls, but then paid.", + }, + { + name: "Bing Search", + value: "bing-search", + logo: BingSearchIcon, + options: (settings) => <BingSearchOptions settings={settings} />, + description: + "Web search powered by the Bing Search API. Free for 1000 queries per month.", + }, + { + name: "Serply.io", + value: "serply-engine", + logo: SerplySearchIcon, + options: (settings) => <SerplySearchOptions settings={settings} />, + description: + "Serply.io web-search. Free account with a 100 calls/month forever.", + }, +]; + +export default function AgentWebSearchSelection({ + skill, + settings, + toggleSkill, + enabled = false, + setHasChanges, +}) { + const searchInputRef = useRef(null); + const [filteredResults, setFilteredResults] = useState([]); + const [selectedProvider, setSelectedProvider] = useState("none"); + const [searchQuery, setSearchQuery] = useState(""); + const [searchMenuOpen, setSearchMenuOpen] = useState(false); + + function updateChoice(selection) { + setSearchQuery(""); + setSelectedProvider(selection); + setSearchMenuOpen(false); + setHasChanges(true); + } + + function handleXButton() { + if (searchQuery.length > 0) { + setSearchQuery(""); + if (searchInputRef.current) searchInputRef.current.value = ""; + } else { + setSearchMenuOpen(!searchMenuOpen); + } + } + + useEffect(() => { + const filtered = SEARCH_PROVIDERS.filter((provider) => + provider.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + setFilteredResults(filtered); + }, [searchQuery, selectedProvider]); + + useEffect(() => { + setSelectedProvider(settings?.preferences?.agent_search_provider ?? "none"); + }, [settings?.preferences?.agent_search_provider]); + + const selectedSearchProviderObject = SEARCH_PROVIDERS.find( + (provider) => provider.value === selectedProvider + ); + + return ( + <div className="p-2"> + <div className="flex flex-col gap-y-[18px] max-w-[500px]"> + <div className="flex items-center gap-x-2"> + <ListMagnifyingGlass size={24} color="white" weight="bold" /> + <label htmlFor="name" className="text-white text-md font-bold"> + Live web search and browsing + </label> + <label className="border-none relative inline-flex cursor-pointer items-center ml-auto"> + <input + type="checkbox" + className="peer sr-only" + checked={enabled} + onChange={() => toggleSkill(skill)} + /> + <div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> + <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> + </label> + </div> + <img + src={WebSearchImage} + alt="Web Search" + className="w-full rounded-md" + /> + <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> + Enable your agent to search the web to answer your questions by + connecting to a web-search (SERP) provider. Web search during agent + sessions will not work until this is set up. + </p> + <div hidden={!enabled}> + <div className="relative"> + <input + type="hidden" + name="system::agent_search_provider" + value={selectedProvider} + /> + {searchMenuOpen && ( + <div + className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10" + onClick={() => setSearchMenuOpen(false)} + /> + )} + {searchMenuOpen ? ( + <div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20"> + <div className="w-full flex flex-col gap-y-1"> + <div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]"> + <MagnifyingGlass + size={20} + weight="bold" + className="absolute left-4 z-30 text-white -ml-4 my-2" + /> + <input + type="text" + name="web-provider-search" + autoComplete="off" + placeholder="Search available web-search providers" + className="border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium" + onChange={(e) => setSearchQuery(e.target.value)} + ref={searchInputRef} + onKeyDown={(e) => { + if (e.key === "Enter") e.preventDefault(); + }} + /> + <X + size={20} + weight="bold" + className="cursor-pointer text-white hover:text-[#9CA3AF]" + onClick={handleXButton} + /> + </div> + <div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4"> + {filteredResults.map((provider) => { + return ( + <SearchProviderItem + provider={provider} + key={provider.name} + checked={selectedProvider === provider.value} + onClick={() => updateChoice(provider.value)} + /> + ); + })} + </div> + </div> + </div> + ) : ( + <button + className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300" + type="button" + onClick={() => setSearchMenuOpen(true)} + > + <div className="flex gap-x-4 items-center"> + <img + src={selectedSearchProviderObject.logo} + alt={`${selectedSearchProviderObject.name} logo`} + className="w-10 h-10 rounded-md" + /> + <div className="flex flex-col text-left"> + <div className="text-sm font-semibold text-white"> + {selectedSearchProviderObject.name} + </div> + <div className="mt-1 text-xs text-[#D2D5DB]"> + {selectedSearchProviderObject.description} + </div> + </div> + </div> + <CaretUpDown size={24} weight="bold" className="text-white" /> + </button> + )} + </div> + {selectedProvider !== "none" && ( + <div className="mt-4 flex flex-col gap-y-1"> + {selectedSearchProviderObject.options(settings)} + </div> + )} + </div> + </div> + </div> + ); +} diff --git a/frontend/src/pages/Admin/Agents/index.jsx b/frontend/src/pages/Admin/Agents/index.jsx new file mode 100644 index 000000000..d22bba823 --- /dev/null +++ b/frontend/src/pages/Admin/Agents/index.jsx @@ -0,0 +1,232 @@ +import { useEffect, useRef, useState } from "react"; +import Sidebar from "@/components/SettingsSidebar"; +import { isMobile } from "react-device-detect"; +import Admin from "@/models/admin"; +import System from "@/models/system"; +import showToast from "@/utils/toast"; +import { CaretRight, Robot } from "@phosphor-icons/react"; +import ContextualSaveBar from "@/components/ContextualSaveBar"; +import { castToType } from "@/utils/types"; +import { FullScreenLoader } from "@/components/Preloader"; +import { defaultSkills, configurableSkills } from "./skills"; +import { DefaultBadge } from "./Badges/default"; + +export default function AdminAgents() { + const [hasChanges, setHasChanges] = useState(false); + const [settings, setSettings] = useState({}); + const [selectedSkill, setSelectedSkill] = useState(""); + const [agentSkills, setAgentSkills] = useState([]); + const [loading, setLoading] = useState(true); + const formEl = useRef(null); + + // Alert user if they try to leave the page with unsaved changes + useEffect(() => { + const handleBeforeUnload = (event) => { + if (hasChanges) { + event.preventDefault(); + event.returnValue = ""; + } + }; + window.addEventListener("beforeunload", handleBeforeUnload); + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, [hasChanges]); + + useEffect(() => { + async function fetchSettings() { + const _settings = await System.keys(); + const _preferences = await Admin.systemPreferences(); + setSettings({ ..._settings, preferences: _preferences.settings } ?? {}); + setAgentSkills(_preferences.settings?.default_agent_skills ?? []); + setLoading(false); + } + fetchSettings(); + }, []); + + const toggleAgentSkill = (skillName) => { + setAgentSkills((prev) => { + const updatedSkills = prev.includes(skillName) + ? prev.filter((name) => name !== skillName) + : [...prev, skillName]; + setHasChanges(true); + return updatedSkills; + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const data = { + workspace: {}, + system: {}, + env: {}, + }; + + const form = new FormData(formEl.current); + for (var [key, value] of form.entries()) { + if (key.startsWith("system::")) { + const [_, label] = key.split("system::"); + data.system[label] = String(value); + continue; + } + + if (key.startsWith("env::")) { + const [_, label] = key.split("env::"); + data.env[label] = String(value); + continue; + } + data.workspace[key] = castToType(key, value); + } + + const { success } = await Admin.updateSystemPreferences(data.system); + await System.updateSystem(data.env); + + if (success) { + const _settings = await System.keys(); + const _preferences = await Admin.systemPreferences(); + setSettings({ ..._settings, preferences: _preferences.settings } ?? {}); + setAgentSkills(_preferences.settings?.default_agent_skills ?? []); + showToast(`Agent preferences saved successfully.`, "success", { + clear: true, + }); + } else { + showToast(`Agent preferences failed to save.`, "error", { clear: true }); + } + + setHasChanges(false); + }; + + const SelectedSkillComponent = + configurableSkills[selectedSkill]?.component || + defaultSkills[selectedSkill]?.component; + + if (loading) { + return ( + <div + style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} + className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex justify-center items-center" + > + <FullScreenLoader /> + </div> + ); + } + return ( + <div + id="workspace-agent-settings-container" + className="w-screen h-screen overflow-hidden bg-sidebar flex" + > + <Sidebar /> + <div + style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }} + className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[16px] w-full h-full flex" + > + <form + onSubmit={handleSubmit} + onChange={() => setHasChanges(true)} + ref={formEl} + className="flex-1 flex gap-x-6 p-4 mt-10" + > + <input + name="system::default_agent_skills" + type="hidden" + value={agentSkills.join(",")} + /> + + {/* Skill settings nav */} + <div className="flex flex-col gap-y-[18px]"> + <div className="text-white flex items-center gap-x-2"> + <Robot size={24} /> + <p className="text-lg font-medium">Agent Skills</p> + </div> + + {/* Default skills list */} + <SkillList + isDefault={true} + skills={defaultSkills} + selectedSkill={selectedSkill} + handleClick={setSelectedSkill} + /> + {/* Configurable skills */} + <SkillList + skills={configurableSkills} + selectedSkill={selectedSkill} + handleClick={setSelectedSkill} + activeSkills={agentSkills} + /> + </div> + + {/* Selected agent skill setting panel */} + <div className="flex-[2] flex flex-col gap-y-[18px] mt-10"> + <div className="bg-[#303237] text-white rounded-xl flex-1 p-4"> + {SelectedSkillComponent ? ( + <SelectedSkillComponent + skill={configurableSkills[selectedSkill]?.skill} + settings={settings} + toggleSkill={toggleAgentSkill} + enabled={agentSkills.includes( + configurableSkills[selectedSkill]?.skill + )} + setHasChanges={setHasChanges} + {...(configurableSkills[selectedSkill] || + defaultSkills[selectedSkill])} + /> + ) : ( + <div className="flex flex-col items-center justify-center h-full text-white/60"> + <Robot size={40} /> + <p className="font-medium">Select an agent skill</p> + </div> + )} + </div> + </div> + </form> + <ContextualSaveBar + showing={hasChanges} + onSave={handleSubmit} + onCancel={() => setHasChanges(false)} + /> + </div> + </div> + ); +} + +function SkillList({ + isDefault = false, + skills = [], + selectedSkill = null, + handleClick = null, + activeSkills = [], +}) { + if (skills.length === 0) return null; + + return ( + <div className="bg-white/5 text-white min-w-[360px] w-fit rounded-xl"> + {Object.entries(skills).map(([skill, settings], index) => ( + <div + key={skill} + className={`py-3 px-4 flex items-center justify-between ${ + index === 0 ? "rounded-t-xl" : "" + } ${ + index === Object.keys(skills).length - 1 + ? "rounded-b-xl" + : "border-b border-white/10" + } cursor-pointer transition-all duration-300 hover:bg-white/5 ${ + selectedSkill === skill ? "bg-white/10" : "" + }`} + onClick={() => handleClick?.(skill)} + > + <div className="text-sm font-light">{settings.title}</div> + <div className="flex items-center gap-x-2"> + {isDefault ? ( + <DefaultBadge title={skill} /> + ) : ( + <div className="text-sm text-white/60 font-medium"> + {activeSkills.includes(skill) ? "On" : "Off"} + </div> + )} + <CaretRight size={14} weight="bold" className="text-white/80" /> + </div> + </div> + ))} + </div> + ); +} diff --git a/frontend/src/pages/Admin/Agents/skills.js b/frontend/src/pages/Admin/Agents/skills.js new file mode 100644 index 000000000..0d5ea7025 --- /dev/null +++ b/frontend/src/pages/Admin/Agents/skills.js @@ -0,0 +1,73 @@ +import AgentWebSearchSelection from "./WebSearchSelection"; +import AgentSQLConnectorSelection from "./SQLConnectorSelection"; +import GenericSkillPanel from "./GenericSkillPanel"; +import DefaultSkillPanel from "./DefaultSkillPanel"; +import { + Brain, + File, + Browser, + ChartBar, + FileMagnifyingGlass, +} from "@phosphor-icons/react"; +import RAGImage from "@/media/agents/rag-memory.png"; +import SummarizeImage from "@/media/agents/view-summarize.png"; +import ScrapeWebsitesImage from "@/media/agents/scrape-websites.png"; +import GenerateChartsImage from "@/media/agents/generate-charts.png"; +import GenerateSaveImages from "@/media/agents/generate-save-files.png"; + +export const defaultSkills = { + "rag-memory": { + title: "RAG & long-term memory", + description: + 'Allow the agent to leverage your local documents to answer a query or ask the agent to "remember" pieces of content for long-term memory retrieval.', + component: DefaultSkillPanel, + icon: Brain, + image: RAGImage, + }, + "view-summarize": { + title: "View & summarize documents", + description: + "Allow the agent to list and summarize the content of workspace files currently embedded.", + component: DefaultSkillPanel, + icon: File, + image: SummarizeImage, + }, + "scrape-websites": { + title: "Scrape websites", + description: "Allow the agent to visit and scrape the content of websites.", + component: DefaultSkillPanel, + icon: Browser, + image: ScrapeWebsitesImage, + }, +}; + +export const configurableSkills = { + "save-file": { + title: "Generate & save files to browser", + description: + "Enable the default agent to generate and write to files that can be saved to your computer.", + component: GenericSkillPanel, + skill: "save-file-to-browser", + icon: FileMagnifyingGlass, + image: GenerateSaveImages, + }, + "create-chart": { + title: "Generate charts", + description: + "Enable the default agent to generate various types of charts from data provided or given in chat.", + component: GenericSkillPanel, + skill: "create-chart", + icon: ChartBar, + image: GenerateChartsImage, + }, + "web-browsing": { + title: "Web Search", + component: AgentWebSearchSelection, + skill: "web-browsing", + }, + "sql-agent": { + title: "SQL Connector", + component: AgentSQLConnectorSelection, + skill: "sql-agent", + }, +}; diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/index.jsx deleted file mode 100644 index 848d44ed9..000000000 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/SQLConnectorSelection/index.jsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useState } from "react"; -import DBConnection from "./DBConnection"; -import { Plus } from "@phosphor-icons/react"; -import NewSQLConnection from "./NewConnectionModal"; -import { useModal } from "@/hooks/useModal"; - -export default function AgentSQLConnectorSelection({ - skill, - settings, - toggleSkill, - enabled = false, - setHasChanges, -}) { - const { isOpen, openModal, closeModal } = useModal(); - const [connections, setConnections] = useState( - settings?.preferences?.agent_sql_connections || [] - ); - - return ( - <> - <div className="border-b border-white/40 pb-4"> - <div className="flex flex-col"> - <div className="flex w-full justify-between items-center"> - <label htmlFor="name" className="block input-label"> - SQL Agent - </label> - <label className="border-none relative inline-flex cursor-pointer items-center mt-2"> - <input - type="checkbox" - className="peer sr-only" - checked={enabled} - onClick={() => toggleSkill(skill)} - /> - <div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> - <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> - </label> - </div> - <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - Enable your agent to be able to leverage SQL to answer you questions - by connecting to various SQL database providers. - </p> - </div> - {enabled && ( - <> - <input - name="system::agent_sql_connections" - type="hidden" - value={JSON.stringify(connections)} - /> - <input - type="hidden" - value={JSON.stringify( - connections.filter((conn) => conn.action !== "remove") - )} - /> - <div className="flex flex-col mt-2 gap-y-2"> - <p className="text-white font-semibold text-sm"> - Your database connections - </p> - <div className="flex flex-col gap-y-3"> - {connections - .filter((connection) => connection.action !== "remove") - .map((connection) => ( - <DBConnection - key={connection.database_id} - connection={connection} - onRemove={(databaseId) => { - setConnections((prev) => - prev.map((conn) => { - if (conn.database_id === databaseId) - return { ...conn, action: "remove" }; - return conn; - }) - ); - }} - setHasChanges={setHasChanges} - /> - ))} - <button - type="button" - onClick={openModal} - className="w-fit relative flex h-[40px] items-center border-none hover:bg-slate-600/20 rounded-lg" - > - <div className="flex w-full gap-x-2 items-center p-4"> - <div className="bg-zinc-600 p-2 rounded-lg h-[24px] w-[24px] flex items-center justify-center"> - <Plus - weight="bold" - size={14} - className="shrink-0 text-slate-100" - /> - </div> - <p className="text-left text-slate-100 text-sm"> - New SQL connection - </p> - </div> - </button> - </div> - </div> - </> - )} - </div> - <NewSQLConnection - isOpen={isOpen} - closeModal={closeModal} - onSubmit={(newDb) => - setConnections((prev) => [...prev, { action: "add", ...newDb }]) - } - /> - </> - ); -} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/index.jsx deleted file mode 100644 index a71ac770b..000000000 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/WebSearchSelection/index.jsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; -import GoogleSearchIcon from "./icons/google.png"; -import SerperDotDevIcon from "./icons/serper.png"; -import BingSearchIcon from "./icons/bing.png"; -import SerplySearchIcon from "./icons/serply.png"; -import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react"; -import SearchProviderItem from "./SearchProviderItem"; -import { - SerperDotDevOptions, - GoogleSearchOptions, - BingSearchOptions, - SerplySearchOptions, -} from "./SearchProviderOptions"; - -const SEARCH_PROVIDERS = [ - { - name: "Please make a selection", - value: "none", - logo: AnythingLLMIcon, - options: () => <React.Fragment />, - description: - "Web search will be disabled until a provider and keys are provided.", - }, - { - name: "Google Search Engine", - value: "google-search-engine", - logo: GoogleSearchIcon, - options: (settings) => <GoogleSearchOptions settings={settings} />, - description: - "Web search powered by a custom Google Search Engine. Free for 100 queries per day.", - }, - { - name: "Serper.dev", - value: "serper-dot-dev", - logo: SerperDotDevIcon, - options: (settings) => <SerperDotDevOptions settings={settings} />, - description: - "Serper.dev web-search. Free account with a 2,500 calls, but then paid.", - }, - { - name: "Bing Search", - value: "bing-search", - logo: BingSearchIcon, - options: (settings) => <BingSearchOptions settings={settings} />, - description: - "Web search powered by the Bing Search API. Free for 1000 queries per month.", - }, - { - name: "Serply.io", - value: "serply-engine", - logo: SerplySearchIcon, - options: (settings) => <SerplySearchOptions settings={settings} />, - description: - "Serply.io web-search. Free account with a 100 calls/month forever.", - }, -]; - -export default function AgentWebSearchSelection({ - skill, - settings, - toggleSkill, - enabled = false, -}) { - const searchInputRef = useRef(null); - const [filteredResults, setFilteredResults] = useState([]); - const [selectedProvider, setSelectedProvider] = useState("none"); - const [searchQuery, setSearchQuery] = useState(""); - const [searchMenuOpen, setSearchMenuOpen] = useState(false); - - function updateChoice(selection) { - setSearchQuery(""); - setSelectedProvider(selection); - setSearchMenuOpen(false); - } - - function handleXButton() { - if (searchQuery.length > 0) { - setSearchQuery(""); - if (searchInputRef.current) searchInputRef.current.value = ""; - } else { - setSearchMenuOpen(!searchMenuOpen); - } - } - - useEffect(() => { - const filtered = SEARCH_PROVIDERS.filter((provider) => - provider.name.toLowerCase().includes(searchQuery.toLowerCase()) - ); - setFilteredResults(filtered); - }, [searchQuery, selectedProvider]); - - useEffect(() => { - setSelectedProvider(settings?.preferences?.agent_search_provider ?? "none"); - }, [settings?.preferences?.agent_search_provider]); - - const selectedSearchProviderObject = SEARCH_PROVIDERS.find( - (provider) => provider.value === selectedProvider - ); - - return ( - <div className="border-b border-white/40 pb-4"> - <div className="flex flex-col"> - <div className="flex w-full justify-between items-center"> - <label htmlFor="name" className="block input-label"> - Live web search and browsing - </label> - <label className="border-none relative inline-flex cursor-pointer items-center mt-2"> - <input - type="checkbox" - className="peer sr-only" - checked={enabled} - onClick={() => toggleSkill(skill)} - /> - <div className="pointer-events-none peer h-6 w-11 rounded-full bg-stone-400 after:absolute after:left-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:shadow-xl after:border after:border-gray-600 after:bg-white after:box-shadow-md after:transition-all after:content-[''] peer-checked:bg-lime-300 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800"></div> - <span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300"></span> - </label> - </div> - <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - Enable your agent to search the web to answer your questions by - connecting to a web-search (SERP) provider. - <br /> - Web search during agent sessions will not work until this is set up. - </p> - </div> - <div hidden={!enabled}> - <div className="relative"> - <input - type="hidden" - name="system::agent_search_provider" - value={selectedProvider} - /> - {searchMenuOpen && ( - <div - className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10" - onClick={() => setSearchMenuOpen(false)} - /> - )} - {searchMenuOpen ? ( - <div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20"> - <div className="w-full flex flex-col gap-y-1"> - <div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]"> - <MagnifyingGlass - size={20} - weight="bold" - className="absolute left-4 z-30 text-white -ml-4 my-2" - /> - <input - type="text" - name="web-provider-search" - autoComplete="off" - placeholder="Search available web-search providers" - className="border-none -ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium" - onChange={(e) => setSearchQuery(e.target.value)} - ref={searchInputRef} - onKeyDown={(e) => { - if (e.key === "Enter") e.preventDefault(); - }} - /> - <X - size={20} - weight="bold" - className="cursor-pointer text-white hover:text-[#9CA3AF]" - onClick={handleXButton} - /> - </div> - <div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4"> - {filteredResults.map((provider) => { - return ( - <SearchProviderItem - provider={provider} - key={provider.name} - checked={selectedProvider === provider.value} - onClick={() => updateChoice(provider.value)} - /> - ); - })} - </div> - </div> - </div> - ) : ( - <button - className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300" - type="button" - onClick={() => setSearchMenuOpen(true)} - > - <div className="flex gap-x-4 items-center"> - <img - src={selectedSearchProviderObject.logo} - alt={`${selectedSearchProviderObject.name} logo`} - className="w-10 h-10 rounded-md" - /> - <div className="flex flex-col text-left"> - <div className="text-sm font-semibold text-white"> - {selectedSearchProviderObject.name} - </div> - <div className="mt-1 text-xs text-[#D2D5DB]"> - {selectedSearchProviderObject.description} - </div> - </div> - </div> - <CaretUpDown size={24} weight="bold" className="text-white" /> - </button> - )} - </div> - {selectedProvider !== "none" && ( - <div className="mt-4 flex flex-col gap-y-1"> - {selectedSearchProviderObject.options(settings)} - </div> - )} - </div> - </div> - ); -} diff --git a/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx b/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx index 0b31b9ae4..78b9a502d 100644 --- a/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/AgentConfig/index.jsx @@ -4,27 +4,25 @@ import showToast from "@/utils/toast"; import { castToType } from "@/utils/types"; import { useEffect, useRef, useState } from "react"; import AgentLLMSelection from "./AgentLLMSelection"; -import AgentWebSearchSelection from "./WebSearchSelection"; -import AgentSQLConnectorSelection from "./SQLConnectorSelection"; -import GenericSkill from "./GenericSkill"; import Admin from "@/models/admin"; import * as Skeleton from "react-loading-skeleton"; import "react-loading-skeleton/dist/skeleton.css"; +import paths from "@/utils/paths"; +import { useNavigate } from "react-router-dom"; export default function WorkspaceAgentConfiguration({ workspace }) { const [settings, setSettings] = useState({}); const [hasChanges, setHasChanges] = useState(false); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(true); - const [agentSkills, setAgentSkills] = useState([]); - + const navigate = useNavigate(); const formEl = useRef(null); + useEffect(() => { async function fetchSettings() { const _settings = await System.keys(); const _preferences = await Admin.systemPreferences(); setSettings({ ..._settings, preferences: _preferences.settings } ?? {}); - setAgentSkills(_preferences.settings?.default_agent_skills ?? []); setLoading(false); } fetchSettings(); @@ -73,14 +71,6 @@ export default function WorkspaceAgentConfiguration({ workspace }) { setHasChanges(false); }; - function toggleAgentSkill(skillName = "") { - setAgentSkills((prev) => { - return prev.includes(skillName) - ? prev.filter((name) => name !== skillName) - : [...prev, skillName]; - }); - } - if (!workspace || loading) return <LoadingSkeleton />; return ( <div id="workspace-agent-settings-container"> @@ -96,12 +86,23 @@ export default function WorkspaceAgentConfiguration({ workspace }) { workspace={workspace} setHasChanges={setHasChanges} /> - <AvailableAgentSkills - skills={agentSkills} - toggleAgentSkill={toggleAgentSkill} - settings={settings} - setHasChanges={setHasChanges} - /> + {!hasChanges && ( + <div className="flex flex-col gap-y-4"> + <button onClick={() => navigate(paths.settings.agentSkills())}> + <div + type="button" + className="w-fit transition-all duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" + > + Configure Agent Skills + </div> + </button> + <p className="text-white text-opacity-60 text-xs font-medium"> + Customize and enhance the default agent's capabilities by enabling + or disabling specific skills. These settings will be applied + across all workspaces. + </p> + </div> + )} {hasChanges && ( <button type="submit" @@ -143,83 +144,3 @@ function LoadingSkeleton() { </div> ); } - -function AvailableAgentSkills({ - skills, - settings, - toggleAgentSkill, - setHasChanges, -}) { - return ( - <div> - <div className="flex flex-col mb-8"> - <div className="flex w-full justify-between items-center"> - <label htmlFor="name" className="text-white text-md font-semibold"> - Default agent skills - </label> - </div> - <p className="text-white text-opacity-60 text-xs font-medium py-1.5"> - Improve the natural abilities of the default agent with these - pre-built skills. This set up applies to all workspaces. - </p> - </div> - <input - name="system::default_agent_skills" - type="hidden" - value={skills.join(",")} - /> - <div className="flex flex-col gap-y-3"> - <GenericSkill - title="RAG & long-term memory" - description='Allow the agent to leverage your local documents to answer a query or ask the agent to "remember" pieces of content for long-term memory retrieval.' - settings={settings} - enabled={true} - disabled={true} - /> - <GenericSkill - title="View & summarize documents" - description="Allow the agent to list and summarize the content of workspace files currently embedded." - settings={settings} - enabled={true} - disabled={true} - /> - <GenericSkill - title="Scrape websites" - description="Allow the agent to visit and scrape the content of websites." - settings={settings} - enabled={true} - disabled={true} - /> - <GenericSkill - title="Generate charts" - description="Enable the default agent to generate various types of charts from data provided or given in chat." - skill="create-chart" - settings={settings} - toggleSkill={toggleAgentSkill} - enabled={skills.includes("create-chart")} - /> - <GenericSkill - title="Generate & save files to browser" - description="Enable the default agent to generate and write to files that save and can be downloaded in your browser." - skill="save-file-to-browser" - settings={settings} - toggleSkill={toggleAgentSkill} - enabled={skills.includes("save-file-to-browser")} - /> - <AgentWebSearchSelection - skill="web-browsing" - settings={settings} - toggleSkill={toggleAgentSkill} - enabled={skills.includes("web-browsing")} - /> - <AgentSQLConnectorSelection - skill="sql-agent" - settings={settings} - toggleSkill={toggleAgentSkill} - enabled={skills.includes("sql-agent")} - setHasChanges={setHasChanges} - /> - </div> - </div> - ); -} diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index cc2b69eee..b1ffcb2bb 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -117,6 +117,9 @@ export default { appearance: () => { return "/settings/appearance"; }, + agentSkills: () => { + return "/settings/agents"; + }, apiKeys: () => { return "/settings/api-keys"; }, diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 59d645447..1bdfd8b18 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -317,6 +317,7 @@ function adminEndpoints(app) { [validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])], async (_, response) => { try { + const embedder = getEmbeddingEngineSelection(); const settings = { users_can_delete_workspaces: (await SystemSettings.get({ label: "users_can_delete_workspaces" })) @@ -337,13 +338,12 @@ function adminEndpoints(app) { text_splitter_chunk_size: (await SystemSettings.get({ label: "text_splitter_chunk_size" })) ?.value || - getEmbeddingEngineSelection()?.embeddingMaxChunkLength || + embedder?.embeddingMaxChunkLength || null, text_splitter_chunk_overlap: (await SystemSettings.get({ label: "text_splitter_chunk_overlap" })) ?.value || null, - max_embed_chunk_size: - getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1000, + max_embed_chunk_size: embedder?.embeddingMaxChunkLength || 1000, agent_search_provider: (await SystemSettings.get({ label: "agent_search_provider" })) ?.value || null,