Agent support for `@agent` default agent inside workspace chat (#1093)

V1 of agent support via built-in `@agent` that can be invoked alongside normal workspace RAG chat.
This commit is contained in:
Timothy Carambat 2024-04-16 10:50:10 -07:00 committed by GitHub
parent 86c01aeb42
commit a5bb77f97a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 5107 additions and 52 deletions

View File

@ -1,3 +1,4 @@
**/server/utils/agents/aibitat/example/**
**/server/storage/documents/**
**/server/storage/vector-cache/**
**/server/storage/*.db

View File

@ -21,6 +21,7 @@ on:
- '**/.env.example'
- '.github/ISSUE_TEMPLATE/**/*'
- 'embed/**/*' # Embed should be published to frontend (yarn build:publish) if any changes are introduced
- 'server/utils/agents/aibitat/example/**/*' # Do not push new image for local dev testing of new aibitat images.
jobs:
push_multi_platform_to_registries:

View File

@ -1,9 +1,12 @@
{
"cSpell.words": [
"AIbitat",
"adoc",
"aibitat",
"anythingllm",
"Astra",
"comkey",
"Deduplicator",
"Dockerized",
"Embeddable",
"epub",
@ -19,6 +22,7 @@
"opendocument",
"openrouter",
"Qdrant",
"Serper",
"vectordbs",
"Weaviate",
"Zilliz"

View File

@ -36,6 +36,7 @@ These instructions are for CLI configuration and assume you are logged in to EC2
These instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user.
1. $sudo vi /etc/nginx/nginx.conf
2. In the nginx.conf file, comment out the default server block configuration for http/port 80. It should look something like the following:
```
# server {
# listen 80;
# listen [::]:80;
@ -53,13 +54,23 @@ These instructions are for CLI configuration and assume you are logged in to EC2
# location = /50x.html {
# }
# }
```
3. Enter ':wq' to save the changes to the nginx default config
## Step 7: Create simple http proxy configuration for AnythingLLM
These instructions are for CLI configuration and assume you are logged in to EC2 instance as the ec2-user.
1. $sudo vi /etc/nginx/conf.d/anything.conf
2. Add the following configuration ensuring that you add your FQDN:.
```
server {
# Enable websocket connections for agent protocol.
location ~* ^/api/agent-invocation/(.*) {
proxy_pass http://0.0.0.0:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
listen 80;
server_name [insert FQDN here];
@ -70,9 +81,16 @@ server {
proxy_read_timeout 605;
send_timeout 605;
keepalive_timeout 605;
# Enable readable HTTP Streaming for LLM streamed responses
proxy_buffering off;
proxy_cache off;
# Proxy your locally running service
proxy_pass http://0.0.0.0:3001;
}
}
```
3. Enter ':wq' to save the changes to the anything config file
## Step 8: Test nginx http proxy config and restart nginx service

View File

@ -9,7 +9,7 @@ const path = require("path");
const { ACCEPTED_MIMES } = require("./utils/constants");
const { reqBody } = require("./utils/http");
const { processSingleFile } = require("./processSingleFile");
const { processLink } = require("./processLink");
const { processLink, getLinkText } = require("./processLink");
const { wipeCollectorStorage } = require("./utils/files");
const extensions = require("./extensions");
const { processRawText } = require("./processRawText");
@ -76,6 +76,26 @@ app.post(
}
);
app.post(
"/util/get-link",
[verifyPayloadIntegrity],
async function (request, response) {
const { link } = reqBody(request);
try {
const { success, content = null } = await getLinkText(link);
response.status(200).json({ url: link, success, content });
} catch (e) {
console.error(e);
response.status(200).json({
url: link,
success: false,
content: null,
});
}
return;
}
);
app.post(
"/process-raw-text",
[verifyPayloadIntegrity],

View File

@ -6,7 +6,7 @@ const { writeToServerDocuments } = require("../../utils/files");
const { tokenizeString } = require("../../utils/tokenizer");
const { default: slugify } = require("slugify");
async function scrapeGenericUrl(link) {
async function scrapeGenericUrl(link, textOnly = false) {
console.log(`-- Working URL ${link} --`);
const content = await getPageContent(link);
@ -19,6 +19,13 @@ async function scrapeGenericUrl(link) {
};
}
if (textOnly) {
return {
success: true,
content,
};
}
const url = new URL(link);
const filename = (url.host + "-" + url.pathname).replace(".", "_");
@ -69,8 +76,26 @@ async function getPageContent(link) {
return pageContents.join(" ");
} catch (error) {
console.error("getPageContent failed!", error);
console.error(
"getPageContent failed to be fetched by puppeteer - falling back to fetch!",
error
);
}
try {
const pageText = await fetch(link, {
method: "GET",
headers: {
"Content-Type": "text/plain",
"User-Agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36,gzip(gfe)",
},
}).then((res) => res.text());
return pageText;
} catch (error) {
console.error("getPageContent failed to be fetched by any method.", error);
}
return null;
}

View File

@ -6,6 +6,12 @@ async function processLink(link) {
return await scrapeGenericUrl(link);
}
async function getLinkText(link) {
if (!validURL(link)) return { success: false, reason: "Not a valid URL." };
return await scrapeGenericUrl(link, true);
}
module.exports = {
processLink,
getLinkText,
};

View File

@ -168,3 +168,16 @@ GID='1000'
#ENABLE_HTTPS="true"
#HTTPS_CERT_PATH="sslcert/cert.pem"
#HTTPS_KEY_PATH="sslcert/key.pem"
###########################################
######## AGENT SERVICE KEYS ###############
###########################################
#------ SEARCH ENGINES -------
#=============================
#------ Google Search -------- https://programmablesearchengine.google.com/controlpanel/create
# AGENT_GSE_KEY=
# AGENT_GSE_CTX=
#------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY=

View File

@ -99,6 +99,10 @@ export default function ChatHistory({ history = [], workspace, sendCommand }) {
const isLastBotReply =
index === history.length - 1 && props.role === "assistant";
if (props?.type === "statusResponse" && !!props.content) {
return <StatusResponse key={props.uuid} props={props} />;
}
if (isLastBotReply && props.animate) {
return (
<PromptReply
@ -147,6 +151,22 @@ export default function ChatHistory({ history = [], workspace, sendCommand }) {
);
}
function StatusResponse({ props }) {
return (
<div className="flex justify-center items-end w-full">
<div className="py-2 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
<div className="flex gap-x-5">
<span
className={`text-xs inline-block p-2 rounded-lg text-white/60 font-mono whitespace-pre-line`}
>
{props.content}
</span>
</div>
</div>
</div>
);
}
function WorkspaceChatSuggestions({ suggestions = [], sendSuggestion }) {
if (suggestions.length === 0) return null;
return (

View File

@ -0,0 +1,189 @@
import { useEffect, useRef, useState } from "react";
import { Tooltip } from "react-tooltip";
import { At, Flask, X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal";
import { useIsAgentSessionActive } from "@/utils/chat/agent";
export default function AvailableAgentsButton({ showing, setShowAgents }) {
const agentSessionActive = useIsAgentSessionActive();
if (agentSessionActive) return null;
return (
<div
id="agent-list-btn"
data-tooltip-id="tooltip-agent-list-btn"
data-tooltip-content="View all available agents you can use for chatting."
aria-label="View all available agents you can use for chatting."
onClick={() => setShowAgents(!showing)}
className={`flex justify-center items-center opacity-60 hover:opacity-100 cursor-pointer ${
showing ? "!opacity-100" : ""
}`}
>
<At className="w-6 h-6 pointer-events-none text-white" />
<Tooltip
id="tooltip-agent-list-btn"
place="top"
delayShow={300}
className="tooltip !text-xs z-99"
/>
</div>
);
}
function AbilityTag({ text }) {
return (
<div className="px-2 bg-white/20 text-white/60 text-black text-xs w-fit rounded-sm">
<p>{text}</p>
</div>
);
}
export function AvailableAgents({
showing,
setShowing,
sendCommand,
promptRef,
}) {
const formRef = useRef(null);
const agentSessionActive = useIsAgentSessionActive();
useEffect(() => {
function listenForOutsideClick() {
if (!showing || !formRef.current) return false;
document.addEventListener("click", closeIfOutside);
}
listenForOutsideClick();
}, [showing, formRef.current]);
const closeIfOutside = ({ target }) => {
if (target.id === "agent-list-btn") return;
const isOutside = !formRef?.current?.contains(target);
if (!isOutside) return;
setShowing(false);
};
if (agentSessionActive) return null;
return (
<>
<div hidden={!showing}>
<div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4">
<div
ref={formRef}
className="w-[600px] p-2 bg-zinc-800 rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex"
>
<button
onClick={() => {
setShowing(false);
sendCommand("@agent ", false);
promptRef?.current?.focus();
}}
className="w-full hover:cursor-pointer hover:bg-zinc-700 px-2 py-2 rounded-xl flex flex-col justify-start group"
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-white text-sm">
<b>@agent</b> - the default agent for this workspace.
</div>
<div className="flex flex-wrap gap-2 mt-2">
<AbilityTag text="rag-search" />
<AbilityTag text="web-scraping" />
<AbilityTag text="web-browsing" />
<AbilityTag text="save-file-to-browser" />
<AbilityTag text="list-documents" />
<AbilityTag text="summarize-document" />
</div>
</div>
</button>
<button
type="button"
disabled={true}
className="w-full rounded-xl flex flex-col justify-start group"
>
<div className="w-full flex-col text-center flex pointer-events-none">
<div className="text-white text-xs text-white/50 italic">
custom agents are coming soon!
</div>
</div>
</button>
</div>
</div>
</div>
{showing && <FirstTimeAgentUser />}
</>
);
}
export function useAvailableAgents() {
const [showAgents, setShowAgents] = useState(false);
return { showAgents, setShowAgents };
}
const SEEN_FT_AGENT_MODAL = "anythingllm_seen_first_time_agent_modal";
function FirstTimeAgentUser() {
const { isOpen, openModal, closeModal } = useModal();
useEffect(() => {
function firstTimeShow() {
if (!window) return;
if (!window.localStorage.getItem(SEEN_FT_AGENT_MODAL)) openModal();
}
firstTimeShow();
}, []);
const dismiss = () => {
closeModal();
window.localStorage.setItem(SEEN_FT_AGENT_MODAL, 1);
};
return (
<ModalWrapper isOpen={isOpen}>
<div className="relative w-[500px] max-w-2xl max-h-full">
<div className="relative bg-main-gradient rounded-lg shadow">
<div className="flex items-center gap-x-1 justify-between p-4 border-b rounded-t border-gray-600">
<Flask className="text-green-400" size={24} weight="fill" />
<h3 className="text-xl font-semibold text-white">
You just discovered Agents!
</h3>
<button
onClick={dismiss}
type="button"
className="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>
<div className="p-6 space-y-6 flex h-full w-full">
<div className="w-full flex flex-col gap-y-4">
<p className="text-white/80 text-xs md:text-sm">
Agents are your LLM, but with special abilities that{" "}
<u>do something beyond chatting with your documents</u>.
<br />
<br />
Now you can use agents for real-time web search and scraping,
saving documents to your browser, summarizing documents, and
more.
<br />
<br />
Currently, agents only work with OpenAI and Anthropic as your
agent LLM. All providers will be supported in the future.
</p>
<p className="text-green-300/60 text-xs md:text-sm">
This feature is currently early access and fully custom agents
with custom integrations & code execution will be in a future
update.
</p>
</div>
</div>
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-600">
<div />
<button
onClick={dismiss}
type="button"
className="px-4 py-2 rounded-lg text-white hover:bg-stone-900 transition-all duration-300"
>
Continue
</button>
</div>
</div>
</div>
</ModalWrapper>
);
}

View File

@ -0,0 +1,23 @@
import { useIsAgentSessionActive } from "@/utils/chat/agent";
export default function EndAgentSession({ setShowing, sendCommand }) {
const isActiveAgentSession = useIsAgentSessionActive();
if (!isActiveAgentSession) return null;
return (
<button
onClick={() => {
setShowing(false);
sendCommand("/exit", true);
}}
className="w-full hover:cursor-pointer hover:bg-zinc-700 px-2 py-2 rounded-xl flex flex-col justify-start"
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-white text-sm font-bold">/exit</div>
<div className="text-white text-opacity-60 text-sm">
Halt the current agent session.
</div>
</div>
</button>
);
}

View File

@ -1,6 +1,8 @@
import { useEffect, useRef, useState } from "react";
import SlashCommandIcon from "./icons/slash-commands-icon.svg";
import { Tooltip } from "react-tooltip";
import ResetCommand from "./reset";
import EndAgentSession from "./endAgentSession";
export default function SlashCommandsButton({ showing, setShowSlashCommand }) {
return (
@ -38,7 +40,6 @@ export function SlashCommands({ showing, setShowing, sendCommand }) {
listenForOutsideClick();
}, [showing, cmdRef.current]);
if (!showing) return null;
const closeIfOutside = ({ target }) => {
if (target.id === "slash-cmd-btn") return;
const isOutside = !cmdRef?.current?.contains(target);
@ -47,25 +48,15 @@ export function SlashCommands({ showing, setShowing, sendCommand }) {
};
return (
<div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4">
<div
ref={cmdRef}
className="w-[600px] p-2 bg-zinc-800 rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex"
>
<button
onClick={() => {
setShowing(false);
sendCommand("/reset", true);
}}
className="w-full hover:cursor-pointer hover:bg-zinc-700 px-2 py-2 rounded-xl flex flex-col justify-start"
<div hidden={!showing}>
<div className="w-full flex justify-center absolute bottom-[130px] md:bottom-[150px] left-0 z-10 px-4">
<div
ref={cmdRef}
className="w-[600px] p-2 bg-zinc-800 rounded-2xl shadow flex-col justify-center items-start gap-2.5 inline-flex"
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-white text-sm font-bold">/reset</div>
<div className="text-white text-opacity-60 text-sm">
Clear your chat history and begin a new chat
</div>
</div>
</button>
<ResetCommand sendCommand={sendCommand} setShowing={setShowing} />
<EndAgentSession sendCommand={sendCommand} setShowing={setShowing} />
</div>
</div>
</div>
);

View File

@ -0,0 +1,23 @@
import { useIsAgentSessionActive } from "@/utils/chat/agent";
export default function ResetCommand({ setShowing, sendCommand }) {
const isActiveAgentSession = useIsAgentSessionActive();
if (isActiveAgentSession) return null; // cannot reset during active agent chat
return (
<button
onClick={() => {
setShowing(false);
sendCommand("/reset", true);
}}
className="w-full hover:cursor-pointer hover:bg-zinc-700 px-2 py-2 rounded-xl flex flex-col justify-start"
>
<div className="w-full flex-col text-left flex pointer-events-none">
<div className="text-white text-sm font-bold">/reset</div>
<div className="text-white text-opacity-60 text-sm">
Clear your chat history and begin a new chat
</div>
</div>
</button>
);
}

View File

@ -28,7 +28,7 @@ export default function StopGenerationButton() {
cx="10"
cy="10.562"
r="9"
stroke-width="2"
strokeWidth="2"
/>
<rect
className="group-hover:fill-[#46C8FF] fill-white"

View File

@ -7,6 +7,10 @@ import { isMobile } from "react-device-detect";
import debounce from "lodash.debounce";
import { PaperPlaneRight } from "@phosphor-icons/react";
import StopGenerationButton from "./StopGenerationButton";
import AvailableAgentsButton, {
AvailableAgents,
useAvailableAgents,
} from "./AgentMenu";
export default function PromptInput({
message,
submit,
@ -15,6 +19,7 @@ export default function PromptInput({
buttonDisabled,
sendCommand,
}) {
const { showAgents, setShowAgents } = useAvailableAgents();
const { showSlashCommand, setShowSlashCommand } = useSlashCommands();
const formRef = useRef(null);
const textareaRef = useRef(null);
@ -45,6 +50,12 @@ export default function PromptInput({
return;
};
const checkForAt = (e) => {
const input = e.target.value;
if (input === "@") return setShowAgents(true);
if (showAgents) return setShowAgents(false);
};
const captureEnter = (event) => {
if (event.keyCode == 13) {
if (!event.shiftKey) {
@ -61,6 +72,7 @@ export default function PromptInput({
};
const watchForSlash = debounce(checkForSlash, 300);
const watchForAt = debounce(checkForAt, 300);
return (
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center">
@ -69,6 +81,12 @@ export default function PromptInput({
setShowing={setShowSlashCommand}
sendCommand={sendCommand}
/>
<AvailableAgents
showing={showAgents}
setShowing={setShowAgents}
sendCommand={sendCommand}
promptRef={textareaRef}
/>
<form
onSubmit={handleSubmit}
className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl"
@ -81,6 +99,7 @@ export default function PromptInput({
onChange={(e) => {
onChange(e);
watchForSlash(e);
watchForAt(e);
adjustTextArea(e);
}}
onKeyDown={captureEnter}
@ -114,6 +133,10 @@ export default function PromptInput({
showing={showSlashCommand}
setShowSlashCommand={setShowSlashCommand}
/>
<AvailableAgentsButton
showing={showAgents}
setShowAgents={setShowAgents}
/>
</div>
</div>
</div>

View File

@ -2,16 +2,24 @@ import { useState, useEffect } from "react";
import ChatHistory from "./ChatHistory";
import PromptInput from "./PromptInput";
import Workspace from "@/models/workspace";
import handleChat from "@/utils/chat";
import handleChat, { ABORT_STREAM_EVENT } from "@/utils/chat";
import { isMobile } from "react-device-detect";
import { SidebarMobileHeader } from "../../Sidebar";
import { useParams } from "react-router-dom";
import { v4 } from "uuid";
import handleSocketResponse, {
websocketURI,
AGENT_SESSION_END,
AGENT_SESSION_START,
} from "@/utils/chat/agent";
export default function ChatContainer({ workspace, knownHistory = [] }) {
const { threadSlug = null } = useParams();
const [message, setMessage] = useState("");
const [loadingResponse, setLoadingResponse] = useState(false);
const [chatHistory, setChatHistory] = useState(knownHistory);
const [socketId, setSocketId] = useState(null);
const [websocket, setWebsocket] = useState(null);
const handleMessageChange = (event) => {
setMessage(event.target.value);
};
@ -68,6 +76,19 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
const remHistory = chatHistory.length > 0 ? chatHistory.slice(0, -1) : [];
var _chatHistory = [...remHistory];
// Override hook for new messages to now go to agents until the connection closes
if (!!websocket) {
if (!promptMessage || !promptMessage?.userMessage) return false;
websocket.send(
JSON.stringify({
type: "awaitingFeedback",
feedback: promptMessage?.userMessage,
})
);
return;
}
// TODO: Simplify this
if (!promptMessage || !promptMessage?.userMessage) return false;
if (!!threadSlug) {
await Workspace.threads.streamChat(
@ -79,7 +100,8 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
setLoadingResponse,
setChatHistory,
remHistory,
_chatHistory
_chatHistory,
setSocketId
)
);
} else {
@ -92,7 +114,8 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
setLoadingResponse,
setChatHistory,
remHistory,
_chatHistory
_chatHistory,
setSocketId
)
);
}
@ -101,6 +124,77 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
loadingResponse === true && fetchReply();
}, [loadingResponse, chatHistory, workspace]);
// TODO: Simplify this WSS stuff
useEffect(() => {
function handleWSS() {
try {
if (!socketId || !!websocket) return;
const socket = new WebSocket(
`${websocketURI()}/api/agent-invocation/${socketId}`
);
window.addEventListener(ABORT_STREAM_EVENT, () => {
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
websocket.close();
});
socket.addEventListener("message", (event) => {
setLoadingResponse(true);
try {
handleSocketResponse(event, setChatHistory);
} catch (e) {
console.error("Failed to parse data");
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
socket.close();
}
setLoadingResponse(false);
});
socket.addEventListener("close", (_event) => {
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
setChatHistory((prev) => [
...prev.filter((msg) => !!msg.content),
{
uuid: v4(),
type: "statusResponse",
content: "Agent session complete.",
role: "assistant",
sources: [],
closed: true,
error: null,
animate: false,
pending: false,
},
]);
setLoadingResponse(false);
setWebsocket(null);
setSocketId(null);
});
setWebsocket(socket);
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));
} catch (e) {
setChatHistory((prev) => [
...prev.filter((msg) => !!msg.content),
{
uuid: v4(),
type: "abort",
content: e.message,
role: "assistant",
sources: [],
closed: true,
error: e.message,
animate: false,
pending: false,
},
]);
setLoadingResponse(false);
setWebsocket(null);
setSocketId(null);
}
}
handleWSS();
}, [socketId]);
return (
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}

View File

@ -553,6 +553,10 @@ dialog::backdrop {
padding-top: 10px;
}
.markdown ol li p a {
text-decoration: underline;
}
.markdown ul {
list-style: revert-layer;
/* color: #cfcfcfcf; */
@ -575,6 +579,10 @@ dialog::backdrop {
line-height: 1.4rem;
}
.markdownul li a {
text-decoration: underline;
}
.markdown ul li > ul {
padding-left: 20px;
margin: 0px;
@ -585,6 +593,11 @@ dialog::backdrop {
margin: 0.35rem;
}
.markdown > p > a,
.markdown p a {
text-decoration: underline;
}
.markdown {
text-wrap: wrap;
}

View File

@ -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")
);
}

View File

@ -0,0 +1,163 @@
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, MagnifyingGlass, X } from "@phosphor-icons/react";
import AgentModelSelection from "../AgentModelSelection";
const ENABLED_PROVIDERS = ["openai", "anthropic"];
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">
<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>
);
}

View File

@ -0,0 +1,112 @@
import useGetProviderModels, {
DISABLED_PROVIDERS,
} from "@/hooks/useGetProvidersModels";
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) => {
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) => {
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) => (
<option
key={model.id}
value={model.id}
selected={workspace?.chatModel === model.id}
>
{model.name}
</option>
))}
</optgroup>
))}
</>
)}
</select>
</div>
);
}

View 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>
);
}

View File

@ -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>
);
}

View File

@ -0,0 +1,84 @@
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"
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"
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>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -0,0 +1,194 @@
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 { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import SearchProviderItem from "./SearchProviderItem";
import {
SerperDotDevOptions,
GoogleSearchOptions,
} 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.",
},
];
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>
);
}

