mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-20 17:25:44 +00:00
Compare commits
1 Commits
danswer_au
...
remove_emp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81e1ac9183 |
@@ -21,8 +21,6 @@ celery_app.config_from_object("danswer.background.celery.configs.beat")
|
||||
@beat_init.connect
|
||||
def on_beat_init(sender: Any, **kwargs: Any) -> None:
|
||||
logger.info("beat_init signal received.")
|
||||
|
||||
# celery beat shouldn't touch the db at all. But just setting a low minimum here.
|
||||
SqlEngine.set_app_name(POSTGRES_CELERY_BEAT_APP_NAME)
|
||||
SqlEngine.init_engine(pool_size=2, max_overflow=0)
|
||||
app_base.wait_for_redis(sender, **kwargs)
|
||||
|
||||
@@ -58,7 +58,7 @@ def on_worker_init(sender: Any, **kwargs: Any) -> None:
|
||||
logger.info(f"Multiprocessing start method: {multiprocessing.get_start_method()}")
|
||||
|
||||
SqlEngine.set_app_name(POSTGRES_CELERY_WORKER_HEAVY_APP_NAME)
|
||||
SqlEngine.init_engine(pool_size=4, max_overflow=12)
|
||||
SqlEngine.init_engine(pool_size=8, max_overflow=0)
|
||||
|
||||
app_base.wait_for_redis(sender, **kwargs)
|
||||
app_base.on_secondary_worker_init(sender, **kwargs)
|
||||
|
||||
@@ -166,6 +166,19 @@ def on_worker_init(sender: Any, **kwargs: Any) -> None:
|
||||
r.delete(key)
|
||||
|
||||
|
||||
# @worker_process_init.connect
|
||||
# def on_worker_process_init(sender: Any, **kwargs: Any) -> None:
|
||||
# """This only runs inside child processes when the worker is in pool=prefork mode.
|
||||
# This may be technically unnecessary since we're finding prefork pools to be
|
||||
# unstable and currently aren't planning on using them."""
|
||||
# logger.info("worker_process_init signal received.")
|
||||
# SqlEngine.set_app_name(POSTGRES_CELERY_WORKER_INDEXING_CHILD_APP_NAME)
|
||||
# SqlEngine.init_engine(pool_size=5, max_overflow=0)
|
||||
|
||||
# # https://stackoverflow.com/questions/43944787/sqlalchemy-celery-with-scoped-session-error
|
||||
# SqlEngine.get_engine().dispose(close=False)
|
||||
|
||||
|
||||
@worker_ready.connect
|
||||
def on_worker_ready(sender: Any, **kwargs: Any) -> None:
|
||||
app_base.on_worker_ready(sender, **kwargs)
|
||||
|
||||
@@ -11,8 +11,7 @@ from typing import Any
|
||||
from typing import Literal
|
||||
from typing import Optional
|
||||
|
||||
from danswer.configs.constants import POSTGRES_CELERY_WORKER_INDEXING_CHILD_APP_NAME
|
||||
from danswer.db.engine import SqlEngine
|
||||
from danswer.db.engine import get_sqlalchemy_engine
|
||||
from danswer.utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger()
|
||||
@@ -38,9 +37,7 @@ def _initializer(
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
logger.info("Initializing spawned worker child process.")
|
||||
SqlEngine.set_app_name(POSTGRES_CELERY_WORKER_INDEXING_CHILD_APP_NAME)
|
||||
SqlEngine.init_engine(pool_size=4, max_overflow=12, pool_recycle=60)
|
||||
get_sqlalchemy_engine().dispose(close=False)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -91,13 +91,12 @@ class ConfluenceConnector(LoadConnector, PollConnector, SlimConnector):
|
||||
cql_page_query += f" and id='{page_id}'"
|
||||
|
||||
self.cql_page_query = cql_page_query
|
||||
self.cql_time_filter = ""
|
||||
|
||||
self.cql_label_filter = ""
|
||||
self.cql_time_filter = ""
|
||||
if labels_to_skip:
|
||||
labels_to_skip = list(set(labels_to_skip))
|
||||
comma_separated_labels = ",".join(f"'{label}'" for label in labels_to_skip)
|
||||
self.cql_label_filter = f" and label not in ({comma_separated_labels})"
|
||||
comma_separated_labels = ",".join(labels_to_skip)
|
||||
self.cql_label_filter = f"&label not in ({comma_separated_labels})"
|
||||
|
||||
def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None:
|
||||
# see https://github.com/atlassian-api/atlassian-python-api/blob/master/atlassian/rest_client.py
|
||||
@@ -126,8 +125,7 @@ class ConfluenceConnector(LoadConnector, PollConnector, SlimConnector):
|
||||
for comment in comments:
|
||||
comment_string += "\nComment:\n"
|
||||
comment_string += extract_text_from_confluence_html(
|
||||
confluence_client=self.confluence_client,
|
||||
confluence_object=comment,
|
||||
confluence_client=self.confluence_client, confluence_object=comment
|
||||
)
|
||||
|
||||
return comment_string
|
||||
|
||||
@@ -231,7 +231,6 @@ def _get_chunks_via_visit_api(
|
||||
return document_chunks
|
||||
|
||||
|
||||
@retry(tries=10, delay=1, backoff=2)
|
||||
def get_all_vespa_ids_for_document_id(
|
||||
document_id: str,
|
||||
index_name: str,
|
||||
|
||||
@@ -104,7 +104,6 @@ class TenantRedis(redis.Redis):
|
||||
"startswith",
|
||||
"sadd",
|
||||
"srem",
|
||||
"scard",
|
||||
] # Regular methods that need simple prefixing
|
||||
|
||||
if item == "scan_iter":
|
||||
|
||||
@@ -61,11 +61,11 @@ class DanswerLoggingAdapter(logging.LoggerAdapter):
|
||||
) -> tuple[str, MutableMapping[str, Any]]:
|
||||
# If this is an indexing job, add the attempt ID to the log message
|
||||
# This helps filter the logs for this specific indexing
|
||||
index_attempt_id = IndexAttemptSingleton.get_index_attempt_id()
|
||||
attempt_id = IndexAttemptSingleton.get_index_attempt_id()
|
||||
cc_pair_id = IndexAttemptSingleton.get_connector_credential_pair_id()
|
||||
|
||||
if index_attempt_id is not None:
|
||||
msg = f"[Index Attempt: {index_attempt_id}] {msg}"
|
||||
if attempt_id is not None:
|
||||
msg = f"[Attempt: {attempt_id}] {msg}"
|
||||
|
||||
if cc_pair_id is not None:
|
||||
msg = f"[CC Pair: {cc_pair_id}] {msg}"
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel
|
||||
from danswer.auth.schemas import UserRole
|
||||
from ee.danswer.configs.app_configs import API_KEY_HASH_ROUNDS
|
||||
|
||||
_DANSWER_API_KEY_HEADER_NAME = "Danswer-Authorization"
|
||||
|
||||
_API_KEY_HEADER_NAME = "Authorization"
|
||||
_BEARER_PREFIX = "Bearer "
|
||||
_API_KEY_PREFIX = "dn_"
|
||||
@@ -43,10 +43,7 @@ def build_displayable_api_key(api_key: str) -> str:
|
||||
|
||||
|
||||
def get_hashed_api_key_from_request(request: Request) -> str | None:
|
||||
# Try both Authorization and Danswer-Authorization headers
|
||||
raw_api_key_header = request.headers.get(
|
||||
_API_KEY_HEADER_NAME
|
||||
) or request.headers.get(_DANSWER_API_KEY_HEADER_NAME)
|
||||
raw_api_key_header = request.headers.get(_API_KEY_HEADER_NAME)
|
||||
if raw_api_key_header is None:
|
||||
return None
|
||||
|
||||
|
||||
@@ -2512,7 +2512,7 @@ export function ChatPage({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FixedLogo chat />
|
||||
<FixedLogo />
|
||||
</div>
|
||||
</div>
|
||||
<DocumentSidebar
|
||||
|
||||
@@ -122,7 +122,7 @@ export function ChatSessionDisplay({
|
||||
);
|
||||
}}
|
||||
>
|
||||
<BasicSelectable chat padding="extra" fullWidth selected={isSelected}>
|
||||
<BasicSelectable padding="extra" fullWidth selected={isSelected}>
|
||||
<>
|
||||
<div className="flex relative">
|
||||
{isRenamingChat ? (
|
||||
@@ -142,11 +142,7 @@ export function ChatSessionDisplay({
|
||||
{chatName || `Chat ${chatSession.id}`}
|
||||
<span
|
||||
className={`absolute right-0 top-0 h-full w-8 bg-gradient-to-r from-transparent
|
||||
${
|
||||
isSelected
|
||||
? "to-background-chat-hover"
|
||||
: " to-background-chat-selected group-hover:to-background-chat-hover"
|
||||
} `}
|
||||
${isSelected ? "to-background-200" : " to-background-100 group-hover:to-background-200"} `}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
@@ -180,9 +176,7 @@ export function ChatSessionDisplay({
|
||||
This chat will expire{" "}
|
||||
{daysUntilExpiration < 1
|
||||
? "today"
|
||||
: `in ${daysUntilExpiration} day${
|
||||
daysUntilExpiration !== 1 ? "s" : ""
|
||||
}`}
|
||||
: `in ${daysUntilExpiration} day${daysUntilExpiration !== 1 ? "s" : ""}`}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -94,7 +94,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
bg-background-sidebar
|
||||
w-full
|
||||
border-r
|
||||
border-sidebar-border
|
||||
border-border
|
||||
flex
|
||||
flex-col relative
|
||||
h-screen
|
||||
|
||||
@@ -8,7 +8,7 @@ import Link from "next/link";
|
||||
import { useContext } from "react";
|
||||
import { FiSidebar } from "react-icons/fi";
|
||||
|
||||
export default function FixedLogo({ chat }: { chat?: boolean }) {
|
||||
export default function FixedLogo() {
|
||||
const combinedSettings = useContext(SettingsContext);
|
||||
const settings = combinedSettings?.settings;
|
||||
const enterpriseSettings = combinedSettings?.enterpriseSettings;
|
||||
@@ -28,15 +28,13 @@ export default function FixedLogo({ chat }: { chat?: boolean }) {
|
||||
<div className="w-full">
|
||||
{enterpriseSettings && enterpriseSettings.application_name ? (
|
||||
<div>
|
||||
<HeaderTitle chat={chat}>
|
||||
{enterpriseSettings.application_name}
|
||||
</HeaderTitle>
|
||||
<HeaderTitle>{enterpriseSettings.application_name}</HeaderTitle>
|
||||
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
|
||||
<p className="text-xs text-subtle">Powered by Danswer</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<HeaderTitle chat={chat}>Danswer</HeaderTitle>
|
||||
<HeaderTitle>Danswer</HeaderTitle>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ const ToggleSwitch = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background-toggle mobile:mt-8 flex rounded-full p-1">
|
||||
<div className="bg-gray-100 mobile:mt-8 flex rounded-full p-1">
|
||||
<div
|
||||
className={`absolute mobile:mt-8 top-1 bottom-1 ${
|
||||
activeTab === "chat" ? "w-[45%]" : "w-[50%]"
|
||||
@@ -148,9 +148,7 @@ export default function FunctionalWrapper({
|
||||
{(!settings ||
|
||||
(settings.search_page_enabled && settings.chat_page_enabled)) && (
|
||||
<div
|
||||
className={`mobile:hidden z-30 flex fixed ${
|
||||
chatBannerPresent ? (twoLines ? "top-20" : "top-14") : "top-4"
|
||||
} left-1/2 transform -translate-x-1/2`}
|
||||
className={`mobile:hidden z-30 flex fixed ${chatBannerPresent ? (twoLines ? "top-20" : "top-14") : "top-4"} left-1/2 transform -translate-x-1/2`}
|
||||
>
|
||||
<div
|
||||
style={{ transition: "width 0.30s ease-out" }}
|
||||
|
||||
@@ -81,14 +81,12 @@ export function BasicSelectable({
|
||||
children,
|
||||
selected,
|
||||
hasBorder,
|
||||
chat,
|
||||
fullWidth = false,
|
||||
padding = "normal",
|
||||
}: {
|
||||
children: string | JSX.Element;
|
||||
selected: boolean;
|
||||
hasBorder?: boolean;
|
||||
chat?: boolean;
|
||||
fullWidth?: boolean;
|
||||
padding?: "none" | "normal" | "extra";
|
||||
}) {
|
||||
@@ -102,15 +100,7 @@ export function BasicSelectable({
|
||||
${padding == "extra" && "p-1.5"}
|
||||
select-none
|
||||
${hasBorder ? "border border-border" : ""}
|
||||
${
|
||||
selected
|
||||
? chat
|
||||
? "bg-background-chat-selected"
|
||||
: "bg-hover"
|
||||
: chat
|
||||
? "bg-background-chat-hover"
|
||||
: "hover:bg-hover"
|
||||
}
|
||||
${selected ? "bg-hover" : "hover:bg-hover"}
|
||||
${fullWidth ? "w-full" : ""}`}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
export function HeaderTitle({
|
||||
children,
|
||||
chat,
|
||||
}: {
|
||||
children: JSX.Element | string;
|
||||
chat?: boolean;
|
||||
}) {
|
||||
export function HeaderTitle({ children }: { children: JSX.Element | string }) {
|
||||
const isString = typeof children === "string";
|
||||
const textSize = isString && children.length > 10 ? "text-xl" : "text-2xl";
|
||||
|
||||
return (
|
||||
<h1
|
||||
className={`${textSize} ${
|
||||
chat ? "text-text-sidebar-header" : "text-text-header"
|
||||
} break-words line-clamp-2 ellipsis text-strong leading-none font-bold`}
|
||||
className={`${textSize} break-words line-clamp-2 ellipsis text-strong leading-none font-bold`}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
|
||||
@@ -70,11 +70,7 @@ export default function SearchAnswer({
|
||||
return (
|
||||
<div
|
||||
ref={answerContainerRef}
|
||||
className={`my-4 ${
|
||||
searchAnswerExpanded ? "min-h-[16rem]" : "h-[16rem]"
|
||||
} ${
|
||||
!searchAnswerExpanded && searchAnswerOverflowing && "overflow-y-hidden"
|
||||
} p-4 border-2 border-search-answer-border rounded-lg relative`}
|
||||
className={`my-4 ${searchAnswerExpanded ? "min-h-[16rem]" : "h-[16rem]"} ${!searchAnswerExpanded && searchAnswerOverflowing && "overflow-y-hidden"} p-4 border-2 border-border rounded-lg relative`}
|
||||
>
|
||||
<div>
|
||||
<div className="flex gap-x-2">
|
||||
|
||||
@@ -68,8 +68,8 @@ export const AnimatedToggle = ({
|
||||
{/* Toggle switch */}
|
||||
<div
|
||||
className={`
|
||||
w-10 h-6 flex items-center rounded-full p-1 transition-all duration-300 ease-in-out
|
||||
${isOn ? "bg-toggled-background" : "bg-untoggled-background"}
|
||||
w-10 h-6 flex items-center rounded-full p-1 transition-all duration-300 ease-in-out
|
||||
${isOn ? "bg-background-400" : "bg-background-200"}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
@@ -178,14 +178,10 @@ export const FullSearchBar = ({
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
<div
|
||||
className={`flex flex-nowrap ${
|
||||
showingSidebar ? " 2xl:justify-between" : "2xl:justify-end"
|
||||
} justify-between 4xl:justify-end w-full max-w-full items-center space-x-3 py-3 px-4`}
|
||||
className={`flex flex-nowrap ${showingSidebar ? " 2xl:justify-between" : "2xl:justify-end"} justify-between 4xl:justify-end w-full max-w-full items-center space-x-3 py-3 px-4`}
|
||||
>
|
||||
<div
|
||||
className={`-my-1 flex-grow 4xl:hidden ${
|
||||
!showingSidebar && "2xl:hidden"
|
||||
}`}
|
||||
className={`-my-1 flex-grow 4xl:hidden ${!showingSidebar && "2xl:hidden"}`}
|
||||
>
|
||||
{(ccPairs.length > 0 || documentSets.length > 0) && (
|
||||
<HorizontalSourceSelector
|
||||
@@ -212,11 +208,7 @@ export const FullSearchBar = ({
|
||||
>
|
||||
<SendIcon
|
||||
size={28}
|
||||
className={`text-emphasis ${
|
||||
disabled || !query
|
||||
? "bg-disabled-submit-background"
|
||||
: "bg-submit-background"
|
||||
} text-white p-1 rounded-full`}
|
||||
className={`text-emphasis ${disabled || !query ? "bg-disabled-submit-background" : "bg-submit-background"} text-white p-1 rounded-full`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -724,9 +724,7 @@ export const SearchSection = ({
|
||||
|
||||
{
|
||||
<div
|
||||
className={`desktop:px-24 w-full ${
|
||||
chatBannerPresent && "mt-10"
|
||||
} pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto`}
|
||||
className={`desktop:px-24 w-full ${chatBannerPresent && "mt-10"} pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto`}
|
||||
>
|
||||
<div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-4 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64">
|
||||
{!settings?.isMobile &&
|
||||
@@ -851,7 +849,7 @@ export const SearchSection = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FixedLogo chat />
|
||||
<FixedLogo />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { CCPairBasicInfo } from "@/lib/types";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
@@ -11,77 +12,58 @@ interface AssistantData {
|
||||
hasImageCompatibleModel: boolean;
|
||||
}
|
||||
export async function fetchAssistantData(): Promise<AssistantData> {
|
||||
// Default state if anything fails
|
||||
const defaultState: AssistantData = {
|
||||
assistants: [],
|
||||
hasAnyConnectors: false,
|
||||
hasImageCompatibleModel: false,
|
||||
};
|
||||
const [assistants, assistantsFetchError] = await fetchAssistantsSS();
|
||||
const ccPairsResponse = await fetchSS("/manage/indexing-status");
|
||||
|
||||
try {
|
||||
// Fetch core assistants data first
|
||||
const [assistants, assistantsFetchError] = await fetchAssistantsSS();
|
||||
if (assistantsFetchError) {
|
||||
console.error(`Failed to fetch assistants - ${assistantsFetchError}`);
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
// Parallel fetch of additional data
|
||||
const [ccPairsResponse, llmProviders] = await Promise.all([
|
||||
fetchSS("/manage/indexing-status").catch((error) => {
|
||||
console.error("Failed to fetch connectors:", error);
|
||||
return null;
|
||||
}),
|
||||
fetchLLMProvidersSS().catch((error) => {
|
||||
console.error("Failed to fetch LLM providers:", error);
|
||||
return [];
|
||||
}),
|
||||
]);
|
||||
|
||||
// Process visible assistants
|
||||
let filteredAssistants = assistants.filter(
|
||||
(assistant) => assistant.is_visible
|
||||
);
|
||||
|
||||
// Process connector status
|
||||
const hasAnyConnectors = ccPairsResponse?.ok
|
||||
? (await ccPairsResponse.json()).length > 0
|
||||
: false;
|
||||
|
||||
// Filter assistants based on connector status
|
||||
if (!hasAnyConnectors) {
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) => assistant.num_chunks === 0
|
||||
);
|
||||
}
|
||||
|
||||
// Sort assistants
|
||||
filteredAssistants.sort(personaComparator);
|
||||
|
||||
// Check for image-compatible models
|
||||
const hasImageCompatibleModel = llmProviders.some(
|
||||
(provider) =>
|
||||
provider.provider === "openai" ||
|
||||
provider.model_names.some((model) => checkLLMSupportsImageInput(model))
|
||||
);
|
||||
|
||||
// Filter out image generation tools if no compatible model
|
||||
if (!hasImageCompatibleModel) {
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) =>
|
||||
!assistant.tools.some(
|
||||
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
assistants: filteredAssistants,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Unexpected error in fetchAssistantData:", error);
|
||||
return defaultState;
|
||||
let ccPairs: CCPairBasicInfo[] = [];
|
||||
if (ccPairsResponse?.ok) {
|
||||
ccPairs = await ccPairsResponse.json();
|
||||
} else {
|
||||
console.log(`Failed to fetch connectors - ${ccPairsResponse?.status}`);
|
||||
}
|
||||
|
||||
const hasAnyConnectors = ccPairs.length > 0;
|
||||
|
||||
// if no connectors are setup, only show personas that are pure
|
||||
// passthrough and don't do any retrieval
|
||||
let filteredAssistants = assistants;
|
||||
if (assistantsFetchError) {
|
||||
console.log(`Failed to fetch assistants - ${assistantsFetchError}`);
|
||||
}
|
||||
|
||||
// remove those marked as hidden by an admin
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) => assistant.is_visible
|
||||
);
|
||||
|
||||
if (!hasAnyConnectors) {
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) => assistant.num_chunks === 0
|
||||
);
|
||||
}
|
||||
|
||||
// sort them in priority order
|
||||
filteredAssistants.sort(personaComparator);
|
||||
|
||||
const llmProviders = await fetchLLMProvidersSS();
|
||||
const hasImageCompatibleModel = llmProviders.some(
|
||||
(provider) =>
|
||||
provider.provider === "openai" ||
|
||||
provider.model_names.some((model) => checkLLMSupportsImageInput(model))
|
||||
);
|
||||
|
||||
if (!hasImageCompatibleModel) {
|
||||
filteredAssistants = filteredAssistants.filter(
|
||||
(assistant) =>
|
||||
!assistant.tools.some(
|
||||
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
assistants: filteredAssistants,
|
||||
hasAnyConnectors,
|
||||
hasImageCompatibleModel,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user