Merge branch 'master' of github.com:Mintplex-Labs/anything-llm

This commit is contained in:
Timothy Carambat
2026-03-18 09:26:09 -07:00
34 changed files with 213 additions and 3 deletions

View File

@@ -390,6 +390,9 @@ GID='1000'
#------ Exa Search ----------- https://www.exa.ai/
# AGENT_EXA_API_KEY=
#------ Perplexity Search ----------- [https://console.perplexity.ai](https://console.perplexity.ai)
# AGENT_PERPLEXITY_API_KEY=
###########################################
######## Other Configurations ############
###########################################

View File

@@ -5,6 +5,7 @@ import { middleTruncate } from "@/utils/directories";
export default function FolderRow({
item,
totalItems = 0,
selected,
onRowClick,
toggleSelection,
@@ -60,6 +61,11 @@ export default function FolderRow({
<p className="whitespace-nowrap overflow-show max-w-[400px]">
{middleTruncate(item.name, 35)}
</p>
{totalItems > 0 && (
<span className="text-theme-text-secondary text-[10px] font-medium ml-1.5 shrink-0">
({totalItems})
</span>
)}
</div>
<p className="col-span-2 pl-3.5" />
<p className="col-span-2 pl-2" />

View File

@@ -193,6 +193,11 @@ function Directory({
setContextMenu({ visible: false, x: 0, y: 0 });
};
const totalDocCount = (files?.items ?? []).reduce((acc, folder) => {
if (folder.type === "folder") return folder.items.length + acc;
return acc;
}, 0);
return (
<>
<div className="px-8 pb-8" onContextMenu={handleContextMenu}>
@@ -232,6 +237,13 @@ function Directory({
<div className="relative w-[560px] h-[310px] bg-theme-settings-input-bg rounded-2xl overflow-hidden border border-theme-modal-border">
<div className="absolute top-0 left-0 right-0 z-10 rounded-t-2xl text-theme-text-primary text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 light:border-theme-modal-border bg-theme-settings-input-bg">
<p className="col-span-6">Name</p>
{totalDocCount > 0 && (
<p className="col-span-6 text-right text-theme-text-secondary">
{t(`connectors.directory.total-documents`, {
count: totalDocCount,
})}
</p>
)}
</div>
<div className="overflow-y-auto h-full pt-8">
@@ -249,6 +261,7 @@ function Directory({
<FolderRow
key={index}
item={item}
totalItems={item.items?.length ?? 0}
selected={isSelected(
item.id,
item.type === "folder" ? item : null
@@ -310,7 +323,6 @@ function Directory({
</div>
)}
</div>
<UploadFile
workspace={workspace}
fetchKeys={fetchKeys}

View File

@@ -28,6 +28,10 @@ function WorkspaceDirectory({
}) {
const { t } = useTranslation();
const [selectedItems, setSelectedItems] = useState({});
const embeddedDocCount = (files?.items ?? []).reduce(
(sum, folder) => sum + (folder.items?.length ?? 0),
0
);
const toggleSelection = (item) => {
setSelectedItems((prevSelectedItems) => {
@@ -101,7 +105,6 @@ function WorkspaceDirectory({
<div className="shrink-0 w-3 h-3" />
<p className="ml-[7px] text-theme-text-primary">Name</p>
</div>
<p className="col-span-2" />
</div>
<div className="w-full h-[calc(100%-40px)] flex items-center justify-center flex-col gap-y-5">
<PreLoader />
@@ -157,7 +160,13 @@ function WorkspaceDirectory({
)}
<p className="ml-[7px] text-theme-text-primary">Name</p>
</div>
<p className="col-span-2" />
{embeddedDocCount > 0 && (
<p className="col-span-2 text-right text-theme-text-secondary pr-2">
{t(`connectors.directory.total-documents`, {
count: embeddedDocCount,
})}
</p>
)}
</div>
<div className="overflow-y-auto h-[calc(100%-40px)]">
{files.items.some((folder) => folder.items.length > 0) ||

View File

@@ -590,6 +590,8 @@ const TRANSLATIONS = {
remove_selected: "حذف المحدد",
costs: "*تكلفة ثابتة لإنشاء التمثيلات",
save_embed: "حفظ و تضمين",
"total-documents_one": "{{count}}",
"total-documents_other": "{{count}} المستندات",
},
upload: {
"processor-offline": "غير متاح",

View File

@@ -728,6 +728,8 @@ const TRANSLATIONS = {
remove_selected: "Odebrat vybrané",
costs: "*Jednorázové náklady pro embeddingy",
save_embed: "Uložit a vložit",
"total-documents_one": "{{count}} dokument",
"total-documents_other": "{{count}} dokumenty",
},
upload: {
"processor-offline": "Procesor dokumentů nedostupný",

View File

@@ -597,6 +597,8 @@ const TRANSLATIONS = {
remove_selected: "Fjern valgte",
costs: "*Engangsomkostning for indlejringer",
save_embed: "Gem og indlejr",
"total-documents_one": "{{count}} dokument",
"total-documents_other": "{{count}} dokumenter",
},
upload: {
"processor-offline": "Dokumentbehandler utilgængelig",

View File

@@ -705,6 +705,8 @@ const TRANSLATIONS = {
remove_selected: "Ausgewähltes entfernen",
costs: "*Einmalige Kosten für das Einbetten",
save_embed: "Speichern und Einbetten",
"total-documents_one": "{{count}} Dokument",
"total-documents_other": "{{count}} Dokumente",
},
upload: {
"processor-offline": "Dokumentenprozessor nicht verfügbar",

View File

@@ -715,6 +715,8 @@ const TRANSLATIONS = {
directory: {
"my-documents": "My Documents",
"new-folder": "New Folder",
"total-documents_one": "{{count}} document",
"total-documents_other": "{{count}} documents",
"search-document": "Search for document",
"no-documents": "No Documents",
"move-workspace": "Move to Workspace",

View File

@@ -715,6 +715,8 @@ const TRANSLATIONS = {
remove_selected: "Eliminar seleccionados",
costs: "*Costo único por incrustaciones",
save_embed: "Guardar e incrustar",
"total-documents_one": "{{count}} documento",
"total-documents_other": "{{count}} documentos",
},
upload: {
"processor-offline": "Procesador de documentos no disponible",

View File

@@ -675,6 +675,8 @@ const TRANSLATIONS = {
remove_selected: "Eemalda valitud",
costs: "*Ühekordne embeddingu kulu",
save_embed: "Salvesta ja põimi",
"total-documents_one": "{{count}} dokument",
"total-documents_other": "{{count}} dokumendid",
},
upload: {
"processor-offline": "Dokumenditöötleja pole saadaval",

View File

@@ -594,6 +594,8 @@ const TRANSLATIONS = {
remove_selected: "حذف انتخاب‌شده",
costs: "*هزینه یکباره برای ایجاد مدل‌های برداری",
save_embed: "ذخیره و وارد کردن",
"total-documents_one": "{{count}} سند",
"total-documents_other": "{{count}} اسناد",
},
upload: {
"processor-offline":

View File

@@ -596,6 +596,8 @@ const TRANSLATIONS = {
remove_selected: "Supprimer la sélection",
costs: "Coûts",
save_embed: "Sauvegarder et intégrer",
"total-documents_one": "{{count}}",
"total-documents_other": "{{count}} documents",
},
upload: {
"processor-offline": "Processeur de documents hors ligne",

View File

@@ -679,6 +679,8 @@ const TRANSLATIONS = {
remove_selected: "הסר נבחרים",
costs: "*עלות חד פעמית להטמעות",
save_embed: "שמור והטמע",
"total-documents_one": "{{count}} מסמך",
"total-documents_other": "מסמכים {{count}}",
},
upload: {
"processor-offline": "מעבד המסמכים אינו זמין",

View File

@@ -600,6 +600,8 @@ const TRANSLATIONS = {
remove_selected: "Elimina gli elementi selezionati",
costs: "*Costo una tantum per le embedding",
save_embed: "Salva e incorpora",
"total-documents_one": "{{count}} documento",
"total-documents_other": "{{count}} documenti",
},
upload: {
"processor-offline": "Il processore di documenti non è disponibile.",

View File

@@ -586,6 +586,8 @@ const TRANSLATIONS = {
remove_selected: "選択したものを削除",
costs: "※埋め込みには一度だけ費用がかかります",
save_embed: "保存して埋め込む",
"total-documents_one": "{{count}} のドキュメント",
"total-documents_other": "{{count}} に関する書類",
},
upload: {
"processor-offline": "ドキュメント処理機能が利用できません",

View File

@@ -687,6 +687,8 @@ const TRANSLATIONS = {
remove_selected: "선택 항목 삭제",
costs: "*임베딩 1회 비용",
save_embed: "저장 및 임베딩",
"total-documents_one": "{{count}} 문서",
"total-documents_other": "{{count}} 관련 문서",
},
upload: {
"processor-offline": "문서 처리기가 오프라인 상태입니다",

View File

@@ -700,6 +700,8 @@ const TRANSLATIONS = {
remove_selected: "Noņemt atlasītos",
costs: "*Vienreizējas izmaksas iegulšanai",
save_embed: "Saglabāt un iegult",
"total-documents_one": "{{count}} dokumenta",
"total-documents_other": "{{count}} dokumenti",
},
upload: {
"processor-offline": "Dokumentu apstrādātājs nav pieejams",

View File

@@ -596,6 +596,8 @@ const TRANSLATIONS = {
remove_selected: "Verwijderen Geselecteerd",
costs: "*Eenmalige kosten voor embedden",
save_embed: "Opslaan en embedden",
"total-documents_one": "{{count}} document",
"total-documents_other": "{{count}} documenten",
},
upload: {
"processor-offline": "Documentverwerker niet beschikbaar",

View File

@@ -702,6 +702,8 @@ const TRANSLATIONS = {
remove_selected: "Usuń wybrane",
costs: "*Jednorazowy koszt dodania danych",
save_embed: "Zapisz",
"total-documents_one": "{{count}} dokument",
"total-documents_other": "{{count}} dokumenty",
},
upload: {
"processor-offline": "Procesor dokumentów niedostępny",

View File

@@ -685,6 +685,8 @@ const TRANSLATIONS = {
remove_selected: "Remover Selecionados",
costs: "*Custo único para vínculos",
save_embed: "Salvar e Inserir",
"total-documents_one": "{{count}} documento",
"total-documents_other": "{{count}} documentos",
},
upload: {
"processor-offline": "Processador de documentos Indisponível",

View File

@@ -448,6 +448,8 @@ const TRANSLATIONS = {
remove_selected: "Elimină selectate",
costs: "*Cost unic pentru embeddings",
save_embed: "Salvează și încorporează",
"total-documents_one": "{{count}}",
"total-documents_other": "{{count}} documente",
},
upload: {
"processor-offline": "Procesorul de documente este offline",

View File

@@ -594,6 +594,8 @@ const TRANSLATIONS = {
remove_selected: "Удалить выбранные",
costs: "*Единоразовая стоимость за внедрение",
save_embed: "Сохранить и внедрить",
"total-documents_one": "{{count}} документ",
"total-documents_other": "{{count}} документы",
},
upload: {
"processor-offline": "Процессор документов недоступен",

View File

@@ -594,6 +594,8 @@ const TRANSLATIONS = {
remove_selected: "Seçilenleri Kaldır",
costs: "*Gömmeler için tek seferlik maliyet",
save_embed: "Kaydet ve Göm",
"total-documents_one": "{{count}} belgesi",
"total-documents_other": "{{count}} belgeleri",
},
upload: {
"processor-offline": "Belge İşleyici Kullanılamıyor",

View File

@@ -591,6 +591,8 @@ const TRANSLATIONS = {
remove_selected: "Xóa Đã chọn",
costs: "*Chi phí một lần cho việc nhúng",
save_embed: "Lưu và Nhúng",
"total-documents_one": "{{count}}",
"total-documents_other": "{{count}}",
},
upload: {
"processor-offline": "Trình xử lý Tài liệu Không khả dụng",

View File

@@ -643,6 +643,8 @@ const TRANSLATIONS = {
remove_selected: "移除所选",
costs: "*嵌入时一次性费用",
save_embed: "保存并嵌入",
"total-documents_one": "{{count}} 文件",
"total-documents_other": "{{count}} 类型的文件",
},
upload: {
"processor-offline": "文档处理器不可用",

View File

@@ -553,6 +553,8 @@ const TRANSLATIONS = {
remove_selected: "移除選擇的項目",
costs: "*嵌入僅會計費一次",
save_embed: "儲存並嵌入",
"total-documents_one": "{{count}} 文件",
"total-documents_other": "{{count}} 文件",
},
upload: {
"processor-offline": "文件處理器無法使用",

View File

@@ -384,3 +384,38 @@ export function ExaSearchOptions({ settings }) {
</>
);
}
export function PerplexitySearchOptions({ settings }) {
return (
<>
<p className="text-sm text-white/60 my-2">
You can get an API key{" "}
<a
href="https://console.perplexity.ai"
target="_blank"
rel="noreferrer"
className="text-blue-300 underline"
>
from Perplexity.
</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-3">
API Key
</label>
<input
type="password"
name="env::AgentPerplexityApiKey"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="Perplexity API Key"
defaultValue={settings?.AgentPerplexityApiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
</div>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -10,6 +10,7 @@ import SearXNGSearchIcon from "./icons/searxng.png";
import TavilySearchIcon from "./icons/tavily.svg";
import DuckDuckGoIcon from "./icons/duckduckgo.png";
import ExaIcon from "./icons/exa.png";
import PerplexitySearchIcon from "./icons/perplexity.png";
import {
CaretUpDown,
MagnifyingGlass,
@@ -29,6 +30,7 @@ import {
TavilySearchOptions,
DuckDuckGoOptions,
ExaSearchOptions,
PerplexitySearchOptions,
} from "./SearchProviderOptions";
const SEARCH_PROVIDERS = [
@@ -109,6 +111,13 @@ const SEARCH_PROVIDERS = [
options: (settings) => <ExaSearchOptions settings={settings} />,
description: "AI-powered search engine optimized for LLM use cases.",
},
{
name: "Perplexity Search",
value: "perplexity-search",
logo: PerplexitySearchIcon,
options: (settings) => <PerplexitySearchOptions settings={settings} />,
description: "AI-powered web search using the Perplexity Search API.",
},
];
export default function AgentWebSearchSelection({

View File

@@ -388,6 +388,9 @@ TTS_PROVIDER="native"
#------ Exa Search ----------- https://www.exa.ai/
# AGENT_EXA_API_KEY=
#------ Perplexity Search ----------- [https://console.perplexity.ai](https://console.perplexity.ai)
# AGENT_PERPLEXITY_API_KEY=
###########################################
######## Other Configurations ############
###########################################

View File

@@ -121,6 +121,7 @@ const SystemSettings = {
"tavily-search",
"duckduckgo-engine",
"exa-search",
"perplexity-search",
].includes(update)
)
throw new Error("Invalid SERP provider.");
@@ -300,6 +301,7 @@ const SystemSettings = {
AgentSearXNGApiUrl: process.env.AGENT_SEARXNG_API_URL || null,
AgentTavilyApiKey: !!process.env.AGENT_TAVILY_API_KEY || null,
AgentExaApiKey: !!process.env.AGENT_EXA_API_KEY || null,
AgentPerplexityApiKey: !!process.env.AGENT_PERPLEXITY_API_KEY || null,
// --------------------------------------------------------
// Compliance Settings

View File

@@ -93,6 +93,9 @@ const webBrowsing = {
case "exa-search":
engine = "_exaSearch";
break;
case "perplexity-search":
engine = "_perplexitySearch";
break;
default:
engine = "_duckDuckGoEngine";
}
@@ -978,6 +981,84 @@ const webBrowsing = {
);
return result;
},
_perplexitySearch: async function (query) {
if (!process.env.AGENT_PERPLEXITY_API_KEY) {
this.super.introspect(
`${this.caller}: I can't use Perplexity searching because the user has not defined the required API key.\nVisit: [https://console.perplexity.ai](https://console.perplexity.ai) to create the API key.`
);
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 Perplexity to search for "${
query.length > 100 ? `${query.slice(0, 100)}...` : query
}"`
);
const { response, error } = await fetch(
"https://api.perplexity.ai/search",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AGENT_PERPLEXITY_API_KEY}`,
},
body: JSON.stringify({
query: query,
max_results: 5,
max_tokens_per_page: 2048,
}),
}
)
.then((res) => {
if (res.ok) return res.json();
throw new Error(
`${res.status} - ${res.statusText}. params: ${JSON.stringify({
auth: this.middleTruncate(
process.env.AGENT_PERPLEXITY_API_KEY,
5
),
q: query,
})}`
);
})
.then((data) => {
return { response: data, error: null };
})
.catch((e) => {
this.super.handlerProps.log(
`Perplexity Search Error: ${e.message}`
);
return { response: null, error: e.message };
});
if (error)
return `There was an error searching for content. ${error}`;
const data = [];
if (response.results) {
response.results.forEach((result) => {
data.push({
title: result.title,
link: result.url,
snippet: result.snippet || "",
});
});
}
if (data.length === 0)
return "No information was found online for the search query.";
this.reportSearchResultsCitations(data);
const result = JSON.stringify(data);
this.super.introspect(
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
);
return result;
},
});
},
};

View File

@@ -602,6 +602,10 @@ const KEY_MAPPING = {
envKey: "AGENT_EXA_API_KEY",
checks: [],
},
AgentPerplexityApiKey: {
envKey: "AGENT_PERPLEXITY_API_KEY",
checks: [],
},
// TTS/STT Integration ENVS
TextToSpeechProvider: {