View File

@ -0,0 +1,203 @@
import System from "@/models/system";
import Workspace from "@/models/workspace";
import showToast from "@/utils/toast";
import { castToType } from "@/utils/types";
import { useEffect, useRef, useState } from "react";
import AgentLLMSelection from "./AgentLLMSelection";
import AgentWebSearchSelection from "./WebSearchSelection";
import GenericSkill from "./GenericSkill";
import Admin from "@/models/admin";
import * as Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
export default function WorkspaceAgentConfiguration({ workspace }) {
const [settings, setSettings] = useState({});
const [hasChanges, setHasChanges] = useState(false);
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(true);
const [agentSkills, setAgentSkills] = useState([]);
const formEl = useRef(null);
useEffect(() => {
async function fetchSettings() {
const _settings = await System.keys();
const _preferences = await Admin.systemPreferences();
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
setLoading(false);
}
fetchSettings();
}, []);
const handleUpdate = async (e) => {
setSaving(true);
e.preventDefault();
const data = {
workspace: {},
system: {},
env: {},
};
const form = new FormData(formEl.current);
for (var [key, value] of form.entries()) {
if (key.startsWith("system::")) {
const [_, label] = key.split("system::");
data.system[label] = String(value);
continue;
}
if (key.startsWith("env::")) {
const [_, label] = key.split("env::");
data.env[label] = String(value);
continue;
}
data.workspace[key] = castToType(key, value);
}
const { workspace: updatedWorkspace, message } = await Workspace.update(
workspace.slug,
data.workspace
);
await Admin.updateSystemPreferences(data.system);
await System.updateSystem(data.env);
if (!!updatedWorkspace) {
showToast("Workspace updated!", "success", { clear: true });
} else {
showToast(`Error: ${message}`, "error", { clear: true });
}
setSaving(false);
setHasChanges(false);
};
function toggleAgentSkill(skillName = "") {
setAgentSkills((prev) => {
return prev.includes(skillName)
? prev.filter((name) => name !== skillName)
: [...prev, skillName];
});
}
if (!workspace || loading) return <LoadingSkeleton />;
return (
<div id="workspace-agent-settings-container">
<form
ref={formEl}
onSubmit={handleUpdate}
onChange={() => setHasChanges(true)}
id="agent-settings-form"
className="w-1/2 flex flex-col gap-y-6"
>
<AgentLLMSelection
settings={settings}
workspace={workspace}
setHasChanges={setHasChanges}
/>
<AvailableAgentSkills
skills={agentSkills}
toggleAgentSkill={toggleAgentSkill}
settings={settings}
/>
{hasChanges && (
<button
type="submit"
form="agent-settings-form"
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"
>
{saving ? "Updating agent..." : "Update workspace agent"}
</button>
)}
</form>
</div>
);
function LoadingSkeleton() {
return (
<div id="workspace-agent-settings-container">
<div className="w-1/2 flex flex-col gap-y-6">
<Skeleton.default
height={100}
width="100%"
count={2}
baseColor="#292524"
highlightColor="#4c4948"
enableAnimation={true}
containerClassName="flex flex-col gap-y-1"
/>
<div className="bg-white/10 h-[1px] w-full" />
<Skeleton.default
height={100}
width="100%"
count={2}
baseColor="#292524"
highlightColor="#4c4948"
enableAnimation={true}
containerClassName="flex flex-col gap-y-1 mt-4"
/>
</div>
</div>
);
}
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 and 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 & 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")}
/>
</div>
</div>
);
}
}

View File

