mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
add ask to run prompt for tool calls (demo) (#5261)
* add ask to run prompt for tools * border-none on buttons * translations * linting * i18n (#5263) * extend approve/deny requests to telegram * break up handler
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { CaretDown, Check, X, Hammer } from "@phosphor-icons/react";
|
||||
import AgentSkillWhitelist from "@/models/agentSkillWhitelist";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ToolApprovalRequest({
|
||||
requestId,
|
||||
skillName,
|
||||
payload = {},
|
||||
description = null,
|
||||
timeoutMs = null,
|
||||
websocket,
|
||||
onResponse,
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [responded, setResponded] = useState(false);
|
||||
const [approved, setApproved] = useState(null);
|
||||
const [alwaysAllow, setAlwaysAllow] = useState(false);
|
||||
const [timeRemaining, setTimeRemaining] = useState(timeoutMs);
|
||||
const startTimeRef = useRef(null);
|
||||
const hasPayload = payload && Object.keys(payload).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!timeoutMs || responded) return;
|
||||
if (startTimeRef.current === null) {
|
||||
startTimeRef.current = Date.now();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
const elapsed = Date.now() - startTimeRef.current;
|
||||
const remaining = Math.max(0, timeoutMs - elapsed);
|
||||
setTimeRemaining(remaining);
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(intervalId);
|
||||
handleTimeout();
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [timeoutMs, responded]);
|
||||
|
||||
function handleTimeout() {
|
||||
if (responded) return;
|
||||
setResponded(true);
|
||||
setApproved(false);
|
||||
onResponse?.(false);
|
||||
}
|
||||
|
||||
async function handleResponse(isApproved) {
|
||||
if (responded) return;
|
||||
|
||||
setResponded(true);
|
||||
setApproved(isApproved);
|
||||
|
||||
// If user approved and checked "Always allow", add to whitelist
|
||||
if (isApproved && alwaysAllow) {
|
||||
await AgentSkillWhitelist.addToWhitelist(skillName);
|
||||
}
|
||||
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
type: "toolApprovalResponse",
|
||||
requestId,
|
||||
approved: isApproved,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onResponse?.(isApproved);
|
||||
}
|
||||
|
||||
const progressPercent = timeoutMs ? (timeRemaining / timeoutMs) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="flex justify-center w-full my-1 pr-4">
|
||||
<div className="w-full flex flex-col">
|
||||
<div className="w-full">
|
||||
<div
|
||||
style={{
|
||||
transition: "all 0.1s ease-in-out",
|
||||
borderRadius: "16px",
|
||||
}}
|
||||
className="relative bg-zinc-800 light:bg-slate-100 p-4 pb-2 flex flex-col gap-y-1 overflow-hidden"
|
||||
>
|
||||
<ToolApprovalHeader
|
||||
skillName={skillName}
|
||||
hasPayload={hasPayload}
|
||||
isExpanded={isExpanded}
|
||||
setIsExpanded={setIsExpanded}
|
||||
/>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
{description && (
|
||||
<span className="text-white/60 light:text-slate-700 font-medium font-mono text-xs">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
<ToolApprovalPayload payload={payload} isExpanded={isExpanded} />
|
||||
<ToolApprovalResponseOption
|
||||
approved={approved}
|
||||
skillName={skillName}
|
||||
alwaysAllow={alwaysAllow}
|
||||
setAlwaysAllow={setAlwaysAllow}
|
||||
onApprove={() => handleResponse(true)}
|
||||
onReject={() => handleResponse(false)}
|
||||
/>
|
||||
<ToolApprovalResponseMessage approved={approved} />
|
||||
</div>
|
||||
{timeoutMs && !responded && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-1 bg-zinc-700 light:bg-slate-300">
|
||||
<div
|
||||
className="h-full bg-sky-500 light:bg-sky-600 transition-none"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolApprovalHeader({
|
||||
skillName,
|
||||
hasPayload,
|
||||
isExpanded,
|
||||
setIsExpanded,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Hammer size={16} />
|
||||
<div className="text-white/80 light:text-slate-900 font-medium text-sm flex gap-x-1">
|
||||
{t("chat_window.agent_invocation.model_wants_to_call")}
|
||||
<span className="font-semibold text-sky-400 light:text-sky-600">
|
||||
{skillName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{hasPayload && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="absolute top-4 right-4 border-none"
|
||||
aria-label={isExpanded ? "Hide details" : "Show details"}
|
||||
>
|
||||
<CaretDown
|
||||
className={`w-4 h-4 transform transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolApprovalPayload({ payload, isExpanded }) {
|
||||
const hasPayload = payload && Object.keys(payload).length > 0;
|
||||
if (!hasPayload || !isExpanded) return null;
|
||||
|
||||
function formatPayload(data) {
|
||||
if (typeof data === "string") return data;
|
||||
try {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch {
|
||||
return String(data);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-zinc-900/50 light:bg-slate-200/50 rounded-lg overflow-x-auto">
|
||||
<pre className="text-xs text-zinc-300 light:text-slate-700 font-mono whitespace-pre-wrap break-words">
|
||||
{formatPayload(payload)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolApprovalResponseOption({
|
||||
approved,
|
||||
skillName,
|
||||
alwaysAllow,
|
||||
setAlwaysAllow,
|
||||
onApprove,
|
||||
onReject,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (approved !== null) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 mt-1 pb-2">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
className="border-none transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
|
||||
>
|
||||
{t("chat_window.agent_invocation.approve")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReject}
|
||||
className="border-none text-white light:text-slate-900 text-sm font-medium w-[70px] h-9 rounded-lg hover:bg-white/5 light:hover:bg-slate-300"
|
||||
>
|
||||
{t("chat_window.agent_invocation.reject")}
|
||||
</button>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 cursor-pointer text-white/60 light:text-slate-600 text-xs hover:text-white/80 light:hover:text-slate-800 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={alwaysAllow}
|
||||
onChange={(e) => setAlwaysAllow(e.target.checked)}
|
||||
className="w-3.5 h-3.5 rounded border-white/20 bg-transparent cursor-pointer"
|
||||
/>
|
||||
<span>
|
||||
{t("chat_window.agent_invocation.always_allow", { skillName })}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolApprovalResponseMessage({ approved }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (approved === null) return null;
|
||||
if (approved === false) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-red-400 light:text-red-500">
|
||||
<X size={16} weight="bold" />
|
||||
<span>{t("chat_window.agent_invocation.tool_call_was_rejected")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-sm font-medium text-green-400 light:text-green-500">
|
||||
<Check size={16} weight="bold" />
|
||||
<span>{t("chat_window.agent_invocation.tool_call_was_approved")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import HistoricalMessage from "./HistoricalMessage";
|
||||
import PromptReply from "./PromptReply";
|
||||
import StatusResponse from "./StatusResponse";
|
||||
import ToolApprovalRequest from "./ToolApprovalRequest";
|
||||
import { useManageWorkspaceModal } from "../../../Modals/ManageWorkspace";
|
||||
import ManageWorkspace from "../../../Modals/ManageWorkspace";
|
||||
import { ArrowDown } from "@phosphor-icons/react";
|
||||
@@ -29,6 +30,7 @@ export default forwardRef(function (
|
||||
sendCommand,
|
||||
updateHistory,
|
||||
regenerateAssistantMessage,
|
||||
websocket = null,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
@@ -179,6 +181,7 @@ export default forwardRef(function (
|
||||
regenerateAssistantMessage,
|
||||
saveEditedMessage,
|
||||
forkThread,
|
||||
websocket,
|
||||
}),
|
||||
[
|
||||
workspace,
|
||||
@@ -186,6 +189,7 @@ export default forwardRef(function (
|
||||
regenerateAssistantMessage,
|
||||
saveEditedMessage,
|
||||
forkThread,
|
||||
websocket,
|
||||
]
|
||||
);
|
||||
const lastMessageInfo = useMemo(() => getLastMessageInfo(history), [history]);
|
||||
@@ -262,6 +266,7 @@ const getLastMessageInfo = (history) => {
|
||||
* @param {Function} param0.regenerateAssistantMessage - The function to regenerate the assistant message.
|
||||
* @param {Function} param0.saveEditedMessage - The function to save the edited message.
|
||||
* @param {Function} param0.forkThread - The function to fork the thread.
|
||||
* @param {WebSocket} param0.websocket - The active websocket connection for agent communication.
|
||||
* @returns {Array} The compiled history of messages.
|
||||
*/
|
||||
function buildMessages({
|
||||
@@ -270,6 +275,7 @@ function buildMessages({
|
||||
regenerateAssistantMessage,
|
||||
saveEditedMessage,
|
||||
forkThread,
|
||||
websocket,
|
||||
}) {
|
||||
return history.reduce((acc, props, index) => {
|
||||
const isLastBotReply =
|
||||
@@ -284,6 +290,21 @@ function buildMessages({
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (props.type === "toolApprovalRequest") {
|
||||
acc.push(
|
||||
<ToolApprovalRequest
|
||||
key={`tool-approval-${props.requestId}`}
|
||||
requestId={props.requestId}
|
||||
skillName={props.skillName}
|
||||
payload={props.payload}
|
||||
description={props.description}
|
||||
timeoutMs={props.timeoutMs}
|
||||
websocket={websocket}
|
||||
/>
|
||||
);
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (props.type === "rechartVisualize" && !!props.content) {
|
||||
acc.push(<Chartable key={props.uuid} props={props} />);
|
||||
} else if (isLastBotReply && props.animate) {
|
||||
|
||||
@@ -428,6 +428,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
sendCommand={sendCommand}
|
||||
updateHistory={setChatHistory}
|
||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||
websocket={websocket}
|
||||
/>
|
||||
</MetricsProvider>
|
||||
<PromptInput
|
||||
|
||||
@@ -757,6 +757,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "ابدأ جلسة الممثل",
|
||||
use_agent_session_to_use_tools:
|
||||
"يمكنك استخدام الأدوات المتاحة في الدردشة عن طريق بدء جلسة مع ممثل خدمة العملاء باستخدام الرمز '@agent' في بداية رسالتك.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "النموذج يرغب في الاتصال.",
|
||||
approve: "الموافقة",
|
||||
reject: "رفض",
|
||||
always_allow: "تأكد دائمًا من {{skillName}}",
|
||||
tool_call_was_approved: "تمت الموافقة على طلب الحصول على الأدوات.",
|
||||
tool_call_was_rejected: "تم رفض طلب الاتصال بالأداة.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "تحرير الحساب",
|
||||
|
||||
@@ -886,6 +886,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Spustit relaci s agentem",
|
||||
use_agent_session_to_use_tools:
|
||||
"Můžete využít nástroje v chatu spuštěním sezení s agentem pomocí příkazu '@agent' na začátku vašeho vstupu.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Model chce zavolat",
|
||||
approve: "Schválit",
|
||||
reject: "Zamítnout",
|
||||
always_allow: "Vždy dbejte na to, aby {{skillName}}",
|
||||
tool_call_was_approved: "Žádost o použití nástroje byla schválena.",
|
||||
tool_call_was_rejected: "Žádost o použití nástroje byla zamítnuta.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Upravit účet",
|
||||
|
||||
@@ -771,6 +771,15 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Start Agent-session",
|
||||
use_agent_session_to_use_tools:
|
||||
"Du kan bruge værktøjer i chat ved at starte en agent-session med '@agent' i starten af din forespørgsel.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Modellen ønsker at ringe",
|
||||
approve: "Godkend",
|
||||
reject: "Afvise",
|
||||
always_allow: "Sørg altid for, at {{skillName}} er tilgængeligt.",
|
||||
tool_call_was_approved:
|
||||
"Anmodningen om at bruge værktøjet blev godkendt.",
|
||||
tool_call_was_rejected: "Anmodningen om at bruge værktøjet blev afvist.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Rediger konto",
|
||||
|
||||
@@ -865,6 +865,15 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Starte eine Agent-Sitzung",
|
||||
use_agent_session_to_use_tools:
|
||||
'Sie können Tools im Chat nutzen, indem Sie eine Agentensitzung mit "@agent" am Anfang Ihrer Anfrage starten.',
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Das Modell möchte anrufen.",
|
||||
approve: "Genehmigen",
|
||||
reject: "Ablehnen",
|
||||
always_allow: "Bitte stellen Sie immer {{skillName}} sicher.",
|
||||
tool_call_was_approved:
|
||||
"Die Genehmigung für die Bestellung der Werkzeuge wurde erteilt.",
|
||||
tool_call_was_rejected: "Die Anfrage nach dem Werkzeug wurde abgelehnt.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Account bearbeiten",
|
||||
|
||||
@@ -962,6 +962,14 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "This provider is missing credentials!",
|
||||
missing_credentials_description: "Set up now",
|
||||
},
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Model wants to call",
|
||||
approve: "Approve",
|
||||
reject: "Reject",
|
||||
always_allow: "Always allow {{skillName}}",
|
||||
tool_call_was_approved: "Tool call was approved",
|
||||
tool_call_was_rejected: "Tool call was rejected",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Edit Account",
|
||||
|
||||
@@ -879,6 +879,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Iniciar sesión como agente",
|
||||
use_agent_session_to_use_tools:
|
||||
"Puede utilizar las herramientas disponibles en el chat iniciando una sesión con un agente utilizando el prefijo '@agent' al principio de su mensaje.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "El modelo quiere llamar",
|
||||
approve: "Aprobar",
|
||||
reject: "Rechazar",
|
||||
always_allow: "Siempre asegúrese de que haya {{skillName}}",
|
||||
tool_call_was_approved: "La solicitud de herramientas ha sido aprobada.",
|
||||
tool_call_was_rejected: "La solicitud de herramienta fue rechazada.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Editar cuenta",
|
||||
|
||||
@@ -830,6 +830,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Alusta agenti sessiooni",
|
||||
use_agent_session_to_use_tools:
|
||||
"Saate kasutada vahendeid vestluses, alustades agenti sessiooni, lisades käskile '@agent' sõna.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Mudel soovib helistada",
|
||||
approve: "Heakskiid",
|
||||
reject: "Hüvasti, keelan",
|
||||
always_allow: "Aeg-ajalt lubage {{skillName}}",
|
||||
tool_call_was_approved: "Vahendite tellimuse kinnitati.",
|
||||
tool_call_was_rejected: "Vahendite taotlus jäeti rahuldamata.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Muuda kontot",
|
||||
|
||||
@@ -766,6 +766,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "شروع جلسه با نماینده",
|
||||
use_agent_session_to_use_tools:
|
||||
"شما میتوانید از ابزارهای موجود در چت با شروع یک جلسه با یک عامل از طریق استفاده از '@agent' در ابتدای پیام خود استفاده کنید.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "مدل میخواهد تماس بگیرد",
|
||||
approve: "تایید",
|
||||
reject: "رد",
|
||||
always_allow: "همیشه، {{skillName}} را در نظر بگیرید.",
|
||||
tool_call_was_approved: "درخواست برای تهیه ابزار تأیید شد.",
|
||||
tool_call_was_rejected: "درخواست استفاده از ابزار رد شد.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "ویرایش حساب",
|
||||
|
||||
@@ -770,6 +770,15 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Démarrer la session de l'agent",
|
||||
use_agent_session_to_use_tools:
|
||||
'Vous pouvez utiliser des outils via le chat en lançant une session avec un agent en utilisant le préfixe "@agent" au début de votre requête.',
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Le modèle souhaite passer un appel.",
|
||||
approve: "Approuver",
|
||||
reject: "Refuser",
|
||||
always_allow: "Il est toujours important de {{skillName}}",
|
||||
tool_call_was_approved: "La demande d'outils a été approuvée.",
|
||||
tool_call_was_rejected:
|
||||
"La demande d'utilisation de l'outil a été rejetée.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Modifier le compte",
|
||||
|
||||
@@ -834,6 +834,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "התחלת סשן עם סוכן",
|
||||
use_agent_session_to_use_tools:
|
||||
"ניתן להשתמש בכלי הדיון באמצעות פתיחת סשן עם נציג על ידי שימוש בסימן '@agent' בתחילת ההודעה.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "המודל רוצה להתקשר",
|
||||
approve: "אישור",
|
||||
reject: "דחייה",
|
||||
always_allow: "יש תמיד להקצות {{skillName}}",
|
||||
tool_call_was_approved: "הבקשה לקבלת הכלי אושרה.",
|
||||
tool_call_was_rejected: "בקשת השימוש בכלי נדחתה.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "ערוך חשבון",
|
||||
|
||||
@@ -779,6 +779,16 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Avvia sessione agente",
|
||||
use_agent_session_to_use_tools:
|
||||
'È possibile utilizzare gli strumenti disponibili tramite chat avviando una sessione con un agente utilizzando il prefisso "@agent" all\'inizio del messaggio.',
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Il modello desidera effettuare una chiamata.",
|
||||
approve: "Approvato",
|
||||
reject: "Rifiutare",
|
||||
always_allow: "Assicurarsi sempre di avere {{skillName}}",
|
||||
tool_call_was_approved:
|
||||
"La richiesta di fornitura di strumenti è stata approvata.",
|
||||
tool_call_was_rejected:
|
||||
"La richiesta di accesso all'attrezzatura è stata rifiutata.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Modifica account",
|
||||
|
||||
@@ -759,6 +759,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "エージェントセッションを開始",
|
||||
use_agent_session_to_use_tools:
|
||||
"チャットでツールを使用するには、プロンプトの冒頭に'@agent'を使用してエージェントセッションを開始してください。",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "モデルは電話をかけたい。",
|
||||
approve: "承認",
|
||||
reject: "拒否",
|
||||
always_allow: "常に、{{skillName}}を確保してください。",
|
||||
tool_call_was_approved: "ツールの使用許可が承認されました",
|
||||
tool_call_was_rejected: "ツール呼び出しは拒否されました",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "アカウントを編集",
|
||||
|
||||
@@ -844,6 +844,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "에이전트 세션 시작",
|
||||
use_agent_session_to_use_tools:
|
||||
"채팅에서 도구를 사용하려면, 프롬프트의 시작 부분에 '@agent'을 사용하여 에이전트 세션을 시작할 수 있습니다.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "모델이 통화하고 싶어",
|
||||
approve: "승인",
|
||||
reject: "거부",
|
||||
always_allow: "항상 {{skillName}}을 허용",
|
||||
tool_call_was_approved: "도구 사용 승인",
|
||||
tool_call_was_rejected: "도구 호출이 거부되었습니다.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "계정 정보 수정",
|
||||
|
||||
@@ -889,6 +889,14 @@ const TRANSLATIONS = {
|
||||
missing_credentials: "Šiam tiekėjui trūksta duomenų!",
|
||||
missing_credentials_description: "Nustatyti dabar",
|
||||
},
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Modelis nori paskambinti",
|
||||
approve: "Patvirtinti",
|
||||
reject: "Atmetti",
|
||||
always_allow: "Visada būkite pasiruošę {{skillName}}",
|
||||
tool_call_was_approved: "Įrankių užsakymas buvo patvirtintas.",
|
||||
tool_call_was_rejected: "Klausimas dėl įrankio buvo atmetamas.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Redaguoti paskyrą",
|
||||
|
||||
@@ -863,6 +863,15 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Sākt aģenta sesiju",
|
||||
use_agent_session_to_use_tools:
|
||||
'Jūs varat izmantot rīkus čatā, sākot aģenta sesiju, ievietojot "@agent" jūsu iniciālajā tekstā.',
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Modeļa vēlējas izrunāt",
|
||||
approve: "Aizmaksā, apstiprināts",
|
||||
reject: "Atgrūst",
|
||||
always_allow: "Vienmēr nodrošiniet {{skillName}}",
|
||||
tool_call_was_approved: "Instrumentu pieprasījums tika apstiprināts.",
|
||||
tool_call_was_rejected:
|
||||
"Pieprasījums par instrumenta izmantošanu tika atgrūstīts.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Rediģēt kontu",
|
||||
|
||||
@@ -767,6 +767,16 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Start Agent Sessie",
|
||||
use_agent_session_to_use_tools:
|
||||
'U kunt tools in de chat gebruiken door een sessie met een agent te starten, beginnend met "@agent" aan het begin van uw bericht.',
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "De klant wil een gesprek plannen.",
|
||||
approve: "Goedkeuren",
|
||||
reject: "Afgewijzen",
|
||||
always_allow: "Zorg er altijd voor dat {{skillName}} aanwezig is.",
|
||||
tool_call_was_approved:
|
||||
"De aanvraag voor het gereedschap is goedgekeurd.",
|
||||
tool_call_was_rejected:
|
||||
"De aanvraag om het gereedschap te gebruiken is afgewezen.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Account bewerken",
|
||||
|
||||
@@ -860,6 +860,15 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Rozpocznij sesję dla agenta",
|
||||
use_agent_session_to_use_tools:
|
||||
"Możesz korzystać z narzędzi w czacie, inicjując sesję z agentem, wpisując '@agent' na początku swojego zapytania.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Model chce zadzwonić",
|
||||
approve: "Zaakceptować",
|
||||
reject: "Odrzucić",
|
||||
always_allow: "Zawsze należy uwzględnić {{skillName}}",
|
||||
tool_call_was_approved:
|
||||
"Zgłoszenie dotyczące narzędzia zostało zatwierdzone.",
|
||||
tool_call_was_rejected: "Żądanie użycia narzędzia zostało odrzucone.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Edytuj konto",
|
||||
|
||||
@@ -844,6 +844,16 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Iniciar Sessão de Agente",
|
||||
use_agent_session_to_use_tools:
|
||||
'Você pode utilizar as ferramentas disponíveis no chat iniciando uma sessão com um agente, adicionando "@agent" no início da sua mensagem.',
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "O modelo deseja fazer uma ligação.",
|
||||
approve: "Aprovar",
|
||||
reject: "Rejeitar",
|
||||
always_allow:
|
||||
"Certifique-se sempre de que {{skillName}} esteja disponível.",
|
||||
tool_call_was_approved: "A solicitação de ferramentas foi aprovada.",
|
||||
tool_call_was_rejected:
|
||||
"A solicitação de acesso à ferramenta foi rejeitada.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Editar conta",
|
||||
|
||||
@@ -569,6 +569,15 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Începe sesiunea de agent",
|
||||
use_agent_session_to_use_tools:
|
||||
'Puteți utiliza instrumentele disponibile în chat, inițiind o sesiune cu un agent, începând mesajul cu "@agent".',
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Persoana respectivă dorește să facă o telefonare.",
|
||||
approve: "Aprobă",
|
||||
reject: "Refuz",
|
||||
always_allow: "Asigurați-vă întotdeauna că {{skillName}}",
|
||||
tool_call_was_approved: "Cererea de achiziție a fost aprobată.",
|
||||
tool_call_was_rejected:
|
||||
"Cererea de utilizare a instrumentului a fost respinsă.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Editează contul",
|
||||
|
||||
@@ -770,6 +770,16 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Начать сеанс для агента",
|
||||
use_agent_session_to_use_tools:
|
||||
"Вы можете использовать инструменты в чате, начав сеанс с агентом, добавив '@agent' в начало вашего сообщения.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Модель хочет позвонить",
|
||||
approve: "Одобрить",
|
||||
reject: "Отказ",
|
||||
always_allow: "Всегда оставляйте {{skillName}}",
|
||||
tool_call_was_approved:
|
||||
"Запрос на предоставление инструмента был одобрен.",
|
||||
tool_call_was_rejected:
|
||||
"Запрос на предоставление инструмента был отклонен.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Редактировать учётную запись",
|
||||
|
||||
@@ -765,6 +765,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Temsilci Oturumu Başlat",
|
||||
use_agent_session_to_use_tools:
|
||||
'Çatınızdaki araçları kullanmak için, isteminizin başında "@agent" ile bir ajan oturumu başlatabilirsiniz.',
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Model, arama yapmak istiyor",
|
||||
approve: "Onayla",
|
||||
reject: "Reddet",
|
||||
always_allow: "Her zaman {{skillName}}'ı sağlayın.",
|
||||
tool_call_was_approved: "Araç talebi onaylandı.",
|
||||
tool_call_was_rejected: "Ara çağrısı reddedildi.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Hesabı Düzenle",
|
||||
|
||||
@@ -762,6 +762,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "Bắt đầu phiên làm việc với đại lý",
|
||||
use_agent_session_to_use_tools:
|
||||
"Bạn có thể sử dụng các công cụ trong cuộc trò chuyện bằng cách bắt đầu một phiên với trợ lý bằng cách sử dụng '@agent' ở đầu yêu cầu của bạn.",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "Người mẫu muốn gọi",
|
||||
approve: "Chấp thuận",
|
||||
reject: "Từ chối",
|
||||
always_allow: "Luôn luôn đảm bảo {{skillName}}",
|
||||
tool_call_was_approved: "Đã được phê duyệt yêu cầu dụng cụ.",
|
||||
tool_call_was_rejected: "Yêu cầu gọi công cụ đã bị từ chối.",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "Chỉnh sửa Tài khoản",
|
||||
|
||||
@@ -805,6 +805,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "开始代理会",
|
||||
use_agent_session_to_use_tools:
|
||||
"您可以通过在提示词的开头使用'@agent'来启动与代理的聊天,从而使用聊天工具。",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "该型号希望进行通话。",
|
||||
approve: "批准",
|
||||
reject: "拒绝",
|
||||
always_allow: "请务必留出 {{skillName}}",
|
||||
tool_call_was_approved: "工具使用申请已获得批准。",
|
||||
tool_call_was_rejected: "请求获取工具已被拒绝。",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "编辑帐户",
|
||||
|
||||
@@ -715,6 +715,14 @@ const TRANSLATIONS = {
|
||||
start_agent_session: "開始智慧代理人工作階段",
|
||||
use_agent_session_to_use_tools:
|
||||
"若要在對話中使用工具,請在提示詞開頭加上 '@agent',即可開始智慧代理人工作階段。",
|
||||
agent_invocation: {
|
||||
model_wants_to_call: "模型想要撥打電話",
|
||||
approve: "批准",
|
||||
reject: "拒絕",
|
||||
always_allow: "請務必確保 {{skillName}}",
|
||||
tool_call_was_approved: "工具請求已獲得批准。",
|
||||
tool_call_was_rejected: "請求已遭拒絕",
|
||||
},
|
||||
},
|
||||
profile_settings: {
|
||||
edit_account: "編輯帳戶",
|
||||
|
||||
21
frontend/src/models/agentSkillWhitelist.js
Normal file
21
frontend/src/models/agentSkillWhitelist.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { API_BASE } from "@/utils/constants";
|
||||
import { baseHeaders } from "@/utils/request";
|
||||
|
||||
const AgentSkillWhitelist = {
|
||||
/**
|
||||
* Add a skill to the whitelist
|
||||
* @param {string} skillName - The skill name to whitelist
|
||||
* @returns {Promise<{success: boolean, error?: string}>}
|
||||
*/
|
||||
addToWhitelist: async function (skillName) {
|
||||
return fetch(`${API_BASE}/agent-skills/whitelist/add`, {
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({ skillName }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((e) => ({ success: false, error: e.message }));
|
||||
},
|
||||
};
|
||||
|
||||
export default AgentSkillWhitelist;
|
||||
@@ -12,6 +12,7 @@ const handledEvents = [
|
||||
"awaitingFeedback",
|
||||
"wssFailure",
|
||||
"rechartVisualize",
|
||||
"toolApprovalRequest",
|
||||
// Streaming events
|
||||
"reportStreamEvent",
|
||||
];
|
||||
@@ -47,7 +48,12 @@ export default function handleSocketResponse(socket, event, setChatHistory) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!handledEvents.includes(data.type) || !data.content) return;
|
||||
// toolApprovalRequest doesn't have content field, so check separately
|
||||
if (data.type === "toolApprovalRequest") {
|
||||
if (!data.requestId || !data.skillName) return;
|
||||
} else if (!handledEvents.includes(data.type) || !data.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === "reportStreamEvent") {
|
||||
// Enable agent streaming for the next message so we can handle streaming or non-streaming responses
|
||||
@@ -220,6 +226,31 @@ export default function handleSocketResponse(socket, event, setChatHistory) {
|
||||
});
|
||||
}
|
||||
|
||||
if (data.type === "toolApprovalRequest") {
|
||||
return setChatHistory((prev) => {
|
||||
return [
|
||||
...prev.filter((msg) => !!msg.content),
|
||||
{
|
||||
uuid: v4(),
|
||||
type: "toolApprovalRequest",
|
||||
requestId: data.requestId,
|
||||
skillName: data.skillName,
|
||||
payload: data.payload,
|
||||
description: data.description,
|
||||
timeoutMs: data.timeoutMs,
|
||||
content: `Approval requested for ${data.skillName}`,
|
||||
role: "assistant",
|
||||
sources: [],
|
||||
closed: false,
|
||||
error: null,
|
||||
animate: false,
|
||||
pending: true,
|
||||
metrics: {},
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return setChatHistory((prev) => {
|
||||
return [
|
||||
...prev.filter((msg) => !!msg.content),
|
||||
|
||||
46
server/endpoints/agentSkillWhitelist.js
Normal file
46
server/endpoints/agentSkillWhitelist.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const { AgentSkillWhitelist } = require("../models/agentSkillWhitelist");
|
||||
const { reqBody, userFromSession } = require("../utils/http");
|
||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||
const {
|
||||
flexUserRoleValid,
|
||||
ROLES,
|
||||
} = require("../utils/middleware/multiUserProtected");
|
||||
|
||||
function agentSkillWhitelistEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.post(
|
||||
"/agent-skills/whitelist/add",
|
||||
[validatedRequest, flexUserRoleValid(ROLES.all)],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const { skillName } = reqBody(request);
|
||||
if (!skillName) {
|
||||
response
|
||||
.status(400)
|
||||
.json({ success: false, error: "Missing skillName" });
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await userFromSession(request, response);
|
||||
if (!user && response.locals?.multiUserMode) {
|
||||
return response
|
||||
.status(401)
|
||||
.json({ success: false, error: "Unauthorized" });
|
||||
}
|
||||
|
||||
const userId = user?.id || null;
|
||||
const { success, error } = await AgentSkillWhitelist.add(
|
||||
skillName,
|
||||
userId
|
||||
);
|
||||
return response.status(success ? 200 : 400).json({ success, error });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return response.status(500).json({ success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { agentSkillWhitelistEndpoints };
|
||||
@@ -11,6 +11,7 @@ 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);
|
||||
if (this.handleToolApproval) return this?.handleToolApproval?.(message);
|
||||
this.checkBailCommand(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ const {
|
||||
const { TemporaryAuthToken } = require("../models/temporaryAuthToken");
|
||||
const { SystemPromptVariables } = require("../models/systemPromptVariables");
|
||||
const { VALID_COMMANDS } = require("../utils/chats");
|
||||
const { AgentSkillWhitelist } = require("../models/agentSkillWhitelist");
|
||||
|
||||
function systemEndpoints(app) {
|
||||
if (!app) return;
|
||||
@@ -619,7 +620,7 @@ function systemEndpoints(app) {
|
||||
multi_user_mode: true,
|
||||
});
|
||||
await BrowserExtensionApiKey.migrateApiKeysToMultiUser(user.id);
|
||||
|
||||
await AgentSkillWhitelist.clearSingleUserWhitelist();
|
||||
await updateENV(
|
||||
{
|
||||
JWTSecret: process.env.JWT_SECRET || v4(),
|
||||
|
||||
@@ -23,6 +23,9 @@ const { bootHTTP, bootSSL } = require("./utils/boot");
|
||||
const { workspaceThreadEndpoints } = require("./endpoints/workspaceThreads");
|
||||
const { documentEndpoints } = require("./endpoints/document");
|
||||
const { agentWebsocket } = require("./endpoints/agentWebsocket");
|
||||
const {
|
||||
agentSkillWhitelistEndpoints,
|
||||
} = require("./endpoints/agentSkillWhitelist");
|
||||
const { experimentalEndpoints } = require("./endpoints/experimental");
|
||||
const { browserExtensionEndpoints } = require("./endpoints/browserExtension");
|
||||
const { communityHubEndpoints } = require("./endpoints/communityHub");
|
||||
@@ -75,6 +78,7 @@ embedManagementEndpoints(apiRouter);
|
||||
utilEndpoints(apiRouter);
|
||||
documentEndpoints(apiRouter);
|
||||
agentWebsocket(apiRouter);
|
||||
agentSkillWhitelistEndpoints(apiRouter);
|
||||
experimentalEndpoints(apiRouter);
|
||||
developerEndpoints(app, apiRouter);
|
||||
communityHubEndpoints(apiRouter);
|
||||
|
||||
@@ -8,6 +8,9 @@ const { WorkspaceThread } = require("../models/workspaceThread");
|
||||
const { streamResponse } = require("../utils/telegramBot/chat/stream");
|
||||
|
||||
process.on("message", async (payload) => {
|
||||
// Ignore tool approval responses - these are handled by http-socket plugin
|
||||
if (payload?.type === "toolApprovalResponse") return;
|
||||
|
||||
const {
|
||||
botToken,
|
||||
chatId,
|
||||
|
||||
100
server/models/agentSkillWhitelist.js
Normal file
100
server/models/agentSkillWhitelist.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const prisma = require("../utils/prisma");
|
||||
const { safeJsonParse } = require("../utils/http");
|
||||
|
||||
const AgentSkillWhitelist = {
|
||||
SINGLE_USER_LABEL: "whitelisted_agent_skills",
|
||||
|
||||
/**
|
||||
* Get the label for storing whitelist in system_settings
|
||||
* @param {number|null} userId - User ID in multi-user mode, null for single-user
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLabel: function (userId = null) {
|
||||
if (userId) return `user_${userId}_whitelisted_agent_skills`;
|
||||
return this.SINGLE_USER_LABEL;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the whitelisted skills for a user or the system
|
||||
* @param {number|null} userId - User ID in multi-user mode, null for single-user
|
||||
* @returns {Promise<string[]>} Array of whitelisted skill names
|
||||
*/
|
||||
get: async function (userId = null) {
|
||||
try {
|
||||
const label = this._getLabel(userId);
|
||||
const setting = await prisma.system_settings.findFirst({
|
||||
where: { label },
|
||||
});
|
||||
return safeJsonParse(setting?.value, []);
|
||||
} catch (error) {
|
||||
console.error("AgentSkillWhitelist.get error:", error.message);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a skill to the whitelist
|
||||
* @param {string} skillName - The skill name to whitelist
|
||||
* @param {number|null} userId - User ID in multi-user mode, null for single-user
|
||||
* @returns {Promise<{success: boolean, error: string|null}>}
|
||||
*/
|
||||
add: async function (skillName, userId = null) {
|
||||
try {
|
||||
if (!skillName || typeof skillName !== "string") {
|
||||
return { success: false, error: "Invalid skill name" };
|
||||
}
|
||||
|
||||
const label = this._getLabel(userId);
|
||||
const currentList = await this.get(userId);
|
||||
|
||||
if (currentList.includes(skillName)) {
|
||||
return { success: true, error: null };
|
||||
}
|
||||
|
||||
const newList = [...currentList, skillName];
|
||||
|
||||
await prisma.system_settings.upsert({
|
||||
where: { label },
|
||||
update: { value: JSON.stringify(newList) },
|
||||
create: { label, value: JSON.stringify(newList) },
|
||||
});
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
console.error("AgentSkillWhitelist.add error:", error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a skill is whitelisted
|
||||
* @param {string} skillName - The skill name to check
|
||||
* @param {number|null} userId - User ID in multi-user mode, null for single-user
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
isWhitelisted: async function (skillName, userId = null) {
|
||||
const whitelist = await this.get(userId);
|
||||
return whitelist.includes(skillName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the single-user whitelist (used when switching to multi-user mode)
|
||||
* @returns {Promise<{success: boolean, error: string|null}>}
|
||||
*/
|
||||
clearSingleUserWhitelist: async function () {
|
||||
try {
|
||||
await prisma.system_settings.deleteMany({
|
||||
where: { label: this.SINGLE_USER_LABEL },
|
||||
});
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"AgentSkillWhitelist.clearSingleUserWhitelist error:",
|
||||
error.message
|
||||
);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { AgentSkillWhitelist };
|
||||
@@ -1,10 +1,48 @@
|
||||
const chalk = require("chalk");
|
||||
const { Telemetry } = require("../../../../models/telemetry");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const TOOL_APPROVAL_TIMEOUT_MS = 120 * 1_000; // 2 mins for tool approval
|
||||
|
||||
/**
|
||||
* Get the IPC channel for worker communication.
|
||||
* Bree workers use worker_threads internally but polyfill process.on("message") for receiving.
|
||||
* Workers send via parentPort.postMessage(), receive via process.on("message").
|
||||
* @returns {{ send: Function, on: Function, removeListener: Function } | null}
|
||||
*/
|
||||
function getWorkerIPC() {
|
||||
try {
|
||||
const { parentPort } = require("node:worker_threads");
|
||||
if (parentPort) {
|
||||
// Bree worker context: send via parentPort, receive via process (Bree polyfill)
|
||||
return {
|
||||
send: (msg) => parentPort.postMessage(msg),
|
||||
on: (event, handler) => process.on(event, handler),
|
||||
removeListener: (event, handler) =>
|
||||
process.removeListener(event, handler),
|
||||
};
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Fallback for child_process workers
|
||||
if (typeof process.send === "function") {
|
||||
return {
|
||||
send: (msg) => process.send(msg),
|
||||
on: (event, handler) => process.on(event, handler),
|
||||
removeListener: (event, handler) =>
|
||||
process.removeListener(event, handler),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP Interface plugin for Aibitat to emulate a websocket interface in the agent
|
||||
* framework so we dont have to modify the interface for passing messages and responses
|
||||
* in REST or WSS.
|
||||
*
|
||||
* When telegramChatId is provided, enables tool approval via Telegram inline keyboards
|
||||
* using IPC messages to communicate with the parent TelegramBotService process.
|
||||
*/
|
||||
const httpSocket = {
|
||||
name: "httpSocket",
|
||||
@@ -21,12 +59,17 @@ const httpSocket = {
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
telegramChatId: {
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugin: function ({
|
||||
handler,
|
||||
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.
|
||||
telegramChatId = null, // When set, enables tool approval via Telegram IPC
|
||||
}) {
|
||||
return {
|
||||
name: this.name,
|
||||
@@ -59,6 +102,125 @@ const httpSocket = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Request user approval before executing a tool/skill.
|
||||
* Only available when running in Telegram context (telegramChatId is set).
|
||||
* Sends IPC message to parent process which shows Telegram inline keyboard.
|
||||
*
|
||||
* @param {Object} options - The approval request options
|
||||
* @param {string} options.skillName - The name of the skill/tool requesting approval
|
||||
* @param {Object} [options.payload={}] - Optional payload data to display to the user
|
||||
* @param {string} [options.description] - Optional description of what the skill will do
|
||||
* @returns {Promise<{approved: boolean, message: string}>} - The approval result
|
||||
*/
|
||||
aibitat.requestToolApproval = async function ({
|
||||
skillName,
|
||||
payload = {},
|
||||
description = null,
|
||||
}) {
|
||||
// Check whitelist first
|
||||
const {
|
||||
AgentSkillWhitelist,
|
||||
} = require("../../../../models/agentSkillWhitelist");
|
||||
const isWhitelisted = await AgentSkillWhitelist.isWhitelisted(
|
||||
skillName,
|
||||
null
|
||||
);
|
||||
if (isWhitelisted) {
|
||||
console.log(
|
||||
chalk.green(`Skill ${skillName} is whitelisted - auto-approved.`)
|
||||
);
|
||||
return {
|
||||
approved: true,
|
||||
message: "Skill is whitelisted - auto-approved.",
|
||||
};
|
||||
}
|
||||
|
||||
// Tool approval only available in Telegram worker context
|
||||
const ipc = getWorkerIPC();
|
||||
if (!telegramChatId || !ipc) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Tool approval requested for ${skillName} but no Telegram context available. Auto-denying for safety.`
|
||||
)
|
||||
);
|
||||
return {
|
||||
approved: false,
|
||||
message:
|
||||
"Tool approval is not available in this context. Operation denied.",
|
||||
};
|
||||
}
|
||||
|
||||
const requestId = uuidv4();
|
||||
console.log(
|
||||
chalk.blue(
|
||||
`Requesting tool approval for ${skillName} (${requestId})`
|
||||
)
|
||||
);
|
||||
|
||||
// Send introspection message before the approval UI appears
|
||||
aibitat.introspect(
|
||||
`Requesting approval to execute: ${skillName}${description ? ` - ${description}` : ""}`
|
||||
);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let timeoutId = null;
|
||||
|
||||
const messageHandler = (msg) => {
|
||||
if (msg?.type !== "toolApprovalResponse") return;
|
||||
if (msg?.requestId !== requestId) return;
|
||||
|
||||
ipc.removeListener("message", messageHandler);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (msg.approved) {
|
||||
console.log(
|
||||
chalk.green(`Tool ${skillName} approved by user via Telegram`)
|
||||
);
|
||||
return resolve({
|
||||
approved: true,
|
||||
message: "User approved the tool execution.",
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.yellow(`Tool ${skillName} denied by user via Telegram`)
|
||||
);
|
||||
return resolve({
|
||||
approved: false,
|
||||
message: "Tool call was rejected by the user.",
|
||||
});
|
||||
};
|
||||
|
||||
ipc.on("message", messageHandler);
|
||||
|
||||
// Send approval request to parent TelegramBotService process
|
||||
ipc.send({
|
||||
type: "toolApprovalRequest",
|
||||
requestId,
|
||||
chatId: telegramChatId,
|
||||
skillName,
|
||||
payload,
|
||||
description,
|
||||
timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
|
||||
});
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
ipc.removeListener("message", messageHandler);
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Tool approval request timed out after ${TOOL_APPROVAL_TIMEOUT_MS}ms`
|
||||
)
|
||||
);
|
||||
resolve({
|
||||
approved: false,
|
||||
message:
|
||||
"Tool approval request timed out. User did not respond in time.",
|
||||
});
|
||||
}, TOOL_APPROVAL_TIMEOUT_MS);
|
||||
});
|
||||
};
|
||||
|
||||
// We can only receive one message response with HTTP
|
||||
// so we end on first response.
|
||||
aibitat.onMessage((message) => {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const chalk = require("chalk");
|
||||
const { Telemetry } = require("../../../../models/telemetry");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { safeJsonParse } = require("../../../http");
|
||||
const SOCKET_TIMEOUT_MS = 300 * 1_000; // 5 mins
|
||||
const TOOL_APPROVAL_TIMEOUT_MS = 120 * 1_000; // 2 mins for tool approval
|
||||
|
||||
/**
|
||||
* Websocket Interface plugin. It prints the messages on the console and asks for feedback
|
||||
@@ -11,6 +14,7 @@ const SOCKET_TIMEOUT_MS = 300 * 1_000; // 5 mins
|
||||
// askForFeedback?: any
|
||||
// awaitResponse?: any
|
||||
// handleFeedback?: (message: string) => void;
|
||||
// handleToolApproval?: (message: string) => void;
|
||||
// }
|
||||
|
||||
const WEBSOCKET_BAIL_COMMANDS = [
|
||||
@@ -43,6 +47,7 @@ const websocket = {
|
||||
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.
|
||||
userId = null, // User ID for multi-user mode whitelist lookups
|
||||
}) {
|
||||
return {
|
||||
name: this.name,
|
||||
@@ -79,6 +84,102 @@ const websocket = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Request user approval before executing a tool/skill.
|
||||
* This sends a request to the frontend and blocks until the user responds.
|
||||
* If the skill is whitelisted, approval is granted automatically.
|
||||
*
|
||||
* @param {Object} options - The approval request options
|
||||
* @param {string} options.skillName - The name of the skill/tool requesting approval
|
||||
* @param {Object} [options.payload={}] - Optional payload data to display to the user
|
||||
* @param {string} [options.description] - Optional description of what the skill will do
|
||||
* @returns {Promise<{approved: boolean, message: string}>} - The approval result
|
||||
*/
|
||||
aibitat.requestToolApproval = async function ({
|
||||
skillName,
|
||||
payload = {},
|
||||
description = null,
|
||||
}) {
|
||||
const {
|
||||
AgentSkillWhitelist,
|
||||
} = require("../../../../models/agentSkillWhitelist");
|
||||
const isWhitelisted = await AgentSkillWhitelist.isWhitelisted(
|
||||
skillName,
|
||||
userId
|
||||
);
|
||||
if (isWhitelisted) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
userId
|
||||
? `User ${userId} - `
|
||||
: "" + `Skill ${skillName} is whitelisted - auto-approved.`
|
||||
)
|
||||
);
|
||||
return {
|
||||
approved: true,
|
||||
message: "Skill is whitelisted - auto-approved.",
|
||||
};
|
||||
}
|
||||
|
||||
const requestId = uuidv4();
|
||||
return new Promise((resolve) => {
|
||||
let timeoutId = null;
|
||||
|
||||
socket.handleToolApproval = (message) => {
|
||||
try {
|
||||
const data = safeJsonParse(message, {});
|
||||
if (
|
||||
data?.type !== "toolApprovalResponse" ||
|
||||
data?.requestId !== requestId
|
||||
)
|
||||
return;
|
||||
|
||||
delete socket.handleToolApproval;
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (data.approved) {
|
||||
return resolve({
|
||||
approved: true,
|
||||
message: "User approved the tool execution.",
|
||||
});
|
||||
}
|
||||
|
||||
return resolve({
|
||||
approved: false,
|
||||
message: "Tool call was rejected by the user.",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error handling tool approval response:", e);
|
||||
}
|
||||
};
|
||||
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
type: "toolApprovalRequest",
|
||||
requestId,
|
||||
skillName,
|
||||
payload,
|
||||
description,
|
||||
timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
|
||||
})
|
||||
);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
delete socket.handleToolApproval;
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Tool approval request timed out after ${TOOL_APPROVAL_TIMEOUT_MS}ms`
|
||||
)
|
||||
);
|
||||
resolve({
|
||||
approved: false,
|
||||
message:
|
||||
"Tool approval request timed out. User did not respond in time.",
|
||||
});
|
||||
}, TOOL_APPROVAL_TIMEOUT_MS);
|
||||
});
|
||||
};
|
||||
|
||||
// aibitat.onStart(() => {
|
||||
// console.log("🚀 starting chat ...");
|
||||
// });
|
||||
|
||||
@@ -436,6 +436,7 @@ class EphemeralAgentHandler extends AgentHandler {
|
||||
async createAIbitat(
|
||||
args = {
|
||||
handler: null,
|
||||
telegramChatId: null,
|
||||
}
|
||||
) {
|
||||
this.aibitat = new AIbitat({
|
||||
@@ -456,12 +457,14 @@ class EphemeralAgentHandler extends AgentHandler {
|
||||
this.aibitat.fetchParsedFileContext = () => this.#fetchParsedFileContext();
|
||||
|
||||
// Attach HTTP response object if defined for chunk streaming.
|
||||
// When telegramChatId is provided, tool approval via Telegram is enabled.
|
||||
this.log(`Attached ${httpSocket.name} plugin to Agent cluster`);
|
||||
this.aibitat.use(
|
||||
httpSocket.plugin({
|
||||
handler: args.handler,
|
||||
muteUserReply: true,
|
||||
introspection: true,
|
||||
telegramChatId: args.telegramChatId,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -726,6 +726,7 @@ class AgentHandler {
|
||||
socket: args.socket,
|
||||
muteUserReply: true,
|
||||
introspection: true,
|
||||
userId: this.invocation.user_id || null,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ async function handleAgentResponse(
|
||||
threadId: thread?.id || null,
|
||||
attachments,
|
||||
}).init();
|
||||
await agentHandler.createAIbitat({ handler });
|
||||
await agentHandler.createAIbitat({ handler, telegramChatId: chatId });
|
||||
|
||||
// httpSocket terminates after the first agent message, but cap rounds
|
||||
// as a safety net so the agent can't loop indefinitely.
|
||||
|
||||
@@ -38,6 +38,8 @@ class TelegramBotService {
|
||||
#pendingPairings = new Map();
|
||||
// Active workers per chat: chatId -> { worker, jobId }
|
||||
#activeWorkers = new Map();
|
||||
// Pending tool approval requests: requestId -> { worker, chatId, messageId }
|
||||
#pendingToolApprovals = new Map();
|
||||
|
||||
constructor() {
|
||||
if (TelegramBotService._instance) return TelegramBotService._instance;
|
||||
@@ -153,6 +155,7 @@ class TelegramBotService {
|
||||
this.#chatState.clear();
|
||||
this.#pendingPairings.clear();
|
||||
this.#activeWorkers.clear();
|
||||
this.#pendingToolApprovals.clear();
|
||||
this.#log("Stopped");
|
||||
}
|
||||
|
||||
@@ -349,9 +352,12 @@ class TelegramBotService {
|
||||
guard(msg, () => handler(ctx, msg.chat.id, msg.text));
|
||||
});
|
||||
|
||||
// Register callback queries, used for workspace/thread selection interactive menus
|
||||
// Register callback queries, used for workspace/thread selection, tool approval, etc.
|
||||
this.#bot.on("callback_query", (query) =>
|
||||
handleKeyboardQueryCallback(ctx, query)
|
||||
handleKeyboardQueryCallback(ctx, query, {
|
||||
pendingToolApprovals: this.#pendingToolApprovals,
|
||||
log: this.#log.bind(this),
|
||||
})
|
||||
);
|
||||
|
||||
this.#bot.on("message", (msg) => {
|
||||
@@ -396,6 +402,9 @@ class TelegramBotService {
|
||||
if (worker) {
|
||||
worker.on("message", (msg) => {
|
||||
if (msg?.type === "closeInvocation") invocationUuid = msg.uuid;
|
||||
if (msg?.type === "toolApprovalRequest") {
|
||||
this.#handleToolApprovalRequest(worker, msg);
|
||||
}
|
||||
});
|
||||
this.#activeWorkers.set(chatId, { worker, jobId, bgService });
|
||||
}
|
||||
@@ -467,6 +476,91 @@ class TelegramBotService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a tool approval request from a worker process.
|
||||
* Sends a Telegram message with Approve/Deny inline keyboard buttons.
|
||||
* @param {Worker} worker - The worker process requesting approval
|
||||
* @param {Object} msg - The tool approval request message
|
||||
*/
|
||||
async #handleToolApprovalRequest(worker, msg) {
|
||||
const { requestId, chatId, skillName, payload, description, timeoutMs } =
|
||||
msg;
|
||||
|
||||
this.#log(
|
||||
`Tool approval request received: ${skillName} (requestId: ${requestId})`
|
||||
);
|
||||
|
||||
try {
|
||||
const payloadText =
|
||||
payload && Object.keys(payload).length > 0
|
||||
? `\n\n<b>Parameters:</b>\n<code>${JSON.stringify(payload, null, 2)}</code>`
|
||||
: "";
|
||||
|
||||
const descText = description ? `\n${description}` : "";
|
||||
|
||||
const messageText =
|
||||
`🔧 <b>Tool Approval Required</b>\n\n` +
|
||||
`The agent wants to execute: <b>${skillName}</b>${descText}${payloadText}\n\n` +
|
||||
`Do you want to allow this action?`;
|
||||
|
||||
const keyboard = {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{
|
||||
text: "✅ Approve",
|
||||
callback_data: `tool:approve:${requestId}`,
|
||||
},
|
||||
{ text: "❌ Deny", callback_data: `tool:deny:${requestId}` },
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const sent = await this.#bot.sendMessage(chatId, messageText, {
|
||||
parse_mode: "HTML",
|
||||
reply_markup: keyboard,
|
||||
});
|
||||
|
||||
this.#pendingToolApprovals.set(requestId, {
|
||||
worker,
|
||||
chatId,
|
||||
messageId: sent.message_id,
|
||||
skillName,
|
||||
});
|
||||
|
||||
// Auto-cleanup if timeout expires (worker will also timeout)
|
||||
setTimeout(() => {
|
||||
if (this.#pendingToolApprovals.has(requestId)) {
|
||||
this.#pendingToolApprovals.delete(requestId);
|
||||
this.#bot
|
||||
.editMessageText(
|
||||
`⏱️ Tool approval for <b>${skillName}</b> timed out.`,
|
||||
{
|
||||
chat_id: chatId,
|
||||
message_id: sent.message_id,
|
||||
parse_mode: "HTML",
|
||||
}
|
||||
)
|
||||
.catch(() => {});
|
||||
}
|
||||
}, timeoutMs + 1000);
|
||||
} catch (error) {
|
||||
this.#log("Failed to send tool approval request:", error.message);
|
||||
// Send denial back to worker if we can't show the UI
|
||||
try {
|
||||
const response = {
|
||||
type: "toolApprovalResponse",
|
||||
requestId,
|
||||
approved: false,
|
||||
};
|
||||
if (worker && typeof worker.send === "function") {
|
||||
worker.send(response);
|
||||
} else if (worker && typeof worker.postMessage === "function") {
|
||||
worker.postMessage(response);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
#shouldVoiceRespond(isVoiceMessage) {
|
||||
if (!this.#config) return false;
|
||||
const mode = this.#config.voice_response_mode || "text_only";
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Handle tool approval callback from an inline keyboard button.
|
||||
* This handler requires access to the pending tool approvals map from TelegramBotService.
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {import("../../commands/index").BotContext} params.ctx
|
||||
* @param {number} params.chatId
|
||||
* @param {{id: string}} params.query
|
||||
* @param {string} params.data
|
||||
* @param {Map} params.pendingToolApprovals - Map of pending tool approval requests
|
||||
* @param {Function} [params.log] - Logging function (falls back to ctx.log)
|
||||
*/
|
||||
async function handleToolApproval({
|
||||
ctx,
|
||||
chatId,
|
||||
query,
|
||||
data,
|
||||
pendingToolApprovals,
|
||||
log,
|
||||
} = {}) {
|
||||
const _log = log || ctx.log;
|
||||
_log(`Tool approval callback received: ${data}`);
|
||||
|
||||
try {
|
||||
const parts = data.split(":");
|
||||
if (parts.length !== 3) {
|
||||
_log(`Invalid callback data format: ${data}`);
|
||||
await ctx.bot.answerCallbackQuery(query.id, {
|
||||
text: "Invalid request format.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const action = parts[1]; // "approve" or "deny"
|
||||
const requestId = parts[2];
|
||||
|
||||
const pending = pendingToolApprovals.get(requestId);
|
||||
if (!pending) {
|
||||
_log(`No pending approval found for requestId: ${requestId}`);
|
||||
await ctx.bot.answerCallbackQuery(query.id, {
|
||||
text: "This approval request has expired.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const { worker, messageId, skillName } = pending;
|
||||
const approved = action === "approve";
|
||||
|
||||
_log(`Processing ${approved ? "approval" : "denial"} for ${skillName}`);
|
||||
|
||||
// Send response back to worker (Bree workers use send(), raw worker_threads use postMessage())
|
||||
try {
|
||||
const response = {
|
||||
type: "toolApprovalResponse",
|
||||
requestId,
|
||||
approved,
|
||||
};
|
||||
|
||||
if (worker && typeof worker.send === "function") {
|
||||
worker.send(response);
|
||||
_log(
|
||||
`Sent tool approval response to worker via send(): ${approved ? "approved" : "denied"}`
|
||||
);
|
||||
} else if (worker && typeof worker.postMessage === "function") {
|
||||
worker.postMessage(response);
|
||||
_log(
|
||||
`Sent tool approval response to worker via postMessage(): ${approved ? "approved" : "denied"}`
|
||||
);
|
||||
} else {
|
||||
_log(
|
||||
`Worker not available to send approval response (send: ${typeof worker?.send}, postMessage: ${typeof worker?.postMessage})`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
_log(`Failed to send approval response: ${err.message}`);
|
||||
}
|
||||
|
||||
pendingToolApprovals.delete(requestId);
|
||||
|
||||
// Update the message to show the result
|
||||
const resultText = approved
|
||||
? `✅ <b>${skillName}</b> was approved.`
|
||||
: `❌ <b>${skillName}</b> was denied.`;
|
||||
|
||||
await ctx.bot
|
||||
.editMessageText(resultText, {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
parse_mode: "HTML",
|
||||
})
|
||||
.catch((err) => _log(`Failed to edit message: ${err.message}`));
|
||||
|
||||
await ctx.bot.answerCallbackQuery(query.id, {
|
||||
text: approved ? "Approved!" : "Denied.",
|
||||
});
|
||||
} catch (error) {
|
||||
_log(`Error handling tool approval callback: ${error.message}`);
|
||||
await ctx.bot.answerCallbackQuery(query.id, {
|
||||
text: "Something went wrong.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { handleToolApproval };
|
||||
@@ -10,6 +10,7 @@ const { handleModelSelect } = require("./handleModelSelect");
|
||||
const { handleSourceSelect } = require("./handleSourceSelect");
|
||||
const { handleSourcePagination } = require("./handleSourcePagination");
|
||||
const { handleBackSources } = require("./handleBackSources");
|
||||
const { handleToolApproval } = require("./handleToolApproval");
|
||||
|
||||
const ExactCallbackHandlers = {
|
||||
"ws-create": handleWorkspaceCreate,
|
||||
@@ -20,6 +21,7 @@ const ExactCallbackHandlers = {
|
||||
};
|
||||
|
||||
const PrefixCallbackHandlers = [
|
||||
{ prefix: "tool:", handler: handleToolApproval },
|
||||
{ prefix: "wspg:", handler: handleWorkspacePagination },
|
||||
{ prefix: "ws:", handler: handleWorkspaceSelect },
|
||||
{ prefix: "thpg:", handler: handleThreadPagination },
|
||||
|
||||
@@ -2,11 +2,12 @@ const { isVerified } = require("../verification");
|
||||
const { resolveCallbackHandler } = require("./callbacks");
|
||||
|
||||
/**
|
||||
* Handle inline keyboard callback queries (workspace/thread selection).
|
||||
* Handle inline keyboard callback queries (workspace/thread selection, tool approval, etc).
|
||||
* @param {BotContext} ctx
|
||||
* @param {object} query - Telegram callback query object
|
||||
* @param {object} [options={}] - Optional dependencies that specific handlers may need
|
||||
*/
|
||||
async function handleKeyboardQueryCallback(ctx, query) {
|
||||
async function handleKeyboardQueryCallback(ctx, query, options = {}) {
|
||||
const chatId = query.message.chat.id;
|
||||
const messageId = query.message.message_id;
|
||||
const data = query.data;
|
||||
@@ -21,7 +22,7 @@ async function handleKeyboardQueryCallback(ctx, query) {
|
||||
try {
|
||||
const handler = resolveCallbackHandler(data);
|
||||
if (!handler) throw new Error(`Callback handler not found: ${data}`);
|
||||
await handler({ ctx, chatId, query, messageId, data });
|
||||
await handler({ ctx, chatId, query, messageId, data, ...options });
|
||||
} catch (error) {
|
||||
ctx.log("Callback error:", error.message);
|
||||
await ctx.bot.answerCallbackQuery(query.id, {
|
||||
|
||||
Reference in New Issue
Block a user