mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-10 17:00:11 +01:00
Merge branch 'master' of github.com:Mintplex-Labs/anything-llm
This commit is contained in:
commit
ed53bbcf15
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -32,7 +32,9 @@
|
||||
"opendocument",
|
||||
"openrouter",
|
||||
"Qdrant",
|
||||
"searxng",
|
||||
"Serper",
|
||||
"Serply",
|
||||
"textgenwebui",
|
||||
"togetherai",
|
||||
"vectordbs",
|
||||
|
@ -245,3 +245,6 @@ GID='1000'
|
||||
|
||||
#------ Serply.io ----------- https://serply.io/
|
||||
# AGENT_SERPLY_API_KEY=
|
||||
|
||||
#------ SearXNG ----------- https://github.com/searxng/searxng
|
||||
# AGENT_SEARXNG_API_URL=
|
@ -9,7 +9,7 @@ i18next
|
||||
.use(LanguageDetector)
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
debug: true,
|
||||
debug: import.meta.env.DEV,
|
||||
defaultNS,
|
||||
resources,
|
||||
lowerCaseLng: true,
|
||||
|
@ -10,10 +10,15 @@
|
||||
// to a specific language file as this will break the other languages. Any new keys should be added to english
|
||||
// and the language file you are working on.
|
||||
|
||||
// Contributor Notice: If you are adding a translation you MUST locally run `yarn verify:translations` from the root prior to PR.
|
||||
// please do not submit PR's without first verifying this test passes as it will tell you about missing keys or values
|
||||
// from the primary dictionary.
|
||||
|
||||
import English from "./en/common.js";
|
||||
import Spanish from "./es/common.js";
|
||||
import French from "./fr/common.js";
|
||||
import Mandarin from "./zh/common.js";
|
||||
import Russian from "./ru/common.js";
|
||||
|
||||
export const defaultNS = "common";
|
||||
export const resources = {
|
||||
@ -29,4 +34,7 @@ export const resources = {
|
||||
fr: {
|
||||
common: French,
|
||||
},
|
||||
ru: {
|
||||
common: Russian,
|
||||
},
|
||||
};
|
||||
|
415
frontend/src/locales/ru/common.js
Normal file
415
frontend/src/locales/ru/common.js
Normal file
@ -0,0 +1,415 @@
|
||||
const TRANSLATIONS = {
|
||||
common: {
|
||||
"workspaces-name": "Имя рабочих пространств",
|
||||
error: "ошибка",
|
||||
success: "успех",
|
||||
user: "Пользователь",
|
||||
selection: "Выбор модели",
|
||||
saving: "Сохранение...",
|
||||
save: "Сохранить изменения",
|
||||
previous: "Предыдущая страница",
|
||||
next: "Следующая страница",
|
||||
},
|
||||
settings: {
|
||||
title: "Настройки экземпляра",
|
||||
system: "Системные настройки",
|
||||
invites: "Приглашение",
|
||||
users: "Пользователи",
|
||||
workspaces: "Рабочие пространства",
|
||||
"workspace-chats": "Чат рабочего пространства",
|
||||
appearance: "Внешний вид",
|
||||
"api-keys": "API ключи",
|
||||
llm: "Предпочтение LLM",
|
||||
transcription: "Модель транскрипции",
|
||||
embedder: "Настройки встраивания",
|
||||
"text-splitting": "Разделение и сегментация текста",
|
||||
"vector-database": "Векторная база данных",
|
||||
embeds: "Виджеты встраивания чата",
|
||||
"embed-chats": "История встраивания чатов",
|
||||
security: "Безопасность",
|
||||
"event-logs": "Журналы событий",
|
||||
privacy: "Конфиденциальность и данные",
|
||||
},
|
||||
login: {
|
||||
"multi-user": {
|
||||
welcome: "Добро пожаловать в",
|
||||
"placeholder-username": "Имя пользователя",
|
||||
"placeholder-password": "Пароль",
|
||||
login: "Войти",
|
||||
validating: "Проверка...",
|
||||
"forgot-pass": "Забыли пароль",
|
||||
reset: "Сбросить",
|
||||
},
|
||||
"sign-in": {
|
||||
start: "Войти в ваш",
|
||||
end: "аккаунт.",
|
||||
},
|
||||
},
|
||||
"workspaces—settings": {
|
||||
general: "Общие настройки",
|
||||
chat: "Настройки чата",
|
||||
vector: "Векторная база данных",
|
||||
members: "Участники",
|
||||
agent: "Конфигурация агента",
|
||||
},
|
||||
general: {
|
||||
vector: {
|
||||
title: "Количество векторов",
|
||||
description: "Общее количество векторов в вашей векторной базе данных.",
|
||||
},
|
||||
names: {
|
||||
description:
|
||||
"Это изменит только отображаемое имя вашего рабочего пространства.",
|
||||
},
|
||||
message: {
|
||||
title: "Предлагаемые сообщения чата",
|
||||
description:
|
||||
"Настройте сообщения, которые будут предложены пользователям вашего рабочего пространства.",
|
||||
add: "Добавить новое сообщение",
|
||||
save: "Сохранить сообщения",
|
||||
heading: "Объясните мне",
|
||||
body: "преимущества AnythingLLM",
|
||||
},
|
||||
pfp: {
|
||||
title: "Изображение профиля помощника",
|
||||
description:
|
||||
"Настройте изображение профиля помощника для этого рабочего пространства.",
|
||||
image: "Изображение рабочего пространства",
|
||||
remove: "Удалить изображение рабочего пространства",
|
||||
},
|
||||
delete: {
|
||||
delete: "Удалить рабочее пространство",
|
||||
deleting: "Удаление рабочего пространства...",
|
||||
"confirm-start": "Вы собираетесь удалить весь ваш",
|
||||
"confirm-end":
|
||||
"рабочее пространство. Это удалит все векторные встраивания в вашей векторной базе данных.\n\nОригинальные исходные файлы останутся нетронутыми. Это действие необратимо.",
|
||||
},
|
||||
},
|
||||
chat: {
|
||||
llm: {
|
||||
title: "Поставщик LLM рабочего пространства",
|
||||
description:
|
||||
"Конкретный поставщик и модель LLM, которые будут использоваться для этого рабочего пространства. По умолчанию используется системный поставщик и настройки LLM.",
|
||||
search: "Искать всех поставщиков LLM",
|
||||
},
|
||||
model: {
|
||||
title: "Модель чата рабочего пространства",
|
||||
description:
|
||||
"Конкретная модель чата, которая будет использоваться для этого рабочего пространства. Если пусто, будет использоваться системное предпочтение LLM.",
|
||||
wait: "-- ожидание моделей --",
|
||||
},
|
||||
mode: {
|
||||
title: "Режим чата",
|
||||
chat: {
|
||||
title: "Чат",
|
||||
"desc-start": "будет предоставлять ответы с общей информацией LLM",
|
||||
and: "и",
|
||||
"desc-end": "найденный контекст документов.",
|
||||
},
|
||||
query: {
|
||||
title: "Запрос",
|
||||
"desc-start": "будет предоставлять ответы",
|
||||
only: "только",
|
||||
"desc-end": "если найден контекст документов.",
|
||||
},
|
||||
},
|
||||
history: {
|
||||
title: "История чата",
|
||||
"desc-start":
|
||||
"Количество предыдущих чатов, которые будут включены в краткосрочную память ответа.",
|
||||
recommend: "Рекомендуем 20.",
|
||||
"desc-end":
|
||||
"Любое количество более 45 может привести к непрерывным сбоям чата в зависимости от размера сообщений.",
|
||||
},
|
||||
prompt: {
|
||||
title: "Подсказка",
|
||||
description:
|
||||
"Подсказка, которая будет использоваться в этом рабочем пространстве. Определите контекст и инструкции для AI для создания ответа. Вы должны предоставить тщательно разработанную подсказку, чтобы AI мог генерировать релевантный и точный ответ.",
|
||||
},
|
||||
refusal: {
|
||||
title: "Ответ об отказе в режиме запроса",
|
||||
"desc-start": "В режиме",
|
||||
query: "запроса",
|
||||
"desc-end":
|
||||
"вы можете вернуть пользовательский ответ об отказе, если контекст не найден.",
|
||||
},
|
||||
temperature: {
|
||||
title: "Температура LLM",
|
||||
"desc-start":
|
||||
"Этот параметр контролирует, насколько 'креативными' будут ответы вашего LLM.",
|
||||
"desc-end":
|
||||
"Чем выше число, тем более креативные ответы. Для некоторых моделей это может привести к несвязным ответам при слишком высоких настройках.",
|
||||
hint: "Большинство LLM имеют различные допустимые диапазоны значений. Проконсультируйтесь с вашим поставщиком LLM для получения этой информации.",
|
||||
},
|
||||
},
|
||||
"vector-workspace": {
|
||||
identifier: "Идентификатор векторной базы данных",
|
||||
snippets: {
|
||||
title: "Максимальное количество контекстных фрагментов",
|
||||
description:
|
||||
"Этот параметр контролирует максимальное количество контекстных фрагментов, которые будут отправлены LLM для каждого чата или запроса.",
|
||||
recommend: "Рекомендуемое количество: 4",
|
||||
},
|
||||
doc: {
|
||||
title: "Порог сходства документов",
|
||||
description:
|
||||
"Минимальная оценка сходства, необходимая для того, чтобы источник считался связанным с чатом. Чем выше число, тем более схожим должен быть источник с чатом.",
|
||||
zero: "Без ограничений",
|
||||
low: "Низкий (оценка сходства ≥ .25)",
|
||||
medium: "Средний (оценка сходства ≥ .50)",
|
||||
high: "Высокий (оценка сходства ≥ .75)",
|
||||
},
|
||||
reset: {
|
||||
reset: "Сброс векторной базы данных",
|
||||
resetting: "Очистка векторов...",
|
||||
confirm:
|
||||
"Вы собираетесь сбросить векторную базу данных этого рабочего пространства. Это удалит все текущие векторные встраивания.\n\nОригинальные исходные файлы останутся нетронутыми. Это действие необратимо.",
|
||||
error: "Не удалось сбросить векторную базу данных рабочего пространства!",
|
||||
success: "Векторная база данных рабочего пространства была сброшена!",
|
||||
},
|
||||
},
|
||||
agent: {
|
||||
"performance-warning":
|
||||
"Производительность LLM, не поддерживающих вызовы инструментов, сильно зависит от возможностей и точности модели. Некоторые способности могут быть ограничены или не функционировать.",
|
||||
provider: {
|
||||
title: "Поставщик LLM агента рабочего пространства",
|
||||
description:
|
||||
"Конкретный поставщик и модель LLM, которые будут использоваться для агента @agent этого рабочего пространства.",
|
||||
},
|
||||
mode: {
|
||||
chat: {
|
||||
title: "Модель чата агента рабочего пространства",
|
||||
description:
|
||||
"Конкретная модель чата, которая будет использоваться для агента @agent этого рабочего пространства.",
|
||||
},
|
||||
title: "Модель агента рабочего пространства",
|
||||
description:
|
||||
"Конкретная модель LLM, которая будет использоваться для агента @agent этого рабочего пространства.",
|
||||
wait: "-- ожидание моделей --",
|
||||
},
|
||||
skill: {
|
||||
title: "Навыки агента по умолчанию",
|
||||
description:
|
||||
"Улучшите естественные способности агента по умолчанию с помощью этих предустановленных навыков. Эта настройка применяется ко всем рабочим пространствам.",
|
||||
rag: {
|
||||
title: "RAG и долговременная память",
|
||||
description:
|
||||
"Позвольте агенту использовать ваши локальные документы для ответа на запрос или попросите агента 'запомнить' части контента для долгосрочного извлечения из памяти.",
|
||||
},
|
||||
view: {
|
||||
title: "Просмотр и резюмирование документов",
|
||||
description:
|
||||
"Позвольте агенту перечислять и резюмировать содержание файлов рабочего пространства, которые в данный момент встроены.",
|
||||
},
|
||||
scrape: {
|
||||
title: "Сбор данных с веб-сайтов",
|
||||
description:
|
||||
"Позвольте агенту посещать и собирать содержимое веб-сайтов.",
|
||||
},
|
||||
generate: {
|
||||
title: "Создание диаграмм",
|
||||
description:
|
||||
"Включите возможность создания различных типов диаграмм из предоставленных данных или данных, указанных в чате.",
|
||||
},
|
||||
save: {
|
||||
title: "Создание и сохранение файлов в браузер",
|
||||
description:
|
||||
"Включите возможность создания и записи файлов, которые можно сохранить и загрузить в вашем браузере.",
|
||||
},
|
||||
web: {
|
||||
title: "Поиск в Интернете и просмотр в реальном времени",
|
||||
"desc-start":
|
||||
"Позвольте вашему агенту искать в Интернете для ответа на ваши вопросы, подключаясь к поставщику поиска (SERP).",
|
||||
"desc-end":
|
||||
"Поиск в Интернете во время сессий агента не будет работать, пока это не настроено.",
|
||||
},
|
||||
},
|
||||
},
|
||||
recorded: {
|
||||
title: "Чаты рабочего пространства",
|
||||
description:
|
||||
"Это все записанные чаты и сообщения, отправленные пользователями, упорядоченные по дате создания.",
|
||||
export: "Экспорт",
|
||||
table: {
|
||||
id: "Идентификатор",
|
||||
by: "Отправлено",
|
||||
workspace: "Рабочее пространство",
|
||||
prompt: "Подсказка",
|
||||
response: "Ответ",
|
||||
at: "Отправлено в",
|
||||
},
|
||||
},
|
||||
appearance: {
|
||||
title: "Внешний вид",
|
||||
description: "Настройте параметры внешнего вида вашей платформы.",
|
||||
logo: {
|
||||
title: "Настроить логотип",
|
||||
description:
|
||||
"Загрузите свой логотип, чтобы персонализировать ваш чат-бот.",
|
||||
add: "Добавить пользовательский логотип",
|
||||
recommended: "Рекомендуемый размер: 800 x 200",
|
||||
remove: "Удалить",
|
||||
replace: "Заменить",
|
||||
},
|
||||
message: {
|
||||
title: "Настроить сообщения",
|
||||
description:
|
||||
"Настройте автоматические сообщения, отображаемые вашим пользователям.",
|
||||
new: "Новое",
|
||||
system: "система",
|
||||
user: "пользователь",
|
||||
message: "сообщение",
|
||||
assistant: "Чат-ассистент AnythingLLM",
|
||||
"double-click": "Дважды щелкните, чтобы редактировать...",
|
||||
save: "Сохранить сообщения",
|
||||
},
|
||||
icons: {
|
||||
title: "Пользовательские иконки в подвале",
|
||||
description:
|
||||
"Настройте иконки в подвале, отображаемые внизу боковой панели.",
|
||||
icon: "Иконка",
|
||||
link: "Ссылка",
|
||||
},
|
||||
},
|
||||
api: {
|
||||
title: "API ключи",
|
||||
description:
|
||||
"API ключи позволяют владельцу программно получать доступ к этому экземпляру AnythingLLM и управлять им.",
|
||||
link: "Прочитать документацию по API",
|
||||
generate: "Создать новый API ключ",
|
||||
table: {
|
||||
key: "API ключ",
|
||||
by: "Создано",
|
||||
created: "Создано",
|
||||
},
|
||||
},
|
||||
llm: {
|
||||
title: "Предпочтение LLM",
|
||||
description:
|
||||
"Это учетные данные и настройки для вашего предпочтительного поставщика чата и встраивания LLM. Важно, чтобы эти ключи были актуальными и правильными, иначе AnythingLLM не будет работать должным образом.",
|
||||
provider: "Поставщик LLM",
|
||||
},
|
||||
transcription: {
|
||||
title: "Предпочтение модели транскрипции",
|
||||
description:
|
||||
"Это учетные данные и настройки для вашего предпочтительного поставщика моделей транскрипции. Важно, чтобы эти ключи были актуальными и правильными, иначе медиафайлы и аудио не будут транскрибироваться.",
|
||||
provider: "Поставщик транскрипции",
|
||||
"warn-start":
|
||||
"Использование локальной модели whisper на машинах с ограниченной оперативной памятью или процессором может привести к зависанию AnythingLLM при обработке медиафайлов.",
|
||||
"warn-recommend":
|
||||
"Мы рекомендуем минимум 2ГБ оперативной памяти и загружать файлы <10МБ.",
|
||||
"warn-end":
|
||||
"Встроенная модель будет автоматически загружена при первом использовании.",
|
||||
},
|
||||
embedding: {
|
||||
title: "Настройки встраивания",
|
||||
"desc-start":
|
||||
"При использовании LLM, который не поддерживает встроенный механизм встраивания - возможно, потребуется дополнительно указать учетные данные для встраивания текста.",
|
||||
"desc-end":
|
||||
"Встраивание - это процесс превращения текста в векторы. Эти учетные данные необходимы для превращения ваших файлов и подсказок в формат, который AnythingLLM может использовать для обработки.",
|
||||
provider: {
|
||||
title: "Поставщик встраивания",
|
||||
description:
|
||||
"Нет необходимости в настройке при использовании встроенного механизма встраивания AnythingLLM.",
|
||||
},
|
||||
},
|
||||
text: {
|
||||
title: "Настройки разделения и сегментации текста",
|
||||
"desc-start":
|
||||
"Иногда может понадобиться изменить стандартный способ разделения и сегментации новых документов перед их вставкой в векторную базу данных.",
|
||||
"desc-end":
|
||||
"Следует изменять этот параметр только при полном понимании работы разделения текста и его побочных эффектов.",
|
||||
"warn-start": "Изменения здесь будут применяться только к",
|
||||
"warn-center": "новым встроенным документам",
|
||||
"warn-end": ", а не к существующим документам.",
|
||||
size: {
|
||||
title: "Размер сегмента текста",
|
||||
description:
|
||||
"Это максимальная длина символов, которые могут присутствовать в одном векторе.",
|
||||
recommend: "Максимальная длина модели встраивания составляет",
|
||||
},
|
||||
overlap: {
|
||||
title: "Перекрытие сегментов текста",
|
||||
description:
|
||||
"Это максимальное перекрытие символов, которое происходит при сегментации между двумя смежными сегментами текста.",
|
||||
},
|
||||
},
|
||||
vector: {
|
||||
title: "Векторная база данных",
|
||||
description:
|
||||
"Это учетные данные и настройки для того, как будет функционировать ваш экземпляр AnythingLLM. Важно, чтобы эти ключи были актуальными и правильными.",
|
||||
provider: {
|
||||
title: "Поставщик векторной базы данных",
|
||||
description: "Настройка для LanceDB не требуется.",
|
||||
},
|
||||
},
|
||||
embeddable: {
|
||||
title: "Встраиваемые виджеты чата",
|
||||
description:
|
||||
"Встраиваемые виджеты чата - это интерфейсы чата, ориентированные на публичное использование и привязанные к одному рабочему пространству. Они позволяют создавать рабочие пространства, которые затем можно публиковать в Интернете.",
|
||||
create: "Создать встраивание",
|
||||
table: {
|
||||
workspace: "Рабочее пространство",
|
||||
chats: "Отправленные чаты",
|
||||
Active: "Активные домены",
|
||||
},
|
||||
},
|
||||
"embed-chats": {
|
||||
title: "Встраивание чатов",
|
||||
description:
|
||||
"Это все записанные чаты и сообщения от любого встраивания, которое вы опубликовали.",
|
||||
table: {
|
||||
embed: "Встраивание",
|
||||
sender: "Отправитель",
|
||||
message: "Сообщение",
|
||||
response: "Ответ",
|
||||
at: "Отправлено в",
|
||||
},
|
||||
},
|
||||
multi: {
|
||||
title: "Многопользовательский режим",
|
||||
description:
|
||||
"Настройте ваш экземпляр для поддержки вашей команды, активировав многопользовательский режим.",
|
||||
enable: {
|
||||
"is-enable": "Многопользовательский режим включен",
|
||||
enable: "Включить многопользовательский режим",
|
||||
description:
|
||||
"По умолчанию, вы будете единственным администратором. Как администратор, вы должны будете создавать учетные записи для всех новых пользователей или администраторов. Не теряйте ваш пароль, так как только администратор может сбросить пароли.",
|
||||
username: "Имя пользователя учетной записи администратора",
|
||||
password: "Пароль учетной записи администратора",
|
||||
},
|
||||
password: {
|
||||
title: "Защита паролем",
|
||||
description:
|
||||
"Защитите ваш экземпляр AnythingLLM паролем. Если вы забудете его, метода восстановления не существует, поэтому убедитесь, что вы сохранили этот пароль.",
|
||||
},
|
||||
instance: {
|
||||
title: "Защитить экземпляр паролем",
|
||||
description:
|
||||
"По умолчанию, вы будете единственным администратором. Как администратор, вы должны будете создавать учетные записи для всех новых пользователей или администраторов. Не теряйте ваш пароль, так как только администратор может сбросить пароли.",
|
||||
password: "Пароль экземпляра",
|
||||
},
|
||||
},
|
||||
event: {
|
||||
title: "Журналы событий",
|
||||
description:
|
||||
"Просматривайте все действия и события, происходящие в этом экземпляре для мониторинга.",
|
||||
clear: "Очистить журналы событий",
|
||||
table: {
|
||||
type: "Тип события",
|
||||
user: "Пользователь",
|
||||
occurred: "Произошло в",
|
||||
},
|
||||
},
|
||||
privacy: {
|
||||
title: "Конфиденциальность и обработка данных",
|
||||
description:
|
||||
"Это ваша конфигурация для того, как подключенные сторонние поставщики и AnythingLLM обрабатывают ваши данные.",
|
||||
llm: "Выбор LLM",
|
||||
embedding: "Предпочтение встраивания",
|
||||
vector: "Векторная база данных",
|
||||
anonymous: "Анонимная телеметрия включена",
|
||||
},
|
||||
};
|
||||
|
||||
export default TRANSLATIONS;
|
@ -9,12 +9,16 @@ function langDisplayName(lang) {
|
||||
|
||||
function compareStructures(lang, a, b, subdir = null) {
|
||||
//if a and b aren't the same type, they can't be equal
|
||||
if (typeof a !== typeof b) {
|
||||
if (typeof a !== typeof b && a !== null && b !== null) {
|
||||
console.log("Invalid type comparison", [
|
||||
{
|
||||
lang,
|
||||
a: typeof a,
|
||||
b: typeof b,
|
||||
values: {
|
||||
a,
|
||||
b,
|
||||
},
|
||||
...(!!subdir ? { subdir } : {}),
|
||||
},
|
||||
]);
|
||||
|
@ -182,3 +182,25 @@ export function SerplySearchOptions({ settings }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearXNGOptions({ settings }) {
|
||||
return (
|
||||
<div className="flex gap-x-4">
|
||||
<div className="flex flex-col w-60">
|
||||
<label className="text-white text-sm font-semibold block mb-4">
|
||||
SearXNG API base URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
name="env::AgentSearXNGApiUrl"
|
||||
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="SearXNG API Key"
|
||||
defaultValue={settings?.AgentSearXNGApiUrl}
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
@ -4,6 +4,7 @@ import GoogleSearchIcon from "./icons/google.png";
|
||||
import SerperDotDevIcon from "./icons/serper.png";
|
||||
import BingSearchIcon from "./icons/bing.png";
|
||||
import SerplySearchIcon from "./icons/serply.png";
|
||||
import SearXNGSearchIcon from "./icons/searxng.png";
|
||||
import {
|
||||
CaretUpDown,
|
||||
MagnifyingGlass,
|
||||
@ -17,6 +18,7 @@ import {
|
||||
GoogleSearchOptions,
|
||||
BingSearchOptions,
|
||||
SerplySearchOptions,
|
||||
SearXNGOptions,
|
||||
} from "./SearchProviderOptions";
|
||||
|
||||
const SEARCH_PROVIDERS = [
|
||||
@ -60,6 +62,14 @@ const SEARCH_PROVIDERS = [
|
||||
description:
|
||||
"Serply.io web-search. Free account with a 100 calls/month forever.",
|
||||
},
|
||||
{
|
||||
name: "SearXNG",
|
||||
value: "searxng-engine",
|
||||
logo: SearXNGSearchIcon,
|
||||
options: (settings) => <SearXNGOptions settings={settings} />,
|
||||
description:
|
||||
"Free, open-source, internet meta-search engine with no tracking.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function AgentWebSearchSelection({
|
||||
|
@ -241,3 +241,6 @@ TTS_PROVIDER="native"
|
||||
|
||||
#------ Serply.io ----------- https://serply.io/
|
||||
# AGENT_SERPLY_API_KEY=
|
||||
|
||||
#------ SearXNG ----------- https://github.com/searxng/searxng
|
||||
# AGENT_SEARXNG_API_URL=
|
@ -4,6 +4,7 @@ const { apiAuthEndpoints } = require("./auth");
|
||||
const { apiDocumentEndpoints } = require("./document");
|
||||
const { apiSystemEndpoints } = require("./system");
|
||||
const { apiWorkspaceEndpoints } = require("./workspace");
|
||||
const { apiWorkspaceThreadEndpoints } = require("./workspaceThread");
|
||||
const { apiUserManagementEndpoints } = require("./userManagement");
|
||||
|
||||
// All endpoints must be documented and pass through the validApiKey Middleware.
|
||||
@ -17,6 +18,7 @@ function developerEndpoints(app, router) {
|
||||
apiSystemEndpoints(router);
|
||||
apiWorkspaceEndpoints(router);
|
||||
apiDocumentEndpoints(router);
|
||||
apiWorkspaceThreadEndpoints(router);
|
||||
apiUserManagementEndpoints(router);
|
||||
}
|
||||
|
||||
|
594
server/endpoints/api/workspaceThread/index.js
Normal file
594
server/endpoints/api/workspaceThread/index.js
Normal file
@ -0,0 +1,594 @@
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const { WorkspaceThread } = require("../../../models/workspaceThread");
|
||||
const { Workspace } = require("../../../models/workspace");
|
||||
const { validApiKey } = require("../../../utils/middleware/validApiKey");
|
||||
const { reqBody, multiUserMode } = require("../../../utils/http");
|
||||
const { chatWithWorkspace } = require("../../../utils/chats");
|
||||
const {
|
||||
streamChatWithWorkspace,
|
||||
VALID_CHAT_MODE,
|
||||
} = require("../../../utils/chats/stream");
|
||||
const { Telemetry } = require("../../../models/telemetry");
|
||||
const { EventLogs } = require("../../../models/eventLogs");
|
||||
const {
|
||||
writeResponseChunk,
|
||||
convertToChatHistory,
|
||||
} = require("../../../utils/helpers/chat/responses");
|
||||
const { WorkspaceChats } = require("../../../models/workspaceChats");
|
||||
const { User } = require("../../../models/user");
|
||||
|
||||
function apiWorkspaceThreadEndpoints(app) {
|
||||
if (!app) return;
|
||||
|
||||
app.post(
|
||||
"/v1/workspace/:slug/thread/new",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspace Threads']
|
||||
#swagger.description = 'Create a new workspace thread'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.requestBody = {
|
||||
description: 'Optional userId associated with the thread',
|
||||
required: false,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
userId: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
thread: {
|
||||
"id": 1,
|
||||
"name": "Thread",
|
||||
"slug": "thread-uuid",
|
||||
"user_id": 1,
|
||||
"workspace_id": 1
|
||||
},
|
||||
message: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug } = request.params;
|
||||
const { userId } = reqBody(request);
|
||||
const workspace = await Workspace.get({ slug });
|
||||
|
||||
if (!workspace) {
|
||||
response.sendStatus(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { thread, message } = await WorkspaceThread.new(
|
||||
workspace,
|
||||
userId ? Number(userId) : null
|
||||
);
|
||||
|
||||
await Telemetry.sendTelemetry("workspace_thread_created", {
|
||||
multiUserMode: multiUserMode(response),
|
||||
LLMSelection: process.env.LLM_PROVIDER || "openai",
|
||||
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
|
||||
VectorDbSelection: process.env.VECTOR_DB || "lancedb",
|
||||
});
|
||||
await EventLogs.logEvent("api_workspace_thread_created", {
|
||||
workspaceName: workspace?.name || "Unknown Workspace",
|
||||
});
|
||||
response.status(200).json({ thread, message });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/v1/workspace/:slug/thread/:threadSlug/update",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspace Threads']
|
||||
#swagger.description = 'Update thread name by its unique slug.'
|
||||
#swagger.path = '/v1/workspace/{slug}/thread/{threadSlug}/update'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.parameters['threadSlug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of thread',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.requestBody = {
|
||||
description: 'JSON object containing new name to update the thread.',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
"name": 'Updated Thread Name'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
thread: {
|
||||
"id": 1,
|
||||
"name": "Updated Thread Name",
|
||||
"slug": "thread-uuid",
|
||||
"user_id": 1,
|
||||
"workspace_id": 1
|
||||
},
|
||||
message: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug, threadSlug } = request.params;
|
||||
const { name } = reqBody(request);
|
||||
const workspace = await Workspace.get({ slug });
|
||||
const thread = await WorkspaceThread.get({
|
||||
slug: threadSlug,
|
||||
workspace_id: workspace.id,
|
||||
});
|
||||
|
||||
if (!workspace || !thread) {
|
||||
response.sendStatus(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const { thread: updatedThread, message } = await WorkspaceThread.update(
|
||||
thread,
|
||||
{ name }
|
||||
);
|
||||
response.status(200).json({ thread: updatedThread, message });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/v1/workspace/:slug/thread/:threadSlug",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspace Threads']
|
||||
#swagger.description = 'Delete a workspace thread'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.parameters['threadSlug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of thread',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
description: 'Thread deleted successfully'
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug, threadSlug } = request.params;
|
||||
const workspace = await Workspace.get({ slug });
|
||||
|
||||
if (!workspace) {
|
||||
response.sendStatus(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
await WorkspaceThread.delete({
|
||||
slug: threadSlug,
|
||||
workspace_id: workspace.id,
|
||||
});
|
||||
response.sendStatus(200).end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/v1/workspace/:slug/thread/:threadSlug/chats",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspace Threads']
|
||||
#swagger.description = 'Get chats for a workspace thread'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.parameters['threadSlug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of thread',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
history: [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is AnythingLLM?",
|
||||
"sentAt": 1692851630
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.",
|
||||
"sources": [{"source": "object about source document and snippets used"}]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug, threadSlug } = request.params;
|
||||
const workspace = await Workspace.get({ slug });
|
||||
const thread = await WorkspaceThread.get({
|
||||
slug: threadSlug,
|
||||
workspace_id: workspace.id,
|
||||
});
|
||||
|
||||
if (!workspace || !thread) {
|
||||
response.sendStatus(400).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const history = await WorkspaceChats.where(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
thread_id: thread.id,
|
||||
include: true,
|
||||
},
|
||||
null,
|
||||
{ id: "asc" }
|
||||
);
|
||||
|
||||
response.status(200).json({ history: convertToChatHistory(history) });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.sendStatus(500).end();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/v1/workspace/:slug/thread/:threadSlug/chat",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspace Threads']
|
||||
#swagger.description = 'Chat with a workspace thread'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.parameters['threadSlug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of thread',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.requestBody = {
|
||||
description: 'Send a prompt to the workspace thread and the type of conversation (query or chat).',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
message: "What is AnythingLLM?",
|
||||
mode: "query | chat",
|
||||
userId: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: 'object',
|
||||
example: {
|
||||
id: 'chat-uuid',
|
||||
type: "abort | textResponse",
|
||||
textResponse: "Response to your query",
|
||||
sources: [{title: "anythingllm.txt", chunk: "This is a context chunk used in the answer of the prompt by the LLM."}],
|
||||
close: true,
|
||||
error: "null | text string of the failure mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug, threadSlug } = request.params;
|
||||
const { message, mode = "query", userId } = reqBody(request);
|
||||
const workspace = await Workspace.get({ slug });
|
||||
const thread = await WorkspaceThread.get({
|
||||
slug: threadSlug,
|
||||
workspace_id: workspace.id,
|
||||
});
|
||||
|
||||
if (!workspace || !thread) {
|
||||
response.status(400).json({
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: `Workspace ${slug} or thread ${threadSlug} is not valid.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message?.length || !VALID_CHAT_MODE.includes(mode)) {
|
||||
response.status(400).json({
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: !message?.length
|
||||
? "message parameter cannot be empty."
|
||||
: `${mode} is not a valid mode.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = userId ? await User.get({ id: Number(userId) }) : null;
|
||||
const result = await chatWithWorkspace(
|
||||
workspace,
|
||||
message,
|
||||
mode,
|
||||
user,
|
||||
thread
|
||||
);
|
||||
await Telemetry.sendTelemetry("sent_chat", {
|
||||
LLMSelection: process.env.LLM_PROVIDER || "openai",
|
||||
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
|
||||
VectorDbSelection: process.env.VECTOR_DB || "lancedb",
|
||||
});
|
||||
await EventLogs.logEvent("api_sent_chat", {
|
||||
workspaceName: workspace?.name,
|
||||
chatModel: workspace?.chatModel || "System Default",
|
||||
threadName: thread?.name,
|
||||
userId: user?.id,
|
||||
});
|
||||
response.status(200).json({ ...result });
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
response.status(500).json({
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/v1/workspace/:slug/thread/:threadSlug/stream-chat",
|
||||
[validApiKey],
|
||||
async (request, response) => {
|
||||
/*
|
||||
#swagger.tags = ['Workspace Threads']
|
||||
#swagger.description = 'Stream chat with a workspace thread'
|
||||
#swagger.parameters['slug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of workspace',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.parameters['threadSlug'] = {
|
||||
in: 'path',
|
||||
description: 'Unique slug of thread',
|
||||
required: true,
|
||||
type: 'string'
|
||||
}
|
||||
#swagger.requestBody = {
|
||||
description: 'Send a prompt to the workspace thread and the type of conversation (query or chat).',
|
||||
required: true,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
message: "What is AnythingLLM?",
|
||||
mode: "query | chat",
|
||||
userId: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"text/event-stream": {
|
||||
schema: {
|
||||
type: 'array',
|
||||
example: [
|
||||
{
|
||||
id: 'uuid-123',
|
||||
type: "abort | textResponseChunk",
|
||||
textResponse: "First chunk",
|
||||
sources: [],
|
||||
close: false,
|
||||
error: "null | text string of the failure mode."
|
||||
},
|
||||
{
|
||||
id: 'uuid-123',
|
||||
type: "abort | textResponseChunk",
|
||||
textResponse: "chunk two",
|
||||
sources: [],
|
||||
close: false,
|
||||
error: "null | text string of the failure mode."
|
||||
},
|
||||
{
|
||||
id: 'uuid-123',
|
||||
type: "abort | textResponseChunk",
|
||||
textResponse: "final chunk of LLM output!",
|
||||
sources: [{title: "anythingllm.txt", chunk: "This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk."}],
|
||||
close: true,
|
||||
error: "null | text string of the failure mode."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[403] = {
|
||||
schema: {
|
||||
"$ref": "#/definitions/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
*/
|
||||
try {
|
||||
const { slug, threadSlug } = request.params;
|
||||
const { message, mode = "query", userId } = reqBody(request);
|
||||
const workspace = await Workspace.get({ slug });
|
||||
const thread = await WorkspaceThread.get({
|
||||
slug: threadSlug,
|
||||
workspace_id: workspace.id,
|
||||
});
|
||||
|
||||
if (!workspace || !thread) {
|
||||
response.status(400).json({
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: `Workspace ${slug} or thread ${threadSlug} is not valid.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message?.length || !VALID_CHAT_MODE.includes(mode)) {
|
||||
response.status(400).json({
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: !message?.length
|
||||
? "Message is empty"
|
||||
: `${mode} is not a valid mode.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = userId ? await User.get({ id: Number(userId) }) : null;
|
||||
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("Content-Type", "text/event-stream");
|
||||
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||
response.setHeader("Connection", "keep-alive");
|
||||
response.flushHeaders();
|
||||
|
||||
await streamChatWithWorkspace(
|
||||
response,
|
||||
workspace,
|
||||
message,
|
||||
mode,
|
||||
user,
|
||||
thread
|
||||
);
|
||||
await Telemetry.sendTelemetry("sent_chat", {
|
||||
LLMSelection: process.env.LLM_PROVIDER || "openai",
|
||||
Embedder: process.env.EMBEDDING_ENGINE || "inherit",
|
||||
VectorDbSelection: process.env.VECTOR_DB || "lancedb",
|
||||
});
|
||||
await EventLogs.logEvent("api_sent_chat", {
|
||||
workspaceName: workspace?.name,
|
||||
chatModel: workspace?.chatModel || "System Default",
|
||||
threadName: thread?.name,
|
||||
userId: user?.id,
|
||||
});
|
||||
response.end();
|
||||
} catch (e) {
|
||||
console.log(e.message, e);
|
||||
writeResponseChunk(response, {
|
||||
id: uuidv4(),
|
||||
type: "abort",
|
||||
textResponse: null,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: e.message,
|
||||
});
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { apiWorkspaceThreadEndpoints };
|
@ -76,6 +76,7 @@ const SystemSettings = {
|
||||
"serper-dot-dev",
|
||||
"bing-search",
|
||||
"serply-engine",
|
||||
"searxng-engine",
|
||||
].includes(update)
|
||||
)
|
||||
throw new Error("Invalid SERP provider.");
|
||||
@ -176,10 +177,11 @@ const SystemSettings = {
|
||||
// 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,
|
||||
AgentBingSearchApiKey: process.env.AGENT_BING_SEARCH_API_KEY || null,
|
||||
AgentSerplyApiKey: process.env.AGENT_SERPLY_API_KEY || null,
|
||||
AgentGoogleSearchEngineKey: !!process.env.AGENT_GSE_KEY || null,
|
||||
AgentSerperApiKey: !!process.env.AGENT_SERPER_DEV_KEY || null,
|
||||
AgentBingSearchApiKey: !!process.env.AGENT_BING_SEARCH_API_KEY || null,
|
||||
AgentSerplyApiKey: !!process.env.AGENT_SERPLY_API_KEY || null,
|
||||
AgentSearXNGApiUrl: process.env.AGENT_SEARXNG_API_URL || null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -7,6 +7,7 @@ const WorkspaceChats = {
|
||||
response = {},
|
||||
user = null,
|
||||
threadId = null,
|
||||
include = true,
|
||||
}) {
|
||||
try {
|
||||
const chat = await prisma.workspace_chats.create({
|
||||
@ -16,6 +17,7 @@ const WorkspaceChats = {
|
||||
response: JSON.stringify(response),
|
||||
user_id: user?.id || null,
|
||||
thread_id: threadId,
|
||||
include,
|
||||
},
|
||||
});
|
||||
return { chat, message: null };
|
||||
|
@ -35,6 +35,7 @@ const endpointsFiles = [
|
||||
"../endpoints/api/document/index.js",
|
||||
"../endpoints/api/workspace/index.js",
|
||||
"../endpoints/api/system/index.js",
|
||||
"../endpoints/api/workspaceThread/index.js",
|
||||
"../endpoints/api/userManagement/index.js",
|
||||
];
|
||||
|
||||
|
@ -2371,6 +2371,487 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/workspace/{slug}/thread/new": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Workspace Threads"
|
||||
],
|
||||
"description": "Create a new workspace thread",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of workspace"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"thread": {
|
||||
"id": 1,
|
||||
"name": "Thread",
|
||||
"slug": "thread-uuid",
|
||||
"user_id": 1,
|
||||
"workspace_id": 1
|
||||
},
|
||||
"message": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"description": "Optional userId associated with the thread",
|
||||
"required": false,
|
||||
"type": "object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"userId": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/workspace/{slug}/thread/{threadSlug}/update": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Workspace Threads"
|
||||
],
|
||||
"description": "Update thread name by its unique slug.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of workspace"
|
||||
},
|
||||
{
|
||||
"name": "threadSlug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of thread"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"thread": {
|
||||
"id": 1,
|
||||
"name": "Updated Thread Name",
|
||||
"slug": "thread-uuid",
|
||||
"user_id": 1,
|
||||
"workspace_id": 1
|
||||
},
|
||||
"message": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"description": "JSON object containing new name to update the thread.",
|
||||
"required": true,
|
||||
"type": "object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"name": "Updated Thread Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/workspace/{slug}/thread/{threadSlug}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Workspace Threads"
|
||||
],
|
||||
"description": "Delete a workspace thread",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of workspace"
|
||||
},
|
||||
{
|
||||
"name": "threadSlug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of thread"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Thread deleted successfully"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/workspace/{slug}/thread/{threadSlug}/chats": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Workspace Threads"
|
||||
],
|
||||
"description": "Get chats for a workspace thread",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of workspace"
|
||||
},
|
||||
{
|
||||
"name": "threadSlug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of thread"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"history": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is AnythingLLM?",
|
||||
"sentAt": 1692851630
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "AnythingLLM is a platform that allows you to convert notes, PDFs, and other source materials into a chatbot. It ensures privacy, cites its answers, and allows multiple people to interact with the same documents simultaneously. It is particularly useful for businesses to enhance the visibility and readability of various written communications such as SOPs, contracts, and sales calls. You can try it out with a free trial to see if it meets your business needs.",
|
||||
"sources": [
|
||||
{
|
||||
"source": "object about source document and snippets used"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/workspace/{slug}/thread/{threadSlug}/chat": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Workspace Threads"
|
||||
],
|
||||
"description": "Chat with a workspace thread",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of workspace"
|
||||
},
|
||||
{
|
||||
"name": "threadSlug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of thread"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"id": "chat-uuid",
|
||||
"type": "abort | textResponse",
|
||||
"textResponse": "Response to your query",
|
||||
"sources": [
|
||||
{
|
||||
"title": "anythingllm.txt",
|
||||
"chunk": "This is a context chunk used in the answer of the prompt by the LLM."
|
||||
}
|
||||
],
|
||||
"close": true,
|
||||
"error": "null | text string of the failure mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"description": "Send a prompt to the workspace thread and the type of conversation (query or chat).",
|
||||
"required": true,
|
||||
"type": "object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"message": "What is AnythingLLM?",
|
||||
"mode": "query | chat",
|
||||
"userId": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/workspace/{slug}/thread/{threadSlug}/stream-chat": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Workspace Threads"
|
||||
],
|
||||
"description": "Stream chat with a workspace thread",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of workspace"
|
||||
},
|
||||
{
|
||||
"name": "threadSlug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Unique slug of thread"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"text/event-stream": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"example": [
|
||||
{
|
||||
"id": "uuid-123",
|
||||
"type": "abort | textResponseChunk",
|
||||
"textResponse": "First chunk",
|
||||
"sources": [],
|
||||
"close": false,
|
||||
"error": "null | text string of the failure mode."
|
||||
},
|
||||
{
|
||||
"id": "uuid-123",
|
||||
"type": "abort | textResponseChunk",
|
||||
"textResponse": "chunk two",
|
||||
"sources": [],
|
||||
"close": false,
|
||||
"error": "null | text string of the failure mode."
|
||||
},
|
||||
{
|
||||
"id": "uuid-123",
|
||||
"type": "abort | textResponseChunk",
|
||||
"textResponse": "final chunk of LLM output!",
|
||||
"sources": [
|
||||
{
|
||||
"title": "anythingllm.txt",
|
||||
"chunk": "This is a context chunk used in the answer of the prompt by the LLM. This will only return in the final chunk."
|
||||
}
|
||||
],
|
||||
"close": true,
|
||||
"error": "null | text string of the failure mode."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
},
|
||||
"application/xml": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/InvalidAPIKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"description": "Send a prompt to the workspace thread and the type of conversation (query or chat).",
|
||||
"required": true,
|
||||
"type": "object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"message": "What is AnythingLLM?",
|
||||
"mode": "query | chat",
|
||||
"userId": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v1/users": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
@ -71,6 +71,9 @@ const webBrowsing = {
|
||||
case "serply-engine":
|
||||
engine = "_serplyEngine";
|
||||
break;
|
||||
case "searxng-engine":
|
||||
engine = "_searXNGEngine";
|
||||
break;
|
||||
default:
|
||||
engine = "_googleSearchEngine";
|
||||
}
|
||||
@ -102,7 +105,7 @@ const webBrowsing = {
|
||||
query.length > 100 ? `${query.slice(0, 100)}...` : query
|
||||
}"`
|
||||
);
|
||||
const searchResponse = await fetch(searchURL)
|
||||
const data = await fetch(searchURL)
|
||||
.then((res) => res.json())
|
||||
.then((searchResult) => searchResult?.items || [])
|
||||
.then((items) => {
|
||||
@ -116,10 +119,15 @@ const webBrowsing = {
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return {};
|
||||
return [];
|
||||
});
|
||||
|
||||
return JSON.stringify(searchResponse);
|
||||
if (data.length === 0)
|
||||
return `No information was found online for the search query.`;
|
||||
this.super.introspect(
|
||||
`${this.caller}: I found ${data.length} results - looking over them now.`
|
||||
);
|
||||
return JSON.stringify(data);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -176,6 +184,9 @@ const webBrowsing = {
|
||||
|
||||
if (data.length === 0)
|
||||
return `No information was found online for the search query.`;
|
||||
this.super.introspect(
|
||||
`${this.caller}: I found ${data.length} results - looking over them now.`
|
||||
);
|
||||
return JSON.stringify(data);
|
||||
},
|
||||
_bingWebSearch: async function (query) {
|
||||
@ -219,6 +230,9 @@ const webBrowsing = {
|
||||
|
||||
if (searchResponse.length === 0)
|
||||
return `No information was found online for the search query.`;
|
||||
this.super.introspect(
|
||||
`${this.caller}: I found ${data.length} results - looking over them now.`
|
||||
);
|
||||
return JSON.stringify(searchResponse);
|
||||
},
|
||||
_serplyEngine: async function (
|
||||
@ -293,6 +307,71 @@ const webBrowsing = {
|
||||
|
||||
if (data.length === 0)
|
||||
return `No information was found online for the search query.`;
|
||||
this.super.introspect(
|
||||
`${this.caller}: I found ${data.length} results - looking over them now.`
|
||||
);
|
||||
return JSON.stringify(data);
|
||||
},
|
||||
_searXNGEngine: async function (query) {
|
||||
let searchURL;
|
||||
if (!process.env.AGENT_SEARXNG_API_URL) {
|
||||
this.super.introspect(
|
||||
`${this.caller}: I can't use SearXNG searching because the user has not defined the required base URL.\nPlease set this value in the agent skill settings.`
|
||||
);
|
||||
return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;
|
||||
}
|
||||
|
||||
try {
|
||||
searchURL = new URL(process.env.AGENT_SEARXNG_API_URL);
|
||||
searchURL.searchParams.append("q", encodeURIComponent(query));
|
||||
searchURL.searchParams.append("format", "json");
|
||||
} catch (e) {
|
||||
this.super.handlerProps.log(`SearXNG Search: ${e.message}`);
|
||||
this.super.introspect(
|
||||
`${this.caller}: I can't use SearXNG searching because the url provided is not a valid URL.`
|
||||
);
|
||||
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 SearXNG to search for "${
|
||||
query.length > 100 ? `${query.slice(0, 100)}...` : query
|
||||
}"`
|
||||
);
|
||||
|
||||
const { response, error } = await fetch(searchURL.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "anything-llm",
|
||||
},
|
||||
})
|
||||
.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 = [];
|
||||
response.results?.forEach((searchResult) => {
|
||||
const { url, title, content, publishedDate } = searchResult;
|
||||
data.push({
|
||||
title,
|
||||
link: url,
|
||||
snippet: content,
|
||||
publishedDate,
|
||||
});
|
||||
});
|
||||
|
||||
if (data.length === 0)
|
||||
return `No information was found online for the search query.`;
|
||||
this.super.introspect(
|
||||
`${this.caller}: I found ${data.length} results - looking over them now.`
|
||||
);
|
||||
return JSON.stringify(data);
|
||||
},
|
||||
});
|
||||
|
@ -77,15 +77,30 @@ async function chatWithWorkspace(
|
||||
// User is trying to query-mode chat a workspace that has no data in it - so
|
||||
// we should exit early as no information can be found under these conditions.
|
||||
if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === "query") {
|
||||
const textResponse =
|
||||
workspace?.queryRefusalResponse ??
|
||||
"There is no relevant information in this workspace to answer your query.";
|
||||
|
||||
await WorkspaceChats.new({
|
||||
workspaceId: workspace.id,
|
||||
prompt: message,
|
||||
response: {
|
||||
text: textResponse,
|
||||
sources: [],
|
||||
type: chatMode,
|
||||
},
|
||||
threadId: thread?.id || null,
|
||||
include: false,
|
||||
user,
|
||||
});
|
||||
|
||||
return {
|
||||
id: uuid,
|
||||
type: "textResponse",
|
||||
sources: [],
|
||||
close: true,
|
||||
error: null,
|
||||
textResponse:
|
||||
workspace?.queryRefusalResponse ??
|
||||
"There is no relevant information in this workspace to answer your query.",
|
||||
textResponse,
|
||||
};
|
||||
}
|
||||
|
||||
@ -172,15 +187,30 @@ async function chatWithWorkspace(
|
||||
// If in query mode and no context chunks are found from search, backfill, or pins - do not
|
||||
// let the LLM try to hallucinate a response or use general knowledge and exit early
|
||||
if (chatMode === "query" && contextTexts.length === 0) {
|
||||
const textResponse =
|
||||
workspace?.queryRefusalResponse ??
|
||||
"There is no relevant information in this workspace to answer your query.";
|
||||
|
||||
await WorkspaceChats.new({
|
||||
workspaceId: workspace.id,
|
||||
prompt: message,
|
||||
response: {
|
||||
text: textResponse,
|
||||
sources: [],
|
||||
type: chatMode,
|
||||
},
|
||||
threadId: thread?.id || null,
|
||||
include: false,
|
||||
user,
|
||||
});
|
||||
|
||||
return {
|
||||
id: uuid,
|
||||
type: "textResponse",
|
||||
sources: [],
|
||||
close: true,
|
||||
error: null,
|
||||
textResponse:
|
||||
workspace?.queryRefusalResponse ??
|
||||
"There is no relevant information in this workspace to answer your query.",
|
||||
textResponse,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -75,16 +75,29 @@ async function streamChatWithWorkspace(
|
||||
// User is trying to query-mode chat a workspace that has no data in it - so
|
||||
// we should exit early as no information can be found under these conditions.
|
||||
if ((!hasVectorizedSpace || embeddingsCount === 0) && chatMode === "query") {
|
||||
const textResponse =
|
||||
workspace?.queryRefusalResponse ??
|
||||
"There is no relevant information in this workspace to answer your query.";
|
||||
writeResponseChunk(response, {
|
||||
id: uuid,
|
||||
type: "textResponse",
|
||||
textResponse:
|
||||
workspace?.queryRefusalResponse ??
|
||||
"There is no relevant information in this workspace to answer your query.",
|
||||
textResponse,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: null,
|
||||
});
|
||||
await WorkspaceChats.new({
|
||||
workspaceId: workspace.id,
|
||||
prompt: message,
|
||||
response: {
|
||||
text: textResponse,
|
||||
sources: [],
|
||||
type: chatMode,
|
||||
},
|
||||
threadId: thread?.id || null,
|
||||
include: false,
|
||||
user,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -177,16 +190,30 @@ async function streamChatWithWorkspace(
|
||||
// If in query mode and no context chunks are found from search, backfill, or pins - do not
|
||||
// let the LLM try to hallucinate a response or use general knowledge and exit early
|
||||
if (chatMode === "query" && contextTexts.length === 0) {
|
||||
const textResponse =
|
||||
workspace?.queryRefusalResponse ??
|
||||
"There is no relevant information in this workspace to answer your query.";
|
||||
writeResponseChunk(response, {
|
||||
id: uuid,
|
||||
type: "textResponse",
|
||||
textResponse:
|
||||
workspace?.queryRefusalResponse ??
|
||||
"There is no relevant information in this workspace to answer your query.",
|
||||
textResponse,
|
||||
sources: [],
|
||||
close: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
await WorkspaceChats.new({
|
||||
workspaceId: workspace.id,
|
||||
prompt: message,
|
||||
response: {
|
||||
text: textResponse,
|
||||
sources: [],
|
||||
type: chatMode,
|
||||
},
|
||||
threadId: thread?.id || null,
|
||||
include: false,
|
||||
user,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -407,6 +407,10 @@ const KEY_MAPPING = {
|
||||
envKey: "AGENT_SERPLY_API_KEY",
|
||||
checks: [],
|
||||
},
|
||||
AgentSearXNGApiUrl: {
|
||||
envKey: "AGENT_SEARXNG_API_URL",
|
||||
checks: [],
|
||||
},
|
||||
|
||||
// TTS/STT Integration ENVS
|
||||
TextToSpeechProvider: {
|
||||
|
Loading…
Reference in New Issue
Block a user