@ -9,6 +9,7 @@ import {
ArrowUUpLeft,
ChatText,
Database,
Robot,
User,
Wrench,
} from "@phosphor-icons/react";
@ -19,6 +20,7 @@ import GeneralAppearance from "./GeneralAppearance";
import ChatSettings from "./ChatSettings";
import VectorDatabase from "./VectorDatabase";
import Members from "./Members";
import WorkspaceAgentConfiguration from "./AgentConfig";
import useUser from "@/hooks/useUser";
const TABS = {
@ -26,6 +28,7 @@ const TABS = {
"chat-settings": ChatSettings,
"vector-database": VectorDatabase,
members: Members,
"agent-config": WorkspaceAgentConfiguration,
};
export default function WorkspaceSettings() {
@ -102,6 +105,11 @@ function ShowWorkspaceChat() {
to={paths.workspace.settings.members(slug)}
visible={["admin", "manager"].includes(user?.role)}
/>
<TabItem
title="Agent Configuration"
icon={<Robot className="h-6 w-6" />}
to={paths.workspace.settings.agentConfig(slug)}
/>
</div>
<div className="px-16 py-6">
<TabContent slug={slug} workspace={workspace} />

View File

@ -0,0 +1,103 @@
import { v4 } from "uuid";
import { safeJsonParse } from "../request";
import { saveAs } from "file-saver";
import { API_BASE } from "../constants";
import { useEffect, useState } from "react";
export const AGENT_SESSION_START = "agentSessionStart";
export const AGENT_SESSION_END = "agentSessionEnd";
const handledEvents = [
"statusResponse",
"fileDownload",
"awaitingFeedback",
"wssFailure",
];
export function websocketURI() {
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
if (API_BASE === "/api") return `${wsProtocol}//${window.location.host}`;
return `${wsProtocol}//${new URL(import.meta.env.VITE_API_BASE).host}`;
}
export default function handleSocketResponse(event, setChatHistory) {
const data = safeJsonParse(event.data, null);
if (data === null) return;
// No message type is defined then this is a generic message
// that we need to print to the user as a system response
if (!data.hasOwnProperty("type")) {
return setChatHistory((prev) => {
return [
...prev.filter((msg) => !!msg.content),
{
uuid: v4(),
content: data.content,
role: "assistant",
sources: [],
closed: true,
error: null,
animate: false,
pending: false,
},
];
});
}
if (!handledEvents.includes(data.type) || !data.content) return;
if (data.type === "fileDownload") {
saveAs(data.content.b64Content, data.content.filename ?? "unknown.txt");
return;
}
if (data.type === "wssFailure") {
return setChatHistory((prev) => {
return [
...prev.filter((msg) => !!msg.content),
{
uuid: v4(),
content: data.content,
role: "assistant",
sources: [],
closed: true,
error: data.content,
animate: false,
pending: false,
},
];
});
}
return setChatHistory((prev) => {
return [
...prev.filter((msg) => !!msg.content),
{
uuid: v4(),
type: data.type,
content: data.content,
role: "assistant",
sources: [],
closed: true,
error: null,
animate: false,
pending: false,
},
];
});
}
export function useIsAgentSessionActive() {
const [activeSession, setActiveSession] = useState(false);
useEffect(() => {
function listenForAgentSession() {
if (!window) return;
window.addEventListener(AGENT_SESSION_START, () =>
setActiveSession(true)
);
window.addEventListener(AGENT_SESSION_END, () => setActiveSession(false));
}
listenForAgentSession();
}, []);
return activeSession;
}

View File

@ -6,7 +6,8 @@ export default function handleChat(
setLoadingResponse,
setChatHistory,
remHistory,
_chatHistory
_chatHistory,
setWebsocket
) {
const {
uuid,
@ -18,11 +19,12 @@ export default function handleChat(
chatId = null,
} = chatResult;
if (type === "abort") {
if (type === "abort" || type === "statusResponse") {
setLoadingResponse(false);
setChatHistory([
...remHistory,
{
type,
uuid,
content: textResponse,
role: "assistant",
@ -34,6 +36,7 @@ export default function handleChat(
},
]);
_chatHistory.push({
type,
uuid,
content: textResponse,
role: "assistant",
@ -99,6 +102,8 @@ export default function handleChat(
});
}
setChatHistory([..._chatHistory]);
} else if (type === "agentInitWebsocketConnection") {
setWebsocket(chatResult.websocketUUID);
} else if (type === "finalizeResponseStream") {
const chatIdx = _chatHistory.findIndex((chat) => chat.uuid === uuid);
if (chatIdx !== -1) {

View File

@ -68,6 +68,9 @@ export default {
members: (slug) => {
return `/workspace/${slug}/settings/members`;
},
agentConfig: (slug) => {
return `/workspace/${slug}/settings/agent-config`;
},
},
thread: (wsSlug, threadSlug) => {
return `/workspace/${wsSlug}/t/${threadSlug}`;

View File

@ -164,3 +164,16 @@ WHISPER_PROVIDER="local"
#ENABLE_HTTPS="true"
#HTTPS_CERT_PATH="sslcert/cert.pem"
#HTTPS_KEY_PATH="sslcert/key.pem"
###########################################
######## AGENT SERVICE KEYS ###############
###########################################
#------ SEARCH ENGINES -------
#=============================
#------ Google Search -------- https://programmablesearchengine.google.com/controlpanel/create
# AGENT_GSE_KEY=
# AGENT_GSE_CTX=
#------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY=

View File

@ -17,7 +17,7 @@ const {
canModifyAdmin,
validCanModify,
} = require("../utils/helpers/admin");
const { reqBody, userFromSession } = require("../utils/http");
const { reqBody, userFromSession, safeJsonParse } = require("../utils/http");
const {
strictMultiUserRoleValid,
flexUserRoleValid,
@ -347,6 +347,15 @@ function adminEndpoints(app) {
?.value || null,
max_embed_chunk_size:
getEmbeddingEngineSelection()?.embeddingMaxChunkLength || 1000,
agent_search_provider:
(await SystemSettings.get({ label: "agent_search_provider" }))
?.value || null,
default_agent_skills:
safeJsonParse(
(await SystemSettings.get({ label: "default_agent_skills" }))
?.value,
[]
) || [],
};
response.status(200).json({ settings });
} catch (e) {

View File

@ -0,0 +1,61 @@
const { Telemetry } = require("../models/telemetry");
const {
WorkspaceAgentInvocation,
} = require("../models/workspaceAgentInvocation");
const { AgentHandler } = require("../utils/agents");
const {
WEBSOCKET_BAIL_COMMANDS,
} = require("../utils/agents/aibitat/plugins/websocket");
const { safeJsonParse } = require("../utils/http");
// Setup listener for incoming messages to relay to socket so it can be handled by agent plugin.
function relayToSocket(message) {
if (this.handleFeedback) return this?.handleFeedback?.(message);
this.checkBailCommand(message);
}
function agentWebsocket(app) {
if (!app) return;
app.ws("/agent-invocation/:uuid", async function (socket, request) {
try {
const agentHandler = await new AgentHandler({
uuid: String(request.params.uuid),
}).init();
if (!agentHandler.invocation) {
socket.close();
return;
}
socket.on("message", relayToSocket);
socket.on("close", () => {
agentHandler.closeAlert();
WorkspaceAgentInvocation.close(String(request.params.uuid));
return;
});
socket.checkBailCommand = (data) => {
const content = safeJsonParse(data)?.feedback;
if (WEBSOCKET_BAIL_COMMANDS.includes(content)) {
agentHandler.log(
`User invoked bail command while processing. Closing session now.`
);
agentHandler.aibitat.abort();
socket.close();
return;
}
};
await Telemetry.sendTelemetry("agent_chat_started");
await agentHandler.createAIbitat({ socket });
await agentHandler.startAgentCluster();
} catch (e) {
console.error(e.message);
socket?.send(JSON.stringify({ type: "wssFailure", content: e.message }));
socket?.close();
}
});
}
module.exports = { agentWebsocket };

View File

@ -21,6 +21,7 @@ const { extensionEndpoints } = require("./endpoints/extensions");
const { bootHTTP, bootSSL } = require("./utils/boot");
const { workspaceThreadEndpoints } = require("./endpoints/workspaceThreads");
const { documentEndpoints } = require("./endpoints/document");
const { agentWebsocket } = require("./endpoints/agentWebsocket");
const app = express();
const apiRouter = express.Router();
const FILE_LIMIT = "3GB";
@ -35,6 +36,7 @@ app.use(
})
);
require("express-ws")(app);
app.use("/api", apiRouter);
systemEndpoints(apiRouter);
extensionEndpoints(apiRouter);
@ -46,6 +48,7 @@ inviteEndpoints(apiRouter);
embedManagementEndpoints(apiRouter);
utilEndpoints(apiRouter);
documentEndpoints(apiRouter);
agentWebsocket(apiRouter);
developerEndpoints(app, apiRouter);
// Externally facing embedder endpoints

View File

@ -202,6 +202,15 @@ const Document = {
return { document: null, message: error.message };
}
},
content: async function (docId) {
if (!docId) throw new Error("No workspace docId provided!");
const document = await this.get({ docId: String(docId) });
if (!document) throw new Error(`Could not find a document by id ${docId}`);
const { fileData } = require("../utils/files");
const data = await fileData(document.docpath);
return { title: data.title, content: data.pageContent };
},
};
module.exports = { Document };

View File

@ -22,6 +22,8 @@ const SystemSettings = {
"support_email",
"text_splitter_chunk_size",
"text_splitter_chunk_overlap",
"agent_search_provider",
"default_agent_skills",
],
validations: {
footer_data: (updates) => {
@ -61,6 +63,28 @@ const SystemSettings = {
return 20;
}
},
agent_search_provider: (update) => {
try {
if (!["google-search-engine", "serper-dot-dev"].includes(update))
throw new Error("Invalid SERP provider.");
return String(update);
} catch (e) {
console.error(
`Failed to run validation function on agent_search_provider`,
e.message
);
return null;
}
},
default_agent_skills: (updates) => {
try {
const skills = updates.split(",").filter((skill) => !!skill);
return JSON.stringify(skills);
} catch (e) {
console.error(`Could not validate agent skills.`);
return JSON.stringify([]);
}
},
},
currentSettings: async function () {
const llmProvider = process.env.LLM_PROVIDER;
@ -104,6 +128,13 @@ const SystemSettings = {
// - then it can be shared.
// --------------------------------------------------------
WhisperProvider: process.env.WHISPER_PROVIDER || "local",
// --------------------------------------------------------
// Agent Settings & Configs
// --------------------------------------------------------
AgentGoogleSearchEngineId: process.env.AGENT_GSE_CTX || null,
AgentGoogleSearchEngineKey: process.env.AGENT_GSE_KEY || null,
AgentSerperApiKey: process.env.AGENT_SERPER_DEV_KEY || null,
};
},

View File

@ -37,18 +37,25 @@ const Telemetry = {
sendTelemetry: async function (
event,
eventProperties = {},
subUserId = null
subUserId = null,
silent = false
) {
try {
const { client, distinctId: systemId } = await this.connect();
if (!client) return;
const distinctId = !!subUserId ? `${systemId}::${subUserId}` : systemId;
const properties = { ...eventProperties, runtime: this.runtime() };
console.log(`\x1b[32m[TELEMETRY SENT]\x1b[0m`, {
event,
distinctId,
properties,
});
// Silence some events to keep logs from being too messy in production
// eg: Tool calls from agents spamming the logs.
if (!silent) {
console.log(`\x1b[32m[TELEMETRY SENT]\x1b[0m`, {
event,
distinctId,
properties,
});
}
client.capture({
event,
distinctId,

View File

@ -24,6 +24,8 @@ const Workspace = {
"topN",
"chatMode",
"pfpFilename",
"agentProvider",
"agentModel",
],
new: async function (name = null, creatorId = null) {

View File

@ -0,0 +1,95 @@
const prisma = require("../utils/prisma");
const { v4: uuidv4 } = require("uuid");
const WorkspaceAgentInvocation = {
// returns array of strings with their @ handle.
parseAgents: function (promptString) {
return promptString.split(/\s+/).filter((v) => v.startsWith("@"));
},
close: async function (uuid) {
if (!uuid) return;
try {
await prisma.workspace_agent_invocations.update({
where: { uuid: String(uuid) },
data: { closed: true },
});
} catch {}
},
new: async function ({ prompt, workspace, user = null, thread = null }) {
try {
const invocation = await prisma.workspace_agent_invocations.create({
data: {
uuid: uuidv4(),
workspace_id: workspace.id,
prompt: String(prompt),
user_id: user?.id,
thread_id: thread?.id,
},
});
return { invocation, message: null };
} catch (error) {
console.error(error.message);
return { invocation: null, message: error.message };
}
},
get: async function (clause = {}) {
try {
const invocation = await prisma.workspace_agent_invocations.findFirst({
where: clause,
});
return invocation || null;
} catch (error) {
console.error(error.message);
return null;
}
},
getWithWorkspace: async function (clause = {}) {
try {
const invocation = await prisma.workspace_agent_invocations.findFirst({
where: clause,
include: {
workspace: true,
},
});
return invocation || null;
} catch (error) {
console.error(error.message);
return null;
}
},
delete: async function (clause = {}) {
try {
await prisma.workspace_agent_invocations.delete({
where: clause,
});
return true;
} catch (error) {
console.error(error.message);
return false;
}
},
where: async function (clause = {}, limit = null, orderBy = null) {
try {
const results = await prisma.workspace_agent_invocations.findMany({
where: clause,
...(limit !== null ? { take: limit } : {}),
...(orderBy !== null ? { orderBy } : {}),
});
return results;
} catch (error) {
console.error(error.message);
return [];
}
},
};
module.exports = { WorkspaceAgentInvocation };

View File

@ -33,11 +33,13 @@
"archiver": "^5.3.1",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.2",
"chalk": "^4",
"check-disk-space": "^3.4.0",
"chromadb": "^1.5.2",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-ws": "^5.0.2",
"extract-zip": "^2.0.1",
"graphql": "^16.7.1",
"joi": "^17.11.0",
@ -48,9 +50,12 @@
"mime": "^3.0.0",
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1",
"node-html-markdown": "^1.3.0",
"node-llama-cpp": "^2.8.0",
"openai": "^3.2.1",
"openai:latest": "npm:openai@latest",
"pinecone-client": "^1.1.0",
"pluralize": "^8.0.0",
"posthog-node": "^3.1.1",
"prisma": "5.3.1",
"slugify": "^1.6.6",
@ -64,6 +69,7 @@
"weaviate-ts-client": "^1.4.0"
},
"devDependencies": {
"@inquirer/prompts": "^4.3.1",
"eslint": "^8.50.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-ft-flow": "^3.0.0",
@ -78,4 +84,4 @@
"nodemon": "^2.0.22",
"prettier": "^3.0.3"
}
}
}

View File

@ -0,0 +1,24 @@
-- AlterTable
ALTER TABLE "workspaces" ADD COLUMN "agentModel" TEXT;
ALTER TABLE "workspaces" ADD COLUMN "agentProvider" TEXT;
-- CreateTable
CREATE TABLE "workspace_agent_invocations" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"uuid" TEXT NOT NULL,
"prompt" TEXT NOT NULL,
"closed" BOOLEAN NOT NULL DEFAULT false,
"user_id" INTEGER,
"thread_id" INTEGER,
"workspace_id" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastUpdatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "workspace_agent_invocations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "workspace_agent_invocations_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "workspace_agent_invocations_uuid_key" ON "workspace_agent_invocations"("uuid");
-- CreateIndex
CREATE INDEX "workspace_agent_invocations_uuid_idx" ON "workspace_agent_invocations"("uuid");

View File

@ -56,19 +56,20 @@ model system_settings {
}
model users {
id Int @id @default(autoincrement())
username String? @unique
password String
pfpFilename String?
role String @default("default")
suspended Int @default(0)
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
workspace_chats workspace_chats[]
workspace_users workspace_users[]
embed_configs embed_configs[]
embed_chats embed_chats[]
threads workspace_threads[]
id Int @id @default(autoincrement())
username String? @unique
password String
pfpFilename String?
role String @default("default")
suspended Int @default(0)
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
workspace_chats workspace_chats[]
workspace_users workspace_users[]
embed_configs embed_configs[]
embed_chats embed_chats[]
threads workspace_threads[]
workspace_agent_invocations workspace_agent_invocations[]
}
model document_vectors {
@ -103,11 +104,14 @@ model workspaces {
topN Int? @default(4)
chatMode String? @default("chat")
pfpFilename String?
agentProvider String?
agentModel String?
workspace_users workspace_users[]
documents workspace_documents[]
workspace_suggested_messages workspace_suggested_messages[]
embed_configs embed_configs[]
threads workspace_threads[]
workspace_agent_invocations workspace_agent_invocations[]
}
model workspace_threads {
@ -151,6 +155,22 @@ model workspace_chats {
users users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
}
model workspace_agent_invocations {
id Int @id @default(autoincrement())
uuid String @unique
prompt String // Contains agent invocation to parse + option additional text for seed.
closed Boolean @default(false)
user_id Int?
thread_id Int? // No relation to prevent whole table migration
workspace_id Int
createdAt DateTime @default(now())
lastUpdatedAt DateTime @default(now())
user users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
workspace workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([uuid])
}
model workspace_users {
id Int @id @default(autoincrement())
user_id Int

View File

@ -0,0 +1,18 @@
class AIbitatError extends Error {}
class APIError extends AIbitatError {
constructor(message) {
super(message);
}
}
/**
* The error when the AI provider returns an error that should be treated as something
* that should be retried.
*/
class RetryError extends APIError {}
module.exports = {
APIError,
RetryError,
};

View File

@ -0,0 +1 @@
history/

View File

@ -0,0 +1,56 @@
// You must execute this example from within the example folder.
const AIbitat = require("../index.js");
const { cli } = require("../plugins/cli.js");
const { NodeHtmlMarkdown } = require("node-html-markdown");
require("dotenv").config({ path: `../../../../.env.development` });
const Agent = {
HUMAN: "🧑",
AI: "🤖",
};
const aibitat = new AIbitat({
provider: "openai",
model: "gpt-3.5-turbo",
})
.use(cli.plugin())
.function({
name: "aibitat-documentations",
description: "The documentation about aibitat AI project.",
parameters: {
type: "object",
properties: {},
},
handler: async () => {
return await fetch(
"https://raw.githubusercontent.com/wladiston/aibitat/main/README.md"
)
.then((res) => res.text())
.then((html) => NodeHtmlMarkdown.translate(html))
.catch((e) => {
console.error(e.message);
return "FAILED TO FETCH";
});
},
})
.agent(Agent.HUMAN, {
interrupt: "ALWAYS",
role: "You are a human assistant.",
})
.agent(Agent.AI, {
functions: ["aibitat-documentations"],
});
async function main() {
if (!process.env.OPEN_AI_KEY)
throw new Error(
"This example requires a valid OPEN_AI_KEY in the env.development file"
);
await aibitat.start({
from: Agent.HUMAN,
to: Agent.AI,
content: `Please, talk about the documentation of AIbitat.`,
});
}
main();

View File

@ -0,0 +1,55 @@
const AIbitat = require("../index.js");
const {
cli,
webBrowsing,
fileHistory,
webScraping,
} = require("../plugins/index.js");
require("dotenv").config({ path: `../../../../.env.development` });
const aibitat = new AIbitat({
model: "gpt-3.5-turbo",
})
.use(cli.plugin())
.use(fileHistory.plugin())
.use(webBrowsing.plugin()) // Does not have introspect so will fail.
.use(webScraping.plugin())
.agent("researcher", {
role: `You are a Researcher. Conduct thorough research to gather all necessary information about the topic
you are writing about. Collect data, facts, and statistics. Analyze competitor blogs for insights.
Provide accurate and up-to-date information that supports the blog post's content to @copywriter.`,
functions: ["web-browsing"],
})
.agent("copywriter", {
role: `You are a Copywriter. Interpret the draft as general idea and write the full blog post using markdown,
ensuring it is tailored to the target audience's preferences, interests, and demographics. Apply genre-specific
writing techniques relevant to the author's genre. Add code examples when needed. Code must be written in
Typescript. Always mention references. Revisit and edit the post for clarity, coherence, and
correctness based on the feedback provided. Ask for feedbacks to the channel when you are done`,
})
.agent("pm", {
role: `You are a Project Manager. Coordinate the project, ensure tasks are completed on time and within budget.
Communicate with team members and stakeholders.`,
interrupt: "ALWAYS",
})
.channel("content-team", ["researcher", "copywriter", "pm"]);
async function main() {
if (!process.env.OPEN_AI_KEY)
throw new Error(
"This example requires a valid OPEN_AI_KEY in the env.development file"
);
await aibitat.start({
from: "pm",
to: "content-team",
content: `We have got this draft of the new blog post, let us start working on it.
--- BEGIN DRAFT OF POST ---
Maui is a beautiful island in the state of Hawaii and is world-reknown for its whale watching season. Here are 2 other additional things to do in Maui, HI:
--- END DRAFT OF POST ---
`,
});
}
main();

View File

@ -0,0 +1,67 @@
<!doctype html>
<html>
<head>
<script type="text/javascript">
window.buttonEl;
window.outputEl;
function handleListen() {
const socket = new WebSocket("ws://localhost:3000/ws");
window.buttonEl.setAttribute("hidden", "true");
socket.addEventListener("message", (event) => {
try {
const data = JSON.parse(event.data);
if (!data.hasOwnProperty("type")) {
window.outputEl.innerHTML += `<p>${data.from} says to ${data.to}: ${data.content}<p></br></br>`;
return;
}
// Handle async input loops
if (data?.type === "WAITING_ON_INPUT") {
// Put in time as hack to now have the prompt block DOM update.
setTimeout(() => {
console.log(
"We are waiting for feedback from the socket. Will timeout in 30s..."
);
const feedback = window.prompt(
"We are waiting for feedback from the socket. Will timeout in 30s..."
);
!!feedback
? socket.send(
JSON.stringify({ type: "awaitingFeedback", feedback })
)
: socket.send(
JSON.stringify({
type: "awaitingFeedback",
feedback: "exit",
})
);
return;
}, 800);
}
} catch (e) {
console.error("Failed to parse data");
}
});
socket.addEventListener("close", (event) => {
window.outputEl.innerHTML = `<p>Socket connection closed. Test is complete.<p></br></br>`;
window.buttonEl.removeAttribute("hidden");
});
}
window.addEventListener("load", function () {
window.buttonEl = document.getElementById("listen");
window.outputEl = document.getElementById("output");
window.buttonEl.addEventListener("click", handleListen);
});
</script>
</head>
<body>
<button type="button" id="listen">Open websocket connection chat</button>
<div id="output"></div>
</body>
</html>

View File

@ -0,0 +1,100 @@
// You can only run this example from within the websocket/ directory.
// NODE_ENV=development node websock-branding-collab.js
// Scraping is enabled, but search requires AGENT_GSE_* keys.
const express = require("express");
const chalk = require("chalk");
const AIbitat = require("../../index.js");
const {
websocket,
webBrowsing,
webScraping,
} = require("../../plugins/index.js");
const path = require("path");
const port = 3000;
const app = express();
require("express-ws")(app);
require("dotenv").config({ path: `../../../../../.env.development` });
// Debugging echo function if this is working for you.
// app.ws('/echo', function (ws, req) {
// ws.on('message', function (msg) {
// ws.send(msg);
// });
// });
// Set up WSS sockets for listening.
app.ws("/ws", function (ws, _response) {
try {
ws.on("message", function (msg) {
if (ws?.handleFeedback) ws.handleFeedback(msg);
});
ws.on("close", function () {
console.log("Socket killed");
return;
});
console.log("Socket online and waiting...");
runAIbitat(ws).catch((error) => {
ws.send(
JSON.stringify({
from: "AI",
to: "HUMAN",
content: error.message,
})
);
});
} catch (error) {}
});
app.all("*", function (_, response) {
response.sendFile(path.join(__dirname, "index.html"));
});
app.listen(port, () => {
console.log(`Testing HTTP/WSS server listening at http://localhost:${port}`);
});
async function runAIbitat(socket) {
console.log(chalk.blue("Booting AIbitat class & starting agent(s)"));
const aibitat = new AIbitat({
provider: "openai",
model: "gpt-4",
})
.use(websocket.plugin({ socket }))
.use(webBrowsing.plugin())
.use(webScraping.plugin())
.agent("creativeDirector", {
role: `You are a Creative Director. Your role is overseeing the entire branding project, ensuring
the client's brief is met, and maintaining consistency across all brand elements, developing the
brand strategy, guiding the visual and conceptual direction, and providing overall creative leadership.`,
})
.agent("marketResearcher", {
role: `You do competitive market analysis via searching on the internet and learning about
comparative products and services. You can search by using keywords and phrases that you think will lead
to competitor research that can help find the unique angle and market of the idea.`,
functions: ["web-browsing"],
})
.agent("PM", {
role: `You are the Project Coordinator. Your role is overseeing the project's progress, timeline,
and budget. Ensure effective communication and coordination among team members, client, and stakeholders.
Your tasks include planning and scheduling project milestones, tracking tasks, and managing any
risks or issues that arise.`,
interrupt: "ALWAYS",
})
.channel("<b>#branding</b>", [
"creativeDirector",
"marketResearcher",
"PM",
]);
await aibitat.start({
from: "PM",
to: "<b>#branding</b>",
content: `I have an idea for a muslim focused meetup called Chai & Vibes.
I want to focus on professionals that are muslim and are in their 18-30 year old range who live in big cities.
Does anything like this exist? How can we differentiate?`,
});
}

View File

@ -0,0 +1,91 @@
// You can only run this example from within the websocket/ directory.
// NODE_ENV=development node websock-multi-turn-chat.js
// Scraping is enabled, but search requires AGENT_GSE_* keys.
const express = require("express");
const chalk = require("chalk");
const AIbitat = require("../../index.js");
const {
websocket,
webBrowsing,
webScraping,
} = require("../../plugins/index.js");
const path = require("path");
const port = 3000;
const app = express();
require("express-ws")(app);
require("dotenv").config({ path: `../../../../../.env.development` });
// Debugging echo function if this is working for you.
// app.ws('/echo', function (ws, req) {
// ws.on('message', function (msg) {
// ws.send(msg);
// });
// });
// Set up WSS sockets for listening.
app.ws("/ws", function (ws, _response) {
try {
ws.on("message", function (msg) {
if (ws?.handleFeedback) ws.handleFeedback(msg);
});
ws.on("close", function () {
console.log("Socket killed");
return;
});
console.log("Socket online and waiting...");
runAIbitat(ws).catch((error) => {
ws.send(
JSON.stringify({
from: Agent.AI,
to: Agent.HUMAN,
content: error.message,
})
);
});
} catch (error) {}
});
app.all("*", function (_, response) {
response.sendFile(path.join(__dirname, "index.html"));
});
app.listen(port, () => {
console.log(`Testing HTTP/WSS server listening at http://localhost:${port}`);
});
const Agent = {
HUMAN: "🧑",
AI: "🤖",
};
async function runAIbitat(socket) {
if (!process.env.OPEN_AI_KEY)
throw new Error(
"This example requires a valid OPEN_AI_KEY in the env.development file"
);
console.log(chalk.blue("Booting AIbitat class & starting agent(s)"));
const aibitat = new AIbitat({
provider: "openai",
model: "gpt-3.5-turbo",
})
.use(websocket.plugin({ socket }))
.use(webBrowsing.plugin())
.use(webScraping.plugin())
.agent(Agent.HUMAN, {
interrupt: "ALWAYS",
role: "You are a human assistant.",
})
.agent(Agent.AI, {
role: "You are a helpful ai assistant who likes to chat with the user who an also browse the web for questions it does not know or have real-time access to.",
functions: ["web-browsing"],
});
await aibitat.start({
from: Agent.HUMAN,
to: Agent.AI,
content: `How are you doing today?`,
});
}

View File

@ -0,0 +1,747 @@
const { EventEmitter } = require("events");
const { APIError } = require("./error.js");
const Providers = require("./providers/index.js");
const { Telemetry } = require("../../../models/telemetry.js");
/**
* AIbitat is a class that manages the conversation between agents.
* It is designed to solve a task with LLM.
*
* Guiding the chat through a graph of agents.
*/
class AIbitat {
emitter = new EventEmitter();
defaultProvider = null;
defaultInterrupt;
maxRounds;
_chats;
agents = new Map();
channels = new Map();
functions = new Map();
constructor(props = {}) {
const {
chats = [],
interrupt = "NEVER",
maxRounds = 100,
provider = "openai",
handlerProps = {}, // Inherited props we can spread so aibitat can access.
...rest
} = props;
this._chats = chats;
this.defaultInterrupt = interrupt;
this.maxRounds = maxRounds;
this.handlerProps = handlerProps;
this.defaultProvider = {
provider,
...rest,
};
}
/**
* Get the chat history between agents and channels.
*/
get chats() {
return this._chats;
}
/**
* Install a plugin.
*/
use(plugin) {
plugin.setup(this);
return this;
}
/**
* Add a new agent to the AIbitat.
*
* @param name
* @param config
* @returns
*/
agent(name = "", config = {}) {
this.agents.set(name, config);
return this;
}
/**
* Add a new channel to the AIbitat.
*
* @param name
* @param members
* @param config
* @returns
*/
channel(name = "", members = [""], config = {}) {
this.channels.set(name, {
members,
...config,
});
return this;
}
/**
* Get the specific agent configuration.
*
* @param agent The name of the agent.
* @throws When the agent configuration is not found.
* @returns The agent configuration.
*/
getAgentConfig(agent = "") {
const config = this.agents.get(agent);
if (!config) {
throw new Error(`Agent configuration "${agent}" not found`);
}
return {
role: "You are a helpful AI assistant.",
// role: `You are a helpful AI assistant.
// Solve tasks using your coding and language skills.
// In the following cases, suggest typescript code (in a typescript coding block) or shell script (in a sh coding block) for the user to execute.
// 1. When you need to collect info, use the code to output the info you need, for example, browse or search the web, download/read a file, print the content of a webpage or a file, get the current date/time, check the operating system. After sufficient info is printed and the task is ready to be solved based on your language skill, you can solve the task by yourself.
// 2. When you need to perform some task with code, use the code to perform the task and output the result. Finish the task smartly.
// Solve the task step by step if you need to. If a plan is not provided, explain your plan first. Be clear which step uses code, and which step uses your language skill.
// When using code, you must indicate the script type in the code block. The user cannot provide any other feedback or perform any other action beyond executing the code you suggest. The user can't modify your code. So do not suggest incomplete code which requires users to modify. Don't use a code block if it's not intended to be executed by the user.
// If you want the user to save the code in a file before executing it, put # filename: <filename> inside the code block as the first line. Don't include multiple code blocks in one response. Do not ask users to copy and paste the result. Instead, use 'print' function for the output when relevant. Check the execution result returned by the user.
// If the result indicates there is an error, fix the error and output the code again. Suggest the full code instead of partial code or code changes. If the error can't be fixed or if the task is not solved even after the code is executed successfully, analyze the problem, revisit your assumption, collect additional info you need, and think of a different approach to try.
// When you find an answer, verify the answer carefully. Include verifiable evidence in your response if possible.
// Reply "TERMINATE" when everything is done.`,
...config,
};
}
/**
* Get the specific channel configuration.
*
* @param channel The name of the channel.
* @throws When the channel configuration is not found.
* @returns The channel configuration.
*/
getChannelConfig(channel = "") {
const config = this.channels.get(channel);
if (!config) {
throw new Error(`Channel configuration "${channel}" not found`);
}
return {
maxRounds: 10,
role: "",
...config,
};
}
/**
* Get the members of a group.
* @throws When the group is not defined as an array in the connections.
* @param node The name of the group.
* @returns The members of the group.
*/
getGroupMembers(node = "") {
const group = this.getChannelConfig(node);
return group.members;
}
/**
* Triggered when a plugin, socket, or command is aborted.
*
* @param listener
* @returns
*/
onAbort(listener = () => null) {
this.emitter.on("abort", listener);
return this;
}
/**
* Abort the running of any plugins that may still be pending (Langchain summarize)
*/
abort() {
this.emitter.emit("abort", null, this);
}
/**
* Triggered when a chat is terminated. After this, the chat can't be continued.
*
* @param listener
* @returns
*/
onTerminate(listener = () => null) {
this.emitter.on("terminate", listener);
return this;
}
/**
* Terminate the chat. After this, the chat can't be continued.
*
* @param node Last node to chat with
*/
terminate(node = "") {
this.emitter.emit("terminate", node, this);
}
/**
* Triggered when a chat is interrupted by a node.
*
* @param listener
* @returns
*/
onInterrupt(listener = () => null) {
this.emitter.on("interrupt", listener);
return this;
}
/**
* Interruption the chat.
*
* @param route The nodes that participated in the interruption.
* @returns
*/
interrupt(route) {
this._chats.push({
...route,
state: "interrupt",
});
this.emitter.emit("interrupt", route, this);
}
/**
* Triggered when a message is added to the chat history.
* This can either be the first message or a reply to a message.
*
* @param listener
* @returns
*/
onMessage(listener = (chat) => null) {
this.emitter.on("message", listener);
return this;
}
/**
* Register a new successful message in the chat history.
* This will trigger the `onMessage` event.
*
* @param message
*/
newMessage(message) {
const chat = {
...message,
state: "success",
};
this._chats.push(chat);
this.emitter.emit("message", chat, this);
}
/**
* Triggered when an error occurs during the chat.
*
* @param listener
* @returns
*/
onError(
listener = (
/**
* The error that occurred.
*
* Native errors are:
* - `APIError`
* - `AuthorizationError`
* - `UnknownError`
* - `RateLimitError`
* - `ServerError`
*/
error = null,
/**
* The message when the error occurred.
*/
{}
) => null
) {
this.emitter.on("replyError", listener);
return this;
}
/**
* Register an error in the chat history.
* This will trigger the `onError` event.
*
* @param route
* @param error
*/
newError(route, error) {
const chat = {
...route,
content: error instanceof Error ? error.message : String(error),
state: "error",
};
this._chats.push(chat);
this.emitter.emit("replyError", error, chat);
}
/**
* Triggered when a chat is interrupted by a node.
*
* @param listener
* @returns
*/
onStart(listener = (chat, aibitat) => null) {
this.emitter.on("start", listener);
return this;
}
/**
* Start a new chat.
*
* @param message The message to start the chat.
*/
async start(message) {
// register the message in the chat history
this.newMessage(message);
this.emitter.emit("start", message, this);
// ask the node to reply
await this.chat({
to: message.from,
from: message.to,
});
return this;
}
/**
* Recursively chat between two nodes.
*
* @param route
* @param keepAlive Whether to keep the chat alive.
*/
async chat(route, keepAlive = true) {
// check if the message is for a group
// if it is, select the next node to chat with from the group
// and then ask them to reply.
if (this.channels.get(route.from)) {
// select a node from the group
let nextNode;
try {
nextNode = await this.selectNext(route.from);
} catch (error) {
if (error instanceof APIError) {
return this.newError({ from: route.from, to: route.to }, error);
}
throw error;
}
if (!nextNode) {
// TODO: should it throw an error or keep the chat alive when there is no node to chat with in the group?
// maybe it should wrap up the chat and reply to the original node
// For now, it will terminate the chat
this.terminate(route.from);
return;
}
const nextChat = {
from: nextNode,
to: route.from,
};
if (this.shouldAgentInterrupt(nextNode)) {
this.interrupt(nextChat);
return;
}
// get chats only from the group's nodes
const history = this.getHistory({ to: route.from });
const group = this.getGroupMembers(route.from);
const rounds = history.filter((chat) => group.includes(chat.from)).length;
const { maxRounds } = this.getChannelConfig(route.from);
if (rounds >= maxRounds) {
this.terminate(route.to);
return;
}
await this.chat(nextChat);
return;
}
// If it's a direct message, reply to the message
let reply = "";
try {
reply = await this.reply(route);
} catch (error) {
if (error instanceof APIError) {
return this.newError({ from: route.from, to: route.to }, error);
}
throw error;
}
if (
reply === "TERMINATE" ||
this.hasReachedMaximumRounds(route.from, route.to)
) {
this.terminate(route.to);
return;
}
const newChat = { to: route.from, from: route.to };
if (
reply === "INTERRUPT" ||
(this.agents.get(route.to) && this.shouldAgentInterrupt(route.to))
) {
this.interrupt(newChat);
return;
}
if (keepAlive) {
// keep the chat alive by replying to the other node
await this.chat(newChat, true);
}
}
/**
* Check if the agent should interrupt the chat based on its configuration.
*
* @param agent
* @returns {boolean} Whether the agent should interrupt the chat.
*/
shouldAgentInterrupt(agent = "") {
const config = this.getAgentConfig(agent);
return this.defaultInterrupt === "ALWAYS" || config.interrupt === "ALWAYS";
}
/**
* Select the next node to chat with from a group. The node will be selected based on the history of chats.
* It will select the node that has not reached the maximum number of rounds yet and has not chatted with the channel in the last round.
* If it could not determine the next node, it will return a random node.
*
* @param channel The name of the group.
* @returns The name of the node to chat with.
*/
async selectNext(channel = "") {
// get all members of the group
const nodes = this.getGroupMembers(channel);
const channelConfig = this.getChannelConfig(channel);
// TODO: move this to when the group is created
// warn if the group is underpopulated
if (nodes.length < 3) {
console.warn(
`- Group (${channel}) is underpopulated with ${nodes.length} agents. Direct communication would be more efficient.`
);
}
// get the nodes that have not reached the maximum number of rounds
const availableNodes = nodes.filter(
(node) => !this.hasReachedMaximumRounds(channel, node)
);
// remove the last node that chatted with the channel, so it doesn't chat again
const lastChat = this._chats.filter((c) => c.to === channel).at(-1);
if (lastChat) {
const index = availableNodes.indexOf(lastChat.from);
if (index > -1) {
availableNodes.splice(index, 1);
}
}
// TODO: what should it do when there is no node to chat with?
if (!availableNodes.length) return;
// get the provider that will be used for the channel
// if the channel has a provider, use that otherwise
// use the GPT-4 because it has a better reasoning
const provider = this.getProviderForConfig({
// @ts-expect-error
model: "gpt-4",
...this.defaultProvider,
...channelConfig,
});
const history = this.getHistory({ to: channel });
// build the messages to send to the provider
const messages = [
{
role: "system",
content: channelConfig.role,
},
{
role: "user",
content: `You are in a role play game. The following roles are available:
${availableNodes
.map((node) => `@${node}: ${this.getAgentConfig(node).role}`)
.join("\n")}.
Read the following conversation.
CHAT HISTORY
${history.map((c) => `@${c.from}: ${c.content}`).join("\n")}
Then select the next role from that is going to speak next.
Only return the role.
`,
},
];
// ask the provider to select the next node to chat with
// and remove the @ from the response
const { result } = await provider.complete(messages);
const name = result?.replace(/^@/g, "");
if (this.agents.get(name)) {
return name;
}
// if the name is not in the nodes, return a random node
return availableNodes[Math.floor(Math.random() * availableNodes.length)];
}
/**
* Check if the chat has reached the maximum number of rounds.
*/
hasReachedMaximumRounds(from = "", to = "") {
return this.getHistory({ from, to }).length >= this.maxRounds;
}
/**
* Ask the for the AI provider to generate a reply to the chat.
*
* @param route.to The node that sent the chat.
* @param route.from The node that will reply to the chat.
*/
async reply(route) {
// get the provider for the node that will reply
const fromConfig = this.getAgentConfig(route.from);
const chatHistory =
// if it is sending message to a group, send the group chat history to the provider
// otherwise, send the chat history between the two nodes
this.channels.get(route.to)
? [
{
role: "user",
content: `You are in a whatsapp group. Read the following conversation and then reply.
Do not add introduction or conclusion to your reply because this will be a continuous conversation. Don't introduce yourself.
CHAT HISTORY
${this.getHistory({ to: route.to })
.map((c) => `@${c.from}: ${c.content}`)
.join("\n")}
@${route.from}:`,
},
]
: this.getHistory(route).map((c) => ({
content: c.content,
role: c.from === route.to ? "user" : "assistant",
}));
// build the messages to send to the provider
const messages = [
{
content: fromConfig.role,
role: "system",
},
// get the history of chats between the two nodes
...chatHistory,
];
// get the functions that the node can call
const functions = fromConfig.functions
?.map((name) => this.functions.get(name))
.filter((a) => !!a);
const provider = this.getProviderForConfig({
...this.defaultProvider,
...fromConfig,
});
// get the chat completion
const content = await this.handleExecution(
provider,
messages,
functions,
route.from
);
this.newMessage({ ...route, content });
return content;
}
async handleExecution(
provider,
messages = [],
functions = [],
byAgent = null
) {
// get the chat completion
const completion = await provider.complete(messages, functions);
if (completion.functionCall) {
const { name, arguments: args } = completion.functionCall;
const fn = this.functions.get(name);
// if provider hallucinated on the function name
// ask the provider to complete again
if (!fn) {
return await this.handleExecution(
provider,
[
...messages,
{
name,
role: "function",
content: `Function "${name}" not found. Try again.`,
},
],
functions,
byAgent
);
}
// Execute the function and return the result to the provider
fn.caller = byAgent || "agent";
const result = await fn.handler(args);
Telemetry.sendTelemetry("agent_tool_call", { tool: name }, null, true);
return await this.handleExecution(
provider,
[
...messages,
{
name,
role: "function",
content: result,
},
],
functions,
byAgent
);
}
return completion?.result;
}
/**
* Continue the chat from the last interruption.
* If the last chat was not an interruption, it will throw an error.
* Provide a feedback where it was interrupted if you want to.
*
* @param feedback The feedback to the interruption if any.
* @returns
*/
async continue(feedback) {
const lastChat = this._chats.at(-1);
if (!lastChat || lastChat.state !== "interrupt") {
throw new Error("No chat to continue");
}
// remove the last chat's that was interrupted
this._chats.pop();
const { from, to } = lastChat;
if (this.hasReachedMaximumRounds(from, to)) {
throw new Error("Maximum rounds reached");
}
if (feedback) {
const message = {
from,
to,
content: feedback,
};
// register the message in the chat history
this.newMessage(message);
// ask the node to reply
await this.chat({
to: message.from,
from: message.to,
});
} else {
await this.chat({ from, to });
}
return this;
}
/**
* Retry the last chat that threw an error.
* If the last chat was not an error, it will throw an error.
*/
async retry() {
const lastChat = this._chats.at(-1);
if (!lastChat || lastChat.state !== "error") {
throw new Error("No chat to retry");
}
// remove the last chat's that threw an error
const { from, to } = this?._chats?.pop();
await this.chat({ from, to });
return this;
}
/**
* Get the chat history between two nodes or all chats to/from a node.
*/
getHistory({ from, to }) {
return this._chats.filter((chat) => {
const isSuccess = chat.state === "success";
// return all chats to the node
if (!from) {
return isSuccess && chat.to === to;
}
// get all chats from the node
if (!to) {
return isSuccess && chat.from === from;
}
// check if the chat is between the two nodes
const hasSent = chat.from === from && chat.to === to;
const hasReceived = chat.from === to && chat.to === from;
const mutual = hasSent || hasReceived;
return isSuccess && mutual;
});
}
/**
* Get provider based on configurations.
* If the provider is a string, it will return the default provider for that string.
*
* @param config The provider configuration.
*/
getProviderForConfig(config) {
if (typeof config.provider === "object") {
return config.provider;
}
switch (config.provider) {
case "openai":
return new Providers.OpenAIProvider({ model: config.model });
case "anthropic":
return new Providers.AnthropicProvider({ model: config.model });
default:
throw new Error(
`Unknown provider: ${config.provider}. Please use "openai"`
);
}
}
/**
* Register a new function to be called by the AIbitat agents.
* You are also required to specify the which node can call the function.
* @param functionConfig The function configuration.
*/
function(functionConfig) {
this.functions.set(functionConfig.name, functionConfig);
return this;
}
}
module.exports = AIbitat;

View File

@ -0,0 +1,49 @@
const { WorkspaceChats } = require("../../../../models/workspaceChats");
/**
* Plugin to save chat history to AnythingLLM DB.
*/
const chatHistory = {
name: "chat-history",
startupConfig: {
params: {},
},
plugin: function () {
return {
name: this.name,
setup: function (aibitat) {
aibitat.onMessage(async () => {
try {
const lastResponses = aibitat.chats.slice(-2);
if (lastResponses.length !== 2) return;
const [prev, last] = lastResponses;
// We need a full conversation reply with prev being from
// the USER and the last being from anyone other than the user.
if (prev.from !== "USER" || last.from === "USER") return;
await this._store(aibitat, {
prompt: prev.content,
response: last.content,
});
} catch {}
});
},
_store: async function (aibitat, { prompt, response } = {}) {
const invocation = aibitat.handlerProps.invocation;
await WorkspaceChats.new({
workspaceId: Number(invocation.workspace_id),
prompt,
response: {
text: response,
sources: [],
type: "chat",
},
user: { id: invocation?.user_id || null },
threadId: invocation?.thread_id || null,
});
},
};
},
};
module.exports = { chatHistory };

View File

@ -0,0 +1,140 @@
// Plugin CAN ONLY BE USE IN DEVELOPMENT.
const { input } = require("@inquirer/prompts");
const chalk = require("chalk");
const { RetryError } = require("../error");
/**
* Command-line Interface plugin. It prints the messages on the console and asks for feedback
* while the conversation is running in the background.
*/
const cli = {
name: "cli",
startupConfig: {
params: {},
},
plugin: function ({ simulateStream = true } = {}) {
return {
name: this.name,
setup(aibitat) {
let printing = [];
aibitat.onError(async (error) => {
console.error(chalk.red(` error: ${error?.message}`));
if (error instanceof RetryError) {
console.error(chalk.red(` retrying in 60 seconds...`));
setTimeout(() => {
aibitat.retry();
}, 60000);
return;
}
});
aibitat.onStart(() => {
console.log();
console.log("🚀 starting chat ...\n");
printing = [Promise.resolve()];
});
aibitat.onMessage(async (message) => {
const next = new Promise(async (resolve) => {
await Promise.all(printing);
await this.print(message, simulateStream);
resolve();
});
printing.push(next);
});
aibitat.onTerminate(async () => {
await Promise.all(printing);
console.log("🚀 chat finished");
});
aibitat.onInterrupt(async (node) => {
await Promise.all(printing);
const feedback = await this.askForFeedback(node);
// Add an extra line after the message
console.log();
if (feedback === "exit") {
console.log("🚀 chat finished");
return process.exit(0);
}
await aibitat.continue(feedback);
});
},
/**
* Print a message on the terminal
*
* @param message
* // message Type { from: string; to: string; content?: string } & {
state: 'loading' | 'error' | 'success' | 'interrupt'
}
* @param simulateStream
*/
print: async function (message = {}, simulateStream = true) {
const replying = chalk.dim(`(to ${message.to})`);
const reference = `${chalk.magenta("✎")} ${chalk.bold(
message.from
)} ${replying}:`;
if (!simulateStream) {
console.log(reference);
console.log(message.content);
// Add an extra line after the message
console.log();
return;
}
process.stdout.write(`${reference}\n`);
// Emulate streaming by breaking the cached response into chunks
const chunks = message.content?.split(" ") || [];
const stream = new ReadableStream({
async start(controller) {
for (const chunk of chunks) {
const bytes = new TextEncoder().encode(chunk + " ");
controller.enqueue(bytes);
await new Promise((r) =>
setTimeout(
r,
// get a random number between 10ms and 50ms to simulate a random delay
Math.floor(Math.random() * 40) + 10
)
);
}
controller.close();
},
});
// Stream the response to the chat
for await (const chunk of stream) {
process.stdout.write(new TextDecoder().decode(chunk));
}
// Add an extra line after the message
console.log();
console.log();
},
/**
* Ask for feedback to the user using the terminal
*
* @param node //{ from: string; to: string }
* @returns
*/
askForFeedback: function (node = {}) {
return input({
message: `Provide feedback to ${chalk.yellow(
node.to
)} as ${chalk.yellow(
node.from
)}. Press enter to skip and use auto-reply, or type 'exit' to end the conversation: `,
});
},
};
},
};
module.exports = { cli };

View File

@ -0,0 +1,37 @@
const fs = require("fs");
const path = require("path");
/**
* Plugin to save chat history to a json file
*/
const fileHistory = {
name: "file-history-plugin",
startupConfig: {
params: {},
},
plugin: function ({
filename = `history/chat-history-${new Date().toISOString()}.json`,
} = {}) {
return {
name: this.name,
setup(aibitat) {
const folderPath = path.dirname(filename);
// get path from filename
if (folderPath) {
fs.mkdirSync(folderPath, { recursive: true });
}
aibitat.onMessage(() => {
const content = JSON.stringify(aibitat.chats, null, 2);
fs.writeFile(filename, content, (err) => {
if (err) {
console.error(err);
}
});
});
},
};
},
};
module.exports = { fileHistory };

View File

@ -0,0 +1,26 @@
const { webBrowsing } = require("./web-browsing.js");
const { webScraping } = require("./web-scraping.js");
const { websocket } = require("./websocket.js");
const { docSummarizer } = require("./summarize.js");
const { saveFileInBrowser } = require("./save-file-browser.js");
const { chatHistory } = require("./chat-history.js");
const { memory } = require("./memory.js");
module.exports = {
webScraping,
webBrowsing,
websocket,
docSummarizer,
saveFileInBrowser,
chatHistory,
memory,
// Plugin name aliases so they can be pulled by slug as well.
[webScraping.name]: webScraping,
[webBrowsing.name]: webBrowsing,
[websocket.name]: websocket,
[docSummarizer.name]: docSummarizer,
[saveFileInBrowser.name]: saveFileInBrowser,
[chatHistory.name]: chatHistory,
[memory.name]: memory,
};

View File

@ -0,0 +1,134 @@
const { v4 } = require("uuid");
const { getVectorDbClass, getLLMProvider } = require("../../../helpers");
const { Deduplicator } = require("../utils/dedupe");
const memory = {
name: "rag-memory",
startupConfig: {
params: {},
},
plugin: function () {
return {
name: this.name,
setup(aibitat) {
aibitat.function({
super: aibitat,
tracker: new Deduplicator(),
name: this.name,
description:
"Search against local documents for context that is relevant to the query or store a snippet of text into memory for retrieval later. Storing information should only be done when the user specifically requests for information to be remembered or saved to long-term memory. You should use this tool before search the internet for information.",
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
action: {
type: "string",
enum: ["search", "store"],
description:
"The action we want to take to search for existing similar context or storage of new context.",
},
content: {
type: "string",
description:
"The plain text to search our local documents with or to store in our vector database.",
},
},
additionalProperties: false,
},
handler: async function ({ action = "", content = "" }) {
try {
if (this.tracker.isDuplicate(this.name, { action, content }))
return `This was a duplicated call and it's output will be ignored.`;
let response = "There was nothing to do.";
if (action === "search") response = await this.search(content);
if (action === "store") response = await this.store(content);
this.tracker.trackRun(this.name, { action, content });
return response;
} catch (error) {
console.log(error);
return `There was an error while calling the function. ${error.message}`;
}
},
search: async function (query = "") {
try {
const workspace = this.super.handlerProps.invocation.workspace;
const LLMConnector = getLLMProvider({
provider: workspace?.chatProvider,
model: workspace?.chatModel,
});
const vectorDB = getVectorDbClass();
const { contextTexts = [] } =
await vectorDB.performSimilaritySearch({
namespace: workspace.slug,
input: query,
LLMConnector,
});
if (contextTexts.length === 0) {
this.super.introspect(
`${this.caller}: I didn't find anything locally that would help answer this question.`
);
return "There was no additional context found for that query. We should search the web for this information.";
}
this.super.introspect(
`${this.caller}: Found ${contextTexts.length} additional piece of context to help answer this question.`
);
let combinedText = "Additional context for query:\n";
for (const text of contextTexts) combinedText += text + "\n\n";
return combinedText;
} catch (error) {
this.super.handlerProps.log(
`memory.search raised an error. ${error.message}`
);
return `An error was raised while searching the vector database. ${error.message}`;
}
},
store: async function (content = "") {
try {
const workspace = this.super.handlerProps.invocation.workspace;
const vectorDB = getVectorDbClass();
const { error } = await vectorDB.addDocumentToNamespace(
workspace.slug,
{
docId: v4(),
id: v4(),
url: "file://embed-via-agent.txt",
title: "agent-memory.txt",
docAuthor: "@agent",
description: "Unknown",
docSource: "a text file stored by the workspace agent.",
chunkSource: "",
published: new Date().toLocaleString(),
wordCount: content.split(" ").length,
pageContent: content,
token_count_estimate: 0,
},
null
);
if (!!error)
return "The content was failed to be embedded properly.";
this.super.introspect(
`${this.caller}: I saved the content to long-term memory in this workspaces vector database.`
);
return "The content given was successfully embedded. There is nothing else to do.";
} catch (error) {
this.super.handlerProps.log(
`memory.store raised an error. ${error.message}`
);
return `Let the user know this action was not successful. An error was raised while storing data in the vector database. ${error.message}`;
}
},
});
},
};
},
};
module.exports = {
memory,
};

View File

@ -0,0 +1,70 @@
const { Deduplicator } = require("../utils/dedupe");
const saveFileInBrowser = {
name: "save-file-to-browser",
startupConfig: {
params: {},
},
plugin: function () {
return {
name: this.name,
setup(aibitat) {
// List and summarize the contents of files that are embedded in the workspace
aibitat.function({
super: aibitat,
tracker: new Deduplicator(),
name: this.name,
description:
"Save content to a file when the user explicity asks for a download of the file.",
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
file_content: {
type: "string",
description: "The content of the file that will be saved.",
},
filename: {
type: "string",
description:
"filename to save the file as with extension. Extension should be plaintext file extension.",
},
},
additionalProperties: false,
},
handler: async function ({ file_content = "", filename }) {
try {
if (
this.tracker.isDuplicate(this.name, { file_content, filename })
) {
this.super.handlerProps.log(
`${this.name} was called, but exited early since it was not a unique call.`
);
return `${filename} file has been saved successfully!`;
}
this.super.socket.send("fileDownload", {
filename,
b64Content:
"data:text/plain;base64," +
Buffer.from(file_content, "utf8").toString("base64"),
});
this.super.introspect(`${this.caller}: Saving file ${filename}.`);
this.tracker.trackRun(this.name, { file_content, filename });
return `${filename} file has been saved successfully and will be downloaded automatically to the users browser.`;
} catch (error) {
this.super.handlerProps.log(
`save-file-to-browser raised an error. ${error.message}`
);
return `Let the user know this action was not successful. An error was raised while saving a file to the browser. ${error.message}`;
}
},
});
},
};
},
};
module.exports = {
saveFileInBrowser,
};

View File

@ -0,0 +1,130 @@
const { Document } = require("../../../../models/documents");
const { safeJsonParse } = require("../../../http");
const { validate } = require("uuid");
const { summarizeContent } = require("../utils/summarize");
const docSummarizer = {
name: "document-summarizer",
startupConfig: {
params: {},
},
plugin: function () {
return {
name: this.name,
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
controller: new AbortController(),
description:
"Can get the list of files available to search with descriptions and can select a single file to open and summarize.",
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
action: {
type: "string",
enum: ["list", "summarize"],
description:
"The action to take. 'list' will return all files available and their document ids. 'summarize' will open and summarize the file by the document_id, in the format of a uuid.",
},
document_id: {
type: "string",
"x-nullable": true,
description:
"A document id to summarize the content of. Document id must be a uuid.",
},
},
additionalProperties: false,
},
handler: async function ({ action, document_id }) {
if (action === "list") return await this.listDocuments();
if (action === "summarize")
return await this.summarizeDoc(document_id);
return "There is nothing we can do. This function call returns no information.";
},
/**
* List all documents available in a workspace
* @returns List of files and their descriptions if available.
*/
listDocuments: async function () {
try {
this.super.introspect(
`${this.caller}: Looking at the available documents.`
);
const documents = await Document.where({
workspaceId: this.super.handlerProps.invocation.workspace_id,
});
if (documents.length === 0)
return "No documents found - nothing can be done. Stop.";
this.super.introspect(
`${this.caller}: Found ${documents.length} documents`
);
const foundDocuments = documents.map((doc) => {
const metadata = safeJsonParse(doc.metadata, {});
return {
document_id: doc.docId,
filename: metadata?.title ?? "unknown.txt",
description: metadata?.description ?? "no description",
};
});
return JSON.stringify(foundDocuments);
} catch (error) {
this.super.handlerProps.log(
`document-summarizer.list raised an error. ${error.message}`
);
return `Let the user know this action was not successful. An error was raised while listing available files. ${error.message}`;
}
},
summarizeDoc: async function (documentId) {
try {
if (!validate(documentId)) {
this.super.handlerProps.log(
`${this.caller}: documentId ${documentId} is not a valid UUID`
);
return "This was not a valid documentID because it was not a uuid. No content was found.";
}
const document = await Document.content(documentId);
this.super.introspect(
`${this.caller}: Grabbing all content for ${
document?.title ?? "a discovered file."
}`
);
if (document?.content?.length < 8000) return content;
this.super.introspect(
`${this.caller}: Summarizing ${document?.title ?? ""}...`
);
this.super.onAbort(() => {
this.super.handlerProps.log(
"Abort was triggered, exiting summarization early."
);
this.controller.abort();
});
return await summarizeContent(
this.controller.signal,
document.content
);
} catch (error) {
this.super.handlerProps.log(
`document-summarizer.summarizeDoc raised an error. ${error.message}`
);
return `Let the user know this action was not successful. An error was raised while summarizing the file. ${error.message}`;
}
},
});
},
};
},
};
module.exports = {
docSummarizer,
};

View File

@ -0,0 +1,169 @@
const { SystemSettings } = require("../../../../models/systemSettings");
const webBrowsing = {
name: "web-browsing",
startupConfig: {
params: {},
},
plugin: function () {
return {
name: this.name,
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
description:
"Searches for a given query online using a search engine.",
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
query: {
type: "string",
description: "A search query.",
},
},
additionalProperties: false,
},
handler: async function ({ query }) {
try {
if (query) return await this.search(query);
return "There is nothing we can do. This function call returns no information.";
} catch (error) {
return `There was an error while calling the function. No data or response was found. Let the user know this was the error: ${error.message}`;
}
},
/**
* Use Google Custom Search Engines
* Free to set up, easy to use, 100 calls/day!
* https://programmablesearchengine.google.com/controlpanel/create
*/
search: async function (query) {
const provider =
(await SystemSettings.get({ label: "agent_search_provider" }))
?.value ?? "unknown";
let engine;
switch (provider) {
case "google-search-engine":
engine = "_googleSearchEngine";
break;
case "serper-dot-dev":
engine = "_serperDotDev";
break;
default:
engine = "_googleSearchEngine";
}
return await this[engine](query);
},
/**
* Use Google Custom Search Engines
* Free to set up, easy to use, 100 calls/day
* https://programmablesearchengine.google.com/controlpanel/create
*/
_googleSearchEngine: async function (query) {
if (!process.env.AGENT_GSE_CTX || !process.env.AGENT_GSE_KEY) {
this.super.introspect(
`${this.caller}: I can't use Google searching because the user has not defined the required API keys.\nVisit: https://programmablesearchengine.google.com/controlpanel/create to create the API keys.`
);
return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;
}
const searchURL = new URL(
"https://www.googleapis.com/customsearch/v1"
);
searchURL.searchParams.append("key", process.env.AGENT_GSE_KEY);
searchURL.searchParams.append("cx", process.env.AGENT_GSE_CTX);
searchURL.searchParams.append("q", query);
this.super.introspect(
`${this.caller}: Searching on Google for "${
query.length > 100 ? `${query.slice(0, 100)}...` : query
}"`
);
const searchResponse = await fetch(searchURL)
.then((res) => res.json())
.then((searchResult) => searchResult?.items || [])
.then((items) => {
return items.map((item) => {
return {
title: item.title,
link: item.link,
snippet: item.snippet,
};
});
})
.catch((e) => {
console.log(e);
return {};
});
return JSON.stringify(searchResponse);
},
/**
* Use Serper.dev
* Free to set up, easy to use, 2,500 calls for free one-time
* https://serper.dev
*/
_serperDotDev: async function (query) {
if (!process.env.AGENT_SERPER_DEV_KEY) {
this.super.introspect(
`${this.caller}: I can't use Serper.dev searching because the user has not defined the required API key.\nVisit: https://serper.dev to create the API key for free.`
);
return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;
}
this.super.introspect(
`${this.caller}: Using Serper.dev to search for "${
query.length > 100 ? `${query.slice(0, 100)}...` : query
}"`
);
const { response, error } = await fetch(
"https://google.serper.dev/search",
{
method: "POST",
headers: {
"X-API-KEY": process.env.AGENT_SERPER_DEV_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ q: query }),
redirect: "follow",
}
)
.then((res) => res.json())
.then((data) => {
return { response: data, error: null };
})
.catch((e) => {
return { response: null, error: e.message };
});
if (error)
return `There was an error searching for content. ${error}`;
const data = [];
if (response.hasOwnProperty("knowledgeGraph"))
data.push(response.knowledgeGraph);
response.organic?.forEach((searchResult) => {
const { title, link, snippet } = searchResult;
data.push({
title,
link,
snippet,
});
});
if (data.length === 0)
return `No information was found online for the search query.`;
return JSON.stringify(data);
},
});
},
};
},
};
module.exports = {
webBrowsing,
};

