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
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@ -6,7 +6,7 @@ concurrency:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
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:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'cloud-deployments/*'
|
- 'cloud-deployments/*'
|
||||||
|
|||||||
@ -156,6 +156,8 @@ const Admin = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// System Preferences
|
// System Preferences
|
||||||
|
// TODO: remove this in favor of systemPreferencesByFields
|
||||||
|
// DEPRECATED: use systemPreferencesByFields instead
|
||||||
systemPreferences: async () => {
|
systemPreferences: async () => {
|
||||||
return await fetch(`${API_BASE}/admin/system-preferences`, {
|
return await fetch(`${API_BASE}/admin/system-preferences`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -167,6 +169,26 @@ const Admin = {
|
|||||||
return null;
|
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 = {}) => {
|
updateSystemPreferences: async (updates = {}) => {
|
||||||
return await fetch(`${API_BASE}/admin/system-preferences`, {
|
return await fetch(`${API_BASE}/admin/system-preferences`, {
|
||||||
method: "POST",
|
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 { baseHeaders, safeJsonParse } from "@/utils/request";
|
||||||
import DataConnector from "./dataConnector";
|
import DataConnector from "./dataConnector";
|
||||||
import LiveDocumentSync from "./experimental/liveSync";
|
import LiveDocumentSync from "./experimental/liveSync";
|
||||||
|
import AgentPlugins from "./experimental/agentPlugins";
|
||||||
|
|
||||||
const System = {
|
const System = {
|
||||||
cacheKeys: {
|
cacheKeys: {
|
||||||
@ -675,6 +676,7 @@ const System = {
|
|||||||
},
|
},
|
||||||
experimentalFeatures: {
|
experimentalFeatures: {
|
||||||
liveSync: LiveDocumentSync,
|
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 Admin from "@/models/admin";
|
||||||
import System from "@/models/system";
|
import System from "@/models/system";
|
||||||
import showToast from "@/utils/toast";
|
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 ContextualSaveBar from "@/components/ContextualSaveBar";
|
||||||
import { castToType } from "@/utils/types";
|
import { castToType } from "@/utils/types";
|
||||||
import { FullScreenLoader } from "@/components/Preloader";
|
import { FullScreenLoader } from "@/components/Preloader";
|
||||||
import { defaultSkills, configurableSkills } from "./skills";
|
import { defaultSkills, configurableSkills } from "./skills";
|
||||||
import { DefaultBadge } from "./Badges/default";
|
import { DefaultBadge } from "./Badges/default";
|
||||||
|
import ImportedSkillList from "./Imported/SkillList";
|
||||||
|
import ImportedSkillConfig from "./Imported/ImportedSkillConfig";
|
||||||
|
|
||||||
export default function AdminAgents() {
|
export default function AdminAgents() {
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [settings, setSettings] = useState({});
|
const [settings, setSettings] = useState({});
|
||||||
const [selectedSkill, setSelectedSkill] = useState("");
|
const [selectedSkill, setSelectedSkill] = useState("");
|
||||||
const [agentSkills, setAgentSkills] = useState([]);
|
const [agentSkills, setAgentSkills] = useState([]);
|
||||||
|
const [importedSkills, setImportedSkills] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showSkillModal, setShowSkillModal] = useState(false);
|
const [showSkillModal, setShowSkillModal] = useState(false);
|
||||||
const formEl = useRef(null);
|
const formEl = useRef(null);
|
||||||
@ -37,9 +40,13 @@ export default function AdminAgents() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchSettings() {
|
async function fetchSettings() {
|
||||||
const _settings = await System.keys();
|
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 } ?? {});
|
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
|
||||||
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
||||||
|
setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
fetchSettings();
|
fetchSettings();
|
||||||
@ -84,9 +91,13 @@ export default function AdminAgents() {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
const _settings = await System.keys();
|
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 } ?? {});
|
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
|
||||||
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
|
||||||
|
setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);
|
||||||
showToast(`Agent preferences saved successfully.`, "success", {
|
showToast(`Agent preferences saved successfully.`, "success", {
|
||||||
clear: true,
|
clear: true,
|
||||||
});
|
});
|
||||||
@ -97,9 +108,10 @@ export default function AdminAgents() {
|
|||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectedSkillComponent =
|
const SelectedSkillComponent = selectedSkill.imported
|
||||||
configurableSkills[selectedSkill]?.component ||
|
? ImportedSkillConfig
|
||||||
defaultSkills[selectedSkill]?.component;
|
: configurableSkills[selectedSkill]?.component ||
|
||||||
|
defaultSkills[selectedSkill]?.component;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -157,6 +169,16 @@ export default function AdminAgents() {
|
|||||||
}}
|
}}
|
||||||
activeSkills={agentSkills}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Selected agent skill modal */}
|
{/* Selected agent skill modal */}
|
||||||
@ -181,17 +203,27 @@ export default function AdminAgents() {
|
|||||||
<div className="flex-1 overflow-y-auto p-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
<div className="bg-[#303237] text-white rounded-xl p-4">
|
<div className="bg-[#303237] text-white rounded-xl p-4">
|
||||||
{SelectedSkillComponent ? (
|
{SelectedSkillComponent ? (
|
||||||
<SelectedSkillComponent
|
<>
|
||||||
skill={configurableSkills[selectedSkill]?.skill}
|
{selectedSkill.imported ? (
|
||||||
settings={settings}
|
<ImportedSkillConfig
|
||||||
toggleSkill={toggleAgentSkill}
|
key={selectedSkill.hubId}
|
||||||
enabled={agentSkills.includes(
|
selectedSkill={selectedSkill}
|
||||||
configurableSkills[selectedSkill]?.skill
|
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">
|
<div className="flex flex-col items-center justify-center h-full text-white/60">
|
||||||
<Robot size={40} />
|
<Robot size={40} />
|
||||||
@ -216,7 +248,7 @@ export default function AdminAgents() {
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
onChange={() => setHasChanges(true)}
|
onChange={() => !selectedSkill.imported && setHasChanges(true)}
|
||||||
ref={formEl}
|
ref={formEl}
|
||||||
className="flex-1 flex gap-x-6 p-4 mt-10"
|
className="flex-1 flex gap-x-6 p-4 mt-10"
|
||||||
>
|
>
|
||||||
@ -247,23 +279,43 @@ export default function AdminAgents() {
|
|||||||
handleClick={setSelectedSkill}
|
handleClick={setSelectedSkill}
|
||||||
activeSkills={agentSkills}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Selected agent skill setting panel */}
|
{/* Selected agent skill setting panel */}
|
||||||
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
|
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
|
||||||
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4">
|
<div className="bg-[#303237] text-white rounded-xl flex-1 p-4">
|
||||||
{SelectedSkillComponent ? (
|
{SelectedSkillComponent ? (
|
||||||
<SelectedSkillComponent
|
<>
|
||||||
skill={configurableSkills[selectedSkill]?.skill}
|
{selectedSkill.imported ? (
|
||||||
settings={settings}
|
<ImportedSkillConfig
|
||||||
toggleSkill={toggleAgentSkill}
|
key={selectedSkill.hubId}
|
||||||
enabled={agentSkills.includes(
|
selectedSkill={selectedSkill}
|
||||||
configurableSkills[selectedSkill]?.skill
|
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">
|
<div className="flex flex-col items-center justify-center h-full text-white/60">
|
||||||
<Robot size={40} />
|
<Robot size={40} />
|
||||||
|
|||||||
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@ -8,6 +8,7 @@ storage/tmp/*
|
|||||||
storage/vector-cache/*.json
|
storage/vector-cache/*.json
|
||||||
storage/exports
|
storage/exports
|
||||||
storage/imports
|
storage/imports
|
||||||
|
storage/plugins/agent-skills/*
|
||||||
!storage/documents/DOCUMENTS.md
|
!storage/documents/DOCUMENTS.md
|
||||||
logs/server.log
|
logs/server.log
|
||||||
*.db
|
*.db
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const {
|
|||||||
ROLES,
|
ROLES,
|
||||||
} = require("../utils/middleware/multiUserProtected");
|
} = require("../utils/middleware/multiUserProtected");
|
||||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||||
|
const ImportedPlugin = require("../utils/agents/imported");
|
||||||
|
|
||||||
function adminEndpoints(app) {
|
function adminEndpoints(app) {
|
||||||
if (!app) return;
|
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(
|
app.get(
|
||||||
"/admin/system-preferences",
|
"/admin/system-preferences",
|
||||||
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
[validatedRequest, flexUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||||
@ -352,6 +455,7 @@ function adminEndpoints(app) {
|
|||||||
?.value,
|
?.value,
|
||||||
[]
|
[]
|
||||||
) || [],
|
) || [],
|
||||||
|
imported_agent_skills: ImportedPlugin.listImportedPlugins(),
|
||||||
custom_app_name:
|
custom_app_name:
|
||||||
(await SystemSettings.get({ label: "custom_app_name" }))?.value ||
|
(await SystemSettings.get({ label: "custom_app_name" }))?.value ||
|
||||||
null,
|
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 { fineTuningEndpoints } = require("./fineTuning");
|
||||||
const { liveSyncEndpoints } = require("./liveSync");
|
const { liveSyncEndpoints } = require("./liveSync");
|
||||||
|
const { importedAgentPluginEndpoints } = require("./imported-agent-plugins");
|
||||||
|
|
||||||
// All endpoints here are not stable and can move around - have breaking changes
|
// All endpoints here are not stable and can move around - have breaking changes
|
||||||
// or are opt-in features that are not fully released.
|
// or are opt-in features that are not fully released.
|
||||||
@ -7,6 +8,7 @@ const { liveSyncEndpoints } = require("./liveSync");
|
|||||||
function experimentalEndpoints(router) {
|
function experimentalEndpoints(router) {
|
||||||
liveSyncEndpoints(router);
|
liveSyncEndpoints(router);
|
||||||
fineTuningEndpoints(router);
|
fineTuningEndpoints(router);
|
||||||
|
importedAgentPluginEndpoints(router);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { experimentalEndpoints };
|
module.exports = { experimentalEndpoints };
|
||||||
|
|||||||
@ -15,6 +15,23 @@ function isNullOrNaN(value) {
|
|||||||
|
|
||||||
const SystemSettings = {
|
const SystemSettings = {
|
||||||
protectedFields: ["multi_user_mode"],
|
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: [
|
supportedFields: [
|
||||||
"limit_user_messages",
|
"limit_user_messages",
|
||||||
"message_limit",
|
"message_limit",
|
||||||
|
|||||||
@ -504,9 +504,13 @@ Only return the role.
|
|||||||
* @param {string} pluginName this name of the plugin being called
|
* @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.
|
* @returns string of the plugin to be called compensating for children denoted by # in the string.
|
||||||
* eg: sql-agent:list-database-connections
|
* eg: sql-agent:list-database-connections
|
||||||
|
* or is a custom plugin
|
||||||
|
* eg: @@custom-plugin-name
|
||||||
*/
|
*/
|
||||||
#parseFunctionName(pluginName = "") {
|
#parseFunctionName(pluginName = "") {
|
||||||
if (!pluginName.includes("#")) return pluginName;
|
if (!pluginName.includes("#") && !pluginName.startsWith("@@"))
|
||||||
|
return pluginName;
|
||||||
|
if (pluginName.startsWith("@@")) return pluginName.replace("@@", "");
|
||||||
return pluginName.split("#")[1];
|
return pluginName.split("#")[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const AgentPlugins = require("./aibitat/plugins");
|
|||||||
const { SystemSettings } = require("../../models/systemSettings");
|
const { SystemSettings } = require("../../models/systemSettings");
|
||||||
const { safeJsonParse } = require("../http");
|
const { safeJsonParse } = require("../http");
|
||||||
const Provider = require("./aibitat/providers/ai-provider");
|
const Provider = require("./aibitat/providers/ai-provider");
|
||||||
|
const ImportedPlugin = require("./imported");
|
||||||
|
|
||||||
const USER_AGENT = {
|
const USER_AGENT = {
|
||||||
name: "USER",
|
name: "USER",
|
||||||
@ -27,6 +28,7 @@ const WORKSPACE_AGENT = {
|
|||||||
functions: [
|
functions: [
|
||||||
...defaultFunctions,
|
...defaultFunctions,
|
||||||
...(await agentSkillsFromSystemSettings()),
|
...(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 { WorkspaceChats } = require("../../models/workspaceChats");
|
||||||
const { safeJsonParse } = require("../http");
|
const { safeJsonParse } = require("../http");
|
||||||
const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults");
|
const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults");
|
||||||
|
const ImportedPlugin = require("./imported");
|
||||||
|
|
||||||
class AgentHandler {
|
class AgentHandler {
|
||||||
#invocationUUID;
|
#invocationUUID;
|
||||||
@ -292,6 +293,27 @@ class AgentHandler {
|
|||||||
continue;
|
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.
|
// Load single-stage plugin.
|
||||||
if (!AgentPlugins.hasOwnProperty(name)) {
|
if (!AgentPlugins.hasOwnProperty(name)) {
|
||||||
this.log(
|
this.log(
|
||||||
|
|||||||
@ -64,6 +64,8 @@ function parseAuthHeader(headerValue = null, apiKey = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function safeJsonParse(jsonString, fallback = null) {
|
function safeJsonParse(jsonString, fallback = null) {
|
||||||
|
if (jsonString === null) return fallback;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(jsonString);
|
return JSON.parse(jsonString);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user