diff --git a/backend/onyx/db/persona.py b/backend/onyx/db/persona.py
index 7bf9a1ab451..f5beb1d71e8 100644
--- a/backend/onyx/db/persona.py
+++ b/backend/onyx/db/persona.py
@@ -1,5 +1,6 @@
from collections.abc import Sequence
from datetime import datetime
+from enum import Enum
from uuid import UUID
from fastapi import HTTPException
@@ -11,7 +12,6 @@
from sqlalchemy import update
from sqlalchemy.orm import aliased
from sqlalchemy.orm import joinedload
-from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
from onyx.auth.schemas import UserRole
@@ -22,6 +22,7 @@
from onyx.configs.constants import NotificationType
from onyx.context.search.enums import RecencyBiasSetting
from onyx.db.constants import SLACK_BOT_PERSONA_PREFIX
+from onyx.db.models import ConnectorCredentialPair
from onyx.db.models import DocumentSet
from onyx.db.models import Persona
from onyx.db.models import Persona__User
@@ -45,6 +46,12 @@
logger = setup_logger()
+class PersonaLoadType(Enum):
+ NONE = "none"
+ MINIMAL = "minimal"
+ FULL = "full"
+
+
def _add_user_filters(
stmt: Select, user: User | None, get_editable: bool = True
) -> Select:
@@ -322,6 +329,8 @@ def update_persona_public_status(
def get_personas_for_user(
+ # defines how much of the persona to pre-load
+ load_type: PersonaLoadType,
# if user is `None` assume the user is an admin or auth is disabled
user: User | None,
db_session: Session,
@@ -329,9 +338,6 @@ def get_personas_for_user(
include_default: bool = True,
include_slack_bot_personas: bool = False,
include_deleted: bool = False,
- joinedload_all: bool = False,
- # a bit jank
- include_prompt: bool = True,
) -> Sequence[Persona]:
stmt = select(Persona)
stmt = _add_user_filters(stmt, user, get_editable)
@@ -343,20 +349,45 @@ def get_personas_for_user(
if not include_deleted:
stmt = stmt.where(Persona.deleted.is_(False))
- if joinedload_all:
+ if load_type == PersonaLoadType.MINIMAL:
+ # For ChatPage, only load essential relationships
+ stmt = stmt.options(
+ # Used for retrieval capability checking
+ joinedload(Persona.tools),
+ # Used for filtering
+ joinedload(Persona.labels),
+ # only show document sets in the UI that the assistant has access to
+ joinedload(Persona.document_sets),
+ joinedload(Persona.document_sets)
+ .joinedload(DocumentSet.connector_credential_pairs)
+ .joinedload(ConnectorCredentialPair.connector),
+ joinedload(Persona.document_sets)
+ .joinedload(DocumentSet.connector_credential_pairs)
+ .joinedload(ConnectorCredentialPair.credential),
+ # user
+ joinedload(Persona.user),
+ )
+ elif load_type == PersonaLoadType.FULL:
stmt = stmt.options(
- selectinload(Persona.tools),
- selectinload(Persona.document_sets),
- selectinload(Persona.groups),
- selectinload(Persona.users),
- selectinload(Persona.labels),
- selectinload(Persona.user_files),
- selectinload(Persona.user_folders),
+ joinedload(Persona.user),
+ joinedload(Persona.tools),
+ joinedload(Persona.document_sets)
+ .joinedload(DocumentSet.connector_credential_pairs)
+ .joinedload(ConnectorCredentialPair.connector),
+ joinedload(Persona.document_sets)
+ .joinedload(DocumentSet.connector_credential_pairs)
+ .joinedload(ConnectorCredentialPair.credential),
+ joinedload(Persona.document_sets).joinedload(DocumentSet.users),
+ joinedload(Persona.document_sets).joinedload(DocumentSet.groups),
+ joinedload(Persona.groups),
+ joinedload(Persona.users),
+ joinedload(Persona.labels),
+ joinedload(Persona.user_files),
+ joinedload(Persona.user_folders),
+ joinedload(Persona.prompts),
)
- if include_prompt:
- stmt = stmt.options(selectinload(Persona.prompts))
- results = db_session.execute(stmt).scalars().all()
+ results = db_session.execute(stmt).unique().scalars().all()
return results
diff --git a/backend/onyx/server/features/persona/api.py b/backend/onyx/server/features/persona/api.py
index 4375a936473..86601603670 100644
--- a/backend/onyx/server/features/persona/api.py
+++ b/backend/onyx/server/features/persona/api.py
@@ -29,6 +29,7 @@
from onyx.db.persona import get_personas_for_user
from onyx.db.persona import mark_persona_as_deleted
from onyx.db.persona import mark_persona_as_not_deleted
+from onyx.db.persona import PersonaLoadType
from onyx.db.persona import update_all_personas_display_priority
from onyx.db.persona import update_persona_is_default
from onyx.db.persona import update_persona_label
@@ -45,6 +46,7 @@
from onyx.server.features.persona.models import FullPersonaSnapshot
from onyx.server.features.persona.models import GenerateStarterMessageRequest
from onyx.server.features.persona.models import ImageGenerationToolStatus
+from onyx.server.features.persona.models import MinimalPersonaSnapshot
from onyx.server.features.persona.models import PersonaLabelCreate
from onyx.server.features.persona.models import PersonaLabelResponse
from onyx.server.features.persona.models import PersonaSharedNotificationData
@@ -154,7 +156,7 @@ def list_personas_admin(
user=user,
get_editable=get_editable,
include_deleted=include_deleted,
- joinedload_all=True,
+ load_type=PersonaLoadType.FULL,
)
]
@@ -393,14 +395,13 @@ def list_personas(
db_session: Session = Depends(get_session),
include_deleted: bool = False,
persona_ids: list[int] = Query(None),
-) -> list[PersonaSnapshot]:
+) -> list[MinimalPersonaSnapshot]:
personas = get_personas_for_user(
+ load_type=PersonaLoadType.MINIMAL,
user=user,
include_deleted=include_deleted,
db_session=db_session,
get_editable=False,
- joinedload_all=True,
- include_prompt=False,
)
if persona_ids:
@@ -416,7 +417,8 @@ def list_personas(
)
]
- return [PersonaSnapshot.from_model(p) for p in personas]
+ result = [MinimalPersonaSnapshot.from_model(p) for p in personas]
+ return result
@basic_router.get("/{persona_id}")
diff --git a/backend/onyx/server/features/persona/models.py b/backend/onyx/server/features/persona/models.py
index 7549cbc476c..a5e1aee8829 100644
--- a/backend/onyx/server/features/persona/models.py
+++ b/backend/onyx/server/features/persona/models.py
@@ -18,6 +18,64 @@
logger = setup_logger()
+class MinimalPersonaSnapshot(BaseModel):
+ """Minimal persona model optimized for ChatPage.tsx - only includes fields actually used"""
+
+ # Core fields used by ChatPage
+ id: int
+ name: str
+ description: str
+ tools: list[ToolSnapshot]
+ starter_messages: list[StarterMessage] | None
+ document_sets: list[DocumentSet]
+ llm_model_version_override: str | None
+ llm_model_provider_override: str | None
+
+ uploaded_image_id: str | None
+ icon_shape: int | None
+ icon_color: str | None
+
+ is_public: bool
+ is_visible: bool
+ display_priority: int | None
+ is_default_persona: bool
+ builtin_persona: bool
+
+ labels: list["PersonaLabelSnapshot"]
+ owner: MinimalUserSnapshot | None
+
+ @classmethod
+ def from_model(cls, persona: Persona) -> "MinimalPersonaSnapshot":
+ return MinimalPersonaSnapshot(
+ # Core fields actually used by ChatPage
+ id=persona.id,
+ name=persona.name,
+ description=persona.description,
+ tools=[ToolSnapshot.from_model(tool) for tool in persona.tools],
+ starter_messages=persona.starter_messages,
+ document_sets=[
+ DocumentSet.from_model(document_set)
+ for document_set in persona.document_sets
+ ],
+ llm_model_version_override=persona.llm_model_version_override,
+ llm_model_provider_override=persona.llm_model_provider_override,
+ uploaded_image_id=persona.uploaded_image_id,
+ icon_shape=persona.icon_shape,
+ icon_color=persona.icon_color,
+ is_public=persona.is_public,
+ is_visible=persona.is_visible,
+ display_priority=persona.display_priority,
+ is_default_persona=persona.is_default_persona,
+ builtin_persona=persona.builtin_persona,
+ labels=[PersonaLabelSnapshot.from_model(label) for label in persona.labels],
+ owner=(
+ MinimalUserSnapshot(id=persona.user.id, email=persona.user.email)
+ if persona.user
+ else None
+ ),
+ )
+
+
class PromptSnapshot(BaseModel):
id: int
name: str
diff --git a/backend/onyx/server/openai_assistants_api/asssistants_api.py b/backend/onyx/server/openai_assistants_api/asssistants_api.py
index 97d182fb057..c1423f694ee 100644
--- a/backend/onyx/server/openai_assistants_api/asssistants_api.py
+++ b/backend/onyx/server/openai_assistants_api/asssistants_api.py
@@ -17,6 +17,7 @@
from onyx.db.persona import get_persona_by_id
from onyx.db.persona import get_personas_for_user
from onyx.db.persona import mark_persona_as_deleted
+from onyx.db.persona import PersonaLoadType
from onyx.db.persona import upsert_persona
from onyx.db.prompts import upsert_prompt
from onyx.db.tools import get_tool_by_name
@@ -244,10 +245,10 @@ def list_assistants(
) -> ListAssistantsResponse:
personas = list(
get_personas_for_user(
+ load_type=PersonaLoadType.FULL,
user=user,
db_session=db_session,
get_editable=False,
- joinedload_all=True,
)
)
diff --git a/web/src/app/admin/assistants/AssistantEditor.tsx b/web/src/app/admin/assistants/AssistantEditor.tsx
index 558982d58fd..641406c3ab2 100644
--- a/web/src/app/admin/assistants/AssistantEditor.tsx
+++ b/web/src/app/admin/assistants/AssistantEditor.tsx
@@ -258,7 +258,7 @@ export function AssistantEditor({
existingPersona?.llm_model_version_override ?? null,
starter_messages: existingPersona?.starter_messages?.length
? existingPersona.starter_messages
- : [{ message: "" }],
+ : [{ message: "", name: "" }],
enabled_tools_map: enabledToolsMap,
icon_color: existingPersona?.icon_color ?? defautIconColor,
icon_shape: existingPersona?.icon_shape ?? defaultIconShape,
@@ -526,10 +526,8 @@ export function AssistantEditor({
// to tell the backend to not fetch any documents
const numChunks = searchToolEnabled ? values.num_chunks || 25 : 0;
const starterMessages = values.starter_messages
- .filter(
- (message: { message: string }) => message.message.trim() !== ""
- )
- .map((message: { message: string; name?: string }) => ({
+ .filter((message: StarterMessage) => message.message.trim() !== "")
+ .map((message: StarterMessage) => ({
message: message.message,
name: message.message,
}));
diff --git a/web/src/app/admin/assistants/PersonaTable.tsx b/web/src/app/admin/assistants/PersonaTable.tsx
index 8f431b27a1a..57504f618f9 100644
--- a/web/src/app/admin/assistants/PersonaTable.tsx
+++ b/web/src/app/admin/assistants/PersonaTable.tsx
@@ -17,7 +17,6 @@ import {
import { FiEdit2 } from "react-icons/fi";
import { TrashIcon } from "@/components/icons/icons";
import { useUser } from "@/components/user/UserProvider";
-import { useAssistants } from "@/components/context/AssistantsContext";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
@@ -40,16 +39,18 @@ function PersonaTypeDisplay({ persona }: { persona: Persona }) {
return Personal {persona.owner && <>({persona.owner.email})>};
}
-export function PersonasTable() {
+export function PersonasTable({
+ personas,
+ refreshPersonas,
+}: {
+ personas: Persona[];
+ refreshPersonas: () => void;
+}) {
const router = useRouter();
const { popup, setPopup } = usePopup();
const { refreshUser, isAdmin } = useUser();
- const {
- allAssistants: assistants,
- refreshAssistants,
- editablePersonas,
- } = useAssistants();
+ const editablePersonas = personas.filter((p) => !p.builtin_persona);
const editablePersonaIds = useMemo(() => {
return new Set(editablePersonas.map((p) => p.id.toString()));
}, [editablePersonas]);
@@ -63,18 +64,18 @@ export function PersonasTable() {
useEffect(() => {
const editable = editablePersonas.sort(personaComparator);
- const nonEditable = assistants
+ const nonEditable = personas
.filter((p) => !editablePersonaIds.has(p.id.toString()))
.sort(personaComparator);
setFinalPersonas([...editable, ...nonEditable]);
- }, [editablePersonas, assistants, editablePersonaIds]);
+ }, [editablePersonas, personas, editablePersonaIds]);
const updatePersonaOrder = async (orderedPersonaIds: UniqueIdentifier[]) => {
- const reorderedAssistants = orderedPersonaIds.map(
- (id) => assistants.find((assistant) => assistant.id.toString() === id)!
+ const reorderedPersonas = orderedPersonaIds.map(
+ (id) => personas.find((persona) => persona.id.toString() === id)!
);
- setFinalPersonas(reorderedAssistants);
+ setFinalPersonas(reorderedPersonas);
const displayPriorityMap = new Map();
orderedPersonaIds.forEach((personaId, ind) => {
@@ -96,12 +97,12 @@ export function PersonasTable() {
type: "error",
message: `Failed to update persona order - ${await response.text()}`,
});
- setFinalPersonas(assistants);
- await refreshAssistants();
+ setFinalPersonas(personas);
+ await refreshPersonas();
return;
}
- await refreshAssistants();
+ await refreshPersonas();
await refreshUser();
};
@@ -119,7 +120,7 @@ export function PersonasTable() {
if (personaToDelete) {
const response = await deletePersona(personaToDelete.id);
if (response.ok) {
- await refreshAssistants();
+ refreshPersonas();
closeDeleteModal();
} else {
setPopup({
@@ -147,7 +148,7 @@ export function PersonasTable() {
personaToToggleDefault.is_default_persona
);
if (response.ok) {
- await refreshAssistants();
+ refreshPersonas();
closeDefaultModal();
} else {
setPopup({
@@ -267,7 +268,7 @@ export function PersonasTable() {
persona.is_visible
);
if (response.ok) {
- await refreshAssistants();
+ refreshPersonas();
} else {
setPopup({
type: "error",
diff --git a/web/src/app/admin/assistants/hooks.ts b/web/src/app/admin/assistants/hooks.ts
new file mode 100644
index 00000000000..ee4c824db56
--- /dev/null
+++ b/web/src/app/admin/assistants/hooks.ts
@@ -0,0 +1,30 @@
+import useSWR from "swr";
+import { errorHandlingFetcher } from "@/lib/fetcher";
+import { buildApiPath } from "@/lib/urlBuilder";
+import { Persona } from "@/app/admin/assistants/interfaces";
+
+interface UseAdminPersonasOptions {
+ includeDeleted?: boolean;
+ getEditable?: boolean;
+}
+
+export const useAdminPersonas = (options?: UseAdminPersonasOptions) => {
+ const { includeDeleted = false, getEditable = false } = options || {};
+
+ const url = buildApiPath("/api/admin/persona", {
+ include_deleted: includeDeleted.toString(),
+ get_editable: getEditable.toString(),
+ });
+
+ const { data, error, isLoading, mutate } = useSWR(
+ url,
+ errorHandlingFetcher
+ );
+
+ return {
+ personas: data,
+ error,
+ isLoading,
+ refresh: mutate,
+ };
+};
diff --git a/web/src/app/admin/assistants/interfaces.ts b/web/src/app/admin/assistants/interfaces.ts
index 77fd63dfa51..fe88e3a9449 100644
--- a/web/src/app/admin/assistants/interfaces.ts
+++ b/web/src/app/admin/assistants/interfaces.ts
@@ -18,29 +18,36 @@ export interface Prompt {
datetime_aware: boolean;
default_prompt: boolean;
}
-export interface Persona {
+
+export interface MinimalPersonaSnapshot {
id: number;
name: string;
description: string;
- is_public: boolean;
- is_visible: boolean;
+ tools: ToolSnapshot[];
+ starter_messages: StarterMessage[] | null;
+ document_sets: DocumentSet[];
+ llm_model_version_override?: string;
+ llm_model_provider_override?: string;
+
+ uploaded_image_id?: string;
icon_shape?: number;
icon_color?: string;
- uploaded_image_id?: string;
- user_file_ids: number[];
- user_folder_ids: number[];
+
+ is_public: boolean;
+ is_visible: boolean;
display_priority: number | null;
is_default_persona: boolean;
builtin_persona: boolean;
- starter_messages: StarterMessage[] | null;
- tools: ToolSnapshot[];
+
labels?: PersonaLabel[];
owner: MinimalUserSnapshot | null;
+}
+
+export interface Persona extends MinimalPersonaSnapshot {
+ user_file_ids: number[];
+ user_folder_ids: number[];
users: MinimalUserSnapshot[];
groups: number[];
- document_sets: DocumentSet[];
- llm_model_provider_override?: string;
- llm_model_version_override?: string;
num_chunks?: number;
}
diff --git a/web/src/app/admin/assistants/lib.ts b/web/src/app/admin/assistants/lib.ts
index 2cfce111272..fed7d0f7d14 100644
--- a/web/src/app/admin/assistants/lib.ts
+++ b/web/src/app/admin/assistants/lib.ts
@@ -1,5 +1,5 @@
import { LLMProviderView } from "../configuration/llm/interfaces";
-import { Persona, StarterMessage } from "./interfaces";
+import { MinimalPersonaSnapshot, Persona, StarterMessage } from "./interfaces";
interface PersonaUpsertRequest {
name: string;
@@ -250,7 +250,10 @@ function closerToZeroNegativesFirstComparator(a: number, b: number) {
return absA > absB ? 1 : -1;
}
-export function personaComparator(a: Persona, b: Persona) {
+export function personaComparator(
+ a: MinimalPersonaSnapshot | Persona,
+ b: MinimalPersonaSnapshot | Persona
+) {
if (a.display_priority === null && b.display_priority === null) {
return closerToZeroNegativesFirstComparator(a.id, b.id);
}
diff --git a/web/src/app/admin/assistants/page.tsx b/web/src/app/admin/assistants/page.tsx
index fc667cbf3dd..9923522d08c 100644
--- a/web/src/app/admin/assistants/page.tsx
+++ b/web/src/app/admin/assistants/page.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { PersonasTable } from "./PersonaTable";
import Text from "@/components/ui/text";
import Title from "@/components/ui/title";
@@ -6,11 +8,20 @@ import { AssistantsIcon } from "@/components/icons/icons";
import { AdminPageTitle } from "@/components/admin/Title";
import { SubLabel } from "@/components/Field";
import CreateButton from "@/components/ui/createButton";
-export default async function Page() {
- return (
-
- } title="Assistants" />
+import { useAdminPersonas } from "./hooks";
+import { Persona } from "./interfaces";
+import { ThreeDotsLoader } from "@/components/Loading";
+import { ErrorCallout } from "@/components/ErrorCallout";
+function MainContent({
+ personas,
+ refreshPersonas,
+}: {
+ personas: Persona[];
+ refreshPersonas: () => void;
+}) {
+ return (
+
Assistants are a way to build custom search/question-answering
experiences for different use cases.
@@ -40,8 +51,35 @@ export default async function Page() {
hidden will not be displayed. Editable assistants are shown at the
top.
-
+