View File

@ -0,0 +1,87 @@
const { CollectorApi } = require("../../../collectorApi");
const { summarizeContent } = require("../utils/summarize");
const webScraping = {
name: "web-scraping",
startupConfig: {
params: {},
},
plugin: function () {
return {
name: this.name,
setup(aibitat) {
aibitat.function({
super: aibitat,
name: this.name,
controller: new AbortController(),
description:
"Scrapes the content of a webpage or online resource from a URL.",
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
url: {
type: "string",
format: "uri",
description: "A web URL.",
},
},
additionalProperties: false,
},
handler: async function ({ url }) {
try {
if (url) return await this.scrape(url);
return "There is nothing we can do. This function call returns no information.";
} catch (error) {
return `There was an error while calling the function. No data or response was found. Let the user know this was the error: ${error.message}`;
}
},
/**
* Scrape a website and summarize the content based on objective if the content is too large.
* Objective is the original objective & task that user give to the agent, url is the url of the website to be scraped.
* Here we can leverage the document collector to get raw website text quickly.
*
* @param url
* @returns
*/
scrape: async function (url) {
this.super.introspect(
`${this.caller}: Scraping the content of ${url}`
);
const { success, content } =
await new CollectorApi().getLinkContent(url);
if (!success) {
this.super.introspect(
`${this.caller}: could not scrape ${url}. I can't use this page's content.`
);
throw new Error(
`URL could not be scraped and no content was found.`
);
}
if (content?.length <= 8000) {
return content;
}
this.super.introspect(
`${this.caller}: This page's content is way too long. I will summarize it right now.`
);
this.super.onAbort(() => {
this.super.handlerProps.log(
"Abort was triggered, exiting summarization early."
);
this.controller.abort();
});
return summarizeContent(this.controller.signal, content);
},
});
},
};
},
};
module.exports = {
webScraping,
};

