mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-13 02:00:10 +01:00
WIP agent settings redesign
This commit is contained in:
parent
4324a8bb4f
commit
4aa9d0a39b
@ -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} />}
|
||||
|
@ -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";
|
||||
@ -257,6 +258,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"
|
||||
|
@ -0,0 +1,151 @@
|
||||
// This component differs from the main LLMItem in that it shows if a provider is
|
||||
// "ready for use" and if not - will then highjack the click handler to show a modal
|
||||
// of the provider options that must be saved to continue.
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
|
||||
export default function WorkspaceLLM({
|
||||
llm,
|
||||
availableLLMs,
|
||||
settings,
|
||||
checked,
|
||||
onClick,
|
||||
}) {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const { name, value, logo, description } = llm;
|
||||
|
||||
function handleProviderSelection() {
|
||||
// Determine if provider needs additional setup because its minimum required keys are
|
||||
// not yet set in settings.
|
||||
const requiresAdditionalSetup = (llm.requiredConfig || []).some(
|
||||
(key) => !settings[key]
|
||||
);
|
||||
if (requiresAdditionalSetup) {
|
||||
openModal();
|
||||
return;
|
||||
}
|
||||
onClick(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={handleProviderSelection}
|
||||
className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-white/10 ${
|
||||
checked ? "bg-white/10" : ""
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
value={value}
|
||||
className="peer hidden"
|
||||
checked={checked}
|
||||
readOnly={true}
|
||||
formNoValidate={true}
|
||||
/>
|
||||
<div className="flex gap-x-4 items-center">
|
||||
<img
|
||||
src={logo}
|
||||
alt={`${name} logo`}
|
||||
className="w-10 h-10 rounded-md"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-sm font-semibold text-white">{name}</div>
|
||||
<div className="mt-1 text-xs text-[#D2D5DB]">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SetupProvider
|
||||
availableLLMs={availableLLMs}
|
||||
isOpen={isOpen}
|
||||
provider={value}
|
||||
closeModal={closeModal}
|
||||
postSubmit={onClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SetupProvider({
|
||||
availableLLMs,
|
||||
isOpen,
|
||||
provider,
|
||||
closeModal,
|
||||
postSubmit,
|
||||
}) {
|
||||
if (!isOpen) return null;
|
||||
const LLMOption = availableLLMs.find((llm) => llm.value === provider);
|
||||
if (!LLMOption) return null;
|
||||
|
||||
async function handleUpdate(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const data = {};
|
||||
const form = new FormData(e.target);
|
||||
for (var [key, value] of form.entries()) data[key] = value;
|
||||
const { error } = await System.updateSystem(data);
|
||||
if (error) {
|
||||
showToast(`Failed to save ${LLMOption.name} settings: ${error}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
closeModal();
|
||||
postSubmit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot do nested forms, it will cause all sorts of issues, so we portal this out
|
||||
// to the parent container form so we don't have nested forms.
|
||||
return createPortal(
|
||||
<ModalWrapper isOpen={isOpen}>
|
||||
<div className="relative w-fit max-w-1/2 max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)]">
|
||||
<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">
|
||||
Setup {LLMOption.name}
|
||||
</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
type="button"
|
||||
className="border-none transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||
data-modal-hide="staticModal"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="provider-form" onSubmit={handleUpdate}>
|
||||
<div className="py-[17px] px-[20px] flex flex-col gap-y-6">
|
||||
<p className="text-sm text-white">
|
||||
To use {LLMOption.name} as this workspace's LLM you need to set
|
||||
it up first.
|
||||
</p>
|
||||
<div>{LLMOption.options({ credentialsOnly: true })}</div>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-3 space-x-2 border-t rounded-b border-gray-500/50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="border-none text-xs px-2 py-1 font-semibold rounded-lg bg-white hover:bg-transparent border-2 border-transparent hover:border-white hover:text-white h-[32px] w-fit -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="provider-form"
|
||||
className="border-none text-xs px-2 py-1 font-semibold rounded-lg bg-[#46C8FF] hover:bg-[#2C2F36] border-2 border-transparent hover:border-[#46C8FF] hover:text-white h-[32px] w-fit -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
Save {LLMOption.name} settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>,
|
||||
document.getElementById("workspace-agent-settings-container")
|
||||
);
|
||||
}
|
206
frontend/src/pages/Admin/Agents/AgentLLMSelection/index.jsx
Normal file
206
frontend/src/pages/Admin/Agents/AgentLLMSelection/index.jsx
Normal file
@ -0,0 +1,206 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
||||
import AgentLLMItem from "./AgentLLMItem";
|
||||
import { AVAILABLE_LLM_PROVIDERS } from "@/pages/GeneralSettings/LLMPreference";
|
||||
import { CaretUpDown, Gauge, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import AgentModelSelection from "../AgentModelSelection";
|
||||
|
||||
const ENABLED_PROVIDERS = [
|
||||
"openai",
|
||||
"anthropic",
|
||||
"lmstudio",
|
||||
"ollama",
|
||||
"localai",
|
||||
"groq",
|
||||
"azure",
|
||||
"koboldcpp",
|
||||
"togetherai",
|
||||
"openrouter",
|
||||
"mistral",
|
||||
"perplexity",
|
||||
"textgenwebui",
|
||||
// TODO: More agent support.
|
||||
// "generic-openai", // Need to support text-input for agent model input for this to be enabled.
|
||||
// "cohere", // Has tool calling and will need to build explicit support
|
||||
// "huggingface" // Can be done but already has issues with no-chat templated. Needs to be tested.
|
||||
// "gemini", // Too rate limited and broken in several ways to use for agents.
|
||||
];
|
||||
const WARN_PERFORMANCE = [
|
||||
"lmstudio",
|
||||
"groq",
|
||||
"azure",
|
||||
"koboldcpp",
|
||||
"ollama",
|
||||
"localai",
|
||||
"openrouter",
|
||||
"generic-openai",
|
||||
"textgenwebui",
|
||||
];
|
||||
|
||||
const LLM_DEFAULT = {
|
||||
name: "Please make a selection",
|
||||
value: "none",
|
||||
logo: AnythingLLMIcon,
|
||||
options: () => <React.Fragment />,
|
||||
description: "Agents will not work until a valid selection is made.",
|
||||
requiredConfig: [],
|
||||
};
|
||||
|
||||
const LLMS = [
|
||||
LLM_DEFAULT,
|
||||
...AVAILABLE_LLM_PROVIDERS.filter((llm) =>
|
||||
ENABLED_PROVIDERS.includes(llm.value)
|
||||
),
|
||||
];
|
||||
|
||||
export default function AgentLLMSelection({
|
||||
settings,
|
||||
workspace,
|
||||
setHasChanges,
|
||||
}) {
|
||||
const [filteredLLMs, setFilteredLLMs] = useState([]);
|
||||
const [selectedLLM, setSelectedLLM] = useState(
|
||||
workspace?.agentProvider ?? "none"
|
||||
);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
|
||||
const searchInputRef = useRef(null);
|
||||
|
||||
function updateLLMChoice(selection) {
|
||||
setSearchQuery("");
|
||||
setSelectedLLM(selection);
|
||||
setSearchMenuOpen(false);
|
||||
setHasChanges(true);
|
||||
}
|
||||
|
||||
function handleXButton() {
|
||||
if (searchQuery.length > 0) {
|
||||
setSearchQuery("");
|
||||
if (searchInputRef.current) searchInputRef.current.value = "";
|
||||
} else {
|
||||
setSearchMenuOpen(!searchMenuOpen);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const filtered = LLMS.filter((llm) =>
|
||||
llm.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
setFilteredLLMs(filtered);
|
||||
}, [searchQuery, selectedLLM]);
|
||||
|
||||
const selectedLLMObject = LLMS.find((llm) => llm.value === selectedLLM);
|
||||
return (
|
||||
<div className="border-b border-white/40 pb-8">
|
||||
{WARN_PERFORMANCE.includes(selectedLLM) && (
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-x-2 text-white mb-4 bg-blue-800/30 w-fit rounded-lg px-4 py-2">
|
||||
<div className="gap-x-2 flex items-center">
|
||||
<Gauge className="shrink-0" size={25} />
|
||||
<p className="text-sm">
|
||||
Performance of LLMs that do not explicitly support tool-calling is
|
||||
highly dependent on the model's capabilities and accuracy. Some
|
||||
abilities may be limited or non-functional.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
Workspace Agent LLM Provider
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
The specific LLM provider & model that will be used for this
|
||||
workspace's @agent agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<input type="hidden" name="agentProvider" value={selectedLLM} />
|
||||
{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="llm-search"
|
||||
autoComplete="off"
|
||||
placeholder="Search available LLM 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">
|
||||
{filteredLLMs.map((llm) => {
|
||||
return (
|
||||
<AgentLLMItem
|
||||
llm={llm}
|
||||
key={llm.name}
|
||||
availableLLMs={LLMS}
|
||||
settings={settings}
|
||||
checked={selectedLLM === llm.value}
|
||||
onClick={() => updateLLMChoice(llm.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={selectedLLMObject.logo}
|
||||
alt={`${selectedLLMObject.name} logo`}
|
||||
className="w-10 h-10 rounded-md"
|
||||
/>
|
||||
<div className="flex flex-col text-left">
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{selectedLLMObject.name}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-[#D2D5DB]">
|
||||
{selectedLLMObject.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CaretUpDown size={24} weight="bold" className="text-white" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{selectedLLM !== "none" && (
|
||||
<div className="mt-4 flex flex-col gap-y-1">
|
||||
<AgentModelSelection
|
||||
provider={selectedLLM}
|
||||
workspace={workspace}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
128
frontend/src/pages/Admin/Agents/AgentModelSelection/index.jsx
Normal file
128
frontend/src/pages/Admin/Agents/AgentModelSelection/index.jsx
Normal file
@ -0,0 +1,128 @@
|
||||
import useGetProviderModels, {
|
||||
DISABLED_PROVIDERS,
|
||||
} from "@/hooks/useGetProvidersModels";
|
||||
|
||||
// These models do NOT support function calling
|
||||
function supportedModel(provider, model = "") {
|
||||
if (provider !== "openai") return true;
|
||||
return (
|
||||
["gpt-3.5-turbo-0301", "gpt-4-turbo-2024-04-09", "gpt-4-turbo"].includes(
|
||||
model
|
||||
) === false
|
||||
);
|
||||
}
|
||||
|
||||
export default function AgentModelSelection({
|
||||
provider,
|
||||
workspace,
|
||||
setHasChanges,
|
||||
}) {
|
||||
const { defaultModels, customModels, loading } =
|
||||
useGetProviderModels(provider);
|
||||
if (DISABLED_PROVIDERS.includes(provider)) return null;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
Workspace Agent Chat model
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
The specific chat model that will be used for this workspace's
|
||||
@agent agent.
|
||||
</p>
|
||||
</div>
|
||||
<select
|
||||
name="agentModel"
|
||||
required={true}
|
||||
disabled={true}
|
||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
>
|
||||
<option disabled={true} selected={true}>
|
||||
-- waiting for models --
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="name" className="block input-label">
|
||||
Workspace Agent model
|
||||
</label>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
The specific LLM model that will be used for this workspace's @agent
|
||||
agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<select
|
||||
name="agentModel"
|
||||
required={true}
|
||||
onChange={() => {
|
||||
setHasChanges(true);
|
||||
}}
|
||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
>
|
||||
{defaultModels.length > 0 && (
|
||||
<optgroup label="General models">
|
||||
{defaultModels.map((model) => {
|
||||
if (!supportedModel(provider, model)) return null;
|
||||
return (
|
||||
<option
|
||||
key={model}
|
||||
value={model}
|
||||
selected={workspace?.agentModel === model}
|
||||
>
|
||||
{model}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
)}
|
||||
{Array.isArray(customModels) && customModels.length > 0 && (
|
||||
<optgroup label="Custom models">
|
||||
{customModels.map((model) => {
|
||||
if (!supportedModel(provider, model.id)) return null;
|
||||
|
||||
return (
|
||||
<option
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
selected={workspace?.agentModel === model.id}
|
||||
>
|
||||
{model.id}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
)}
|
||||
{/* For providers like TogetherAi where we partition model by creator entity. */}
|
||||
{!Array.isArray(customModels) &&
|
||||
Object.keys(customModels).length > 0 && (
|
||||
<>
|
||||
{Object.entries(customModels).map(([organization, models]) => (
|
||||
<optgroup key={organization} label={organization}>
|
||||
{models.map((model) => {
|
||||
if (!supportedModel(provider, model.id)) return null;
|
||||
return (
|
||||
<option
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
selected={workspace?.agentModel === model.id}
|
||||
>
|
||||
{model.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</optgroup>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
39
frontend/src/pages/Admin/Agents/GenericSkill/index.jsx
Normal file
39
frontend/src/pages/Admin/Agents/GenericSkill/index.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
export default function GenericSkill({
|
||||
title,
|
||||
description,
|
||||
skill,
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
disabled = false,
|
||||
}) {
|
||||
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">
|
||||
{title}
|
||||
</label>
|
||||
<label
|
||||
className={`border-none relative inline-flex items-center mt-2 ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
className="peer sr-only"
|
||||
checked={enabled}
|
||||
onClick={() => 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>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import PostgreSQLLogo from "./icons/postgresql.png";
|
||||
import MySQLLogo from "./icons/mysql.png";
|
||||
import MSSQLLogo from "./icons/mssql.png";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
|
||||
export const DB_LOGOS = {
|
||||
postgresql: PostgreSQLLogo,
|
||||
mysql: MySQLLogo,
|
||||
"sql-server": MSSQLLogo,
|
||||
};
|
||||
|
||||
export default function DBConnection({ connection, onRemove }) {
|
||||
const { database_id, engine } = connection;
|
||||
function removeConfirmation() {
|
||||
if (
|
||||
!window.confirm(
|
||||
`Delete ${database_id} from the list of available SQL connections? This cannot be undone.`
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
onRemove(database_id);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-x-4 items-center">
|
||||
<img
|
||||
src={DB_LOGOS?.[engine] ?? null}
|
||||
alt={`${engine} logo`}
|
||||
className="w-10 h-10 rounded-md"
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div className="text-sm font-semibold text-white">{database_id}</div>
|
||||
<div className="mt-1 text-xs text-[#D2D5DB]">{engine}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={removeConfirmation}
|
||||
className="border-none text-white/40 hover:text-red-500"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
import { useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { WarningOctagon, X } from "@phosphor-icons/react";
|
||||
import { DB_LOGOS } from "./DBConnection";
|
||||
|
||||
function assembleConnectionString({
|
||||
engine,
|
||||
username = "",
|
||||
password = "",
|
||||
host = "",
|
||||
port = "",
|
||||
database = "",
|
||||
}) {
|
||||
if ([username, password, host, database].every((i) => !!i) === false)
|
||||
return `Please fill out all the fields above.`;
|
||||
switch (engine) {
|
||||
case "postgresql":
|
||||
return `postgres://${username}:${password}@${host}:${port}/${database}`;
|
||||
case "mysql":
|
||||
return `mysql://${username}:${password}@${host}:${port}/${database}`;
|
||||
case "sql-server":
|
||||
return `mssql://${username}:${password}@${host}:${port}/${database}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_ENGINE = "postgresql";
|
||||
const DEFAULT_CONFIG = {
|
||||
username: null,
|
||||
password: null,
|
||||
host: null,
|
||||
port: null,
|
||||
database: null,
|
||||
};
|
||||
|
||||
export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
|
||||
const [engine, setEngine] = useState(DEFAULT_ENGINE);
|
||||
const [config, setConfig] = useState(DEFAULT_CONFIG);
|
||||
if (!isOpen) return null;
|
||||
|
||||
function handleClose() {
|
||||
setEngine(DEFAULT_ENGINE);
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function onFormChange() {
|
||||
const form = new FormData(document.getElementById("sql-connection-form"));
|
||||
setConfig({
|
||||
username: form.get("username").trim(),
|
||||
password: form.get("password"),
|
||||
host: form.get("host").trim(),
|
||||
port: form.get("port").trim(),
|
||||
database: form.get("database").trim(),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleUpdate(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const form = new FormData(e.target);
|
||||
onSubmit({
|
||||
engine,
|
||||
database_id: form.get("name"),
|
||||
connectionString: assembleConnectionString({ engine, ...config }),
|
||||
});
|
||||
handleClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot do nested forms, it will cause all sorts of issues, so we portal this out
|
||||
// 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="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
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
className="border-none transition-all duration-300 text-gray-400 bg-transparent hover:border-white/60 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
||||
data-modal-hide="staticModal"
|
||||
>
|
||||
<X className="text-gray-300 text-lg" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
id="sql-connection-form"
|
||||
onSubmit={handleUpdate}
|
||||
onChange={onFormChange}
|
||||
>
|
||||
<div className="py-[17px] px-[20px] flex flex-col gap-y-6">
|
||||
<p className="text-sm text-white">
|
||||
Add the connection information for your database below and it
|
||||
will be available for future SQL agent calls.
|
||||
</p>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="border border-red-800 bg-zinc-800 p-4 rounded-lg flex items-center gap-x-2 text-sm text-red-400">
|
||||
<WarningOctagon size={28} className="shrink-0" />
|
||||
<p>
|
||||
<b>WARNING:</b> The SQL agent has been <i>instructed</i> to
|
||||
only perform non-modifying queries. This <b>does not</b>{" "}
|
||||
prevent a hallucination from still deleting data. Only
|
||||
connect with a user who has <b>READ_ONLY</b> permissions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="text-white text-sm font-semibold block my-4">
|
||||
Select your SQL engine
|
||||
</label>
|
||||
<div className="flex w-full flex-wrap gap-x-4">
|
||||
<DBEngine
|
||||
provider="postgresql"
|
||||
active={engine === "postgresql"}
|
||||
onClick={() => setEngine("postgresql")}
|
||||
/>
|
||||
<DBEngine
|
||||
provider="mysql"
|
||||
active={engine === "mysql"}
|
||||
onClick={() => setEngine("mysql")}
|
||||
/>
|
||||
<DBEngine
|
||||
provider="sql-server"
|
||||
active={engine === "sql-server"}
|
||||
onClick={() => setEngine("sql-server")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Connection name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="a unique name to identify this SQL connection"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-x-2">
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Database user
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="username"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="root"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Database user password
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="password"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="password123"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-x-2">
|
||||
<div className="flex flex-col w-full">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Server endpoint
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="host"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="the hostname or endpoint for your database"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-30">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="port"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="3306"
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Database
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="database"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="the database the agent will interact with"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-white/40 text-sm">
|
||||
{assembleConnectionString({ engine, ...config })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex w-full justify-between items-center p-3 space-x-2 border-t rounded-b border-gray-500/50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="border-none text-xs px-2 py-1 font-semibold rounded-lg bg-white hover:bg-transparent border-2 border-transparent hover:border-white hover:text-white h-[32px] w-fit -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form="sql-connection-form"
|
||||
className="border-none text-xs px-2 py-1 font-semibold rounded-lg bg-[#46C8FF] hover:bg-[#2C2F36] border-2 border-transparent hover:border-[#46C8FF] hover:text-white h-[32px] w-fit -mr-8 whitespace-nowrap shadow-[0_4px_14px_rgba(0,0,0,0.25)]"
|
||||
>
|
||||
Save connection
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>,
|
||||
document.getElementById("workspace-agent-settings-container")
|
||||
);
|
||||
}
|
||||
|
||||
function DBEngine({ provider, active, onClick }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`flex flex-col p-4 border border-white/40 bg-zinc-800 rounded-lg w-fit hover:bg-zinc-700 ${
|
||||
active ? "!bg-blue-500/50" : ""
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={DB_LOGOS[provider]}
|
||||
className="h-[100px] rounded-md"
|
||||
alt="PostgreSQL"
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
109
frontend/src/pages/Admin/Agents/SQLConnectorSelection/index.jsx
Normal file
109
frontend/src/pages/Admin/Agents/SQLConnectorSelection/index.jsx
Normal file
@ -0,0 +1,109 @@
|
||||
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,
|
||||
}) {
|
||||
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;
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<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 }])
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
export default function SearchProviderItem({ provider, checked, onClick }) {
|
||||
const { name, value, logo, description } = provider;
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-white/10 ${
|
||||
checked ? "bg-white/10" : ""
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
value={value}
|
||||
className="peer hidden"
|
||||
checked={checked}
|
||||
readOnly={true}
|
||||
formNoValidate={true}
|
||||
/>
|
||||
<div className="flex gap-x-4 items-center">
|
||||
<img src={logo} alt={`${name} logo`} className="w-10 h-10 rounded-md" />
|
||||
<div className="flex flex-col">
|
||||
<div className="text-sm font-semibold text-white">{name}</div>
|
||||
<div className="mt-1 text-xs text-[#D2D5DB]">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
export function GoogleSearchOptions({ settings }) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm text-white/60 my-2">
|
||||
You can get a free search engine & API key{" "}
|
||||
<a
|
||||
href="https://programmablesearchengine.google.com/controlpanel/create"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
from Google here.
|
||||
</a>
|
||||
</p>
|
||||
<div className="flex gap-x-4">
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Search engine ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="env::AgentGoogleSearchEngineId"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="Google Search Engine Id"
|
||||
defaultValue={settings?.AgentGoogleSearchEngineId}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
Programmatic Access API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="env::AgentGoogleSearchEngineKey"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="Google Search Engine API Key"
|
||||
defaultValue={
|
||||
settings?.AgentGoogleSearchEngineKey ? "*".repeat(20) : ""
|
||||
}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SerperDotDevOptions({ settings }) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm text-white/60 my-2">
|
||||
You can get a free API key{" "}
|
||||
<a
|
||||
href="https://serper.dev"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
from Serper.dev.
|
||||
</a>
|
||||
</p>
|
||||
<div className="flex gap-x-4">
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="env::AgentSerperApiKey"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="Serper.dev API Key"
|
||||
defaultValue={settings?.AgentSerperApiKey ? "*".repeat(20) : ""}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function BingSearchOptions({ settings }) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-sm text-white/60 my-2">
|
||||
You can get a Bing Web Search API subscription key{" "}
|
||||
<a
|
||||
href="https://portal.azure.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
from the Azure portal.
|
||||
</a>
|
||||
</p>
|
||||
<div className="flex gap-x-4">
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="env::AgentBingSearchApiKey"
|
||||
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||
placeholder="Bing Web Search API Key"
|
||||
defaultValue={settings?.AgentBingSearchApiKey ? "*".repeat(20) : ""}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-white/60 my-2">
|
||||
To set up a Bing Web Search API subscription:
|
||||
</p>
|
||||
<ol className="list-decimal text-sm text-white/60 ml-6">
|
||||
<li>
|
||||
Go to the Azure portal:{" "}
|
||||
<a
|
||||
href="https://portal.azure.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-300 underline"
|
||||
>
|
||||
https://portal.azure.com/
|
||||
</a>
|
||||
</li>
|
||||
<li>Create a new Azure account or sign in with an existing one.</li>
|
||||
<li>
|
||||
Navigate to the "Create a resource" section and search for "Bing
|
||||
Search v7".
|
||||
</li>
|
||||
<li>
|
||||
Select the "Bing Search v7" resource and create a new subscription.
|
||||
</li>
|
||||
<li>
|
||||
Choose the pricing tier that suits your needs (free tier available).
|
||||
</li>
|
||||
<li>Obtain the API key for your Bing Web Search subscription.</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
204
frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx
Normal file
204
frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx
Normal file
@ -0,0 +1,204 @@
|
||||
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 { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
|
||||
import SearchProviderItem from "./SearchProviderItem";
|
||||
import {
|
||||
SerperDotDevOptions,
|
||||
GoogleSearchOptions,
|
||||
BingSearchOptions,
|
||||
} 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.",
|
||||
},
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
162
frontend/src/pages/Admin/Agents/index.jsx
Normal file
162
frontend/src/pages/Admin/Agents/index.jsx
Normal file
@ -0,0 +1,162 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Sidebar from "@/components/SettingsSidebar";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import Admin from "@/models/admin";
|
||||
import showToast from "@/utils/toast";
|
||||
import CTAButton from "@/components/lib/CTAButton";
|
||||
import AgentWebSearchSelection from "./WebSearchSelection";
|
||||
import AgentSQLConnectorSelection from "./SQLConnectorSelection";
|
||||
import GenericSkill from "./GenericSkill";
|
||||
|
||||
const skillComponents = {
|
||||
"web-search": AgentWebSearchSelection,
|
||||
"sql-connector": AgentSQLConnectorSelection,
|
||||
"rag-memory": GenericSkill,
|
||||
"view-summarize": GenericSkill,
|
||||
"scrape-websites": GenericSkill,
|
||||
"create-chart": GenericSkill,
|
||||
"save-file": GenericSkill,
|
||||
};
|
||||
|
||||
const skillSettings = {
|
||||
"web-search": {
|
||||
title: "Web Search",
|
||||
},
|
||||
"sql-connector": {
|
||||
title: "SQL Connector",
|
||||
},
|
||||
"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.',
|
||||
enabled: true,
|
||||
disabled: true,
|
||||
},
|
||||
"view-summarize": {
|
||||
title: "View & summarize documents",
|
||||
description:
|
||||
"Allow the agent to list and summarize the content of workspace files currently embedded.",
|
||||
enabled: true,
|
||||
disabled: true,
|
||||
},
|
||||
"scrape-websites": {
|
||||
title: "Scrape websites",
|
||||
description: "Allow the agent to visit and scrape the content of websites.",
|
||||
enabled: true,
|
||||
disabled: true,
|
||||
},
|
||||
"create-chart": {
|
||||
title: "Generate charts",
|
||||
description:
|
||||
"Enable the default agent to generate various types of charts from data provided or given in chat.",
|
||||
skill: "create-chart",
|
||||
},
|
||||
"save-file": {
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
export default function AdminAgents() {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [settings, setSettings] = useState({});
|
||||
const [selectedSkill, setSelectedSkill] = useState("web-search");
|
||||
const [agentSkills, setAgentSkills] = useState([]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
await Admin.updateSystemPreferences({
|
||||
...settings,
|
||||
default_agent_skills: agentSkills,
|
||||
});
|
||||
setSaving(false);
|
||||
setHasChanges(false);
|
||||
showToast("System preferences updated successfully.", "success");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchSettings() {
|
||||
const _settings = await Admin.systemPreferences();
|
||||
setSettings(_settings?.settings ?? {});
|
||||
setAgentSkills(_settings?.settings?.default_agent_skills ?? []);
|
||||
}
|
||||
fetchSettings();
|
||||
}, []);
|
||||
|
||||
function toggleAgentSkill(skillName = "") {
|
||||
setAgentSkills((prev) => {
|
||||
setHasChanges(true);
|
||||
return prev.includes(skillName)
|
||||
? prev.filter((name) => name !== skillName)
|
||||
: [...prev, skillName];
|
||||
});
|
||||
}
|
||||
|
||||
const SelectedSkillComponent = skillComponents[selectedSkill];
|
||||
|
||||
return (
|
||||
<div 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] bg-main-gradient w-full h-full flex flex-col"
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col w-full px-1 md:pl-6 md:pr-[50px] md:py-6 py-16 flex-grow"
|
||||
>
|
||||
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||
<div className="items-center">
|
||||
<p className="text-lg leading-6 font-bold text-white">
|
||||
Available Agents
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs leading-[18px] font-base text-white text-opacity-60">
|
||||
Improve the natural abilities of the default agent with these
|
||||
pre-built skills. This setup applies to all workspaces.
|
||||
</p>
|
||||
</div>
|
||||
{hasChanges && (
|
||||
<div className="flex justify-end">
|
||||
<CTAButton type="submit" className="mt-3 mr-0">
|
||||
{saving ? "Saving..." : "Save changes"}
|
||||
</CTAButton>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-[#222628] rounded-lg mt-5 flex flex-grow overflow-y-scroll">
|
||||
<div className="w-1/4 min-w-[200px] p-5 flex flex-col gap-y-2">
|
||||
{Object.keys(skillComponents).map((skill) => (
|
||||
<button
|
||||
key={skill}
|
||||
type="button"
|
||||
className={`text-white w-full justify-start flex text-sm ${
|
||||
selectedSkill === skill ? "bg-white/10 font-semibold" : ""
|
||||
}`}
|
||||
onClick={() => setSelectedSkill(skill)}
|
||||
>
|
||||
{skillSettings[skill]?.title || skill}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-[2px] bg-white/20 mx-4" />
|
||||
<div className="w-3/4 flex-grow p-6">
|
||||
<SelectedSkillComponent
|
||||
skill={selectedSkill}
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={agentSkills.includes(
|
||||
skillSettings[selectedSkill]?.skill
|
||||
)}
|
||||
{...skillSettings[selectedSkill]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -10,6 +10,8 @@ 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({});
|
||||
@ -18,6 +20,8 @@ export default function WorkspaceAgentConfiguration({ workspace }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [agentSkills, setAgentSkills] = useState([]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const formEl = useRef(null);
|
||||
useEffect(() => {
|
||||
async function fetchSettings() {
|
||||
@ -96,11 +100,23 @@ export default function WorkspaceAgentConfiguration({ workspace }) {
|
||||
workspace={workspace}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
<AvailableAgentSkills
|
||||
skills={agentSkills}
|
||||
toggleAgentSkill={toggleAgentSkill}
|
||||
settings={settings}
|
||||
/>
|
||||
{!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,76 +159,77 @@ function LoadingSkeleton() {
|
||||
);
|
||||
}
|
||||
|
||||
function AvailableAgentSkills({ skills, settings, toggleAgentSkill }) {
|
||||
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")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// TODO: remove
|
||||
// function AvailableAgentSkills({ skills, settings, toggleAgentSkill }) {
|
||||
// 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")}
|
||||
// />
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
@ -117,6 +117,9 @@ export default {
|
||||
appearance: () => {
|
||||
return "/settings/appearance";
|
||||
},
|
||||
agentSkills: () => {
|
||||
return "/settings/agents";
|
||||
},
|
||||
apiKeys: () => {
|
||||
return "/settings/api-keys";
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user