Performance Improvements (#2162)

This commit is contained in:
Chris Weaver
2024-08-19 11:07:00 -07:00
committed by GitHub
parent ea53977617
commit af647959f6
22 changed files with 426 additions and 403 deletions

View File

@@ -0,0 +1,65 @@
"""chosen_assistants changed to jsonb
Revision ID: da4c21c69164
Revises: c5b692fa265c
Create Date: 2024-08-18 19:06:47.291491
"""
import json
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "da4c21c69164"
down_revision = "c5b692fa265c"
branch_labels = None
depends_on = None
def upgrade() -> None:
conn = op.get_bind()
existing_ids_and_chosen_assistants = conn.execute(
sa.text("select id, chosen_assistants from public.user")
)
op.drop_column(
"user",
"chosen_assistants",
)
op.add_column(
"user",
sa.Column(
"chosen_assistants",
postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
),
)
for id, chosen_assistants in existing_ids_and_chosen_assistants:
conn.execute(
sa.text(
"update public.user set chosen_assistants = :chosen_assistants where id = :id"
),
{"chosen_assistants": json.dumps(chosen_assistants), "id": id},
)
def downgrade() -> None:
conn = op.get_bind()
existing_ids_and_chosen_assistants = conn.execute(
sa.text("select id, chosen_assistants from public.user")
)
op.drop_column(
"user",
"chosen_assistants",
)
op.add_column(
"user",
sa.Column("chosen_assistants", postgresql.ARRAY(sa.Integer()), nullable=True),
)
for id, chosen_assistants in existing_ids_and_chosen_assistants:
conn.execute(
sa.text(
"update public.user set chosen_assistants = :chosen_assistants where id = :id"
),
{"chosen_assistants": chosen_assistants, "id": id},
)

View File

@@ -59,9 +59,7 @@ from danswer.db.users import get_user_by_email
from danswer.utils.logger import setup_logger
from danswer.utils.telemetry import optional_telemetry
from danswer.utils.telemetry import RecordType
from danswer.utils.variable_functionality import (
fetch_versioned_implementation,
)
from danswer.utils.variable_functionality import fetch_versioned_implementation
logger = setup_logger()

View File

@@ -326,6 +326,9 @@ LOG_VESPA_TIMING_INFORMATION = (
)
LOG_ENDPOINT_LATENCY = os.environ.get("LOG_ENDPOINT_LATENCY", "").lower() == "true"
LOG_POSTGRES_LATENCY = os.environ.get("LOG_POSTGRES_LATENCY", "").lower() == "true"
LOG_POSTGRES_CONN_COUNTS = (
os.environ.get("LOG_POSTGRES_CONN_COUNTS", "").lower() == "true"
)
# Anonymous usage telemetry
DISABLE_TELEMETRY = os.environ.get("DISABLE_TELEMETRY", "").lower() == "true"

View File

@@ -117,6 +117,7 @@ def get_chat_sessions_by_user(
deleted: bool | None,
db_session: Session,
only_one_shot: bool = False,
limit: int = 50,
) -> list[ChatSession]:
stmt = select(ChatSession).where(ChatSession.user_id == user_id)
@@ -130,6 +131,9 @@ def get_chat_sessions_by_user(
if deleted is not None:
stmt = stmt.where(ChatSession.deleted == deleted)
if limit:
stmt = stmt.limit(limit)
result = db_session.execute(stmt)
chat_sessions = result.scalars().all()

View File

@@ -15,6 +15,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from danswer.configs.app_configs import LOG_POSTGRES_CONN_COUNTS
from danswer.configs.app_configs import LOG_POSTGRES_LATENCY
from danswer.configs.app_configs import POSTGRES_DB
from danswer.configs.app_configs import POSTGRES_HOST
@@ -65,6 +66,37 @@ if LOG_POSTGRES_LATENCY:
)
if LOG_POSTGRES_CONN_COUNTS:
# Global counter for connection checkouts and checkins
checkout_count = 0
checkin_count = 0
@event.listens_for(Engine, "checkout")
def log_checkout(dbapi_connection, connection_record, connection_proxy): # type: ignore
global checkout_count
checkout_count += 1
active_connections = connection_proxy._pool.checkedout()
idle_connections = connection_proxy._pool.checkedin()
pool_size = connection_proxy._pool.size()
logger.debug(
"Connection Checkout\n"
f"Active Connections: {active_connections};\n"
f"Idle: {idle_connections};\n"
f"Pool Size: {pool_size};\n"
f"Total connection checkouts: {checkout_count}"
)
@event.listens_for(Engine, "checkin")
def log_checkin(dbapi_connection, connection_record): # type: ignore
global checkin_count
checkin_count += 1
logger.debug(f"Total connection checkins: {checkin_count}")
"""END DEBUGGING LOGGING"""
def get_db_current_time(db_session: Session) -> datetime:
"""Get the current time from Postgres representing the start of the transaction
Within the same transaction this value will not update
@@ -152,7 +184,7 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async def warm_up_connections(
sync_connections_to_warm_up: int = 10, async_connections_to_warm_up: int = 10
sync_connections_to_warm_up: int = 20, async_connections_to_warm_up: int = 20
) -> None:
sync_postgres_engine = get_sqlalchemy_engine()
connections = [

View File

@@ -1,11 +1,9 @@
from collections.abc import Sequence
from sqlalchemy import and_
from sqlalchemy import ColumnElement
from sqlalchemy import delete
from sqlalchemy import desc
from sqlalchemy import func
from sqlalchemy import or_
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.orm import joinedload
@@ -184,13 +182,12 @@ def get_last_attempt(
def get_latest_index_attempts(
connector_credential_pair_identifiers: list[ConnectorCredentialPairIdentifier],
secondary_index: bool,
db_session: Session,
) -> Sequence[IndexAttempt]:
ids_stmt = select(
IndexAttempt.connector_credential_pair_id,
func.max(IndexAttempt.time_created).label("max_time_created"),
func.max(IndexAttempt.id).label("max_id"),
).join(EmbeddingModel, IndexAttempt.embedding_model_id == EmbeddingModel.id)
if secondary_index:
@@ -198,23 +195,6 @@ def get_latest_index_attempts(
else:
ids_stmt = ids_stmt.where(EmbeddingModel.status == IndexModelStatus.PRESENT)
where_stmts: list[ColumnElement] = []
for connector_credential_pair_identifier in connector_credential_pair_identifiers:
where_stmts.append(
IndexAttempt.connector_credential_pair_id
== (
select(ConnectorCredentialPair.id)
.where(
ConnectorCredentialPair.connector_id
== connector_credential_pair_identifier.connector_id,
ConnectorCredentialPair.credential_id
== connector_credential_pair_identifier.credential_id,
)
.scalar_subquery()
)
)
if where_stmts:
ids_stmt = ids_stmt.where(or_(*where_stmts))
ids_stmt = ids_stmt.group_by(IndexAttempt.connector_credential_pair_id)
ids_subquery = ids_stmt.subquery()
@@ -225,7 +205,7 @@ def get_latest_index_attempts(
IndexAttempt.connector_credential_pair_id
== ids_subquery.c.connector_credential_pair_id,
)
.where(IndexAttempt.time_created == ids_subquery.c.max_time_created)
.where(IndexAttempt.id == ids_subquery.c.max_id)
)
return db_session.execute(stmt).scalars().all()

View File

@@ -120,7 +120,7 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
# if specified, controls the assistants that are shown to the user + their order
# if not specified, all assistants are shown
chosen_assistants: Mapped[list[int]] = mapped_column(
postgresql.ARRAY(Integer), nullable=True
postgresql.JSONB(), nullable=True
)
oidc_expiry: Mapped[datetime.datetime] = mapped_column(

View File

@@ -9,6 +9,7 @@ from sqlalchemy import not_
from sqlalchemy import or_
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
@@ -169,6 +170,7 @@ def get_personas(
include_default: bool = True,
include_slack_bot_personas: bool = False,
include_deleted: bool = False,
joinedload_all: bool = False,
) -> Sequence[Persona]:
stmt = select(Persona).distinct()
if user_id is not None:
@@ -200,7 +202,16 @@ def get_personas(
if not include_deleted:
stmt = stmt.where(Persona.deleted.is_(False))
return db_session.scalars(stmt).all()
if joinedload_all:
stmt = stmt.options(
joinedload(Persona.prompts),
joinedload(Persona.tools),
joinedload(Persona.document_sets),
joinedload(Persona.groups),
joinedload(Persona.users),
)
return db_session.execute(stmt).unique().scalars().all()
def mark_persona_as_deleted(

View File

@@ -387,7 +387,6 @@ def get_connector_indexing_status(
]
latest_index_attempts = get_latest_index_attempts(
connector_credential_pair_identifiers=cc_pair_identifiers,
secondary_index=secondary_index,
db_session=db_session,
)

View File

@@ -79,6 +79,7 @@ def list_personas_admin(
db_session=db_session,
user_id=None, # user_id = None -> give back all personas
include_deleted=include_deleted,
joinedload_all=True,
)
]
@@ -190,7 +191,10 @@ def list_personas(
return [
PersonaSnapshot.from_model(persona)
for persona in get_personas(
user_id=user_id, include_deleted=include_deleted, db_session=db_session
user_id=user_id,
include_deleted=include_deleted,
db_session=db_session,
joinedload_all=True,
)
]

View File

@@ -88,7 +88,9 @@ services:
# (time spent on finding the right docs + time spent fetching summaries from disk)
- LOG_VESPA_TIMING_INFORMATION=${LOG_VESPA_TIMING_INFORMATION:-}
- LOG_ENDPOINT_LATENCY=${LOG_ENDPOINT_LATENCY:-}
- LOG_POSTGRES_LATENCY=${LOG_POSTGRES_LATENCY:-}
- LOG_POSTGRES_CONN_COUNTS=${LOG_POSTGRES_CONN_COUNTS:-}
# Enterprise Edition only
- ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=${ENABLE_PAID_ENTERPRISE_EDITION_FEATURES:-false}
- API_KEY_HASH_ROUNDS=${API_KEY_HASH_ROUNDS:-}

View File

@@ -1,7 +1,4 @@
import { HistorySidebar } from "@/app/chat/sessionSidebar/HistorySidebar";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { UserDropdown } from "@/components/UserDropdown";
import { ChatProvider } from "@/components/context/ChatContext";
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import { unstable_noStore as noStore } from "next/cache";
@@ -24,47 +21,27 @@ export default async function GalleryPage({
const {
user,
chatSessions,
availableSources,
documentSets,
assistants,
tags,
llmProviders,
folders,
openedFolders,
shouldShowWelcomeModal,
toggleSidebar,
userInputPrompts,
} = data;
return (
<>
<InstantSSRAutoRefresh />
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
<ChatProvider
value={{
user,
chatSessions,
availableSources,
availableDocumentSets: documentSets,
availableAssistants: assistants,
availableTags: tags,
llmProviders,
folders,
openedFolders,
userInputPrompts,
}}
>
<WrappedAssistantsGallery
initiallyToggled={toggleSidebar}
chatSessions={chatSessions}
folders={folders}
openedFolders={openedFolders}
user={user}
assistants={assistants}
/>
</ChatProvider>
<InstantSSRAutoRefresh />
<WrappedAssistantsGallery
initiallyToggled={toggleSidebar}
chatSessions={chatSessions}
folders={folders}
openedFolders={openedFolders}
user={user}
assistants={assistants}
/>
</>
);
}

View File

@@ -48,7 +48,6 @@ export default function WrappedPrompts({
content={(contentProps) => (
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
<AssistantsPageTitle>Prompt Gallery</AssistantsPageTitle>
<InstantSSRAutoRefresh />
<PromptSection
promptLibrary={promptLibrary || []}
isLoading={promptLibraryIsLoading}

View File

@@ -1,5 +1,4 @@
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { ChatProvider } from "@/components/context/ChatContext";
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import { unstable_noStore as noStore } from "next/cache";
@@ -22,47 +21,27 @@ export default async function GalleryPage({
const {
user,
chatSessions,
availableSources,
documentSets,
assistants,
tags,
llmProviders,
folders,
openedFolders,
shouldShowWelcomeModal,
toggleSidebar,
userInputPrompts,
} = data;
return (
<>
<InstantSSRAutoRefresh />
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
<ChatProvider
value={{
user,
chatSessions,
availableSources,
availableDocumentSets: documentSets,
availableAssistants: assistants,
availableTags: tags,
llmProviders,
folders,
openedFolders,
userInputPrompts,
}}
>
<WrappedAssistantsMine
initiallyToggled={toggleSidebar}
chatSessions={chatSessions}
folders={folders}
openedFolders={openedFolders}
user={user}
assistants={assistants}
/>
</ChatProvider>
<InstantSSRAutoRefresh />
<WrappedAssistantsMine
initiallyToggled={toggleSidebar}
chatSessions={chatSessions}
folders={folders}
openedFolders={openedFolders}
user={user}
assistants={assistants}
/>
</>
);
}

View File

@@ -1,7 +1,6 @@
"use client";
import ReactMarkdown from "react-markdown";
import { redirect, useRouter, useSearchParams } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import {
BackendChatSession,
BackendMessage,
@@ -23,7 +22,6 @@ import Cookies from "js-cookie";
import { HistorySidebar } from "./sessionSidebar/HistorySidebar";
import { Persona } from "../admin/assistants/interfaces";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import {
buildChatUrl,
buildLatestMessageChain,
@@ -84,7 +82,6 @@ import FixedLogo from "./shared_chat_search/FixedLogo";
import { getSecondsUntilExpiration } from "@/lib/time";
import { SetDefaultModelModal } from "./modal/SetDefaultModelModal";
import { DeleteChatModal } from "./modal/DeleteChatModal";
import remarkGfm from "remark-gfm";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
@@ -1304,7 +1301,6 @@ export function ChatPage({
return (
<>
<HealthCheckBanner secondsUntilExpiration={secondsUntilExpiration} />
<InstantSSRAutoRefresh />
{/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit.
Only used in the EE version of the app. */}
{popup}

View File

@@ -1,281 +0,0 @@
import { useChatContext } from "@/components/context/ChatContext";
import { FilterManager } from "@/lib/hooks";
import { listSourceMetadata } from "@/lib/sources";
import { useEffect, useRef, useState } from "react";
import {
DateRangePicker,
DateRangePickerItem,
Divider,
Text,
} from "@tremor/react";
import { getXDaysAgo } from "@/lib/dateUtils";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
import { Bubble } from "@/components/Bubble";
import { FiX } from "react-icons/fi";
import { getValidTags } from "@/lib/tags/tagUtils";
import debounce from "lodash/debounce";
import { Tag } from "@/lib/types";
export function FiltersTab({
filterManager,
}: {
filterManager: FilterManager;
}): JSX.Element {
const { availableSources, availableDocumentSets, availableTags } =
useChatContext();
const [filterValue, setFilterValue] = useState<string>("");
const [filteredTags, setFilteredTags] = useState<Tag[]>(availableTags);
const inputRef = useRef<HTMLInputElement>(null);
const allSources = listSourceMetadata();
const availableSourceMetadata = allSources.filter((source) =>
availableSources.includes(source.internalName)
);
const debouncedFetchTags = useRef(
debounce(async (value: string) => {
if (value) {
const fetchedTags = await getValidTags(value);
setFilteredTags(fetchedTags);
} else {
setFilteredTags(availableTags);
}
}, 50)
).current;
useEffect(() => {
debouncedFetchTags(filterValue);
return () => {
debouncedFetchTags.cancel();
};
}, [filterValue, availableTags, debouncedFetchTags]);
return (
<div className="overflow-hidden flex flex-col">
<div className="overflow-y-auto">
<div>
<div className="pb-4">
<h3 className="text-lg font-semibold">Time Range</h3>
<Text>
Choose the time range we should search over. If only one date is
selected, will only search after the specified date.
</Text>
<div className="mt-2">
<DateRangePicker
className="w-96"
value={{
from: filterManager.timeRange?.from,
to: filterManager.timeRange?.to,
selectValue: filterManager.timeRange?.selectValue,
}}
onValueChange={(value) =>
filterManager.setTimeRange({
from: value.from,
to: value.to,
selectValue: value.selectValue,
})
}
selectPlaceholder="Select range"
enableSelect
>
<DateRangePickerItem
key="Last 30 Days"
value="Last 30 Days"
from={getXDaysAgo(30)}
to={new Date()}
>
Last 30 Days
</DateRangePickerItem>
<DateRangePickerItem
key="Last 7 Days"
value="Last 7 Days"
from={getXDaysAgo(7)}
to={new Date()}
>
Last 7 Days
</DateRangePickerItem>
<DateRangePickerItem
key="Today"
value="Today"
from={getXDaysAgo(1)}
to={new Date()}
>
Today
</DateRangePickerItem>
</DateRangePicker>
</div>
</div>
<Divider />
<div className="mb-8">
<h3 className="text-lg font-semibold">Knowledge Sets</h3>
<Text>
Choose which knowledge sets we should search over. If multiple are
selected, we will search through all of them.
</Text>
<ul className="mt-3">
{availableDocumentSets.length > 0 ? (
availableDocumentSets.map((set) => {
const isSelected =
filterManager.selectedDocumentSets.includes(set.name);
return (
<DocumentSetSelectable
key={set.id}
documentSet={set}
isSelected={isSelected}
onSelect={() =>
filterManager.setSelectedDocumentSets((prev) =>
isSelected
? prev.filter((s) => s !== set.name)
: [...prev, set.name]
)
}
/>
);
})
) : (
<li>No knowledge sets available</li>
)}
</ul>
</div>
<Divider />
<div className="mb-4">
<h3 className="text-lg font-semibold">Sources</h3>
<Text>
Choose which sources we should search over. If multiple sources
are selected, we will search through all of them.
</Text>
<ul className="mt-3 flex gap-2">
{availableSourceMetadata.length > 0 ? (
availableSourceMetadata.map((sourceMetadata) => {
const isSelected = filterManager.selectedSources.some(
(selectedSource) =>
selectedSource.internalName ===
sourceMetadata.internalName
);
return (
<Bubble
key={sourceMetadata.internalName}
isSelected={isSelected}
onClick={() =>
filterManager.setSelectedSources((prev) =>
isSelected
? prev.filter(
(s) =>
s.internalName !== sourceMetadata.internalName
)
: [...prev, sourceMetadata]
)
}
showCheckbox={true}
>
<div className="flex items-center space-x-2">
{sourceMetadata?.icon({ size: 16 })}
<span>{sourceMetadata.displayName}</span>
</div>
</Bubble>
);
})
) : (
<li>No sources available</li>
)}
</ul>
</div>
<Divider />
<div className="mb-8">
<h3 className="text-lg font-semibold">Tags</h3>
<ul className="space-2 gap-2 flex flex-wrap mt-2">
{filterManager.selectedTags.length > 0 ? (
filterManager.selectedTags.map((tag) => (
<Bubble
key={tag.tag_key + tag.tag_value}
isSelected={true}
onClick={() =>
filterManager.setSelectedTags((prev) =>
prev.filter(
(t) =>
t.tag_key !== tag.tag_key ||
t.tag_value !== tag.tag_value
)
)
}
>
<div className="flex items-center space-x-2 text-sm">
<p>
{tag.tag_key}={tag.tag_value}
</p>{" "}
<FiX />
</div>
</Bubble>
))
) : (
<p className="text-xs italic">No selected tags</p>
)}
</ul>
<div className="w-96 mt-2">
<div>
<div className="mb-2 pt-2">
<input
ref={inputRef}
className="w-full border border-border py-0.5 px-2 rounded text-sm h-8"
placeholder="Find a tag"
value={filterValue}
onChange={(event) => setFilterValue(event.target.value)}
/>
</div>
<div className="max-h-48 flex flex-col gap-y-1 overflow-y-auto">
{filteredTags.length > 0 ? (
filteredTags
.filter(
(tag) =>
!filterManager.selectedTags.some(
(selectedTag) =>
selectedTag.tag_key === tag.tag_key &&
selectedTag.tag_value === tag.tag_value
)
)
.slice(0, 12)
.map((tag) => (
<Bubble
key={tag.tag_key + tag.tag_value}
isSelected={filterManager.selectedTags.includes(tag)}
onClick={() =>
filterManager.setSelectedTags((prev) =>
filterManager.selectedTags.includes(tag)
? prev.filter(
(t) =>
t.tag_key !== tag.tag_key ||
t.tag_value !== tag.tag_value
)
: [...prev, tag]
)
}
>
<>
{tag.tag_key}={tag.tag_value}
</>
</Bubble>
))
) : (
<div className="text-sm px-2 py-2">
No matching tags found
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -3,11 +3,8 @@ import { unstable_noStore as noStore } from "next/cache";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
import { ChatProvider } from "@/components/context/ChatContext";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import FunctionalWrapper from "./shared_chat_search/FunctionalWrapper";
import { ChatPage } from "./ChatPage";
import WrappedChat from "./WrappedChat";
export default async function Page({

View File

@@ -69,11 +69,11 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
const currentChatId = currentChatSession?.id;
// prevent the NextJS Router cache from causing the chat sidebar to not
// update / show an outdated list of chats
useEffect(() => {
router.refresh();
}, [currentChatId]);
// NOTE: do not do something like the below - assume that the parent
// will handle properly refreshing the existingChats
// useEffect(() => {
// router.refresh();
// }, [currentChatId]);
const combinedSettings = useContext(SettingsContext);
if (!combinedSettings) {

View File

@@ -1,12 +1,19 @@
import "./globals.css";
import { getCombinedSettings } from "@/components/settings/lib";
import { CUSTOM_ANALYTICS_ENABLED } from "@/lib/constants";
import {
fetchEnterpriseSettingsSS,
getCombinedSettings,
} from "@/components/settings/lib";
import {
CUSTOM_ANALYTICS_ENABLED,
SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED,
} from "@/lib/constants";
import { SettingsProvider } from "@/components/settings/SettingsProvider";
import { Metadata } from "next";
import { buildClientUrl } from "@/lib/utilsSS";
import { Inter } from "next/font/google";
import Head from "next/head";
import { EnterpriseSettings } from "./admin/settings/interfaces";
const inter = Inter({
subsets: ["latin"],
@@ -15,15 +22,18 @@ const inter = Inter({
});
export async function generateMetadata(): Promise<Metadata> {
const dynamicSettings = await getCombinedSettings({ forceRetrieval: true });
const logoLocation =
dynamicSettings.enterpriseSettings &&
dynamicSettings.enterpriseSettings?.use_custom_logo
? "/api/enterprise-settings/logo"
: buildClientUrl("/danswer.ico");
let logoLocation = buildClientUrl("/danswer.ico");
let enterpriseSettings: EnterpriseSettings | null = null;
if (SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED) {
enterpriseSettings = await (await fetchEnterpriseSettingsSS()).json();
logoLocation =
enterpriseSettings && enterpriseSettings.use_custom_logo
? "/api/enterprise-settings/logo"
: buildClientUrl("/danswer.ico");
}
return {
title: dynamicSettings.enterpriseSettings?.application_name ?? "Danswer",
title: enterpriseSettings?.application_name ?? "Danswer",
description: "Question answering for your documents",
icons: {
icon: logoLocation,

View File

@@ -185,6 +185,7 @@ export default async function Home() {
<>
<HealthCheckBanner secondsUntilExpiration={secondsUntilExpiration} />
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
<InstantSSRAutoRefresh />
{!shouldShowWelcomeModal &&
!shouldDisplayNoSourcesModal &&
@@ -200,7 +201,6 @@ export default async function Home() {
Only used in the EE version of the app. */}
<ChatPopup />
<InstantSSRAutoRefresh />
<WrappedSearch
disabledAgentic={DISABLE_LLM_DOC_RELEVANCE}
initiallyToggled={toggleSidebar}

View File

@@ -10,12 +10,24 @@ import {
import { fetchSS } from "@/lib/utilsSS";
import { getWebVersion } from "@/lib/version";
export async function fetchStandardSettingsSS() {
return fetchSS("/settings");
}
export async function fetchEnterpriseSettingsSS() {
return fetchSS("/enterprise-settings");
}
export async function fetchCustomAnalyticsScriptSS() {
return fetchSS("/enterprise-settings/custom-analytics-script");
}
export async function fetchSettingsSS() {
const tasks = [fetchSS("/settings")];
const tasks = [fetchStandardSettingsSS()];
if (SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED) {
tasks.push(fetchSS("/enterprise-settings"));
tasks.push(fetchEnterpriseSettingsSS());
if (CUSTOM_ANALYTICS_ENABLED) {
tasks.push(fetchSS("/enterprise-settings/custom-analytics-script"));
tasks.push(fetchCustomAnalyticsScriptSS());
}
}

View File

@@ -0,0 +1,236 @@
import {
AuthTypeMetadata,
getAuthTypeMetadataSS,
getCurrentUserSS,
} from "@/lib/userSS";
import { fetchSS } from "@/lib/utilsSS";
import {
CCPairBasicInfo,
DocumentSet,
Tag,
User,
ValidSources,
} from "@/lib/types";
import { ChatSession } from "@/app/chat/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
import { FullEmbeddingModelResponse } from "@/components/embedding/interfaces";
import { Settings } from "@/app/admin/settings/interfaces";
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { Folder } from "@/app/chat/folders/interfaces";
import { personaComparator } from "@/app/admin/assistants/lib";
import { cookies } from "next/headers";
import {
SIDEBAR_TOGGLED_COOKIE_NAME,
DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME,
} from "@/components/resizable/constants";
import { hasCompletedWelcomeFlowSS } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { fetchAssistantsSS } from "../assistants/fetchAssistantsSS";
import { NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN } from "../constants";
interface FetchChatDataResult {
user?: User | null;
chatSessions?: ChatSession[];
ccPairs?: CCPairBasicInfo[];
availableSources?: ValidSources[];
documentSets?: DocumentSet[];
assistants?: Persona[];
tags?: Tag[];
llmProviders?: LLMProviderDescriptor[];
folders?: Folder[];
openedFolders?: Record<string, boolean>;
defaultAssistantId?: number;
toggleSidebar?: boolean;
finalDocumentSidebarInitialWidth?: number;
shouldShowWelcomeModal?: boolean;
shouldDisplaySourcesIncompleteModal?: boolean;
userInputPrompts?: InputPrompt[];
}
type FetchOption =
| "user"
| "chatSessions"
| "ccPairs"
| "documentSets"
| "assistants"
| "tags"
| "llmProviders"
| "folders"
| "userInputPrompts";
/*
NOTE: currently unused, but leaving here for future use.
*/
export async function fetchSomeChatData(
searchParams: { [key: string]: string },
fetchOptions: FetchOption[] = []
): Promise<FetchChatDataResult | { redirect: string }> {
const tasks: Promise<any>[] = [];
const taskMap: Record<FetchOption, () => Promise<any>> = {
user: getCurrentUserSS,
chatSessions: () => fetchSS("/chat/get-user-chat-sessions"),
ccPairs: () => fetchSS("/manage/indexing-status"),
documentSets: () => fetchSS("/manage/document-set"),
assistants: fetchAssistantsSS,
tags: () => fetchSS("/query/valid-tags"),
llmProviders: fetchLLMProvidersSS,
folders: () => fetchSS("/folder"),
userInputPrompts: () => fetchSS("/input_prompt?include_public=true"),
};
// Always fetch auth type metadata
tasks.push(getAuthTypeMetadataSS());
// Add tasks based on fetchOptions
fetchOptions.forEach((option) => {
if (taskMap[option]) {
tasks.push(taskMap[option]());
}
});
let results: any[] = await Promise.all(tasks);
const authTypeMetadata = results.shift() as AuthTypeMetadata | null;
const authDisabled = authTypeMetadata?.authType === "disabled";
let user: User | null = null;
if (fetchOptions.includes("user")) {
user = results.shift();
if (!authDisabled && !user) {
return { redirect: "/auth/login" };
}
if (user && !user.is_verified && authTypeMetadata?.requiresVerification) {
return { redirect: "/auth/waiting-on-verification" };
}
}
const result: FetchChatDataResult = {};
for (let i = 0; i < fetchOptions.length; i++) {
const option = fetchOptions[i];
const result = results[i];
switch (option) {
case "user":
result.user = user;
break;
case "chatSessions":
result.chatSessions = result?.ok
? ((await result.json()) as { sessions: ChatSession[] }).sessions
: [];
break;
case "ccPairs":
result.ccPairs = result?.ok
? ((await result.json()) as CCPairBasicInfo[])
: [];
break;
case "documentSets":
result.documentSets = result?.ok
? ((await result.json()) as DocumentSet[])
: [];
break;
case "assistants":
const [rawAssistantsList, assistantsFetchError] = result as [
Persona[],
string | null,
];
result.assistants = rawAssistantsList
.filter((assistant) => assistant.is_visible)
.sort(personaComparator);
break;
case "tags":
result.tags = result?.ok
? ((await result.json()) as { tags: Tag[] }).tags
: [];
break;
case "llmProviders":
result.llmProviders = result || [];
break;
case "folders":
result.folders = result?.ok
? ((await result.json()) as { folders: Folder[] }).folders
: [];
break;
case "userInputPrompts":
result.userInputPrompts = result?.ok
? ((await result.json()) as InputPrompt[])
: [];
break;
}
}
if (result.ccPairs) {
result.availableSources = Array.from(
new Set(result.ccPairs.map((ccPair) => ccPair.source))
);
}
if (result.chatSessions) {
result.chatSessions.sort((a, b) => (a.id > b.id ? -1 : 1));
}
if (fetchOptions.includes("assistants") && result.assistants) {
const hasAnyConnectors = result.ccPairs && result.ccPairs.length > 0;
if (!hasAnyConnectors) {
result.assistants = result.assistants.filter(
(assistant) => assistant.num_chunks === 0
);
}
const hasOpenAIProvider =
result.llmProviders &&
result.llmProviders.some((provider) => provider.provider === "openai");
if (!hasOpenAIProvider) {
result.assistants = result.assistants.filter(
(assistant) =>
!assistant.tools.some(
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
)
);
}
}
if (fetchOptions.includes("folders")) {
const openedFoldersCookie = cookies().get("openedFolders");
result.openedFolders = openedFoldersCookie
? JSON.parse(openedFoldersCookie.value)
: {};
}
const defaultAssistantIdRaw = searchParams["assistantId"];
result.defaultAssistantId = defaultAssistantIdRaw
? parseInt(defaultAssistantIdRaw)
: undefined;
const documentSidebarCookieInitialWidth = cookies().get(
DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME
);
const sidebarToggled = cookies().get(SIDEBAR_TOGGLED_COOKIE_NAME);
result.toggleSidebar = sidebarToggled
? sidebarToggled.value.toLowerCase() === "true"
: NEXT_PUBLIC_DEFAULT_SIDEBAR_OPEN;
result.finalDocumentSidebarInitialWidth = documentSidebarCookieInitialWidth
? parseInt(documentSidebarCookieInitialWidth.value)
: undefined;
if (fetchOptions.includes("ccPairs") && result.ccPairs) {
const hasAnyConnectors = result.ccPairs.length > 0;
result.shouldShowWelcomeModal =
!hasCompletedWelcomeFlowSS() &&
!hasAnyConnectors &&
(!user || user.role === "admin");
result.shouldDisplaySourcesIncompleteModal =
hasAnyConnectors &&
!result.shouldShowWelcomeModal &&
!result.ccPairs.some(
(ccPair) => ccPair.has_successful_run && ccPair.docs_indexed > 0
) &&
(!user || user.role === "admin");
}
return result;
}