View File

@ -0,0 +1,150 @@
const chalk = require("chalk");
const { RetryError } = require("../error");
const { Telemetry } = require("../../../../models/telemetry");
const SOCKET_TIMEOUT_MS = 300 * 1_000; // 5 mins
/**
* Websocket Interface plugin. It prints the messages on the console and asks for feedback
* while the conversation is running in the background.
*/
// export interface AIbitatWebSocket extends ServerWebSocket<unknown> {
// askForFeedback?: any
// awaitResponse?: any
// handleFeedback?: (message: string) => void;
// }
const WEBSOCKET_BAIL_COMMANDS = [
"exit",
"/exit",
"stop",
"/stop",
"halt",
"/halt",
];
const websocket = {
name: "websocket",
startupConfig: {
params: {
socket: {
required: true,
},
muteUserReply: {
required: false,
default: true,
},
introspection: {
required: false,
default: true,
},
},
},
plugin: function ({
socket, // @type AIbitatWebSocket
muteUserReply = true, // Do not post messages to "USER" back to frontend.
introspection = false, // when enabled will attach socket to Aibitat object with .introspect method which reports status updates to frontend.
}) {
return {
name: this.name,
setup(aibitat) {
aibitat.onError(async (error) => {
console.error(chalk.red(` error: ${error?.message}`));
if (error instanceof RetryError) {
console.error(chalk.red(` retrying in 60 seconds...`));
setTimeout(() => {
aibitat.retry();
}, 60000);
return;
}
});
aibitat.introspect = (messageText) => {
if (!introspection) return; // Dump thoughts when not wanted.
socket.send(
JSON.stringify({ type: "statusResponse", content: messageText })
);
};
// expose function for sockets across aibitat
// type param must be set or else msg will not be shown or handled in UI.
aibitat.socket = {
send: (type = "__unhandled", content = "") => {
socket.send(JSON.stringify({ type, content }));
},
};
// aibitat.onStart(() => {
// console.log("🚀 starting chat ...");
// });
aibitat.onMessage((message) => {
if (message.from !== "USER")
Telemetry.sendTelemetry("agent_chat_sent");
if (message.from === "USER" && muteUserReply) return;
socket.send(JSON.stringify(message));
});
aibitat.onTerminate(() => {
// console.log("🚀 chat finished");
socket.close();
});
aibitat.onInterrupt(async (node) => {
const feedback = await socket.askForFeedback(socket, node);
if (WEBSOCKET_BAIL_COMMANDS.includes(feedback)) {
socket.close();
return;
}
await aibitat.continue(feedback);
});
/**
* Socket wait for feedback on socket
*
* @param socket The content to summarize. // AIbitatWebSocket & { receive: any, echo: any }
* @param node The chat node // { from: string; to: string }
* @returns The summarized content.
*/
socket.askForFeedback = (socket, node) => {
socket.awaitResponse = (question = "waiting...") => {
socket.send(JSON.stringify({ type: "WAITING_ON_INPUT", question }));
return new Promise(function (resolve) {
let socketTimeout = null;
socket.handleFeedback = (message) => {
const data = JSON.parse(message);
if (data.type !== "awaitingFeedback") return;
delete socket.handleFeedback;
clearTimeout(socketTimeout);
resolve(data.feedback);
return;
};
socketTimeout = setTimeout(() => {
console.log(
chalk.red(
`Client took too long to respond, chat thread is dead after ${SOCKET_TIMEOUT_MS}ms`
)
);
resolve("exit");
return;
}, SOCKET_TIMEOUT_MS);
});
};
return socket.awaitResponse(`Provide feedback to ${chalk.yellow(
node.to
)} as ${chalk.yellow(node.from)}.
Press enter to skip and use auto-reply, or type 'exit' to end the conversation: \n`);
};
// console.log("🚀 WS plugin is complete.");
},
};
},
};
module.exports = {
websocket,
WEBSOCKET_BAIL_COMMANDS,
};

