mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2025-03-13 21:52:22 +00:00
Add support for custom agent skills via plugins (#2202)
* Add support for custom agent skills via plugins Update Admin.systemPreferences to updated endpoint (legacy has deprecation notice * lint * dev build * patch safeJson patch label loading * allow plugins with no config options * lint * catch invalid setupArgs in frontend * update link to docs page for agent skills * remove unneeded files --------- Co-authored-by: shatfield4 <seanhatfield5@gmail.com>
This commit is contained in:
parent
f3f6299aae
commit
d1103e2b71
17 changed files with 768 additions and 30 deletions
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
|
@ -6,7 +6,7 @@ concurrency:
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: ['chrome-extension'] # put your current branch to create a build. Core team only.
|
||||
branches: ['agent-skill-plugins'] # put your current branch to create a build. Core team only.
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'cloud-deployments/*'
|
||||
|
|
|
@ -156,6 +156,8 @@ const Admin = {
|
|||
},
|
||||
|
||||
// System Preferences
|
||||
// TODO: remove this in favor of systemPreferencesByFields
|
||||
// DEPRECATED: use systemPreferencesByFields instead
|
||||
systemPreferences: async () => {
|
||||
return await fetch(`${API_BASE}/admin/system-preferences`, {
|
||||
method: "GET",
|
||||
|
@ -167,6 +169,26 @@ const Admin = {
|
|||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches system preferences by fields
|
||||
* @param {string[]} labels - Array of labels for settings
|
||||
* @returns {Promise<{settings: Object, error: string}>} - System preferences object
|
||||
*/
|
||||
systemPreferencesByFields: async (labels = []) => {
|
||||
return await fetch(
|
||||
`${API_BASE}/admin/system-preferences-for?labels=${labels.join(",")}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return null;
|
||||
});
|
||||
},
|
||||
updateSystemPreferences: async (updates = {}) => {
|
||||
return await fetch(`${API_BASE}/admin/system-preferences`, {
|
||||
method: "POST",
|
||||
|
|
43
frontend/src/models/experimental/agentPlugins.js
Normal file
43
frontend/src/models/experimental/agentPlugins.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { API_BASE } from "@/utils/constants";
|
||||
import { baseHeaders } from "@/utils/request";
|
||||
|
||||
const AgentPlugins = {
|
||||
toggleFeature: async function (hubId, active = false) {
|
||||
return await fetch(
|
||||
`${API_BASE}/experimental/agent-plugins/${hubId}/toggle`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ active }),
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Could not update agent plugin status.");
|
||||
return true;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
updatePluginConfig: async function (hubId, updates = {}) {
|
||||
return await fetch(
|
||||
`${API_BASE}/experimental/agent-plugins/${hubId}/config`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ updates }),
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error("Could not update agent plugin config.");
|
||||
return true;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default AgentPlugins;
|
|
@ -2,6 +2,7 @@ import { API_BASE, AUTH_TIMESTAMP, fullApiUrl } from "@/utils/constants";
|
|||
import { baseHeaders, safeJsonParse } from "@/utils/request";
|
||||
import DataConnector from "./dataConnector";
|
||||
import LiveDocumentSync from "./experimental/liveSync";
|
||||
import AgentPlugins from "./experimental/agentPlugins";
|
||||
|
||||
const System = {
|
||||
cacheKeys: {
|
||||
|
@ -675,6 +676,7 @@ const System = {
|
|||
},
|
||||
experimentalFeatures: {
|
||||
liveSync: LiveDocumentSync,
|
||||
agentPlugins: AgentPlugins,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { Plug } from "@phosphor-icons/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { sentenceCase } from "text-case";
|
||||
|
||||
/**
|
||||
* Converts setup_args to inputs for the form builder
|
||||
* @param {object} setupArgs - The setup arguments object
|
||||
* @returns {object} - The inputs object
|
||||
*/
|
||||
function inputsFromArgs(setupArgs) {
|
||||
if (
|
||||
!setupArgs ||
|
||||
setupArgs.constructor?.call?.().toString() !== "[object Object]"
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return Object.entries(setupArgs).reduce(
|
||||
(acc, [key, props]) => ({
|
||||
...acc,
|
||||
[key]: props.hasOwnProperty("value")
|
||||
? props.value
|
||||
: props?.input?.default || "",
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imported skill config component for imported skills only.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function ImportedSkillConfig({
|
||||
selectedSkill, // imported skill config object
|
||||
setImportedSkills, // function to set imported skills since config is file-write
|
||||
}) {
|
||||
const [config, setConfig] = useState(selectedSkill);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [inputs, setInputs] = useState(
|
||||
inputsFromArgs(selectedSkill?.setup_args)
|
||||
);
|
||||
|
||||
const hasSetupArgs =
|
||||
selectedSkill?.setup_args &&
|
||||
Object.keys(selectedSkill.setup_args).length > 0;
|
||||
|
||||
async function toggleSkill() {
|
||||
const updatedConfig = { ...selectedSkill, active: !config.active };
|
||||
await System.experimentalFeatures.agentPlugins.updatePluginConfig(
|
||||
config.hubId,
|
||||
{ active: !config.active }
|
||||
);
|
||||
setImportedSkills((prev) =>
|
||||
prev.map((s) => (s.hubId === config.hubId ? updatedConfig : s))
|
||||
);
|
||||
setConfig(updatedConfig);
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const errors = [];
|
||||
const updatedConfig = { ...config };
|
||||
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
const settings = config.setup_args[key];
|
||||
if (settings.required && !value) {
|
||||
errors.push(`${key} is required to have a value.`);
|
||||
continue;
|
||||
}
|
||||
if (typeof value !== settings.type) {
|
||||
errors.push(`${key} must be of type ${settings.type}.`);
|
||||
continue;
|
||||
}
|
||||
updatedConfig.setup_args[key].value = value;
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach((error) => showToast(error, "error"));
|
||||
return;
|
||||
}
|
||||
|
||||
await System.experimentalFeatures.agentPlugins.updatePluginConfig(
|
||||
config.hubId,
|
||||
updatedConfig
|
||||
);
|
||||
setConfig(updatedConfig);
|
||||
setImportedSkills((prev) =>
|
||||
prev.map((skill) =>
|
||||
skill.hubId === config.hubId ? updatedConfig : skill
|
||||
)
|
||||
);
|
||||
showToast("Skill config updated successfully.", "success");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setHasChanges(
|
||||
JSON.stringify(inputs) !==
|
||||
JSON.stringify(inputsFromArgs(selectedSkill.setup_args))
|
||||
);
|
||||
}, [inputs]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-2">
|
||||
<div className="flex flex-col gap-y-[18px] max-w-[500px]">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Plug size={24} color="white" weight="bold" />
|
||||
<label htmlFor="name" className="text-white text-md font-bold">
|
||||
{sentenceCase(config.name)}
|
||||
</label>
|
||||
<label className="border-none relative inline-flex cursor-pointer items-center ml-auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="peer sr-only"
|
||||
checked={config.active}
|
||||
onChange={() => toggleSkill()}
|
||||
/>
|
||||
<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">
|
||||
{config.description} by{" "}
|
||||
<a
|
||||
href={config.author_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white hover:underline"
|
||||
>
|
||||
{config.author}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{hasSetupArgs ? (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{Object.entries(config.setup_args).map(([key, props]) => (
|
||||
<div key={key} className="flex flex-col gap-y-1">
|
||||
<label htmlFor={key} className="text-white text-sm font-bold">
|
||||
{key}
|
||||
</label>
|
||||
<input
|
||||
type={props?.input?.type || "text"}
|
||||
required={props?.input?.required}
|
||||
defaultValue={
|
||||
props.hasOwnProperty("value")
|
||||
? props.value
|
||||
: props?.input?.default || ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
setInputs({ ...inputs, [key]: e.target.value })
|
||||
}
|
||||
placeholder={props?.input?.placeholder || ""}
|
||||
className="bg-transparent border border-white border-opacity-20 rounded-md p-2 text-white text-sm"
|
||||
/>
|
||||
<p className="text-white text-opacity-60 text-xs font-medium py-1.5">
|
||||
{props?.input?.hint}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
{hasChanges && (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
type="button"
|
||||
className="bg-blue-500 text-white rounded-md p-2"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-white text-opacity-60 text-sm font-medium py-1.5">
|
||||
There are no options to modify for this skill.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
59
frontend/src/pages/Admin/Agents/Imported/SkillList/index.jsx
Normal file
59
frontend/src/pages/Admin/Agents/Imported/SkillList/index.jsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { CaretRight } from "@phosphor-icons/react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { sentenceCase } from "text-case";
|
||||
|
||||
export default function ImportedSkillList({
|
||||
skills = [],
|
||||
selectedSkill = null,
|
||||
handleClick = null,
|
||||
}) {
|
||||
if (skills.length === 0)
|
||||
return (
|
||||
<div className="text-white/60 text-center text-xs flex flex-col gap-y-2">
|
||||
<p>No imported skills found</p>
|
||||
<p>
|
||||
Learn about agent skills in the{" "}
|
||||
<a
|
||||
href="https://docs.anythingllm.com/agent/custom/developer-guide"
|
||||
target="_blank"
|
||||
className="text-white/80 hover:underline"
|
||||
>
|
||||
AnythingLLM Agent Docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-white/5 text-white rounded-xl ${
|
||||
isMobile ? "w-full" : "min-w-[360px] w-fit"
|
||||
}`}
|
||||
>
|
||||
{skills.map((config, index) => (
|
||||
<div
|
||||
key={config.hubId}
|
||||
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 === config.hubId ? "bg-white/10" : ""
|
||||
}`}
|
||||
onClick={() => handleClick?.({ ...config, imported: true })}
|
||||
>
|
||||
<div className="text-sm font-light">{sentenceCase(config.name)}</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="text-sm text-white/60 font-medium">
|
||||
{config.active ? "On" : "Off"}
|
||||
</div>
|
||||
<CaretRight size={14} weight="bold" className="text-white/80" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,18 +4,21 @@ import { isMobile } from "react-device-detect";
|
|||
import Admin from "@/models/admin";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { CaretLeft, CaretRight, Robot } from "@phosphor-icons/react";
|
||||
import { CaretLeft, CaretRight, Plug, 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";
|
||||
import ImportedSkillList from "./Imported/SkillList";
|
||||
import ImportedSkillConfig from "./Imported/ImportedSkillConfig";
|
||||
|
||||
export default function AdminAgents() {
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [settings, setSettings] = useState({});
|
||||
const [selectedSkill, setSelectedSkill] = useState("");
|
||||
const [agentSkills, setAgentSkills] = useState([]);
|
||||
const [importedSkills, setImportedSkills] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showSkillModal, setShowSkillModal] = useState(false);
|
||||
const formEl = useRef(null);
|
||||
|
@ -37,9 +40,13 @@ export default function AdminAgents() {
|
|||
useEffect(() => {
|
||||
async function fetchSettings() {
|
||||
const _settings = await System.keys();
|
||||
const _preferences = await Admin.systemPreferences();
|
||||
const _preferences = await Admin.systemPreferencesByFields([
|
||||
"default_agent_skills",
|
||||
"imported_agent_skills",
|
||||
]);
|
||||
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
|
||||
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
||||
setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);
|
||||
setLoading(false);
|
||||
}
|
||||
fetchSettings();
|
||||
|
@ -84,9 +91,13 @@ export default function AdminAgents() {
|
|||
|
||||
if (success) {
|
||||
const _settings = await System.keys();
|
||||
const _preferences = await Admin.systemPreferences();
|
||||
const _preferences = await Admin.systemPreferencesByFields([
|
||||
"default_agent_skills",
|
||||
"imported_agent_skills",
|
||||
]);
|
||||
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
|
||||
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
||||
setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);
|
||||
showToast(`Agent preferences saved successfully.`, "success", {
|
||||
clear: true,
|
||||
});
|
||||
|
@ -97,9 +108,10 @@ export default function AdminAgents() {
|
|||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const SelectedSkillComponent =
|
||||
configurableSkills[selectedSkill]?.component ||
|
||||
defaultSkills[selectedSkill]?.component;
|
||||
const SelectedSkillComponent = selectedSkill.imported
|
||||
? ImportedSkillConfig
|
||||
: configurableSkills[selectedSkill]?.component ||
|
||||
defaultSkills[selectedSkill]?.component;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
@ -157,6 +169,16 @@ export default function AdminAgents() {
|
|||
}}
|
||||
activeSkills={agentSkills}
|
||||
/>
|
||||
|
||||
<div className="text-white flex items-center gap-x-2">
|
||||
<Plug size={24} />
|
||||
<p className="text-lg font-medium">Custom Skills</p>
|
||||
</div>
|
||||
<ImportedSkillList
|
||||
skills={importedSkills}
|
||||
selectedSkill={selectedSkill}
|
||||
handleClick={setSelectedSkill}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Selected agent skill modal */}
|
||||
|
@ -181,17 +203,27 @@ export default function AdminAgents() {
|
|||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="bg-[#303237] text-white rounded-xl p-4">
|
||||
{SelectedSkillComponent ? (
|
||||
<SelectedSkillComponent
|
||||
skill={configurableSkills[selectedSkill]?.skill}
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={agentSkills.includes(
|
||||
configurableSkills[selectedSkill]?.skill
|
||||
<>
|
||||
{selectedSkill.imported ? (
|
||||
<ImportedSkillConfig
|
||||
key={selectedSkill.hubId}
|
||||
selectedSkill={selectedSkill}
|
||||
setImportedSkills={setImportedSkills}
|
||||
/>
|
||||
) : (
|
||||
<SelectedSkillComponent
|
||||
skill={configurableSkills[selectedSkill]?.skill}
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={agentSkills.includes(
|
||||
configurableSkills[selectedSkill]?.skill
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
{...(configurableSkills[selectedSkill] ||
|
||||
defaultSkills[selectedSkill])}
|
||||
/>
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
{...(configurableSkills[selectedSkill] ||
|
||||
defaultSkills[selectedSkill])}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-white/60">
|
||||
<Robot size={40} />
|
||||
|
@ -216,7 +248,7 @@ export default function AdminAgents() {
|
|||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
onChange={() => setHasChanges(true)}
|
||||
onChange={() => !selectedSkill.imported && setHasChanges(true)}
|
||||
ref={formEl}
|
||||
className="flex-1 flex gap-x-6 p-4 mt-10"
|
||||
>
|
||||
|
@ -247,23 +279,43 @@ export default function AdminAgents() {
|
|||
handleClick={setSelectedSkill}
|
||||
activeSkills={agentSkills}
|
||||
/>
|
||||
|
||||
<div className="text-white flex items-center gap-x-2">
|
||||
<Plug size={24} />
|
||||
<p className="text-lg font-medium">Custom Skills</p>
|
||||
</div>
|
||||
<ImportedSkillList
|
||||
skills={importedSkills}
|
||||
selectedSkill={selectedSkill}
|
||||
handleClick={setSelectedSkill}
|
||||
/>
|
||||
</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
|
||||
<>
|
||||
{selectedSkill.imported ? (
|
||||
<ImportedSkillConfig
|
||||
key={selectedSkill.hubId}
|
||||
selectedSkill={selectedSkill}
|
||||
setImportedSkills={setImportedSkills}
|
||||
/>
|
||||
) : (
|
||||
<SelectedSkillComponent
|
||||
skill={configurableSkills[selectedSkill]?.skill}
|
||||
settings={settings}
|
||||
toggleSkill={toggleAgentSkill}
|
||||
enabled={agentSkills.includes(
|
||||
configurableSkills[selectedSkill]?.skill
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
{...(configurableSkills[selectedSkill] ||
|
||||
defaultSkills[selectedSkill])}
|
||||
/>
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
{...(configurableSkills[selectedSkill] ||
|
||||
defaultSkills[selectedSkill])}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full text-white/60">
|
||||
<Robot size={40} />
|
||||
|
|
1
server/.gitignore
vendored
1
server/.gitignore
vendored
|
@ -8,6 +8,7 @@ storage/tmp/*
|
|||
storage/vector-cache/*.json
|
||||
storage/exports
|
||||
storage/imports
|
||||
storage/plugins/agent-skills/*
|
||||
!storage/documents/DOCUMENTS.md
|
||||
logs/server.log
|
||||
*.db
|
||||
|
|
|
@ -24,6 +24,7 @@ const {
|
|||
ROLES,
|
||||
} = require("../utils/middleware/multiUserProtected");
|
||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||
const ImportedPlugin = require("../utils/agents/imported");
|
||||
|
||||
function adminEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
@ -311,7 +312,109 @@ function adminEndpoints(app) {
|
|||
}
|
||||
);
|
||||
|
||||
// TODO: Allow specification of which props to get instead of returning all of them all the time.
|
||||
// System preferences but only by array of labels
|
||||
app.get(
|
||||
"/admin/system-preferences-for",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const requestedSettings = {};
|
||||
const labels = request.query.labels?.split(",") || [];
|
||||
const needEmbedder = [
|
||||
"text_splitter_chunk_size",
|
||||
"max_embed_chunk_size",
|
||||
];
|
||||
const noRecord = [
|
||||
"max_embed_chunk_size",
|
||||
"agent_sql_connections",
|
||||
"imported_agent_skills",
|
||||
"feature_flags",
|
||||
"meta_page_title",
|
||||
"meta_page_favicon",
|
||||
];
|
||||
|
||||
for (const label of labels) {
|
||||
// Skip any settings that are not explicitly defined as public
|
||||
if (!SystemSettings.publicFields.includes(label)) continue;
|
||||
|
||||
// Only get the embedder if the setting actually needs it
|
||||
let embedder = needEmbedder.includes(label)
|
||||
? getEmbeddingEngineSelection()
|
||||
: null;
|
||||
// Only get the record from db if the setting actually needs it
|
||||
let setting = noRecord.includes(label)
|
||||
? null
|
||||
: await SystemSettings.get({ label });
|
||||
|
||||
switch (label) {
|
||||
case "limit_user_messages":
|
||||
requestedSettings[label] = setting?.value === "true";
|
||||
break;
|
||||
case "message_limit":
|
||||
requestedSettings[label] = setting?.value
|
||||
? Number(setting.value)
|
||||
: 10;
|
||||
break;
|
||||
case "footer_data":
|
||||
requestedSettings[label] = setting?.value ?? JSON.stringify([]);
|
||||
break;
|
||||
case "support_email":
|
||||
requestedSettings[label] = setting?.value || null;
|
||||
break;
|
||||
case "text_splitter_chunk_size":
|
||||
requestedSettings[label] =
|
||||
setting?.value || embedder?.embeddingMaxChunkLength || null;
|
||||
break;
|
||||
case "text_splitter_chunk_overlap":
|
||||
requestedSettings[label] = setting?.value || null;
|
||||
break;
|
||||
case "max_embed_chunk_size":
|
||||
requestedSettings[label] =
|
||||
embedder?.embeddingMaxChunkLength || 1000;
|
||||
break;
|
||||
case "agent_search_provider":
|
||||
requestedSettings[label] = setting?.value || null;
|
||||
break;
|
||||
case "agent_sql_connections":
|
||||
requestedSettings[label] =
|
||||
await SystemSettings.brief.agent_sql_connections();
|
||||
break;
|
||||
case "default_agent_skills":
|
||||
requestedSettings[label] = safeJsonParse(setting?.value, []);
|
||||
break;
|
||||
case "imported_agent_skills":
|
||||
requestedSettings[label] = ImportedPlugin.listImportedPlugins();
|
||||
break;
|
||||
case "custom_app_name":
|
||||
requestedSettings[label] = setting?.value || null;
|
||||
break;
|
||||
case "feature_flags":
|
||||
requestedSettings[label] =
|
||||
(await SystemSettings.getFeatureFlags()) || {};
|
||||
break;
|
||||
case "meta_page_title":
|
||||
requestedSettings[label] =
|
||||
await SystemSettings.getValueOrFallback({ label }, null);
|
||||
break;
|
||||
case "meta_page_favicon":
|
||||
requestedSettings[label] =
|
||||
await SystemSettings.getValueOrFallback({ label }, null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.status(200).json({ settings: requestedSettings });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: Delete this endpoint
|
||||
// DEPRECATED - use /admin/system-preferences-for instead with ?labels=... comma separated string of labels
|
||||
app.get(
|
||||
"/admin/system-preferences",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||
|
@ -352,6 +455,7 @@ function adminEndpoints(app) {
|
|||
?.value,
|
||||
[]
|
||||
) || [],
|
||||
imported_agent_skills: ImportedPlugin.listImportedPlugins(),
|
||||
custom_app_name:
|
||||
(await SystemSettings.get({ label: "custom_app_name" }))?.value ||
|
||||
null,
|
||||
|
|
50
server/endpoints/experimental/imported-agent-plugins.js
Normal file
50
server/endpoints/experimental/imported-agent-plugins.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
const ImportedPlugin = require("../../utils/agents/imported");
|
||||
const { reqBody } = require("../../utils/http");
|
||||
const {
|
||||
flexUserRoleValid,
|
||||
ROLES,
|
||||
} = require("../../utils/middleware/multiUserProtected");
|
||||
const { validatedRequest } = require("../../utils/middleware/validatedRequest");
|
||||
|
||||
function importedAgentPluginEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.post(
|
||||
"/experimental/agent-plugins/:hubId/toggle",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
||||
(request, response) => {
|
||||
try {
|
||||
const { hubId } = request.params;
|
||||
const { active } = reqBody(request);
|
||||
const updatedConfig = ImportedPlugin.updateImportedPlugin(hubId, {
|
||||
active: Boolean(active),
|
||||
});
|
||||
response.status(200).json(updatedConfig);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.status(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/experimental/agent-plugins/:hubId/config",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.admin])],
|
||||
(request, response) => {
|
||||
try {
|
||||
const { hubId } = request.params;
|
||||
const { updates } = reqBody(request);
|
||||
const updatedConfig = ImportedPlugin.updateImportedPlugin(
|
||||
hubId,
|
||||
updates
|
||||
);
|
||||
response.status(200).json(updatedConfig);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response.status(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { importedAgentPluginEndpoints };
|
|
@ -1,5 +1,6 @@
|
|||
const { fineTuningEndpoints } = require("./fineTuning");
|
||||
const { liveSyncEndpoints } = require("./liveSync");
|
||||
const { importedAgentPluginEndpoints } = require("./imported-agent-plugins");
|
||||
|
||||
// All endpoints here are not stable and can move around - have breaking changes
|
||||
// or are opt-in features that are not fully released.
|
||||
|
@ -7,6 +8,7 @@ const { liveSyncEndpoints } = require("./liveSync");
|
|||
function experimentalEndpoints(router) {
|
||||
liveSyncEndpoints(router);
|
||||
fineTuningEndpoints(router);
|
||||
importedAgentPluginEndpoints(router);
|
||||
}
|
||||
|
||||
module.exports = { experimentalEndpoints };
|
||||
|
|
|
@ -15,6 +15,23 @@ function isNullOrNaN(value) {
|
|||
|
||||
const SystemSettings = {
|
||||
protectedFields: ["multi_user_mode"],
|
||||
publicFields: [
|
||||
"limit_user_messages",
|
||||
"message_limit",
|
||||
"footer_data",
|
||||
"support_email",
|
||||
"text_splitter_chunk_size",
|
||||
"text_splitter_chunk_overlap",
|
||||
"max_embed_chunk_size",
|
||||
"agent_search_provider",
|
||||
"agent_sql_connections",
|
||||
"default_agent_skills",
|
||||
"imported_agent_skills",
|
||||
"custom_app_name",
|
||||
"feature_flags",
|
||||
"meta_page_title",
|
||||
"meta_page_favicon",
|
||||
],
|
||||
supportedFields: [
|
||||
"limit_user_messages",
|
||||
"message_limit",
|
||||
|
|
|
@ -504,9 +504,13 @@ Only return the role.
|
|||
* @param {string} pluginName this name of the plugin being called
|
||||
* @returns string of the plugin to be called compensating for children denoted by # in the string.
|
||||
* eg: sql-agent:list-database-connections
|
||||
* or is a custom plugin
|
||||
* eg: @@custom-plugin-name
|
||||
*/
|
||||
#parseFunctionName(pluginName = "") {
|
||||
if (!pluginName.includes("#")) return pluginName;
|
||||
if (!pluginName.includes("#") && !pluginName.startsWith("@@"))
|
||||
return pluginName;
|
||||
if (pluginName.startsWith("@@")) return pluginName.replace("@@", "");
|
||||
return pluginName.split("#")[1];
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ const AgentPlugins = require("./aibitat/plugins");
|
|||
const { SystemSettings } = require("../../models/systemSettings");
|
||||
const { safeJsonParse } = require("../http");
|
||||
const Provider = require("./aibitat/providers/ai-provider");
|
||||
const ImportedPlugin = require("./imported");
|
||||
|
||||
const USER_AGENT = {
|
||||
name: "USER",
|
||||
|
@ -27,6 +28,7 @@ const WORKSPACE_AGENT = {
|
|||
functions: [
|
||||
...defaultFunctions,
|
||||
...(await agentSkillsFromSystemSettings()),
|
||||
...(await ImportedPlugin.activeImportedPlugins()),
|
||||
],
|
||||
};
|
||||
},
|
||||
|
|
176
server/utils/agents/imported.js
Normal file
176
server/utils/agents/imported.js
Normal file
|
@ -0,0 +1,176 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { safeJsonParse } = require("../http");
|
||||
const { isWithin, normalizePath } = require("../files");
|
||||
const pluginsPath =
|
||||
process.env.NODE_ENV === "development"
|
||||
? path.resolve(__dirname, "../../storage/plugins/agent-skills")
|
||||
: path.resolve(process.env.STORAGE_DIR, "plugins", "agent-skills");
|
||||
|
||||
class ImportedPlugin {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.handlerLocation = path.resolve(
|
||||
pluginsPath,
|
||||
this.config.hubId,
|
||||
"handler.js"
|
||||
);
|
||||
delete require.cache[require.resolve(this.handlerLocation)];
|
||||
this.handler = require(this.handlerLocation);
|
||||
this.name = config.hubId;
|
||||
this.startupConfig = {
|
||||
params: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the imported plugin handler.
|
||||
* @param {string} hubId - The hub ID of the plugin.
|
||||
* @returns {ImportedPlugin} - The plugin handler.
|
||||
*/
|
||||
static loadPluginByHubId(hubId) {
|
||||
const configLocation = path.resolve(
|
||||
pluginsPath,
|
||||
normalizePath(hubId),
|
||||
"plugin.json"
|
||||
);
|
||||
if (!this.isValidLocation(configLocation)) return;
|
||||
const config = safeJsonParse(fs.readFileSync(configLocation, "utf8"));
|
||||
return new ImportedPlugin(config);
|
||||
}
|
||||
|
||||
static isValidLocation(pathToValidate) {
|
||||
if (!isWithin(pluginsPath, pathToValidate)) return false;
|
||||
if (!fs.existsSync(pathToValidate)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads plugins from `plugins` folder in storage that are custom loaded and defined.
|
||||
* only loads plugins that are active: true.
|
||||
* @returns {Promise<string[]>} - array of plugin names to be loaded later.
|
||||
*/
|
||||
static async activeImportedPlugins() {
|
||||
const plugins = [];
|
||||
const folders = fs.readdirSync(path.resolve(pluginsPath));
|
||||
for (const folder of folders) {
|
||||
const configLocation = path.resolve(
|
||||
pluginsPath,
|
||||
normalizePath(folder),
|
||||
"plugin.json"
|
||||
);
|
||||
if (!this.isValidLocation(configLocation)) continue;
|
||||
const config = safeJsonParse(fs.readFileSync(configLocation, "utf8"));
|
||||
if (config.active) plugins.push(`@@${config.hubId}`);
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all imported plugins.
|
||||
* @returns {Array} - array of plugin configurations (JSON).
|
||||
*/
|
||||
static listImportedPlugins() {
|
||||
const plugins = [];
|
||||
if (!fs.existsSync(pluginsPath)) return plugins;
|
||||
|
||||
const folders = fs.readdirSync(path.resolve(pluginsPath));
|
||||
for (const folder of folders) {
|
||||
const configLocation = path.resolve(
|
||||
pluginsPath,
|
||||
normalizePath(folder),
|
||||
"plugin.json"
|
||||
);
|
||||
if (!this.isValidLocation(configLocation)) continue;
|
||||
const config = safeJsonParse(fs.readFileSync(configLocation, "utf8"));
|
||||
plugins.push(config);
|
||||
}
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a plugin configuration.
|
||||
* @param {string} hubId - The hub ID of the plugin.
|
||||
* @param {object} config - The configuration to update.
|
||||
* @returns {object} - The updated configuration.
|
||||
*/
|
||||
static updateImportedPlugin(hubId, config) {
|
||||
const configLocation = path.resolve(
|
||||
pluginsPath,
|
||||
normalizePath(hubId),
|
||||
"plugin.json"
|
||||
);
|
||||
if (!this.isValidLocation(configLocation)) return;
|
||||
|
||||
const currentConfig = safeJsonParse(
|
||||
fs.readFileSync(configLocation, "utf8"),
|
||||
null
|
||||
);
|
||||
if (!currentConfig) return;
|
||||
|
||||
const updatedConfig = { ...currentConfig, ...config };
|
||||
fs.writeFileSync(configLocation, JSON.stringify(updatedConfig, null, 2));
|
||||
return updatedConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the handler.js file exists for the given plugin.
|
||||
* @param {string} hubId - The hub ID of the plugin.
|
||||
* @returns {boolean} - True if the handler.js file exists, false otherwise.
|
||||
*/
|
||||
static validateImportedPluginHandler(hubId) {
|
||||
const handlerLocation = path.resolve(
|
||||
pluginsPath,
|
||||
normalizePath(hubId),
|
||||
"handler.js"
|
||||
);
|
||||
return this.isValidLocation(handlerLocation);
|
||||
}
|
||||
|
||||
parseCallOptions() {
|
||||
const callOpts = {};
|
||||
if (!this.config.setup_args || typeof this.config.setup_args !== "object") {
|
||||
return callOpts;
|
||||
}
|
||||
for (const [param, definition] of Object.entries(this.config.setup_args)) {
|
||||
if (definition.required && !definition?.value) {
|
||||
console.log(
|
||||
`'${param}' required value for '${this.name}' plugin is missing. Plugin may not function or crash agent.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
callOpts[param] = definition.value || definition.default || null;
|
||||
}
|
||||
return callOpts;
|
||||
}
|
||||
|
||||
plugin(runtimeArgs = {}) {
|
||||
const customFunctions = this.handler.runtime;
|
||||
return {
|
||||
runtimeArgs,
|
||||
name: this.name,
|
||||
config: this.config,
|
||||
setup(aibitat) {
|
||||
aibitat.function({
|
||||
super: aibitat,
|
||||
name: this.name,
|
||||
config: this.config,
|
||||
runtimeArgs: this.runtimeArgs,
|
||||
description: this.config.description,
|
||||
logger: aibitat?.handlerProps?.log || console.log, // Allows plugin to log to the console.
|
||||
introspect: aibitat?.introspect || console.log, // Allows plugin to display a "thought" the chat window UI.
|
||||
examples: this.config.examples ?? [],
|
||||
parameters: {
|
||||
$schema: "http://json-schema.org/draft-07/schema#",
|
||||
type: "object",
|
||||
properties: this.config.entrypoint.params ?? {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
...customFunctions,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ImportedPlugin;
|
|
@ -6,6 +6,7 @@ const {
|
|||
const { WorkspaceChats } = require("../../models/workspaceChats");
|
||||
const { safeJsonParse } = require("../http");
|
||||
const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults");
|
||||
const ImportedPlugin = require("./imported");
|
||||
|
||||
class AgentHandler {
|
||||
#invocationUUID;
|
||||
|
@ -292,6 +293,27 @@ class AgentHandler {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Load imported plugin. This is marked by `@@` in the array of functions to load.
|
||||
// and is the @@hubID of the plugin.
|
||||
if (name.startsWith("@@")) {
|
||||
const hubId = name.replace("@@", "");
|
||||
const valid = ImportedPlugin.validateImportedPluginHandler(hubId);
|
||||
if (!valid) {
|
||||
this.log(
|
||||
`Imported plugin by hubId ${hubId} not found in plugin directory. Skipping inclusion to agent cluster.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const plugin = ImportedPlugin.loadPluginByHubId(hubId);
|
||||
const callOpts = plugin.parseCallOptions();
|
||||
this.aibitat.use(plugin.plugin(callOpts));
|
||||
this.log(
|
||||
`Attached ${plugin.name} (${hubId}) imported plugin to Agent cluster`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load single-stage plugin.
|
||||
if (!AgentPlugins.hasOwnProperty(name)) {
|
||||
this.log(
|
||||
|
|
|
@ -64,6 +64,8 @@ function parseAuthHeader(headerValue = null, apiKey = null) {
|
|||
}
|
||||
|
||||
function safeJsonParse(jsonString, fallback = null) {
|
||||
if (jsonString === null) return fallback;
|
||||
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch {}
|
||||
|
|
Loading…
Add table
Reference in a new issue