View File

@ -0,0 +1,19 @@
/**
* A service that provides an AI client to create a completion.
*/
class Provider {
_client;
constructor(client) {
if (this.constructor == Provider) {
throw new Error("Class is of abstract type and can't be instantiated");
}
this._client = client;
}
get client() {
return this._client;
}
}
module.exports = Provider;

View File

@ -0,0 +1,151 @@
const Anthropic = require("@anthropic-ai/sdk");
const { RetryError } = require("../error.js");
const Provider = require("./ai-provider.js");
/**
* The provider for the Anthropic API.
* By default, the model is set to 'claude-2'.
*/
class AnthropicProvider extends Provider {
model;
constructor(config = {}) {
const {
options = {
apiKey: process.env.ANTHROPIC_API_KEY,
maxRetries: 3,
},
model = "claude-2",
} = config;
const client = new Anthropic(options);
super(client);
this.model = model;
}
/**
* Create a completion based on the received messages.
*
* @param messages A list of messages to send to the Anthropic API.
* @param functions
* @returns The completion.
*/
async complete(messages, functions) {
// clone messages to avoid mutating the original array
const promptMessages = [...messages];
if (functions) {
const functionPrompt = this.getFunctionPrompt(functions);
// add function prompt after the first message
promptMessages.splice(1, 0, {
content: functionPrompt,
role: "system",
});
}
const prompt = promptMessages
.map((message) => {
const { content, role } = message;
switch (role) {
case "system":
return content
? `${Anthropic.HUMAN_PROMPT} <admin>${content}</admin>`
: "";
case "function":
case "user":
return `${Anthropic.HUMAN_PROMPT} ${content}`;
case "assistant":
return `${Anthropic.AI_PROMPT} ${content}`;
default:
return content;
}
})
.filter(Boolean)
.join("\n")
.concat(` ${Anthropic.AI_PROMPT}`);
try {
const response = await this.client.completions.create({
model: this.model,
max_tokens_to_sample: 3000,
stream: false,
prompt,
});
const result = response.completion.trim();
// TODO: get cost from response
const cost = 0;
// Handle function calls if the model returns a function call
if (result.includes("function_name") && functions) {
let functionCall;
try {
functionCall = JSON.parse(result);
} catch (error) {
// call the complete function again in case it gets a json error
return await this.complete(
[
...messages,
{
role: "function",
content: `You gave me this function call: ${result} but I couldn't parse it.
${error?.message}
Please try again.`,
},
],
functions
);
}
return {
result: null,
functionCall,
cost,
};
}
return {
result,
cost,
};
} catch (error) {
if (
error instanceof Anthropic.RateLimitError ||
error instanceof Anthropic.InternalServerError ||
error instanceof Anthropic.APIError
) {
throw new RetryError(error.message);
}
throw error;
}
}
getFunctionPrompt(functions = []) {
const functionPrompt = `<functions>You have been trained to directly call a Javascript function passing a JSON Schema parameter as a response to this chat. This function will return a string that you can use to keep chatting.
Here is a list of functions available to you:
${JSON.stringify(functions, null, 2)}
When calling any of those function in order to complete your task, respond only this JSON format. Do not include any other information or any other stuff.
Function call format:
{
function_name: "givenfunctionname",
parameters: {}
}
</functions>`;
return functionPrompt;
}
}
module.exports = AnthropicProvider;

View File

@ -0,0 +1,7 @@
const OpenAIProvider = require("./openai.js");
const AnthropicProvider = require("./anthropic.js");
module.exports = {
OpenAIProvider,
AnthropicProvider,
};

View File

@ -0,0 +1,144 @@
const OpenAI = require("openai:latest");
const Provider = require("./ai-provider.js");
const { RetryError } = require("../error.js");
/**
* The provider for the OpenAI API.
* By default, the model is set to 'gpt-3.5-turbo'.
*/
class OpenAIProvider extends Provider {
model;
static COST_PER_TOKEN = {
"gpt-4": {
input: 0.03,
output: 0.06,
},
"gpt-4-32k": {
input: 0.06,
output: 0.12,
},
"gpt-3.5-turbo": {
input: 0.0015,
output: 0.002,
},
"gpt-3.5-turbo-16k": {
input: 0.003,
output: 0.004,
},
};
constructor(config = {}) {
const {
options = {
apiKey: process.env.OPEN_AI_KEY,
maxRetries: 3,
},
model = "gpt-3.5-turbo",
} = config;
const client = new OpenAI(options);
super(client);
this.model = model;
}
/**
* Create a completion based on the received messages.
*
* @param messages A list of messages to send to the OpenAI API.
* @param functions
* @returns The completion.
*/
async complete(messages, functions = null) {
try {
const response = await this.client.chat.completions.create({
model: this.model,
// stream: true,
messages,
...(Array.isArray(functions) && functions?.length > 0
? { functions }
: {}),
});
// Right now, we only support one completion,
// so we just take the first one in the list
const completion = response.choices[0].message;
const cost = this.getCost(response.usage);
// treat function calls
if (completion.function_call) {
let functionArgs = {};
try {
functionArgs = JSON.parse(completion.function_call.arguments);
} catch (error) {
// call the complete function again in case it gets a json error
return this.complete(
[
...messages,
{
role: "function",
name: completion.function_call.name,
function_call: completion.function_call,
content: error?.message,
},
],
functions
);
}
// console.log(completion, { functionArgs })
return {
result: null,
functionCall: {
name: completion.function_call.name,
arguments: functionArgs,
},
cost,
};
}
return {
result: completion.content,
cost,
};
} catch (error) {
console.log(error);
if (
error instanceof OpenAI.RateLimitError ||
error instanceof OpenAI.InternalServerError ||
error instanceof OpenAI.APIError
) {
throw new RetryError(error.message);
}
throw error;
}
}
/**
* Get the cost of the completion.
*
* @param usage The completion to get the cost for.
* @returns The cost of the completion.
*/
getCost(usage) {
if (!usage) {
return Number.NaN;
}
// regex to remove the version number from the model
const modelBase = this.model.replace(/-(\d{4})$/, "");
if (!(modelBase in OpenAIProvider.COST_PER_TOKEN)) {
return Number.NaN;
}
const costPerToken = OpenAIProvider.COST_PER_TOKEN?.[modelBase];
const inputCost = (usage.prompt_tokens / 1000) * costPerToken.input;
const outputCost = (usage.completion_tokens / 1000) * costPerToken.output;
return inputCost + outputCost;
}
}
module.exports = OpenAIProvider;

View File

@ -0,0 +1,35 @@
// Some models may attempt to call an expensive or annoying function many times and in that case we will want
// to implement some stateful tracking during that agent session. GPT4 and other more powerful models are smart
// enough to realize this, but models like 3.5 lack this. Open source models suffer greatly from this issue.
// eg: "save something to file..."
// agent -> saves
// agent -> saves
// agent -> saves
// agent -> saves
// ... do random # of times.
// We want to block all the reruns of a plugin, so we can add this to prevent that behavior from
// spamming the user (or other costly function) that have the exact same signatures.
const crypto = require("crypto");
class Deduplicator {
#hashes = {};
constructor() {}
trackRun(key, params = {}) {
const hash = crypto
.createHash("sha256")
.update(JSON.stringify({ key, params }))
.digest("hex");
this.#hashes[hash] = Number(new Date());
}
isDuplicate(key, params = {}) {
const newSig = crypto
.createHash("sha256")
.update(JSON.stringify({ key, params }))
.digest("hex");
return this.#hashes.hasOwnProperty(newSig);
}
}
module.exports.Deduplicator = Deduplicator;

View File

@ -0,0 +1,52 @@
const { loadSummarizationChain } = require("langchain/chains");
const { ChatOpenAI } = require("langchain/chat_models/openai");
const { PromptTemplate } = require("langchain/prompts");
const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter");
/**
* Summarize content using OpenAI's GPT-3.5 model.
*
* @param self The context of the caller function
* @param content The content to summarize.
* @returns The summarized content.
*/
async function summarizeContent(controllerSignal, content) {
const llm = new ChatOpenAI({
openAIApiKey: process.env.OPEN_AI_KEY,
temperature: 0,
modelName: "gpt-3.5-turbo-16k-0613",
});
const textSplitter = new RecursiveCharacterTextSplitter({
separators: ["\n\n", "\n"],
chunkSize: 10000,
chunkOverlap: 500,
});
const docs = await textSplitter.createDocuments([content]);
const mapPrompt = `
Write a detailed summary of the following text for a research purpose:
"{text}"
SUMMARY:
`;
const mapPromptTemplate = new PromptTemplate({
template: mapPrompt,
inputVariables: ["text"],
});
// This convenience function creates a document chain prompted to summarize a set of documents.
const chain = loadSummarizationChain(llm, {
type: "map_reduce",
combinePrompt: mapPromptTemplate,
combineMapPrompt: mapPromptTemplate,
verbose: process.env.NODE_ENV === "development",
});
const res = await chain.call({
...(controllerSignal ? { signal: controllerSignal } : {}),
input_documents: docs,
});
return res.text;
}
module.exports = { summarizeContent };

View File

@ -0,0 +1,42 @@
const AgentPlugins = require("./aibitat/plugins");
const { SystemSettings } = require("../../models/systemSettings");
const { safeJsonParse } = require("../http");
const USER_AGENT = {
name: "USER",
getDefinition: async () => {
return {
interrupt: "ALWAYS",
role: "I am the human monitor and oversee this chat. Any questions on action or decision making should be directed to me.",
};
},
};
const WORKSPACE_AGENT = {
name: "@agent",
getDefinition: async () => {
const defaultFunctions = [
AgentPlugins.memory.name, // RAG
AgentPlugins.docSummarizer.name, // Doc Summary
AgentPlugins.webScraping.name, // Collector web-scraping
];
const _setting = (
await SystemSettings.get({ label: "default_agent_skills" })
)?.value;
safeJsonParse(_setting, []).forEach((skillName) => {
if (!AgentPlugins.hasOwnProperty(skillName)) return;
defaultFunctions.push(AgentPlugins[skillName].name);
});
return {
role: "You are a helpful ai assistant who can assist the user and use tools available to help answer the users prompts and questions.",
functions: defaultFunctions,
};
},
};
module.exports = {
USER_AGENT,
WORKSPACE_AGENT,
};

View File

@ -0,0 +1,201 @@
const AIbitat = require("./aibitat");
const AgentPlugins = require("./aibitat/plugins");
const {
WorkspaceAgentInvocation,
} = require("../../models/workspaceAgentInvocation");
const { WorkspaceChats } = require("../../models/workspaceChats");
const { safeJsonParse } = require("../http");
const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults");
class AgentHandler {
#invocationUUID;
#funcsToLoad = [];
invocation = null;
aibitat = null;
channel = null;
provider = null;
model = null;
constructor({ uuid }) {
this.#invocationUUID = uuid;
}
log(text, ...args) {
console.log(`\x1b[36m[AgentHandler]\x1b[0m ${text}`, ...args);
}
closeAlert() {
this.log(`End ${this.#invocationUUID}::${this.provider}:${this.model}`);
}
async #chatHistory(limit = 10) {
try {
const rawHistory = (
await WorkspaceChats.where(
{
workspaceId: this.invocation.workspace_id,
user_id: this.invocation.user_id || null,
thread_id: this.invocation.user_id || null,
include: true,
},
limit,
{ id: "desc" }
)
).reverse();
const agentHistory = [];
rawHistory.forEach((chatLog) => {
agentHistory.push(
{
from: USER_AGENT.name,
to: WORKSPACE_AGENT.name,
content: chatLog.prompt,
},
{
from: WORKSPACE_AGENT.name,
to: USER_AGENT.name,
content: safeJsonParse(chatLog.response)?.text || "",
state: "success",
}
);
});
return agentHistory;
} catch (e) {
this.log("Error loading chat history", e.message);
return [];
}
}
#checkSetup() {
switch (this.provider) {
case "openai":
if (!process.env.OPEN_AI_KEY)
throw new Error("OpenAI API key must be provided to use agents.");
break;
case "anthropic":
if (!process.env.ANTHROPIC_API_KEY)
throw new Error("Anthropic API key must be provided to use agents.");
break;
default:
throw new Error("No provider found to power agent cluster.");
}
}
#providerSetupAndCheck() {
this.provider = this.invocation.workspace.agentProvider || "openai";
this.model = this.invocation.workspace.agentModel || "gpt-3.5-turbo";
this.log(`Start ${this.#invocationUUID}::${this.provider}:${this.model}`);
this.#checkSetup();
}
async #validInvocation() {
const invocation = await WorkspaceAgentInvocation.getWithWorkspace({
uuid: String(this.#invocationUUID),
});
if (invocation?.closed)
throw new Error("This agent invocation is already closed");
this.invocation = invocation ?? null;
}
#attachPlugins(args) {
for (const name of this.#funcsToLoad) {
if (!AgentPlugins.hasOwnProperty(name)) {
this.log(
`${name} is not a valid plugin. Skipping inclusion to agent cluster.`
);
continue;
}
const callOpts = {};
for (const [param, definition] of Object.entries(
AgentPlugins[name].startupConfig.params
)) {
if (
definition.required &&
(!args.hasOwnProperty(param) || args[param] === null)
) {
this.log(
`'${param}' required parameter for '${name}' plugin is missing. Plugin may not function or crash agent.`
);
continue;
}
callOpts[param] = args.hasOwnProperty(param)
? args[param]
: definition.default || null;
}
const AIbitatPlugin = AgentPlugins[name];
this.aibitat.use(AIbitatPlugin.plugin(callOpts));
this.log(`Attached ${name} plugin to Agent cluster`);
}
}
async #loadAgents() {
// Default User agent and workspace agent
this.log(`Attaching user and default agent to Agent cluster.`);
this.aibitat.agent(USER_AGENT.name, await USER_AGENT.getDefinition());
this.aibitat.agent(
WORKSPACE_AGENT.name,
await WORKSPACE_AGENT.getDefinition()
);
this.#funcsToLoad = [
...((await USER_AGENT.getDefinition())?.functions || []),
...((await WORKSPACE_AGENT.getDefinition())?.functions || []),
];
}
async init() {
await this.#validInvocation();
this.#providerSetupAndCheck();
return this;
}
async createAIbitat(
args = {
socket,
}
) {
this.aibitat = new AIbitat({
provider: "openai",
model: "gpt-3.5-turbo",
chats: await this.#chatHistory(20),
handlerProps: {
invocation: this.invocation,
log: this.log,
},
});
// Attach standard websocket plugin for frontend communication.
this.log(`Attached ${AgentPlugins.websocket.name} plugin to Agent cluster`);
this.aibitat.use(
AgentPlugins.websocket.plugin({
socket: args.socket,
muteUserReply: true,
introspection: true,
})
);
// Attach standard chat-history plugin for message storage.
this.log(
`Attached ${AgentPlugins.chatHistory.name} plugin to Agent cluster`
);
this.aibitat.use(AgentPlugins.chatHistory.plugin());
// Load required agents (Default + custom)
await this.#loadAgents();
// Attach all required plugins for functions to operate.
this.#attachPlugins(args);
}
startAgentCluster() {
return this.aibitat.start({
from: USER_AGENT.name,
to: this.channel ?? WORKSPACE_AGENT.name,
content: this.invocation.prompt,
});
}
}
module.exports.AgentHandler = AgentHandler;

View File

@ -0,0 +1,71 @@
const pluralize = require("pluralize");
const {
WorkspaceAgentInvocation,
} = require("../../models/workspaceAgentInvocation");
const { writeResponseChunk } = require("../helpers/chat/responses");
async function grepAgents({
uuid,
response,
message,
workspace,
user = null,
thread = null,
}) {
const agentHandles = WorkspaceAgentInvocation.parseAgents(message);
if (agentHandles.length > 0) {
const { invocation: newInvocation } = await WorkspaceAgentInvocation.new({
prompt: message,
workspace: workspace,
user: user,
thread: thread,
});
if (!newInvocation) {
writeResponseChunk(response, {
id: uuid,
type: "statusResponse",
textResponse: `${pluralize(
"Agent",
agentHandles.length
)} ${agentHandles.join(
", "
)} could not be called. Chat will be handled as default chat.`,
sources: [],
close: true,
error: null,
});
return;
}
writeResponseChunk(response, {
id: uuid,
type: "agentInitWebsocketConnection",
textResponse: null,
sources: [],
close: false,
error: null,
websocketUUID: newInvocation.uuid,
});
// Close HTTP stream-able chunk response method because we will swap to agents now.
writeResponseChunk(response, {
id: uuid,
type: "statusResponse",
textResponse: `${pluralize(
"Agent",
agentHandles.length
)} ${agentHandles.join(
", "
)} invoked.\nSwapping over to agent chat. Type /exit to exit agent execution loop early.`,
sources: [],
close: true,
error: null,
});
return true;
}
return false;
}
module.exports = { grepAgents };

View File

@ -3,6 +3,7 @@ const { DocumentManager } = require("../DocumentManager");
const { WorkspaceChats } = require("../../models/workspaceChats");
const { getVectorDbClass, getLLMProvider } = require("../helpers");
const { writeResponseChunk } = require("../helpers/chat/responses");
const { grepAgents } = require("./agents");
const {
grepCommand,
VALID_COMMANDS,
@ -35,6 +36,17 @@ async function streamChatWithWorkspace(
return;
}
// If is agent enabled chat we will exit this flow early.
const isAgentChat = await grepAgents({
uuid,
response,
message,
user,
workspace,
thread,
});
if (isAgentChat) return;
const LLMConnector = getLLMProvider({
provider: workspace?.chatProvider,
model: workspace?.chatModel,

View File

@ -133,6 +133,29 @@ class CollectorApi {
return { success: false, data: {}, reason: e.message };
});
}
async getLinkContent(link = "") {
if (!link) return false;
const data = JSON.stringify({ link });
return await fetch(`${this.endpoint}/util/get-link`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Integrity": this.comkey.sign(data),
},
body: data,
})
.then((res) => {
if (!res.ok) throw new Error("Response could not be completed");
return res.json();
})
.then((res) => res)
.catch((e) => {
this.log(e.message);
return { success: false, content: null };
});
}
}
module.exports.CollectorApi = CollectorApi;

View File

@ -292,6 +292,20 @@ const KEY_MAPPING = {
envKey: "DISABLE_TELEMETRY",
checks: [],
},
// Agent Integration ENVs
AgentGoogleSearchEngineId: {
envKey: "AGENT_GSE_CTX",
checks: [],
},
AgentGoogleSearchEngineKey: {
envKey: "AGENT_GSE_KEY",
checks: [],
},
AgentSerperApiKey: {
envKey: "AGENT_SERPER_DEV_KEY",
checks: [],
},
};
function isNotEmpty(input = "") {
@ -571,6 +585,12 @@ async function dumpENV() {
"HTTPS_KEY_PATH",
// DISABLED TELEMETRY
"DISABLE_TELEMETRY",
// Agent Integrations
// Search engine integrations
"AGENT_GSE_CTX",
"AGENT_GSE_KEY",
"AGENT_SERPER_DEV_KEY",
];
// Simple sanitization of each value to prevent ENV injection via newline or quote escaping.

View File

@ -302,6 +302,119 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044"
integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==
"@inquirer/checkbox@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-2.2.1.tgz#100fcade0209a9b5eaef80403e06130401a0b438"
integrity sha512-eYdhZWZMOaliMBPOL/AO3uId58lp+zMyrJdoZ2xw9hfUY4IYJlIMvgW80RJdvCY3q9fGMUyZI5GwguH2tO51ew==
dependencies:
"@inquirer/core" "^7.1.1"
"@inquirer/type" "^1.2.1"
ansi-escapes "^4.3.2"
chalk "^4.1.2"
figures "^3.2.0"
"@inquirer/confirm@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-3.1.1.tgz#e17c9eafa3d8f494fad3f848ba1e4c61d0a7ddcf"
integrity sha512-epf2RVHJJxX5qF85U41PBq9qq2KTJW9sKNLx6+bb2/i2rjXgeoHVGUm8kJxZHavrESgXgBLKCABcfOJYIso8cQ==
dependencies:
"@inquirer/core" "^7.1.1"
"@inquirer/type" "^1.2.1"
"@inquirer/core@^7.1.1":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-7.1.1.tgz#9339095720c00cfd1f85943977ae15d2f66f336a"
integrity sha512-rD1UI3eARN9qJBcLRXPOaZu++Bg+xsk0Tuz1EUOXEW+UbYif1sGjr0Tw7lKejHzKD9IbXE1CEtZ+xR/DrNlQGQ==
dependencies:
"@inquirer/type" "^1.2.1"
"@types/mute-stream" "^0.0.4"
"@types/node" "^20.11.30"
"@types/wrap-ansi" "^3.0.0"
ansi-escapes "^4.3.2"
chalk "^4.1.2"
cli-spinners "^2.9.2"
cli-width "^4.1.0"
figures "^3.2.0"
mute-stream "^1.0.0"
signal-exit "^4.1.0"
strip-ansi "^6.0.1"
wrap-ansi "^6.2.0"
"@inquirer/editor@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-2.1.1.tgz#e2d50246fd7dd4b4c2f20b86c969912be4c36899"
integrity sha512-SGVAmSKY2tt62+5KUySYFeMwJEXX866Ws5MyjwbrbB+WqC8iZAtPcK0pz8KVsO0ak/DB3/vCZw0k2nl7TifV5g==
dependencies:
"@inquirer/core" "^7.1.1"
"@inquirer/type" "^1.2.1"
external-editor "^3.1.0"
"@inquirer/expand@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-2.1.1.tgz#5364c5ddf0fb6358c5610103efde6a4aa366c2fe"
integrity sha512-FTHf56CgE24CtweB+3sF4mOFa6Q7H8NfTO+SvYio3CgQwhIWylSNueEeJ7sYBnWaXHNUfiX883akgvSbWqSBoQ==
dependencies:
"@inquirer/core" "^7.1.1"
"@inquirer/type" "^1.2.1"
chalk "^4.1.2"
"@inquirer/input@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-2.1.1.tgz#a293a1d1bef103a1f4176d5b41df6d3272b7b48f"
integrity sha512-Ag5PDh3/V3B68WGD/5LKXDqbdWKlF7zyfPAlstzu0NoZcZGBbZFjfgXlZIcb6Gs+AfdSi7wNf7soVAaMGH7moQ==
dependencies:
"@inquirer/core" "^7.1.1"
"@inquirer/type" "^1.2.1"
"@inquirer/password@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-2.1.1.tgz#9465dc1afa28bc75de2ee5fdb18852a25b2fe00e"
integrity sha512-R5R6NVXDKXEjAOGBqgRGrchFlfdZIx/qiDvH63m1u1NQVOQFUMfHth9VzVwuTZ2LHzbb9UrYpBumh2YytFE9iQ==
dependencies:
"@inquirer/core" "^7.1.1"
"@inquirer/type" "^1.2.1"
ansi-escapes "^4.3.2"
"@inquirer/prompts@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-4.3.1.tgz#f2906a5d7b4c2c8af9bd5bd8d495466bdd52f411"
integrity sha512-FI8jhVm3GRJ/z40qf7YZnSP0TfPKDPdIYZT9W6hmiYuaSmAXL66YMXqonKyysE5DwtKQBhIqt0oSoTKp7FCvQQ==
dependencies:
"@inquirer/checkbox" "^2.2.1"
"@inquirer/confirm" "^3.1.1"
"@inquirer/core" "^7.1.1"
"@inquirer/editor" "^2.1.1"
"@inquirer/expand" "^2.1.1"
"@inquirer/input" "^2.1.1"
"@inquirer/password" "^2.1.1"
"@inquirer/rawlist" "^2.1.1"
"@inquirer/select" "^2.2.1"
"@inquirer/rawlist@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-2.1.1.tgz#07ba2f9c4185e3787954e4023ae16d1a44d6da92"
integrity sha512-PIpJdNqVhjnl2bDz8iUKqMmgGdspN4s7EZiuNPnNrqZLP+LRUDDHVyd7X7xjiEMulBt3lt2id4SjTbra+v/Ajg==
dependencies:
"@inquirer/core" "^7.1.1"
"@inquirer/type" "^1.2.1"
chalk "^4.1.2"
"@inquirer/select@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-2.2.1.tgz#cd1f8b7869a74ff7f409a01f27998d06e234ea98"
integrity sha512-JR4FeHvuxPSPWQy8DzkIvoIsJ4SWtSFb4xVLvLto84dL+jkv12lm8ILtuax4bMHvg5MBj3wYUF6Tk9izJ07gdw==
dependencies:
"@inquirer/core" "^7.1.1"
"@inquirer/type" "^1.2.1"
ansi-escapes "^4.3.2"
chalk "^4.1.2"
figures "^3.2.0"
"@inquirer/type@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-1.2.1.tgz#fbc7ab3a2e5050d0c150642d5e8f5e88faa066b8"
integrity sha512-xwMfkPAxeo8Ji/IxfUSqzRi0/+F2GIqJmpc5/thelgMGsjNZcjDDRBO9TLXT1s/hdx/mK5QbVIvgoLIFgXhTMQ==
"@kwsites/file-exists@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99"
@ -802,6 +915,13 @@
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==
"@types/mute-stream@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@types/mute-stream/-/mute-stream-0.0.4.tgz#77208e56a08767af6c5e1237be8888e2f255c478"
integrity sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==
dependencies:
"@types/node" "*"
"@types/node-fetch@^2.6.4":
version "2.6.7"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.7.tgz#a1abe2ce24228b58ad97f99480fdcf9bbc6ab16d"
@ -841,6 +961,13 @@
dependencies:
undici-types "~5.26.4"
"@types/node@^20.11.30":
version "20.12.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.4.tgz#af5921bd75ccdf3a3d8b3fa75bf3d3359268cd11"
integrity sha512-E+Fa9z3wSQpzgYQdYmme5X3OTuejnnTx88A6p6vkkJosR3KBz+HpE3kqNm98VE6cfLFcISx7zW7MsJkH6KwbTw==
dependencies:
undici-types "~5.26.4"
"@types/pad-left@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@types/pad-left/-/pad-left-2.1.1.tgz#17d906fc75804e1cc722da73623f1d978f16a137"
@ -861,6 +988,11 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8"
integrity sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==
"@types/wrap-ansi@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd"
integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==
"@types/yauzl@^2.9.1":
version "2.10.0"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
@ -982,6 +1114,13 @@ ajv@^8.12.0:
require-from-string "^2.0.2"
uri-js "^4.2.2"
ansi-escapes@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
dependencies:
type-fest "^0.21.3"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@ -1335,6 +1474,11 @@ body-parser@^1.20.2:
type-is "~1.6.18"
unpipe "1.0.0"
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
bottleneck@^2.15.3:
version "2.19.5"
resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.19.5.tgz#5df0b90f59fd47656ebe63c78a98419205cadd91"
@ -1475,7 +1619,7 @@ chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0.0:
chalk@^4, chalk@^4.0.0, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -1488,6 +1632,11 @@ chalk@^5.0.0, chalk@^5.3.0:
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
charenc@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
@ -1554,11 +1703,16 @@ cli-progress@^3.12.0:
dependencies:
string-width "^4.2.3"
cli-spinners@^2.9.0:
cli-spinners@^2.9.0, cli-spinners@^2.9.2:
version "2.9.2"
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41"
integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==
cli-width@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5"
integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@ -1796,6 +1950,22 @@ crypt@0.0.2:
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==
css-select@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
dependencies:
boolbase "^1.0.0"
css-what "^6.1.0"
domhandler "^5.0.2"
domutils "^3.0.1"
nth-check "^2.0.1"
css-what@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
dayjs@^1.11.7:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
@ -1919,6 +2089,36 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-serializer@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.2"
entities "^4.2.0"
domelementtype@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
dependencies:
domelementtype "^2.3.0"
domutils@^3.0.1:
version "3.1.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
dependencies:
dom-serializer "^2.0.0"
domelementtype "^2.3.0"
domhandler "^5.0.3"
dotenv@^16.0.3:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
@ -1980,6 +2180,11 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
dependencies:
once "^1.4.0"
entities@^4.2.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
env-paths@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
@ -2273,6 +2478,13 @@ expr-eval@^2.0.2:
resolved "https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201"
integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==
express-ws@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/express-ws/-/express-ws-5.0.2.tgz#5b02d41b937d05199c6c266d7cc931c823bda8eb"
integrity sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==
dependencies:
ws "^7.4.6"
express@^4.18.2:
version "4.18.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
@ -2315,6 +2527,15 @@ extend@^3.0.2:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
external-editor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
dependencies:
chardet "^0.7.0"
iconv-lite "^0.4.24"
tmp "^0.0.33"
extract-files@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a"
@ -2380,6 +2601,13 @@ fecha@^4.2.0:
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
figures@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
dependencies:
escape-string-regexp "^1.0.5"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -2868,6 +3096,11 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
he@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hermes-eslint@^0.15.0:
version "0.15.1"
resolved "https://registry.yarnpkg.com/hermes-eslint/-/hermes-eslint-0.15.1.tgz#c5919a6fdbd151febc3d5ed8ff17e5433913528c"
@ -2950,7 +3183,7 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
iconv-lite@0.4.24:
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -3928,6 +4161,11 @@ multer@^1.4.5-lts.1:
type-is "^1.6.4"
xtend "^4.0.0"
mute-stream@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-1.0.0.tgz#e31bd9fe62f0aed23520aa4324ea6671531e013e"
integrity sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==
napi-build-utils@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
@ -4008,6 +4246,21 @@ node-gyp@8.x:
tar "^6.1.2"
which "^2.0.2"
node-html-markdown@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/node-html-markdown/-/node-html-markdown-1.3.0.tgz#ef0b19a3bbfc0f1a880abb9ff2a0c9aa6bbff2a9"
integrity sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==
dependencies:
node-html-parser "^6.1.1"
node-html-parser@^6.1.1:
version "6.1.13"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4"
integrity sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==
dependencies:
css-select "^5.1.0"
he "1.2.0"
node-llama-cpp@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/node-llama-cpp/-/node-llama-cpp-2.8.0.tgz#c01e469761caa4b9c51dbcf7555260caf7fb7bd6"
@ -4090,6 +4343,13 @@ npmlog@^6.0.0, npmlog@^6.0.2:
gauge "^4.0.3"
set-blocking "^2.0.0"
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
dependencies:
boolbase "^1.0.0"
num-sort@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/num-sort/-/num-sort-2.1.0.tgz#1cbb37aed071329fdf41151258bc011898577a9b"
@ -4235,6 +4495,20 @@ onnxruntime-web@1.14.0:
onnxruntime-common "~1.14.0"
platform "^1.3.6"
"openai:latest@npm:openai@latest":
version "4.32.1"
resolved "https://registry.yarnpkg.com/openai/-/openai-4.32.1.tgz#9e375fdbc727330c5ea5d287beb325db3e6f9ad7"
integrity sha512-3e9QyCY47tgOkxBe2CSVKlXOE2lLkMa24Y0s3LYZR40yYjiBU9dtVze+C3mu1TwWDGiRX52STpQAEJZvRNuIrA==
dependencies:
"@types/node" "^18.11.18"
"@types/node-fetch" "^2.6.4"
abort-controller "^3.0.0"
agentkeepalive "^4.2.1"
form-data-encoder "1.7.2"
formdata-node "^4.3.2"
node-fetch "^2.6.7"
web-streams-polyfill "^3.2.1"
openai@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/openai/-/openai-3.3.0.tgz#a6408016ad0945738e1febf43f2fccca83a3f532"
@ -4290,6 +4564,11 @@ ora@^7.0.1:
string-width "^6.1.0"
strip-ansi "^7.1.0"
os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
@ -4412,6 +4691,11 @@ platform@^1.3.6:
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
pluralize@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
posthog-node@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-3.1.1.tgz#f92c44a871552c9bfb98bf4cc8fd326d36af6cbd"
@ -4931,6 +5215,11 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.7:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
signal-exit@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
@ -5271,6 +5560,13 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
dependencies:
os-tmpdir "~1.0.2"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@ -5334,6 +5630,11 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^0.21.3:
version "0.21.3"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
type-is@^1.6.4, type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@ -5675,6 +5976,15 @@ wordwrapjs@^4.0.0:
reduce-flatten "^2.0.0"
typical "^5.2.0"
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
@ -5689,6 +5999,11 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^7.4.6:
version "7.5.9"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
xtend@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"