Compare commits

...

8 Commits

Author SHA1 Message Date
Weves
621b705a8c Fix build 2025-07-15 15:30:19 -07:00
Chris Weaver
8646247c83 Persona simplification r2 (#5031)
* Revert "Revert "Reduce amount of stuff we fetch on `/persona` (#4988)" (#5024)"

This reverts commit f7ed7cd3cd.

* Enhancements / fix re-render

* re-arrange

* greptile
2025-07-15 15:24:58 -07:00
Chris Weaver
521ff64608 Send over less data for document sets (#5018)
* Send over less data for document sets

* Fix type errors

* Fix tests

* Fixes

* Don't change packages
2025-07-15 15:22:53 -07:00
Chris Weaver
5d7c1f6012 Add option to disable my documents (#5020)
* Add option to disable my documents

* cleanup
2025-07-14 23:17:50 -07:00
joachim-danswer
4fd88b4e06 docker dev and prod template (#4936)
* docker dev and prod template

* more dev files
2025-06-26 22:13:45 -07:00
joachim-danswer
97928e2d6f Forcing vespa language 2025-06-26 22:10:54 -07:00
joachim-danswer
7bce2d287d Dual search pipeline for non-tool-calling LLMs (#4872)
Added dual pipeline also for non-tool-calling LLMs. 
A helper function was created.
2025-06-12 19:51:19 -07:00
Evan Lohn
71712df320 jira daylight savings handling (#4797) 2025-06-03 09:17:11 -07:00
82 changed files with 873 additions and 438 deletions

View File

@@ -83,6 +83,22 @@ def _expand_query(
return rephrased_query
def _expand_query_non_tool_calling_llm(
expanded_keyword_thread: TimeoutThread[str],
expanded_semantic_thread: TimeoutThread[str],
) -> QueryExpansions | None:
keyword_expansion: str | None = wait_on_background(expanded_keyword_thread)
semantic_expansion: str | None = wait_on_background(expanded_semantic_thread)
if keyword_expansion is None or semantic_expansion is None:
return None
return QueryExpansions(
keywords_expansions=[keyword_expansion],
semantic_expansions=[semantic_expansion],
)
# TODO: break this out into an implementation function
# and a function that handles extracting the necessary fields
# from the state and config
@@ -186,6 +202,28 @@ def choose_tool(
is_keyword, keywords = wait_on_background(keyword_thread)
override_kwargs.precomputed_is_keyword = is_keyword
override_kwargs.precomputed_keywords = keywords
# dual keyword expansion needs to be added here for non-tool calling LLM case
if (
USE_SEMANTIC_KEYWORD_EXPANSIONS_BASIC_SEARCH
and expanded_keyword_thread
and expanded_semantic_thread
and tool.name == SearchTool._NAME
):
override_kwargs.expanded_queries = _expand_query_non_tool_calling_llm(
expanded_keyword_thread=expanded_keyword_thread,
expanded_semantic_thread=expanded_semantic_thread,
)
if (
USE_SEMANTIC_KEYWORD_EXPANSIONS_BASIC_SEARCH
and tool.name == SearchTool._NAME
and override_kwargs.expanded_queries
):
if (
override_kwargs.expanded_queries.keywords_expansions is None
or override_kwargs.expanded_queries.semantic_expansions is None
):
raise ValueError("No expanded keyword or semantic threads found.")
return ToolChoiceUpdate(
tool_choice=ToolChoice(
tool=tool,
@@ -283,18 +321,23 @@ def choose_tool(
and expanded_keyword_thread
and expanded_semantic_thread
):
keyword_expansion = wait_on_background(expanded_keyword_thread)
semantic_expansion = wait_on_background(expanded_semantic_thread)
override_kwargs.expanded_queries = QueryExpansions(
keywords_expansions=[keyword_expansion],
semantic_expansions=[semantic_expansion],
)
logger.info(
f"Original query: {agent_config.inputs.prompt_builder.raw_user_query}"
override_kwargs.expanded_queries = _expand_query_non_tool_calling_llm(
expanded_keyword_thread=expanded_keyword_thread,
expanded_semantic_thread=expanded_semantic_thread,
)
logger.info(f"Expanded keyword queries: {keyword_expansion}")
logger.info(f"Expanded semantic queries: {semantic_expansion}")
if (
USE_SEMANTIC_KEYWORD_EXPANSIONS_BASIC_SEARCH
and selected_tool.name == SearchTool._NAME
and override_kwargs.expanded_queries
):
# TODO: this is a hack to handle the case where the expanded queries are not found.
# We should refactor this to be more robust.
if (
override_kwargs.expanded_queries.keywords_expansions is None
or override_kwargs.expanded_queries.semantic_expansions is None
):
raise ValueError("No expanded keyword or semantic threads found.")
return ToolChoiceUpdate(
tool_choice=ToolChoice(

View File

@@ -35,6 +35,9 @@ GENERATIVE_MODEL_ACCESS_CHECK_FREQ = int(
) # 1 day
DISABLE_GENERATIVE_AI = os.environ.get("DISABLE_GENERATIVE_AI", "").lower() == "true"
# Controls whether users can use User Knowledge (personal documents) in assistants
DISABLE_USER_KNOWLEDGE = os.environ.get("DISABLE_USER_KNOWLEDGE", "").lower() == "true"
# Controls whether to allow admin query history reports with:
# 1. associated user emails
# 2. anonymized user emails
@@ -746,3 +749,7 @@ IMAGE_ANALYSIS_SYSTEM_PROMPT = os.environ.get(
DISABLE_AUTO_AUTH_REFRESH = (
os.environ.get("DISABLE_AUTO_AUTH_REFRESH", "").lower() == "true"
)
# Forcing Vespa Language
# English: en, German:de, etc. See: https://docs.vespa.ai/en/linguistics.html
VESPA_LANGUAGE_OVERRIDE = os.environ.get("VESPA_LANGUAGE_OVERRIDE")

View File

@@ -21,6 +21,9 @@ from onyx.connectors.confluence.utils import datetime_from_string
from onyx.connectors.confluence.utils import process_attachment
from onyx.connectors.confluence.utils import update_param_in_path
from onyx.connectors.confluence.utils import validate_attachment_filetype
from onyx.connectors.cross_connector_utils.miscellaneous_utils import (
is_atlassian_date_error,
)
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
from onyx.connectors.exceptions import InsufficientPermissionsError
@@ -76,10 +79,6 @@ ONE_DAY = ONE_HOUR * 24
MAX_CACHED_IDS = 100
def _should_propagate_error(e: Exception) -> bool:
return "field 'updated' is invalid" in str(e)
class ConfluenceCheckpoint(ConnectorCheckpoint):
next_page_url: str | None
@@ -367,7 +366,7 @@ class ConfluenceConnector(
)
except Exception as e:
logger.error(f"Error converting page {page.get('id', 'unknown')}: {e}")
if _should_propagate_error(e):
if is_atlassian_date_error(e): # propagate error to be caught and retried
raise
return ConnectorFailure(
failed_document=DocumentFailure(
@@ -446,7 +445,9 @@ class ConfluenceConnector(
f"Failed to extract/summarize attachment {attachment['title']}",
exc_info=e,
)
if _should_propagate_error(e):
if is_atlassian_date_error(
e
): # propagate error to be caught and retried
raise
return ConnectorFailure(
failed_document=DocumentFailure(
@@ -536,7 +537,7 @@ class ConfluenceConnector(
try:
return self._fetch_document_batches(checkpoint, start, end)
except Exception as e:
if _should_propagate_error(e) and start is not None:
if is_atlassian_date_error(e) and start is not None:
logger.warning(
"Confluence says we provided an invalid 'updated' field. This may indicate"
"a real issue, but can also appear during edge cases like daylight"

View File

@@ -86,3 +86,7 @@ def get_oauth_callback_uri(base_domain: str, connector_id: str) -> str:
# Used for development
base_domain = CONNECTOR_LOCALHOST_OVERRIDE
return f"{base_domain.strip('/')}/connector/oauth/callback/{connector_id}"
def is_atlassian_date_error(e: Exception) -> bool:
return "field 'updated' is invalid" in str(e)

View File

@@ -12,6 +12,9 @@ from onyx.configs.app_configs import INDEX_BATCH_SIZE
from onyx.configs.app_configs import JIRA_CONNECTOR_LABELS_TO_SKIP
from onyx.configs.app_configs import JIRA_CONNECTOR_MAX_TICKET_SIZE
from onyx.configs.constants import DocumentSource
from onyx.connectors.cross_connector_utils.miscellaneous_utils import (
is_atlassian_date_error,
)
from onyx.connectors.cross_connector_utils.miscellaneous_utils import time_str_to_utc
from onyx.connectors.exceptions import ConnectorValidationError
from onyx.connectors.exceptions import CredentialExpiredError
@@ -40,6 +43,8 @@ from onyx.utils.logger import setup_logger
logger = setup_logger()
ONE_HOUR = 3600
JIRA_API_VERSION = os.environ.get("JIRA_API_VERSION") or "2"
_JIRA_SLIM_PAGE_SIZE = 500
_JIRA_FULL_PAGE_SIZE = 50
@@ -240,7 +245,17 @@ class JiraConnector(CheckpointedConnector[JiraConnectorCheckpoint], SlimConnecto
checkpoint: JiraConnectorCheckpoint,
) -> CheckpointOutput[JiraConnectorCheckpoint]:
jql = self._get_jql_query(start, end)
try:
return self._load_from_checkpoint(jql, checkpoint)
except Exception as e:
if is_atlassian_date_error(e):
jql = self._get_jql_query(start - ONE_HOUR, end)
return self._load_from_checkpoint(jql, checkpoint)
raise e
def _load_from_checkpoint(
self, jql: str, checkpoint: JiraConnectorCheckpoint
) -> CheckpointOutput[JiraConnectorCheckpoint]:
# Get the current offset from checkpoint or start at 0
starting_offset = checkpoint.offset or 0
current_offset = starting_offset

View File

@@ -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
@@ -10,7 +11,6 @@ from sqlalchemy import Select
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.orm import aliased
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
@@ -37,7 +37,9 @@ from onyx.db.models import UserFolder
from onyx.db.models import UserGroup
from onyx.db.notification import create_notification
from onyx.server.features.persona.models import FullPersonaSnapshot
from onyx.server.features.persona.models import MinimalPersonaSnapshot
from onyx.server.features.persona.models import PersonaSharedNotificationData
from onyx.server.features.persona.models import PersonaSnapshot
from onyx.server.features.persona.models import PersonaUpsertRequest
from onyx.utils.logger import setup_logger
from onyx.utils.variable_functionality import fetch_versioned_implementation
@@ -45,9 +47,15 @@ from onyx.utils.variable_functionality import fetch_versioned_implementation
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:
stmt: Select[tuple[Persona]], user: User | None, get_editable: bool = True
) -> Select[tuple[Persona]]:
# If user is None and auth is disabled, assume the user is an admin
if (user is None and DISABLE_AUTH) or (user and user.role == UserRole.ADMIN):
return stmt
@@ -321,7 +329,45 @@ def update_persona_public_status(
db_session.commit()
def get_personas_for_user(
def _build_persona_filters(
stmt: Select[tuple[Persona]],
include_default: bool,
include_slack_bot_personas: bool,
include_deleted: bool,
) -> Select[tuple[Persona]]:
if not include_default:
stmt = stmt.where(Persona.builtin_persona.is_(False))
if not include_slack_bot_personas:
stmt = stmt.where(not_(Persona.name.startswith(SLACK_BOT_PERSONA_PREFIX)))
if not include_deleted:
stmt = stmt.where(Persona.deleted.is_(False))
return stmt
def get_minimal_persona_snapshots_for_user(
user: User | None,
db_session: Session,
get_editable: bool = True,
include_default: bool = True,
include_slack_bot_personas: bool = False,
include_deleted: bool = False,
) -> list[MinimalPersonaSnapshot]:
stmt = select(Persona)
stmt = _add_user_filters(stmt, user, get_editable)
stmt = _build_persona_filters(
stmt, include_default, include_slack_bot_personas, include_deleted
)
stmt = stmt.options(
selectinload(Persona.tools),
selectinload(Persona.labels),
selectinload(Persona.document_sets),
selectinload(Persona.user),
)
results = db_session.scalars(stmt).all()
return [MinimalPersonaSnapshot.from_model(persona) for persona in results]
def get_persona_snapshots_for_user(
# if user is `None` assume the user is an admin or auth is disabled
user: User | None,
db_session: Session,
@@ -329,32 +375,37 @@ def get_personas_for_user(
include_default: bool = True,
include_slack_bot_personas: bool = False,
include_deleted: bool = False,
joinedload_all: bool = False,
) -> list[PersonaSnapshot]:
stmt = select(Persona)
stmt = _add_user_filters(stmt, user, get_editable)
stmt = _build_persona_filters(
stmt, include_default, include_slack_bot_personas, include_deleted
)
stmt = stmt.options(
selectinload(Persona.tools),
selectinload(Persona.labels),
selectinload(Persona.document_sets),
selectinload(Persona.user),
)
results = db_session.scalars(stmt).all()
return [PersonaSnapshot.from_model(persona) for persona in results]
def get_raw_personas_for_user(
user: User | None,
db_session: Session,
get_editable: bool = True,
include_default: bool = True,
include_slack_bot_personas: bool = False,
include_deleted: bool = False,
) -> Sequence[Persona]:
stmt = select(Persona)
stmt = _add_user_filters(stmt, user, get_editable)
if not include_default:
stmt = stmt.where(Persona.builtin_persona.is_(False))
if not include_slack_bot_personas:
stmt = stmt.where(not_(Persona.name.startswith(SLACK_BOT_PERSONA_PREFIX)))
if not include_deleted:
stmt = stmt.where(Persona.deleted.is_(False))
if joinedload_all:
stmt = stmt.options(
selectinload(Persona.prompts),
selectinload(Persona.tools),
selectinload(Persona.document_sets),
selectinload(Persona.groups),
selectinload(Persona.users),
selectinload(Persona.labels),
selectinload(Persona.user_files),
selectinload(Persona.user_folders),
)
results = db_session.execute(stmt).scalars().all()
return results
stmt = _build_persona_filters(
stmt, include_default, include_slack_bot_personas, include_deleted
)
return db_session.scalars(stmt).all()
def get_personas(db_session: Session) -> Sequence[Persona]:
@@ -815,7 +866,7 @@ def delete_persona_label(label_id: int, db_session: Session) -> None:
def persona_has_search_tool(persona_id: int, db_session: Session) -> bool:
persona = (
db_session.query(Persona)
.options(joinedload(Persona.tools))
.options(selectinload(Persona.tools))
.filter(Persona.id == persona_id)
.one_or_none()
)

View File

@@ -11,6 +11,7 @@ import httpx
from retry import retry
from onyx.configs.app_configs import LOG_VESPA_TIMING_INFORMATION
from onyx.configs.app_configs import VESPA_LANGUAGE_OVERRIDE
from onyx.context.search.models import IndexFilters
from onyx.context.search.models import InferenceChunkUncleaned
from onyx.document_index.interfaces import VespaChunkRequest
@@ -308,6 +309,9 @@ def query_vespa(
),
)
if VESPA_LANGUAGE_OVERRIDE:
params["language"] = VESPA_LANGUAGE_OVERRIDE
try:
with get_vespa_http_client() as http_client:
response = http_client.post(SEARCH_ENDPOINT, json=params)

View File

@@ -17,6 +17,9 @@ from onyx.db.persona import upsert_persona
from onyx.db.prompts import get_prompt_by_name
from onyx.db.prompts import upsert_prompt
from onyx.db.user_documents import upsert_user_folder
from onyx.tools.tool_implementations.images.image_generation_tool import (
ImageGenerationTool,
)
def load_user_folders_from_yaml(
@@ -136,7 +139,7 @@ def load_personas_from_yaml(
if persona.get("image_generation"):
image_gen_tool = (
db_session.query(ToolDBModel)
.filter(ToolDBModel.name == "ImageGenerationTool")
.filter(ToolDBModel.name == ImageGenerationTool.__name__)
.first()
)
if image_gen_tool:

View File

@@ -405,6 +405,26 @@ class ConnectorCredentialPairDescriptor(BaseModel):
access_type: AccessType
class CCPairSummary(BaseModel):
"""Simplified connector-credential pair information with just essential data"""
id: int
name: str | None
source: DocumentSource
access_type: AccessType
@classmethod
def from_cc_pair_descriptor(
cls, descriptor: ConnectorCredentialPairDescriptor
) -> "CCPairSummary":
return cls(
id=descriptor.id,
name=descriptor.name,
source=descriptor.connector.source,
access_type=descriptor.access_type,
)
class RunConnectorRequest(BaseModel):
connector_id: int
credential_ids: list[int] | None = None

View File

@@ -19,8 +19,8 @@ from onyx.db.engine import get_session
from onyx.db.models import User
from onyx.server.features.document_set.models import CheckDocSetPublicRequest
from onyx.server.features.document_set.models import CheckDocSetPublicResponse
from onyx.server.features.document_set.models import DocumentSet
from onyx.server.features.document_set.models import DocumentSetCreationRequest
from onyx.server.features.document_set.models import DocumentSetSummary
from onyx.server.features.document_set.models import DocumentSetUpdateRequest
from onyx.utils.variable_functionality import fetch_ee_implementation_or_noop
@@ -125,13 +125,11 @@ def list_document_sets_for_user(
get_editable: bool = Query(
False, description="If true, return editable document sets"
),
) -> list[DocumentSet]:
return [
DocumentSet.from_model(ds)
for ds in fetch_all_document_sets_for_user(
db_session=db_session, user=user, get_editable=get_editable
)
]
) -> list[DocumentSetSummary]:
document_sets = fetch_all_document_sets_for_user(
db_session=db_session, user=user, get_editable=get_editable
)
return [DocumentSetSummary.from_model(ds) for ds in document_sets]
@router.get("/document-set-public")

View File

@@ -4,6 +4,7 @@ from pydantic import BaseModel
from pydantic import Field
from onyx.db.models import DocumentSet as DocumentSetDBModel
from onyx.server.documents.models import CCPairSummary
from onyx.server.documents.models import ConnectorCredentialPairDescriptor
from onyx.server.documents.models import ConnectorSnapshot
from onyx.server.documents.models import CredentialSnapshot
@@ -77,3 +78,38 @@ class DocumentSet(BaseModel):
users=[user.id for user in document_set_model.users],
groups=[group.id for group in document_set_model.groups],
)
class DocumentSetSummary(BaseModel):
"""Simplified document set model with minimal data for list views"""
id: int
name: str
description: str | None
cc_pair_summaries: list[CCPairSummary]
is_up_to_date: bool
is_public: bool
users: list[UUID]
groups: list[int]
@classmethod
def from_model(cls, document_set: DocumentSetDBModel) -> "DocumentSetSummary":
"""Create a summary from a DocumentSet database model"""
return cls(
id=document_set.id,
name=document_set.name,
description=document_set.description,
cc_pair_summaries=[
CCPairSummary(
id=cc_pair.id,
name=cc_pair.name,
source=cc_pair.connector.source,
access_type=cc_pair.access_type,
)
for cc_pair in document_set.connector_credential_pairs
],
is_up_to_date=document_set.is_up_to_date,
is_public=document_set.is_public,
users=[user.id for user in document_set.users],
groups=[group.id for group in document_set.groups],
)

View File

@@ -26,8 +26,9 @@ from onyx.db.persona import create_assistant_label
from onyx.db.persona import create_update_persona
from onyx.db.persona import delete_persona_label
from onyx.db.persona import get_assistant_labels
from onyx.db.persona import get_minimal_persona_snapshots_for_user
from onyx.db.persona import get_persona_by_id
from onyx.db.persona import get_personas_for_user
from onyx.db.persona import get_persona_snapshots_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 update_all_personas_display_priority
@@ -46,6 +47,7 @@ from onyx.secondary_llm_flows.starter_message_creation import (
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
@@ -53,6 +55,10 @@ from onyx.server.features.persona.models import PersonaSnapshot
from onyx.server.features.persona.models import PersonaUpsertRequest
from onyx.server.features.persona.models import PromptSnapshot
from onyx.server.models import DisplayPriorityRequest
from onyx.server.settings.store import load_settings
from onyx.tools.tool_implementations.images.image_generation_tool import (
ImageGenerationTool,
)
from onyx.tools.utils import is_image_generation_available
from onyx.utils.logger import setup_logger
from onyx.utils.telemetry import create_milestone_and_report
@@ -60,6 +66,23 @@ from shared_configs.contextvars import get_current_tenant_id
logger = setup_logger()
def _validate_user_knowledge_enabled(
persona_upsert_request: PersonaUpsertRequest, action: str
) -> None:
"""Check if user knowledge is enabled when user files/folders are provided."""
settings = load_settings()
if not settings.user_knowledge_enabled:
if (
persona_upsert_request.user_file_ids
or persona_upsert_request.user_folder_ids
):
raise HTTPException(
status_code=400,
detail=f"User Knowledge is disabled. Cannot {action} assistant with user files or folders.",
)
admin_router = APIRouter(prefix="/admin/persona")
basic_router = APIRouter(prefix="/persona")
@@ -148,16 +171,12 @@ def list_personas_admin(
include_deleted: bool = False,
get_editable: bool = Query(False, description="If true, return editable personas"),
) -> list[PersonaSnapshot]:
return [
PersonaSnapshot.from_model(persona)
for persona in get_personas_for_user(
db_session=db_session,
user=user,
get_editable=get_editable,
include_deleted=include_deleted,
joinedload_all=True,
)
]
return get_persona_snapshots_for_user(
user=user,
db_session=db_session,
get_editable=get_editable,
include_deleted=include_deleted,
)
@admin_router.patch("/{persona_id}/undelete")
@@ -204,6 +223,8 @@ def create_persona(
) -> PersonaSnapshot:
tenant_id = get_current_tenant_id()
_validate_user_knowledge_enabled(persona_upsert_request, "create")
prompt_id = (
persona_upsert_request.prompt_ids[0]
if persona_upsert_request.prompt_ids
@@ -251,6 +272,7 @@ def update_persona(
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> PersonaSnapshot:
_validate_user_knowledge_enabled(persona_upsert_request, "update")
prompt_id = (
persona_upsert_request.prompt_ids[0]
if persona_upsert_request.prompt_ids
@@ -396,13 +418,12 @@ def list_personas(
db_session: Session = Depends(get_session),
include_deleted: bool = False,
persona_ids: list[int] = Query(None),
) -> list[PersonaSnapshot]:
personas = get_personas_for_user(
) -> list[MinimalPersonaSnapshot]:
personas = get_minimal_persona_snapshots_for_user(
user=user,
include_deleted=include_deleted,
db_session=db_session,
get_editable=False,
joinedload_all=True,
)
if persona_ids:
@@ -413,12 +434,14 @@ def list_personas(
p
for p in personas
if not (
any(tool.in_code_tool_id == "ImageGenerationTool" for tool in p.tools)
any(
tool.in_code_tool_id == ImageGenerationTool.__name__ for tool in p.tools
)
and not is_image_generation_available(db_session=db_session)
)
]
return [PersonaSnapshot.from_model(p) for p in personas]
return personas
@basic_router.get("/{persona_id}")

View File

@@ -9,7 +9,7 @@ from onyx.db.models import Persona
from onyx.db.models import PersonaLabel
from onyx.db.models import Prompt
from onyx.db.models import StarterMessage
from onyx.server.features.document_set.models import DocumentSet
from onyx.server.features.document_set.models import DocumentSetSummary
from onyx.server.features.tool.models import ToolSnapshot
from onyx.server.models import MinimalUserSnapshot
from onyx.utils.logger import setup_logger
@@ -89,6 +89,70 @@ class PersonaUpsertRequest(BaseModel):
user_folder_ids: list[int] | None = None
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
# Used for retrieval capability checking
tools: list[ToolSnapshot]
starter_messages: list[StarterMessage] | None
# only show document sets in the UI that the assistant has access to
document_sets: list[DocumentSetSummary]
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
# Used for filtering
labels: list["PersonaLabelSnapshot"]
# Used to display ownership
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=[
DocumentSetSummary.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 PersonaSnapshot(BaseModel):
id: int
name: str
@@ -109,7 +173,7 @@ class PersonaSnapshot(BaseModel):
owner: MinimalUserSnapshot | None
users: list[MinimalUserSnapshot]
groups: list[int]
document_sets: list[DocumentSet]
document_sets: list[DocumentSetSummary]
llm_model_provider_override: str | None
llm_model_version_override: str | None
num_chunks: float | None
@@ -144,7 +208,7 @@ class PersonaSnapshot(BaseModel):
],
groups=[user_group.id for user_group in persona.groups],
document_sets=[
DocumentSet.from_model(document_set_model)
DocumentSetSummary.from_model(document_set_model)
for document_set_model in persona.document_sets
],
llm_model_provider_override=persona.llm_model_provider_override,
@@ -200,7 +264,7 @@ class FullPersonaSnapshot(PersonaSnapshot):
else None
),
document_sets=[
DocumentSet.from_model(document_set_model)
DocumentSetSummary.from_model(document_set_model)
for document_set_model in persona.document_sets
],
num_chunks=persona.num_chunks,

View File

@@ -15,7 +15,7 @@ from onyx.db.engine import get_session
from onyx.db.models import Persona
from onyx.db.models import User
from onyx.db.persona import get_persona_by_id
from onyx.db.persona import get_personas_for_user
from onyx.db.persona import get_raw_personas_for_user
from onyx.db.persona import mark_persona_as_deleted
from onyx.db.persona import upsert_persona
from onyx.db.prompts import upsert_prompt
@@ -242,32 +242,31 @@ def list_assistants(
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> ListAssistantsResponse:
personas = list(
get_personas_for_user(
persona_snapshots = list(
get_raw_personas_for_user(
user=user,
db_session=db_session,
get_editable=False,
joinedload_all=True,
)
)
# Apply filtering based on after and before
if after:
personas = [p for p in personas if p.id > int(after)]
persona_snapshots = [p for p in persona_snapshots if p.id > int(after)]
if before:
personas = [p for p in personas if p.id < int(before)]
persona_snapshots = [p for p in persona_snapshots if p.id < int(before)]
# Apply ordering
personas.sort(key=lambda p: p.id, reverse=(order == "desc"))
persona_snapshots.sort(key=lambda p: p.id, reverse=(order == "desc"))
# Apply limit
personas = personas[:limit]
persona_snapshots = persona_snapshots[:limit]
assistants = [persona_to_assistant(p) for p in personas]
assistants = [persona_to_assistant(p) for p in persona_snapshots]
return ListAssistantsResponse(
data=assistants,
first_id=assistants[0].id if assistants else None,
last_id=assistants[-1].id if assistants else None,
has_more=len(personas) == limit,
has_more=len(persona_snapshots) == limit,
)

View File

@@ -59,6 +59,9 @@ class Settings(BaseModel):
search_time_image_analysis_enabled: bool | None = False
image_analysis_max_size_mb: int | None = 20
# User Knowledge settings
user_knowledge_enabled: bool | None = True
class UserSettings(Settings):
notifications: list[Notification]

View File

@@ -1,3 +1,4 @@
from onyx.configs.app_configs import DISABLE_USER_KNOWLEDGE
from onyx.configs.app_configs import ONYX_QUERY_HISTORY_TYPE
from onyx.configs.constants import KV_SETTINGS_KEY
from onyx.configs.constants import OnyxRedisLocks
@@ -48,6 +49,10 @@ def load_settings() -> Settings:
settings.anonymous_user_enabled = anonymous_user_enabled
settings.query_history_type = ONYX_QUERY_HISTORY_TYPE
# Override user knowledge setting if disabled via environment variable
if DISABLE_USER_KNOWLEDGE:
settings.user_knowledge_enabled = False
return settings

View File

@@ -1,4 +1,6 @@
import time
from typing import Any
from uuid import UUID
from uuid import uuid4
import requests
@@ -19,6 +21,7 @@ class DocumentSetManager:
is_public: bool = True,
users: list[str] | None = None,
groups: list[int] | None = None,
federated_connectors: list[dict[str, Any]] | None = None,
user_performing_action: DATestUser | None = None,
) -> DATestDocumentSet:
if name is None:
@@ -29,8 +32,9 @@ class DocumentSetManager:
"description": description or name,
"cc_pair_ids": cc_pair_ids or [],
"is_public": is_public,
"users": users or [],
"users": [str(UUID(user_id)) for user_id in (users or [])],
"groups": groups or [],
"federated_connectors": federated_connectors or [],
}
response = requests.post(
@@ -53,6 +57,7 @@ class DocumentSetManager:
is_up_to_date=True,
users=users or [],
groups=groups or [],
federated_connectors=federated_connectors or [],
)
@staticmethod
@@ -65,8 +70,9 @@ class DocumentSetManager:
"description": document_set.description,
"cc_pair_ids": document_set.cc_pair_ids,
"is_public": document_set.is_public,
"users": document_set.users,
"users": [str(UUID(user_id)) for user_id in document_set.users],
"groups": document_set.groups,
"federated_connectors": document_set.federated_connectors,
}
response = requests.patch(
f"{API_SERVER_URL}/manage/admin/document-set",
@@ -114,13 +120,12 @@ class DocumentSetManager:
id=doc_set["id"],
name=doc_set["name"],
description=doc_set["description"],
cc_pair_ids=[
cc_pair["id"] for cc_pair in doc_set["cc_pair_descriptors"]
],
cc_pair_ids=[cc_pair["id"] for cc_pair in doc_set["cc_pair_summaries"]],
is_public=doc_set["is_public"],
is_up_to_date=doc_set["is_up_to_date"],
users=doc_set["users"],
users=[str(user_id) for user_id in doc_set["users"]],
groups=doc_set["groups"],
federated_connectors=doc_set["federated_connector_summaries"],
)
for doc_set in response.json()
]
@@ -186,6 +191,8 @@ class DocumentSetManager:
and doc_set.is_public == document_set.is_public
and set(doc_set.users) == set(document_set.users)
and set(doc_set.groups) == set(document_set.groups)
and doc_set.federated_connectors
== document_set.federated_connectors
):
return
if not verify_deleted:

View File

@@ -1,4 +1,5 @@
import logging
import os
import time
from types import SimpleNamespace
@@ -398,6 +399,10 @@ def reset_vespa_multitenant() -> None:
def reset_all() -> None:
if os.environ.get("SKIP_RESET", "").lower() == "true":
logger.info("Skipping reset.")
return
logger.info("Resetting Postgres...")
reset_postgres()
logger.info("Resetting Vespa...")

View File

@@ -117,6 +117,7 @@ class DATestDocumentSet(BaseModel):
is_up_to_date: bool
users: list[str] = Field(default_factory=list)
groups: list[int] = Field(default_factory=list)
federated_connectors: list[dict[str, Any]] = Field(default_factory=list)
class DATestPersona(BaseModel):

View File

@@ -124,6 +124,11 @@ services:
# Seeding configuration
- USE_IAM_AUTH=${USE_IAM_AUTH:-}
- ONYX_QUERY_HISTORY_TYPE=${ONYX_QUERY_HISTORY_TYPE:-}
# Vespa Language Forcing
# See: https://docs.vespa.ai/en/linguistics.html
- VESPA_LANGUAGE_OVERRIDE=${VESPA_LANGUAGE_OVERRIDE:-}
# Uncomment the line below to use if IAM_AUTH is true and you are using iam auth for postgres
# volumes:
# - ./bundle.pem:/app/bundle.pem:ro

View File

@@ -92,6 +92,10 @@ services:
# Chat Configs
- HARD_DELETE_CHATS=${HARD_DELETE_CHATS:-}
# Vespa Language Forcing
# See: https://docs.vespa.ai/en/linguistics.html
- VESPA_LANGUAGE_OVERRIDE=${VESPA_LANGUAGE_OVERRIDE:-}
# Enterprise Edition only
- API_KEY_HASH_ROUNDS=${API_KEY_HASH_ROUNDS:-}
- ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=${ENABLE_PAID_ENTERPRISE_EDITION_FEATURES:-false}

View File

@@ -106,6 +106,10 @@ services:
- API_KEY_HASH_ROUNDS=${API_KEY_HASH_ROUNDS:-}
# Seeding configuration
- USE_IAM_AUTH=${USE_IAM_AUTH:-}
# Vespa Language Forcing
# See: https://docs.vespa.ai/en/linguistics.html
- VESPA_LANGUAGE_OVERRIDE=${VESPA_LANGUAGE_OVERRIDE:-}
extra_hosts:
- "host.docker.internal:host-gateway"
logging:

View File

@@ -55,3 +55,8 @@ SESSION_EXPIRE_TIME_SECONDS=604800
# Default values here are what Postgres uses by default, feel free to change.
POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
# If setting the vespa language is required, set this ('en', 'de', etc.).
# See: https://docs.vespa.ai/en/linguistics.html
#VESPA_LANGUAGE_OVERRIDE=

View File

@@ -5,7 +5,7 @@ import { Option } from "@/components/Dropdown";
import { generateRandomIconShape } from "@/lib/assistantIconUtils";
import {
CCPairBasicInfo,
DocumentSet,
DocumentSetSummary,
User,
UserGroup,
UserRole,
@@ -40,8 +40,9 @@ import {
} from "@/components/ui/tooltip";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { useContext, useEffect, useMemo, useState } from "react";
import * as Yup from "yup";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { FullPersona, PersonaLabel, StarterMessage } from "./interfaces";
import {
PersonaUpsertParameters,
@@ -130,7 +131,7 @@ export function AssistantEditor({
}: {
existingPersona?: FullPersona | null;
ccPairs: CCPairBasicInfo[];
documentSets: DocumentSet[];
documentSets: DocumentSetSummary[];
user: User | null;
defaultPublic: boolean;
llmProviders: LLMProviderView[];
@@ -147,6 +148,7 @@ export function AssistantEditor({
const { popup, setPopup } = usePopup();
const { labels, refreshLabels, createLabel, updateLabel, deleteLabel } =
useLabels();
const settings = useContext(SettingsContext);
const colorOptions = [
"#FF6FBF",
@@ -237,6 +239,14 @@ export function AssistantEditor({
const [showVisibilityWarning, setShowVisibilityWarning] = useState(false);
const canShowKnowledgeSource =
ccPairs.length > 0 &&
searchTool &&
!(user?.role === UserRole.BASIC && documentSets.length === 0);
const userKnowledgeEnabled =
settings?.settings?.user_knowledge_enabled ?? true;
const initialValues = {
name: existingPersona?.name ?? "",
description: existingPersona?.description ?? "",
@@ -259,7 +269,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,
@@ -275,11 +285,14 @@ export function AssistantEditor({
selectedGroups: existingPersona?.groups ?? [],
user_file_ids: existingPersona?.user_file_ids ?? [],
user_folder_ids: existingPersona?.user_folder_ids ?? [],
knowledge_source:
(existingPersona?.user_file_ids?.length ?? 0) > 0 ||
(existingPersona?.user_folder_ids?.length ?? 0) > 0
? "user_files"
: "team_knowledge",
knowledge_source: !canShowKnowledgeSource
? "user_files"
: !userKnowledgeEnabled
? "team_knowledge"
: (existingPersona?.user_file_ids?.length ?? 0) > 0 ||
(existingPersona?.user_folder_ids?.length ?? 0) > 0
? "user_files"
: "team_knowledge",
is_default_persona: existingPersona?.is_default_persona ?? false,
};
@@ -374,11 +387,6 @@ export function AssistantEditor({
}
};
const canShowKnowledgeSource =
ccPairs.length > 0 &&
searchTool &&
!(user?.role != "admin" && documentSets.length === 0);
return (
<div className="mx-auto max-w-4xl">
<style>
@@ -534,10 +542,8 @@ export function AssistantEditor({
// to tell the backend to not fetch any documents
const numChunks = searchToolEnabled ? values.num_chunks || 10 : 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,
}));
@@ -950,26 +956,28 @@ export function AssistantEditor({
</p>
</div>
<div
className={`w-[150px] h-[110px] rounded-lg border flex flex-col items-center justify-center cursor-pointer transition-all ${
values.knowledge_source === "user_files"
? "border-2 border-blue-500 bg-blue-50 dark:bg-blue-950/20"
: "border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600"
}`}
onClick={() =>
setFieldValue(
"knowledge_source",
"user_files"
)
}
>
<div className="text-blue-500 mb-2">
<FileIcon size={24} />
{userKnowledgeEnabled && (
<div
className={`w-[150px] h-[110px] rounded-lg border flex flex-col items-center justify-center cursor-pointer transition-all ${
values.knowledge_source === "user_files"
? "border-2 border-blue-500 bg-blue-50 dark:bg-blue-950/20"
: "border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600"
}`}
onClick={() =>
setFieldValue(
"knowledge_source",
"user_files"
)
}
>
<div className="text-blue-500 mb-2">
<FileIcon size={24} />
</div>
<p className="font-medium text-xs">
User Knowledge
</p>
</div>
<p className="font-medium text-xs">
User Knowledge
</p>
</div>
)}
</div>
</div>
</>

View File

@@ -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,15 +39,20 @@ function PersonaTypeDisplay({ persona }: { persona: Persona }) {
return <Text>Personal {persona.owner && <>({persona.owner.email})</>}</Text>;
}
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 = useMemo(() => {
return personas.filter((p) => !p.builtin_persona);
}, [personas]);
const editablePersonaIds = useMemo(() => {
return new Set(editablePersonas.map((p) => p.id.toString()));
@@ -63,18 +67,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<UniqueIdentifier, number>();
orderedPersonaIds.forEach((personaId, ind) => {
@@ -96,12 +100,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 +123,7 @@ export function PersonasTable() {
if (personaToDelete) {
const response = await deletePersona(personaToDelete.id);
if (response.ok) {
await refreshAssistants();
refreshPersonas();
closeDeleteModal();
} else {
setPopup({
@@ -147,7 +151,7 @@ export function PersonasTable() {
personaToToggleDefault.is_default_persona
);
if (response.ok) {
await refreshAssistants();
refreshPersonas();
closeDefaultModal();
} else {
setPopup({
@@ -267,7 +271,7 @@ export function PersonasTable() {
persona.is_visible
);
if (response.ok) {
await refreshAssistants();
refreshPersonas();
} else {
setPopup({
type: "error",

View File

@@ -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,
get_editable: getEditable,
});
const { data, error, isLoading, mutate } = useSWR<Persona[]>(
url,
errorHandlingFetcher
);
return {
personas: data || [],
error,
isLoading,
refresh: mutate,
};
};

View File

@@ -1,5 +1,5 @@
import { ToolSnapshot } from "@/lib/tools/interfaces";
import { DocumentSet, MinimalUserSnapshot } from "@/lib/types";
import { DocumentSetSummary, MinimalUserSnapshot } from "@/lib/types";
export interface StarterMessageBase {
message: string;
@@ -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: DocumentSetSummary[];
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;
}

View File

@@ -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);
}

View File

@@ -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/admin/connectors/Field";
import CreateButton from "@/components/ui/createButton";
export default async function Page() {
return (
<div className="mx-auto container">
<AdminPageTitle icon={<AssistantsIcon size={32} />} 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 (
<div>
<Text className="mb-2">
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.
</SubLabel>
<PersonasTable />
<PersonasTable personas={personas} refreshPersonas={refreshPersonas} />
</div>
</div>
);
}
export default function Page() {
const { personas, isLoading, error, refresh } = useAdminPersonas();
return (
<div className="mx-auto container">
<AdminPageTitle icon={<AssistantsIcon size={32} />} title="Assistants" />
{isLoading && <ThreeDotsLoader />}
{error && (
<ErrorCallout
errorTitle="Failed to load assistants"
errorMsg={
error?.info?.message ||
error?.info?.detail ||
"An unknown error occurred"
}
/>
)}
{!isLoading && !error && (
<MainContent personas={personas} refreshPersonas={refresh} />
)}
</div>
);
}

View File

@@ -3,7 +3,6 @@
import { PageSelector } from "@/components/PageSelector";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { FiEdit } from "react-icons/fi";
import {
Table,
TableBody,

View File

@@ -5,7 +5,7 @@ import { Formik, Form } from "formik";
import * as Yup from "yup";
import { usePopup } from "@/components/admin/connectors/Popup";
import {
DocumentSet,
DocumentSetSummary,
SlackChannelConfig,
SlackBotResponseType,
} from "@/lib/types";
@@ -16,7 +16,7 @@ import {
} from "../lib";
import CardSection from "@/components/admin/CardSection";
import { useRouter } from "next/navigation";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
import { SlackChannelConfigFormFields } from "./SlackChannelConfigFormFields";
@@ -29,8 +29,8 @@ export const SlackChannelConfigCreationForm = ({
existingSlackChannelConfig,
}: {
slack_bot_id: number;
documentSets: DocumentSet[];
personas: Persona[];
documentSets: DocumentSetSummary[];
personas: MinimalPersonaSnapshot[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
existingSlackChannelConfig?: SlackChannelConfig;
}) => {
@@ -59,7 +59,7 @@ export const SlackChannelConfigCreationForm = ({
}
return acc;
},
[[], []] as [Persona[], Persona[]]
[[], []] as [MinimalPersonaSnapshot[], MinimalPersonaSnapshot[]]
);
}, [personas]);

View File

@@ -2,7 +2,7 @@
import React, { useState, useEffect, useMemo } from "react";
import { FieldArray, useFormikContext, ErrorMessage, Field } from "formik";
import { CCPairDescriptor, DocumentSet } from "@/lib/types";
import { CCPairDescriptor, DocumentSetSummary } from "@/lib/types";
import {
Label,
SelectorFormField,
@@ -11,7 +11,7 @@ import {
TextFormField,
} from "@/components/admin/connectors/Field";
import { Button } from "@/components/ui/button";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
import CollapsibleSection from "@/app/admin/assistants/CollapsibleSection";
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
@@ -47,9 +47,9 @@ import { CheckFormField } from "@/components/ui/CheckField";
export interface SlackChannelConfigFormFieldsProps {
isUpdate: boolean;
isDefault: boolean;
documentSets: DocumentSet[];
searchEnabledAssistants: Persona[];
nonSearchAssistants: Persona[];
documentSets: DocumentSetSummary[];
searchEnabledAssistants: MinimalPersonaSnapshot[];
nonSearchAssistants: MinimalPersonaSnapshot[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
setPopup: (popup: {
message: string;
@@ -76,14 +76,28 @@ export function SlackChannelConfigFormFields({
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
useState(false);
const documentSetContainsSync = (documentSet: DocumentSet) =>
documentSet.cc_pair_descriptors.some(
(descriptor) => descriptor.access_type === "sync"
// Helper function to check if a document set contains sync connectors
const documentSetContainsSync = (documentSet: DocumentSetSummary) => {
return documentSet.cc_pair_summaries.some(
(summary) => summary.access_type === "sync"
);
};
// Helper function to check if a document set contains private connectors
const documentSetContainsPrivate = (documentSet: DocumentSetSummary) => {
return documentSet.cc_pair_summaries.some(
(summary) => summary.access_type === "private"
);
};
// Helper function to get cc_pair_summaries from DocumentSetSummary
const getCcPairSummaries = (documentSet: DocumentSetSummary) => {
return documentSet.cc_pair_summaries;
};
const [syncEnabledAssistants, availableAssistants] = useMemo(() => {
const sync: Persona[] = [];
const available: Persona[] = [];
const sync: MinimalPersonaSnapshot[] = [];
const available: MinimalPersonaSnapshot[] = [];
searchEnabledAssistants.forEach((persona) => {
const hasSyncSet = persona.document_sets.some(documentSetContainsSync);
@@ -98,21 +112,19 @@ export function SlackChannelConfigFormFields({
}, [searchEnabledAssistants]);
const unselectableSets = useMemo(() => {
return documentSets.filter((ds) =>
ds.cc_pair_descriptors.some(
(descriptor) => descriptor.access_type === "sync"
)
);
return documentSets.filter(documentSetContainsSync);
}, [documentSets]);
const memoizedPrivateConnectors = useMemo(() => {
const uniqueDescriptors = new Map();
documentSets.forEach((ds) => {
ds.cc_pair_descriptors.forEach((descriptor) => {
documentSets.forEach((ds: DocumentSetSummary) => {
const ccPairSummaries = getCcPairSummaries(ds);
ccPairSummaries.forEach((summary: any) => {
if (
descriptor.access_type === "private" &&
!uniqueDescriptors.has(descriptor.id)
summary.access_type === "private" &&
!uniqueDescriptors.has(summary.id)
) {
uniqueDescriptors.set(descriptor.id, descriptor);
uniqueDescriptors.set(summary.id, summary);
}
});
});
@@ -138,12 +150,6 @@ export function SlackChannelConfigFormFields({
}
}, [unselectableSets, values.document_sets, setFieldValue, setPopup]);
const documentSetContainsPrivate = (documentSet: DocumentSet) => {
return documentSet.cc_pair_descriptors.some(
(descriptor) => descriptor.access_type === "private"
);
};
const shouldShowPrivacyAlert = useMemo(() => {
if (values.knowledge_source === "document_sets") {
const selectedSets = documentSets.filter((ds) =>
@@ -165,9 +171,7 @@ export function SlackChannelConfigFormFields({
const selectableSets = useMemo(() => {
return documentSets.filter(
(ds) =>
!ds.cc_pair_descriptors.some(
(descriptor) => descriptor.access_type === "sync"
)
!ds.cc_pair_summaries.some((summary) => summary.access_type === "sync")
);
}, [documentSets]);
@@ -462,23 +466,25 @@ export function SlackChannelConfigFormFields({
Un-selectable assistants:
</p>
<div className="mb-3 mt-2 flex gap-2 flex-wrap text-sm">
{syncEnabledAssistants.map((persona: Persona) => (
<button
type="button"
onClick={() =>
router.push(`/admin/assistants/${persona.id}`)
}
key={persona.id}
className="p-2 bg-background-100 cursor-pointer rounded-md flex items-center gap-2"
>
<AssistantIcon
assistant={persona}
size={16}
className="flex-none"
/>
{persona.name}
</button>
))}
{syncEnabledAssistants.map(
(persona: MinimalPersonaSnapshot) => (
<button
type="button"
onClick={() =>
router.push(`/admin/assistants/${persona.id}`)
}
key={persona.id}
className="p-2 bg-background-100 cursor-pointer rounded-md flex items-center gap-2"
>
<AssistantIcon
assistant={persona}
size={16}
className="flex-none"
/>
{persona.name}
</button>
)
)}
</div>
</div>
)}

View File

@@ -3,7 +3,11 @@ import { SourceIcon } from "@/components/SourceIcon";
import { SlackChannelConfigCreationForm } from "../SlackChannelConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet, SlackChannelConfig, ValidSources } from "@/lib/types";
import {
DocumentSetSummary,
SlackChannelConfig,
ValidSources,
} from "@/lib/types";
import { BackButton } from "@/components/BackButton";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import {
@@ -68,7 +72,7 @@ async function EditslackChannelConfigPage(props: {
);
}
const response = await documentSetsResponse.json();
const documentSets = response as DocumentSet[];
const documentSets = response as DocumentSetSummary[];
if (assistantsFetchError) {
return (

View File

@@ -2,15 +2,11 @@ import { AdminPageTitle } from "@/components/admin/Title";
import { SlackChannelConfigCreationForm } from "../SlackChannelConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet, ValidSources } from "@/lib/types";
import { DocumentSetSummary, ValidSources } from "@/lib/types";
import { BackButton } from "@/components/BackButton";
import { fetchAssistantsSS } from "@/lib/assistants/fetchAssistantsSS";
import {
getStandardAnswerCategoriesIfEE,
StandardAnswerCategoryResponse,
} from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { redirect } from "next/navigation";
import { Persona } from "../../../../assistants/interfaces";
import { SourceIcon } from "@/components/SourceIcon";
async function NewChannelConfigPage(props: {
@@ -32,8 +28,8 @@ async function NewChannelConfigPage(props: {
standardAnswerCategoryResponse,
] = await Promise.all([
fetchSS("/manage/document-set") as Promise<Response>,
fetchAssistantsSS() as Promise<[Persona[], string | null]>,
getStandardAnswerCategoriesIfEE() as Promise<StandardAnswerCategoryResponse>,
fetchAssistantsSS(),
getStandardAnswerCategoriesIfEE(),
]);
if (!documentSetsResponse.ok) {
@@ -44,7 +40,8 @@ async function NewChannelConfigPage(props: {
/>
);
}
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
const documentSets =
(await documentSetsResponse.json()) as DocumentSetSummary[];
if (assistantsResponse[1]) {
return (

View File

@@ -2,7 +2,7 @@
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { usePopup, PopupSpec } from "@/components/admin/connectors/Popup";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { triggerIndexing } from "./lib";
import { Modal } from "@/components/Modal";
import Text from "@/components/ui/text";

View File

@@ -14,7 +14,7 @@ import { useRouter } from "next/navigation";
import { useFilters } from "@/lib/hooks";
import { buildFilters } from "@/lib/search/utils";
import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBadge";
import { DocumentSet } from "@/lib/types";
import { DocumentSetSummary } from "@/lib/types";
import { SourceIcon } from "@/components/SourceIcon";
import { Connector } from "@/lib/connectors/connectors";
import { HorizontalFilters } from "@/components/filters/SourceSelector";
@@ -110,7 +110,7 @@ export function Explorer({
}: {
initialSearchValue: string | undefined;
connectors: Connector<any>[];
documentSets: DocumentSet[];
documentSets: DocumentSetSummary[];
}) {
const router = useRouter();
const { popup, setPopup } = usePopup();

View File

@@ -8,7 +8,12 @@ import {
updateDocumentSet,
DocumentSetCreationRequest,
} from "./lib";
import { ConnectorStatus, DocumentSet, UserGroup, UserRole } from "@/lib/types";
import {
ConnectorStatus,
DocumentSetSummary,
UserGroup,
UserRole,
} from "@/lib/types";
import { TextFormField } from "@/components/admin/connectors/Field";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
@@ -24,7 +29,7 @@ interface SetCreationPopupProps {
userGroups: UserGroup[] | undefined;
onClose: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
existingDocumentSet?: DocumentSet;
existingDocumentSet?: DocumentSetSummary;
}
export const DocumentSetCreationForm = ({
@@ -52,8 +57,8 @@ export const DocumentSetCreationForm = ({
name: existingDocumentSet?.name ?? "",
description: existingDocumentSet?.description ?? "",
cc_pair_ids:
existingDocumentSet?.cc_pair_descriptors.map(
(ccPairDescriptor) => ccPairDescriptor.id
existingDocumentSet?.cc_pair_summaries.map(
(ccPairSummary) => ccPairSummary.id
) ?? [],
is_public: existingDocumentSet?.is_public ?? true,
users: existingDocumentSet?.users ?? [],

View File

@@ -1,5 +1,5 @@
import { errorHandlingFetcher } from "@/lib/fetcher";
import { DocumentSet } from "@/lib/types";
import { DocumentSetSummary } from "@/lib/types";
import useSWR, { mutate } from "swr";
const DOCUMENT_SETS_URL = "/api/manage/document-set";
@@ -13,7 +13,7 @@ export function refreshDocumentSets() {
export function useDocumentSets(getEditable: boolean = false) {
const url = getEditable ? GET_EDITABLE_DOCUMENT_SETS_URL : DOCUMENT_SETS_URL;
const swrResponse = useSWR<DocumentSet[]>(url, errorHandlingFetcher, {
const swrResponse = useSWR<DocumentSetSummary[]>(url, errorHandlingFetcher, {
refreshInterval: 5000, // 5 seconds
});

View File

@@ -13,7 +13,7 @@ import {
import Text from "@/components/ui/text";
import Title from "@/components/ui/title";
import { Separator } from "@/components/ui/separator";
import { DocumentSet } from "@/lib/types";
import { DocumentSetSummary } from "@/lib/types";
import { useState } from "react";
import { useDocumentSets } from "./hooks";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
@@ -39,6 +39,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import CreateButton from "@/components/ui/createButton";
import { SourceIcon } from "@/components/SourceIcon";
const numToDisplay = 50;
@@ -46,7 +47,7 @@ const EditRow = ({
documentSet,
isEditable,
}: {
documentSet: DocumentSet;
documentSet: DocumentSetSummary;
isEditable: boolean;
}) => {
const router = useRouter();
@@ -96,11 +97,11 @@ const EditRow = ({
};
interface DocumentFeedbackTableProps {
documentSets: DocumentSet[];
documentSets: DocumentSetSummary[];
refresh: () => void;
refreshEditable: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
editableDocumentSets: DocumentSet[];
editableDocumentSets: DocumentSetSummary[];
}
const DocumentSetTable = ({
@@ -162,24 +163,27 @@ const DocumentSetTable = ({
</TableCell>
<TableCell>
<div>
{documentSet.cc_pair_descriptors.map(
(ccPairDescriptor, ind) => {
{/* Regular Connectors */}
{documentSet.cc_pair_summaries.map(
(ccPairSummary, ind) => {
return (
<div
className={
ind !==
documentSet.cc_pair_descriptors.length - 1
ind !== documentSet.cc_pair_summaries.length - 1
? "mb-3"
: ""
}
key={ccPairDescriptor.id}
key={ccPairSummary.id}
>
<ConnectorTitle
connector={ccPairDescriptor.connector}
ccPairName={ccPairDescriptor.name}
ccPairId={ccPairDescriptor.id}
showMetadata={false}
/>
<div className="text-blue-500 dark:text-blue-100 flex w-fit">
<SourceIcon
sourceType={ccPairSummary.source}
iconSize={16}
/>
<div className="ml-1 my-auto text-xs font-medium truncate">
{ccPairSummary.name || "Unnamed"}
</div>
</div>
</div>
);
}
@@ -191,7 +195,7 @@ const DocumentSetTable = ({
<Badge variant="success" icon={FiCheckCircle}>
Up to Date
</Badge>
) : documentSet.cc_pair_descriptors.length > 0 ? (
) : documentSet.cc_pair_summaries.length > 0 ? (
<Badge variant="in_progress" icon={FiClock}>
Syncing
</Badge>

View File

@@ -27,6 +27,9 @@ export interface Settings {
image_extraction_and_analysis_enabled?: boolean;
search_time_image_analysis_enabled?: boolean;
image_analysis_max_size_mb?: number | null;
// User Knowledge settings
user_knowledge_enabled?: boolean;
}
export enum NotificationType {

View File

@@ -15,7 +15,7 @@ import {
PopoverContent,
} from "@/components/ui/popover";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { useUser } from "@/components/user/UserProvider";
import { useAssistants } from "@/components/context/AssistantsContext";
import { checkUserOwnsAssistant } from "@/lib/assistants/utils";
@@ -54,7 +54,7 @@ export const AssistantBadge = ({
};
const AssistantCard: React.FC<{
persona: Persona;
persona: MinimalPersonaSnapshot;
pinned: boolean;
closeModal: () => void;
}> = ({ persona, pinned, closeModal }) => {

View File

@@ -9,7 +9,6 @@ import * as Yup from "yup";
import { requestEmailVerification } from "../lib";
import { useState } from "react";
import { Spinner } from "@/components/Spinner";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
import Link from "next/link";
import { useUser } from "@/components/user/UserProvider";
import { useRouter } from "next/navigation";

View File

@@ -1,7 +1,11 @@
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { Persona } from "../admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "../admin/assistants/interfaces";
export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
export function ChatIntro({
selectedPersona,
}: {
selectedPersona: MinimalPersonaSnapshot;
}) {
return (
<div data-testid="chat-intro" className="flex flex-col items-center gap-6">
<div className="relative flex flex-col gap-y-4 w-fit mx-auto justify-center">

View File

@@ -29,7 +29,7 @@ import {
import Prism from "prismjs";
import Cookies from "js-cookie";
import { HistorySidebar } from "./sessionSidebar/HistorySidebar";
import { Persona } from "../admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "../admin/assistants/interfaces";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import {
buildChatUrl,
@@ -49,7 +49,6 @@ import {
setMessageAsLatest,
updateLlmOverrideForChatSession,
updateParentChildren,
uploadFilesForChat,
useScrollonStream,
} from "./lib";
import {
@@ -302,7 +301,7 @@ export function ChatPage({
const existingChatSessionAssistantId = selectedChatSession?.persona_id;
const [selectedAssistant, setSelectedAssistant] = useState<
Persona | undefined
MinimalPersonaSnapshot | undefined
>(
// NOTE: look through available assistants here, so that even if the user
// has hidden this assistant it still shows the correct assistant when
@@ -332,7 +331,7 @@ export function ChatPage({
};
const [alternativeAssistant, setAlternativeAssistant] =
useState<Persona | null>(null);
useState<MinimalPersonaSnapshot | null>(null);
const [presentingDocument, setPresentingDocument] =
useState<MinimalOnyxDocument | null>(null);
@@ -343,7 +342,7 @@ export function ChatPage({
// 3. First pinned assistants (ordered list of pinned assistants)
// 4. Available assistants (ordered list of available assistants)
// Relevant test: `live_assistant.spec.ts`
const liveAssistant: Persona | undefined = useMemo(
const liveAssistant: MinimalPersonaSnapshot | undefined = useMemo(
() =>
alternativeAssistant ||
selectedAssistant ||
@@ -412,7 +411,7 @@ export function ChatPage({
// 2. we "@"ed the `GPT` assistant and sent a message
// 3. while the `GPT` assistant message is generating, we "@" the `Paraphrase` assistant
const [alternativeGeneratingAssistant, setAlternativeGeneratingAssistant] =
useState<Persona | null>(null);
useState<MinimalPersonaSnapshot | null>(null);
// used to track whether or not the initial "submit on load" has been performed
// this only applies if `?submit-on-load=true` or `?submit-on-load=1` is in the URL
@@ -1196,7 +1195,7 @@ export function ChatPage({
queryOverride?: string;
forceSearch?: boolean;
isSeededChat?: boolean;
alternativeAssistantOverride?: Persona | null;
alternativeAssistantOverride?: MinimalPersonaSnapshot | null;
modelOverride?: LlmDescriptor;
regenerationRequest?: RegenerationRequest | null;
overrideFileDescriptors?: FileDescriptor[];
@@ -2081,10 +2080,7 @@ export function ChatPage({
useEffect(() => {
if (liveAssistant) {
const hasSearchTool = liveAssistant.tools.some(
(tool) =>
tool.in_code_tool_id === SEARCH_TOOL_ID &&
liveAssistant.user_file_ids?.length == 0 &&
liveAssistant.user_folder_ids?.length == 0
(tool) => tool.in_code_tool_id === SEARCH_TOOL_ID
);
setRetrievalEnabled(hasSearchTool);
if (!hasSearchTool) {
@@ -2096,10 +2092,7 @@ export function ChatPage({
const [retrievalEnabled, setRetrievalEnabled] = useState(() => {
if (liveAssistant) {
return liveAssistant.tools.some(
(tool) =>
tool.in_code_tool_id === SEARCH_TOOL_ID &&
liveAssistant.user_file_ids?.length == 0 &&
liveAssistant.user_folder_ids?.length == 0
(tool) => tool.in_code_tool_id === SEARCH_TOOL_ID
);
}
return false;

View File

@@ -5,7 +5,7 @@ import {
useLlmManager,
} from "@/lib/hooks";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { parseLlmDescriptor } from "@/lib/llm/utils";
import { useState } from "react";
import { Hoverable } from "@/components/Hoverable";
@@ -19,7 +19,7 @@ export default function RegenerateOption({
overriddenModel,
onDropdownVisibleChange,
}: {
selectedAssistant: Persona;
selectedAssistant: MinimalPersonaSnapshot;
regenerate: (modelOverRide: LlmDescriptor) => Promise<void>;
overriddenModel?: string;
onDropdownVisibleChange: (isVisible: boolean) => void;

View File

@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { FiPlusCircle, FiPlus, FiX, FiFilter } from "react-icons/fi";
import { FiLoader } from "react-icons/fi";
import { ChatInputOption } from "./ChatInputOption";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import LLMPopover from "./LLMPopover";
import { InputPrompt } from "@/app/chat/interfaces";
@@ -29,7 +29,7 @@ import UnconfiguredProviderText from "@/components/chat/UnconfiguredProviderText
import { useAssistants } from "@/components/context/AssistantsContext";
import { CalendarIcon, TagIcon, XIcon, FolderIcon } from "lucide-react";
import { FilterPopup } from "@/components/search/filtering/FilterPopup";
import { DocumentSet, Tag } from "@/lib/types";
import { DocumentSetSummary, Tag } from "@/lib/types";
import { SourceIcon } from "@/components/SourceIcon";
import { getFormattedDateRangeString } from "@/lib/dateUtils";
import { truncateString } from "@/lib/utils";
@@ -182,17 +182,19 @@ interface ChatInputBarProps {
onSubmit: () => void;
llmManager: LlmManager;
chatState: ChatState;
alternativeAssistant: Persona | null;
alternativeAssistant: MinimalPersonaSnapshot | null;
// assistants
selectedAssistant: Persona;
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
selectedAssistant: MinimalPersonaSnapshot;
setAlternativeAssistant: (
alternativeAssistant: MinimalPersonaSnapshot | null
) => void;
toggleDocumentSidebar: () => void;
setFiles: (files: FileDescriptor[]) => void;
handleFileUpload: (files: File[], intent: UploadIntent) => void;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
filterManager: FilterManager;
availableSources: SourceMetadata[];
availableDocumentSets: DocumentSet[];
availableDocumentSets: DocumentSetSummary[];
availableTags: Tag[];
retrievalEnabled: boolean;
proSearchEnabled: boolean;
@@ -306,7 +308,7 @@ export function ChatInputBar({
};
}, []);
const updatedTaggedAssistant = (assistant: Persona) => {
const updatedTaggedAssistant = (assistant: MinimalPersonaSnapshot) => {
setAlternativeAssistant(
assistant.id == selectedAssistant.id ? null : assistant
);

View File

@@ -12,7 +12,7 @@ import {
} from "@/lib/llm/utils";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { getProviderIcon } from "@/app/admin/configuration/llm/utils";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { LlmManager } from "@/lib/hooks";
import {
@@ -32,7 +32,7 @@ interface LLMPopoverProps {
llmProviders: LLMProviderDescriptor[];
llmManager: LlmManager;
requiresImageGeneration?: boolean;
currentAssistant?: Persona;
currentAssistant?: MinimalPersonaSnapshot;
trigger?: React.ReactElement;
onSelect?: (value: string) => void;
currentModelName?: string;

View File

@@ -28,7 +28,7 @@ import {
AgenticMessageResponseIDInfo,
UserKnowledgeFilePacket,
} from "./interfaces";
import { Persona } from "../admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "../admin/assistants/interfaces";
import { ReadonlyURLSearchParams } from "next/navigation";
import { SEARCH_PARAM_NAMES } from "./searchParams";
import { Settings } from "../admin/settings/interfaces";
@@ -634,8 +634,8 @@ export function removeMessage(
export function checkAnyAssistantHasSearch(
messageHistory: Message[],
availableAssistants: Persona[],
livePersona: Persona
availableAssistants: MinimalPersonaSnapshot[],
livePersona: MinimalPersonaSnapshot
): boolean {
const response =
messageHistory.some((message) => {
@@ -656,19 +656,17 @@ export function checkAnyAssistantHasSearch(
return response;
}
export function personaIncludesRetrieval(selectedPersona: Persona) {
export function personaIncludesRetrieval(
selectedPersona: MinimalPersonaSnapshot
) {
return selectedPersona.tools.some(
(tool) =>
tool.in_code_tool_id &&
[SEARCH_TOOL_ID, INTERNET_SEARCH_TOOL_ID].includes(
tool.in_code_tool_id
) &&
selectedPersona.user_file_ids?.length === 0 &&
selectedPersona.user_folder_ids?.length === 0
[SEARCH_TOOL_ID, INTERNET_SEARCH_TOOL_ID].includes(tool.in_code_tool_id)
);
}
export function personaIncludesImage(selectedPersona: Persona) {
export function personaIncludesImage(selectedPersona: MinimalPersonaSnapshot) {
return selectedPersona.tools.some(
(tool) =>
tool.in_code_tool_id && tool.in_code_tool_id == IIMAGE_GENERATION_TOOL_ID

View File

@@ -27,7 +27,7 @@ import rehypePrism from "rehype-prism-plus";
import "prismjs/themes/prism-tomorrow.css";
import "./custom-code-styles.css";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { LikeFeedback, DislikeFeedback } from "@/components/icons/icons";
@@ -114,8 +114,8 @@ export const AgenticMessage = ({
selectedDocuments?: OnyxDocument[] | null;
toggleDocumentSelection?: (second: boolean) => void;
docs?: OnyxDocument[] | null;
alternativeAssistant?: Persona | null;
currentPersona: Persona;
alternativeAssistant?: MinimalPersonaSnapshot | null;
currentPersona: MinimalPersonaSnapshot;
messageId: number | null;
content: string | JSX.Element;
files?: FileDescriptor[];

View File

@@ -40,7 +40,7 @@ import { CodeBlock } from "./CodeBlock";
import rehypePrism from "rehype-prism-plus";
import "prismjs/themes/prism-tomorrow.css";
import "./custom-code-styles.css";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { LikeFeedback, DislikeFeedback } from "@/components/icons/icons";
import {
@@ -254,8 +254,8 @@ export const AIMessage = ({
selectedDocuments?: OnyxDocument[] | null;
toggleDocumentSelection?: () => void;
docs?: OnyxDocument[] | null;
alternativeAssistant?: Persona | null;
currentPersona: Persona;
alternativeAssistant?: MinimalPersonaSnapshot | null;
currentPersona: MinimalPersonaSnapshot;
messageId: number | null;
content: string | JSX.Element;
files?: FileDescriptor[];

View File

@@ -13,7 +13,7 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { getFinalLLM } from "@/lib/llm/utils";
import React, { useEffect, useState } from "react";
@@ -27,9 +27,9 @@ export function AssistantsTab({
llmProviders,
onSelect,
}: {
selectedAssistant: Persona;
selectedAssistant: MinimalPersonaSnapshot;
llmProviders: LLMProviderDescriptor[];
onSelect: (assistant: Persona) => void;
onSelect: (assistant: MinimalPersonaSnapshot) => void;
}) {
const { refreshUser } = useUser();
const [_, llmName] = getFinalLLM(llmProviders, null, null);

View File

@@ -6,7 +6,6 @@ import {
ArrowUp,
ArrowDown,
Trash,
Upload,
} from "lucide-react";
import { useDocumentsContext } from "../DocumentsContext";
import { useChatContext } from "@/components/context/ChatContext";

View File

@@ -34,7 +34,6 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip";
import { useRouter } from "next/navigation";
import { usePopup } from "@/components/admin/connectors/Popup";
import { getFormattedDateTime } from "@/lib/dateUtils";
import { FileUploadSection } from "../[id]/components/upload/FileUploadSection";

View File

@@ -2,7 +2,6 @@ import React from "react";
import { cn, truncateString } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { X, FolderIcon, Loader2 } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { FolderResponse, FileResponse } from "../DocumentsContext";
import { getFileIconFromFileNameAndLink } from "@/lib/assistantIconUtils";
import { MinimalOnyxDocument } from "@/lib/search/interfaces";

View File

@@ -27,7 +27,7 @@ import {
import { PagesTab } from "./PagesTab";
import { pageType } from "./types";
import LogoWithText from "@/components/header/LogoWithText";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { DragEndEvent } from "@dnd-kit/core";
import { useAssistants } from "@/components/context/AssistantsContext";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
@@ -56,7 +56,7 @@ import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { TruncatedText } from "@/components/ui/truncatedText";
interface HistorySidebarProps {
liveAssistant?: Persona | null;
liveAssistant?: MinimalPersonaSnapshot | null;
page: pageType;
existingChats?: ChatSession[];
currentChatSession?: ChatSession | null | undefined;
@@ -73,7 +73,7 @@ interface HistorySidebarProps {
}
interface SortableAssistantProps {
assistant: Persona;
assistant: MinimalPersonaSnapshot;
active: boolean;
onClick: () => void;
onPinAction: (e: React.MouseEvent) => void;
@@ -213,18 +213,22 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
const { active, over } = event;
if (active.id !== over?.id) {
setPinnedAssistants((prevAssistants: Persona[]) => {
setPinnedAssistants((prevAssistants: MinimalPersonaSnapshot[]) => {
const oldIndex = prevAssistants.findIndex(
(a: Persona) => (a.id === 0 ? "assistant-0" : a.id) === active.id
(a: MinimalPersonaSnapshot) =>
(a.id === 0 ? "assistant-0" : a.id) === active.id
);
const newIndex = prevAssistants.findIndex(
(a: Persona) => (a.id === 0 ? "assistant-0" : a.id) === over?.id
(a: MinimalPersonaSnapshot) =>
(a.id === 0 ? "assistant-0" : a.id) === over?.id
);
const newOrder = arrayMove(prevAssistants, oldIndex, newIndex);
// Ensure we're sending the correct IDs to the API
const reorderedIds = newOrder.map((a: Persona) => a.id);
const reorderedIds = newOrder.map(
(a: MinimalPersonaSnapshot) => a.id
);
reorderPinnedAssistants(reorderedIds);
return newOrder;
@@ -351,7 +355,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
strategy={verticalListSortingStrategy}
>
<div className="flex px-0 mr-4 flex-col gap-y-1 mt-1">
{pinnedAssistants.map((assistant: Persona) => (
{pinnedAssistants.map((assistant: MinimalPersonaSnapshot) => (
<SortableAssistant
key={assistant.id === 0 ? "assistant-0" : assistant.id}
assistant={assistant}

View File

@@ -15,14 +15,12 @@ import { useContext, useEffect, useState } from "react";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader";
import { Persona } from "@/app/admin/assistants/interfaces";
import { Button } from "@/components/ui/button";
import { MinimalOnyxDocument } from "@/lib/search/interfaces";
import TextView from "@/components/chat/TextView";
import { DocumentResults } from "../../documentSidebar/DocumentResults";
import { Modal } from "@/components/Modal";
import FunctionalHeader from "@/components/chat/Header";
import FixedLogo from "@/components/logo/FixedLogo";
import { useRouter } from "next/navigation";
import Link from "next/link";
function BackToOnyxButton({

View File

@@ -25,7 +25,6 @@ import { deleteStandardAnswer } from "./lib";
import { FilterDropdown } from "@/components/search/filtering/FilterDropdown";
import { FiTag } from "react-icons/fi";
import { PageSelector } from "@/components/PageSelector";
import { CustomCheckbox } from "@/components/CustomCheckbox";
import Text from "@/components/ui/text";
import { TableHeader } from "@/components/ui/table";
import CreateButton from "@/components/ui/createButton";

View File

@@ -1,3 +1,5 @@
"use client";
import React, { useState, useEffect } from "react";
import "./loading.css";
import { ThreeDots } from "react-loader-spinner";

View File

@@ -1,4 +1,4 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { useSortable } from "@dnd-kit/sortable";
import React from "react";
@@ -14,9 +14,9 @@ export const AssistantCard = ({
isSelected,
onSelect,
}: {
assistant: Persona;
assistant: MinimalPersonaSnapshot;
isSelected: boolean;
onSelect: (assistant: Persona) => void;
onSelect: (assistant: MinimalPersonaSnapshot) => void;
}) => {
const renderBadgeContent = (tool: { name: string }) => {
switch (tool.name) {
@@ -73,9 +73,9 @@ export const AssistantCard = ({
};
export function DraggableAssistantCard(props: {
assistant: Persona;
assistant: MinimalPersonaSnapshot;
isSelected: boolean;
onSelect: (assistant: Persona) => void;
onSelect: (assistant: MinimalPersonaSnapshot) => void;
llmName: string;
}) {
const {

View File

@@ -1,6 +1,9 @@
import React from "react";
import crypto from "crypto";
import { Persona } from "@/app/admin/assistants/interfaces";
import {
MinimalPersonaSnapshot,
Persona,
} from "@/app/admin/assistants/interfaces";
import { buildImgUrl } from "@/app/chat/files/images/utils";
import {
ArtAsistantIcon,
@@ -92,7 +95,7 @@ export function AssistantIcon({
className,
disableToolip,
}: {
assistant: Persona;
assistant: MinimalPersonaSnapshot | Persona;
size?: IconSize;
className?: string;
border?: boolean;

View File

@@ -1,12 +1,12 @@
import { useContext } from "react";
import { Persona } from "../../app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "../../app/admin/assistants/interfaces";
import { SettingsContext } from "../settings/SettingsProvider";
export function StarterMessages({
currentPersona,
onSubmit,
}: {
currentPersona: Persona;
currentPersona: MinimalPersonaSnapshot;
onSubmit: (messageOverride: string) => void;
}) {
const settings = useContext(SettingsContext);

View File

@@ -4,7 +4,7 @@ import { UserProvider } from "../user/UserProvider";
import { ProviderContextProvider } from "../chat/ProviderContext";
import { SettingsProvider } from "../settings/SettingsProvider";
import { AssistantsProvider } from "./AssistantsContext";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { User } from "@/lib/types";
import { ModalProvider } from "./ModalContext";
import { AuthTypeMetadata } from "@/lib/userSS";
@@ -13,7 +13,7 @@ interface AppProviderProps {
children: React.ReactNode;
user: User | null;
settings: CombinedSettings;
assistants: Persona[];
assistants: MinimalPersonaSnapshot[];
hasAnyConnectors: boolean;
hasImageCompatibleModel: boolean;
authTypeMetadata: AuthTypeMetadata;

View File

@@ -8,7 +8,7 @@ import React, {
SetStateAction,
Dispatch,
} from "react";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import {
classifyAssistants,
orderAssistantsForUser,
@@ -18,18 +18,18 @@ import {
import { useUser } from "../user/UserProvider";
interface AssistantsContextProps {
assistants: Persona[];
visibleAssistants: Persona[];
hiddenAssistants: Persona[];
finalAssistants: Persona[];
ownedButHiddenAssistants: Persona[];
assistants: MinimalPersonaSnapshot[];
visibleAssistants: MinimalPersonaSnapshot[];
hiddenAssistants: MinimalPersonaSnapshot[];
finalAssistants: MinimalPersonaSnapshot[];
ownedButHiddenAssistants: MinimalPersonaSnapshot[];
refreshAssistants: () => Promise<void>;
isImageGenerationAvailable: boolean;
// Admin only
editablePersonas: Persona[];
allAssistants: Persona[];
pinnedAssistants: Persona[];
setPinnedAssistants: Dispatch<SetStateAction<Persona[]>>;
editablePersonas: MinimalPersonaSnapshot[];
allAssistants: MinimalPersonaSnapshot[];
pinnedAssistants: MinimalPersonaSnapshot[];
setPinnedAssistants: Dispatch<SetStateAction<MinimalPersonaSnapshot[]>>;
}
const AssistantsContext = createContext<AssistantsContextProps | undefined>(
@@ -38,27 +38,36 @@ const AssistantsContext = createContext<AssistantsContextProps | undefined>(
export const AssistantsProvider: React.FC<{
children: React.ReactNode;
initialAssistants: Persona[];
hasAnyConnectors: boolean;
hasImageCompatibleModel: boolean;
initialAssistants: MinimalPersonaSnapshot[];
hasAnyConnectors?: boolean;
hasImageCompatibleModel?: boolean;
}> = ({
children,
initialAssistants,
hasAnyConnectors,
hasImageCompatibleModel,
}) => {
const [assistants, setAssistants] = useState<Persona[]>(
const [assistants, setAssistants] = useState<MinimalPersonaSnapshot[]>(
initialAssistants || []
);
const { user, isAdmin, isCurator } = useUser();
const [editablePersonas, setEditablePersonas] = useState<Persona[]>([]);
const [allAssistants, setAllAssistants] = useState<Persona[]>([]);
const [editablePersonas, setEditablePersonas] = useState<
MinimalPersonaSnapshot[]
>([]);
const [allAssistants, setAllAssistants] = useState<MinimalPersonaSnapshot[]>(
[]
);
const [pinnedAssistants, setPinnedAssistants] = useState<Persona[]>(() => {
const [pinnedAssistants, setPinnedAssistants] = useState<
MinimalPersonaSnapshot[]
>(() => {
if (user?.preferences.pinned_assistants) {
return user.preferences.pinned_assistants
.map((id) => assistants.find((assistant) => assistant.id === id))
.filter((assistant): assistant is Persona => assistant !== undefined);
.filter(
(assistant): assistant is MinimalPersonaSnapshot =>
assistant !== undefined
);
} else {
return assistants.filter((a) => a.is_default_persona);
}
@@ -69,7 +78,10 @@ export const AssistantsProvider: React.FC<{
if (user?.preferences.pinned_assistants) {
return user.preferences.pinned_assistants
.map((id) => assistants.find((assistant) => assistant.id === id))
.filter((assistant): assistant is Persona => assistant !== undefined);
.filter(
(assistant): assistant is MinimalPersonaSnapshot =>
assistant !== undefined
);
} else {
return assistants.filter((a) => a.is_default_persona);
}
@@ -135,13 +147,9 @@ export const AssistantsProvider: React.FC<{
},
});
if (!response.ok) throw new Error("Failed to fetch assistants");
let assistants: Persona[] = await response.json();
let assistants: MinimalPersonaSnapshot[] = await response.json();
let filteredAssistants = filterAssistants(
assistants,
hasAnyConnectors,
hasImageCompatibleModel
);
let filteredAssistants = filterAssistants(assistants);
setAssistants(filteredAssistants);

View File

@@ -1,7 +1,12 @@
"use client";
import React, { createContext, useContext, useState } from "react";
import { CCPairBasicInfo, DocumentSet, Tag, ValidSources } from "@/lib/types";
import {
CCPairBasicInfo,
DocumentSetSummary,
Tag,
ValidSources,
} from "@/lib/types";
import { ChatSession, InputPrompt } from "@/app/chat/interfaces";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { Folder } from "@/app/chat/folders/interfaces";
@@ -14,8 +19,8 @@ interface ChatContextProps {
availableSources: ValidSources[];
ccPairs: CCPairBasicInfo[];
tags: Tag[];
documentSets: DocumentSet[];
availableDocumentSets: DocumentSet[];
documentSets: DocumentSetSummary[];
availableDocumentSets: DocumentSetSummary[];
availableTags: Tag[];
llmProviders: LLMProviderDescriptor[];
folders: Folder[];

View File

@@ -1,7 +1,7 @@
"use client";
import React, { createContext, useContext } from "react";
import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
import { CCPairBasicInfo, DocumentSetSummary, Tag } from "@/lib/types";
import { Persona } from "@/app/admin/assistants/interfaces";
import { ChatSession } from "@/app/chat/interfaces";
@@ -9,7 +9,7 @@ interface SearchContextProps {
querySessions: ChatSession[];
ccPairs: CCPairBasicInfo[];
tags: Tag[];
documentSets: DocumentSet[];
documentSets: DocumentSetSummary[];
assistants: Persona[];
agenticSearchEnabled: boolean;
disabledAgentic: boolean;

View File

@@ -1,6 +1,6 @@
"use client";
import { DocumentSet, ValidSources } from "@/lib/types";
import { DocumentSetSummary, ValidSources } from "@/lib/types";
import { CustomCheckbox } from "../CustomCheckbox";
import { SourceIcon } from "../SourceIcon";
import {
@@ -17,7 +17,7 @@ export function DocumentSetSelectable({
disabled,
disabledTooltip,
}: {
documentSet: DocumentSet;
documentSet: DocumentSetSummary;
isSelected: boolean;
onSelect: () => void;
disabled?: boolean;
@@ -25,8 +25,8 @@ export function DocumentSetSelectable({
}) {
// Collect unique connector sources
const uniqueSources = new Set<ValidSources>();
documentSet.cc_pair_descriptors.forEach((ccPairDescriptor) => {
uniqueSources.add(ccPairDescriptor.connector.source);
documentSet.cc_pair_summaries.forEach((ccPairSummary) => {
uniqueSources.add(ccPairSummary.source);
});
return (

View File

@@ -1,5 +1,5 @@
import React from "react";
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
import { DocumentSetSummary, Tag, ValidSources } from "@/lib/types";
import { SourceMetadata } from "@/lib/search/interfaces";
import { InfoIcon, defaultTailwindCSS } from "@/components/icons/icons";
import { HoverPopup } from "@/components/HoverPopup";
@@ -40,7 +40,7 @@ export interface SourceSelectorProps {
setSelectedDocumentSets: React.Dispatch<React.SetStateAction<string[]>>;
selectedTags: Tag[];
setSelectedTags: React.Dispatch<React.SetStateAction<Tag[]>>;
availableDocumentSets: DocumentSet[];
availableDocumentSets: DocumentSetSummary[];
existingSources: ValidSources[];
availableTags: Tag[];
toggleFilters: () => void;

View File

@@ -9,9 +9,9 @@ interface FullSearchBarProps {
agentic?: boolean;
toggleAgentic?: () => void;
ccPairs: CCPairBasicInfo[];
documentSets: DocumentSet[];
documentSets: DocumentSetSummary[];
filterManager: any; // You might want to replace 'any' with a more specific type
finalAvailableDocumentSets: DocumentSet[];
finalAvailableDocumentSets: DocumentSetSummary[];
finalAvailableSources: string[];
tags: Tag[];
showingSidebar: boolean;
@@ -27,7 +27,7 @@ import { useRef } from "react";
import { SendIcon } from "../icons/icons";
import { Separator } from "@/components/ui/separator";
import KeyboardSymbol from "@/lib/browserUtilities";
import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types";
import { CCPairBasicInfo, DocumentSetSummary, Tag } from "@/lib/types";
import { HorizontalSourceSelector } from "./filtering/HorizontalSourceSelector";
export const AnimatedToggle = ({

View File

@@ -13,7 +13,7 @@ import {
FiBook,
} from "react-icons/fi";
import { FilterManager } from "@/lib/hooks";
import { DocumentSet, Tag } from "@/lib/types";
import { DocumentSetSummary, Tag } from "@/lib/types";
import { SourceMetadata } from "@/lib/search/interfaces";
import { Checkbox } from "@/components/ui/checkbox";
import { Separator } from "@/components/ui/separator";
@@ -26,7 +26,7 @@ interface FilterPopupProps {
filterManager: FilterManager;
trigger: React.ReactNode;
availableSources: SourceMetadata[];
availableDocumentSets: DocumentSet[];
availableDocumentSets: DocumentSetSummary[];
availableTags: Tag[];
}
@@ -50,7 +50,7 @@ export function FilterPopup({
const [currentDate, setCurrentDate] = useState(new Date());
const [documentSetSearch, setDocumentSetSearch] = useState("");
const [filteredDocumentSets, setFilteredDocumentSets] = useState<
DocumentSet[]
DocumentSetSummary[]
>(availableDocumentSets);
useEffect(() => {
@@ -238,10 +238,10 @@ export function FilterPopup({
}
};
const isDocumentSetSelected = (docSet: DocumentSet) =>
const isDocumentSetSelected = (docSet: DocumentSetSummary) =>
filterManager.selectedDocumentSets.includes(docSet.name);
const toggleDocumentSet = (docSet: DocumentSet) => {
const toggleDocumentSet = (docSet: DocumentSetSummary) => {
filterManager.setSelectedDocumentSets((prev) =>
prev.includes(docSet.name)
? prev.filter((id) => id !== docSet.name)

View File

@@ -1,14 +1,20 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import {
MinimalPersonaSnapshot,
Persona,
} from "@/app/admin/assistants/interfaces";
import { User } from "../types";
import { checkUserIsNoAuthUser } from "../user";
export function checkUserOwnsAssistant(user: User | null, assistant: Persona) {
export function checkUserOwnsAssistant(
user: User | null,
assistant: MinimalPersonaSnapshot | Persona
) {
return checkUserIdOwnsAssistant(user?.id, assistant);
}
export function checkUserIdOwnsAssistant(
userId: string | undefined,
assistant: Persona
assistant: MinimalPersonaSnapshot | Persona
) {
return (
(!userId ||

View File

@@ -1,12 +1,12 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { fetchSS } from "../utilsSS";
export type FetchAssistantsResponse = [Persona[], string | null];
export type FetchAssistantsResponse = [MinimalPersonaSnapshot[], string | null];
export async function fetchAssistantsSS(): Promise<FetchAssistantsResponse> {
const response = await fetchSS("/persona");
if (response.ok) {
return [(await response.json()) as Persona[], null];
return [(await response.json()) as MinimalPersonaSnapshot[], null];
}
return [[], (await response.json()).detail || "Unknown Error"];
}

View File

@@ -1,5 +1,5 @@
import { FullPersona } from "@/app/admin/assistants/interfaces";
import { CCPairBasicInfo, DocumentSet, User } from "../types";
import { CCPairBasicInfo, DocumentSetSummary, User } from "../types";
import { getCurrentUserSS } from "../userSS";
import { fetchSS } from "../utilsSS";
import { LLMProviderView } from "@/app/admin/configuration/llm/interfaces";
@@ -12,7 +12,7 @@ export async function fetchAssistantEditorInfoSS(
| [
{
ccPairs: CCPairBasicInfo[];
documentSets: DocumentSet[];
documentSets: DocumentSetSummary[];
llmProviders: LLMProviderView[];
user: User | null;
existingPersona: FullPersona | null;
@@ -67,7 +67,8 @@ export async function fetchAssistantEditorInfoSS(
`Failed to fetch document sets - ${await documentSetsResponse.text()}`,
];
}
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
const documentSets =
(await documentSetsResponse.json()) as DocumentSetSummary[];
if (!toolsResponse) {
return [null, `Failed to fetch tools`];

View File

@@ -1,15 +1,18 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { User } from "../types";
import { checkUserIsNoAuthUser } from "../user";
import { personaComparator } from "@/app/admin/assistants/lib";
export function checkUserOwnsAssistant(user: User | null, assistant: Persona) {
export function checkUserOwnsAssistant(
user: User | null,
assistant: MinimalPersonaSnapshot
) {
return checkUserIdOwnsAssistant(user?.id, assistant);
}
export function checkUserIdOwnsAssistant(
userId: string | undefined,
assistant: Persona
assistant: MinimalPersonaSnapshot
) {
return (
(!userId ||
@@ -19,7 +22,10 @@ export function checkUserIdOwnsAssistant(
);
}
export function classifyAssistants(user: User | null, assistants: Persona[]) {
export function classifyAssistants(
user: User | null,
assistants: MinimalPersonaSnapshot[]
) {
if (!user) {
return {
visibleAssistants: assistants.filter(
@@ -59,7 +65,7 @@ export function classifyAssistants(user: User | null, assistants: Persona[]) {
}
export function orderAssistantsForUser(
assistants: Persona[],
assistants: MinimalPersonaSnapshot[],
user: User | null
) {
let orderedAssistants = [...assistants];
@@ -112,7 +118,7 @@ export function orderAssistantsForUser(
export function getUserCreatedAssistants(
user: User | null,
assistants: Persona[]
assistants: MinimalPersonaSnapshot[]
) {
return assistants.filter((assistant) =>
checkUserOwnsAssistant(user, assistant)
@@ -121,29 +127,10 @@ export function getUserCreatedAssistants(
// Filter assistants based on connector status, image compatibility and visibility
export function filterAssistants(
assistants: Persona[],
hasAnyConnectors: boolean,
hasImageCompatibleModel: boolean
): Persona[] {
assistants: MinimalPersonaSnapshot[]
): MinimalPersonaSnapshot[] {
let filteredAssistants = assistants.filter(
(assistant) => assistant.is_visible
);
if (!hasAnyConnectors) {
filteredAssistants = filteredAssistants.filter(
(assistant) =>
assistant.num_chunks === 0 || assistant.document_sets.length > 0
);
}
if (!hasImageCompatibleModel) {
filteredAssistants = filteredAssistants.filter(
(assistant) =>
!assistant.tools.some(
(tool) => tool.in_code_tool_id === "ImageGenerationTool"
)
);
}
return filteredAssistants.sort(personaComparator);
}

View File

@@ -1,12 +1,12 @@
import { fetchSS } from "@/lib/utilsSS";
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
import { fetchAssistantsSS } from "../assistants/fetchAssistantsSS";
import { modelSupportsImageInput } from "../llm/utils";
import { filterAssistants } from "../assistants/utils";
interface AssistantData {
assistants: Persona[];
assistants: MinimalPersonaSnapshot[];
hasAnyConnectors: boolean;
hasImageCompatibleModel: boolean;
}
@@ -51,11 +51,7 @@ export async function fetchAssistantData(): Promise<AssistantData> {
)
);
let filteredAssistants = filterAssistants(
assistants,
hasAnyConnectors,
hasImageCompatibleModel
);
let filteredAssistants = filterAssistants(assistants);
return {
assistants: filteredAssistants,

View File

@@ -6,7 +6,7 @@ import {
import { fetchSS } from "@/lib/utilsSS";
import {
CCPairBasicInfo,
DocumentSet,
DocumentSetSummary,
Tag,
User,
ValidSources,
@@ -35,7 +35,7 @@ interface FetchChatDataResult {
chatSessions: ChatSession[];
ccPairs: CCPairBasicInfo[];
availableSources: ValidSources[];
documentSets: DocumentSet[];
documentSets: DocumentSetSummary[];
tags: Tag[];
llmProviders: LLMProviderDescriptor[];
folders: Folder[];
@@ -167,7 +167,7 @@ export async function fetchChatData(searchParams: {
new Date(b.time_updated).getTime() - new Date(a.time_updated).getTime()
);
let documentSets: DocumentSet[] = [];
let documentSets: DocumentSetSummary[] = [];
if (documentSetsResponse?.ok) {
documentSets = await documentSetsResponse.json();
} else {

View File

@@ -6,7 +6,7 @@ import {
import { fetchSS } from "@/lib/utilsSS";
import {
CCPairBasicInfo,
DocumentSet,
DocumentSetSummary,
Tag,
User,
ValidSources,
@@ -31,7 +31,7 @@ interface FetchChatDataResult {
chatSessions?: ChatSession[];
ccPairs?: CCPairBasicInfo[];
availableSources?: ValidSources[];
documentSets?: DocumentSet[];
documentSets?: DocumentSetSummary[];
assistants?: Persona[];
tags?: Tag[];
llmProviders?: LLMProviderDescriptor[];
@@ -123,7 +123,7 @@ export async function fetchSomeChatData(
break;
case "documentSets":
result.documentSets = result?.ok
? ((await result.json()) as DocumentSet[])
? ((await result.json()) as DocumentSetSummary[])
: [];
break;
case "assistants":

View File

@@ -1,5 +1,5 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { DocumentSet, ValidSources } from "./types";
import { DocumentSetSummary, ValidSources } from "./types";
import { getSourcesForPersona } from "./sources";
export function computeAvailableFilters({
@@ -9,8 +9,8 @@ export function computeAvailableFilters({
}: {
selectedPersona: Persona | undefined | null;
availableSources: ValidSources[];
availableDocumentSets: DocumentSet[];
}): [ValidSources[], DocumentSet[]] {
availableDocumentSets: DocumentSetSummary[];
}): [ValidSources[], DocumentSetSummary[]] {
const finalAvailableSources =
selectedPersona && selectedPersona.document_sets.length
? getSourcesForPersona(selectedPersona)

View File

@@ -22,7 +22,10 @@ import { ChatSession } from "@/app/chat/interfaces";
import { AllUsersResponse } from "./types";
import { Credential } from "./connectors/credentials";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { Persona, PersonaLabel } from "@/app/admin/assistants/interfaces";
import {
MinimalPersonaSnapshot,
PersonaLabel,
} from "@/app/admin/assistants/interfaces";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { isAnthropic } from "@/app/admin/configuration/llm/utils";
import { getSourceMetadata } from "./sources";
@@ -371,7 +374,7 @@ export interface LlmManager {
updateModelOverrideBasedOnChatSession: (chatSession?: ChatSession) => void;
imageFilesPresent: boolean;
updateImageFilesPresent: (present: boolean) => void;
liveAssistant: Persona | null;
liveAssistant: MinimalPersonaSnapshot | null;
maxTemperature: number;
}
@@ -419,7 +422,7 @@ providing appropriate defaults for new conversations based on the available tool
export function useLlmManager(
llmProviders: LLMProviderDescriptor[],
currentChatSession?: ChatSession,
liveAssistant?: Persona
liveAssistant?: MinimalPersonaSnapshot
): LlmManager {
const { user } = useUser();

View File

@@ -1,4 +1,4 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import {
LLMProviderDescriptor,
ModelConfiguration,
@@ -7,7 +7,7 @@ import { LlmDescriptor } from "@/lib/hooks";
export function getFinalLLM(
llmProviders: LLMProviderDescriptor[],
persona: Persona | null,
persona: MinimalPersonaSnapshot | null,
currentLlm: LlmDescriptor | null
): [string, string] {
const defaultProvider = llmProviders.find(
@@ -38,7 +38,7 @@ export function getFinalLLM(
}
export function getLLMProviderOverrideForPersona(
liveAssistant: Persona,
liveAssistant: MinimalPersonaSnapshot,
llmProviders: LLMProviderDescriptor[]
): LlmDescriptor | null {
const overrideProvider = liveAssistant.llm_model_provider_override;

View File

@@ -1,4 +1,4 @@
import { DocumentSet } from "../types";
import { DocumentSetSummary } from "../types";
import { fetchSS } from "../utilsSS";
import { Connector } from "../connectors/connectors";
@@ -17,9 +17,9 @@ export async function fetchValidFilterInfo() {
);
}
let documentSets = [] as DocumentSet[];
let documentSets = [] as DocumentSetSummary[];
if (documentSetResponse.ok) {
documentSets = (await documentSetResponse.json()) as DocumentSet[];
documentSets = (await documentSetResponse.json()) as DocumentSetSummary[];
} else {
console.log(
`Failed to fetch document sets - ${documentSetResponse.status} - ${documentSetResponse.statusText}`

View File

@@ -402,9 +402,9 @@ export function getSourceMetadataForSources(sources: ValidSources[]) {
export function getSourcesForPersona(persona: Persona): ValidSources[] {
const personaSources: ValidSources[] = [];
persona.document_sets.forEach((documentSet) => {
documentSet.cc_pair_descriptors.forEach((ccPair) => {
if (!personaSources.includes(ccPair.connector.source)) {
personaSources.push(ccPair.connector.source);
documentSet.cc_pair_summaries.forEach((ccPair) => {
if (!personaSources.includes(ccPair.source)) {
personaSources.push(ccPair.source);
}
});
});

View File

@@ -243,11 +243,26 @@ export interface CCPairDescriptor<ConnectorType, CredentialType> {
access_type: AccessType;
}
export interface DocumentSet {
// Simplified interfaces with minimal data
export interface CCPairSummary {
id: number;
name: string | null;
source: ValidSources;
access_type: AccessType;
}
export interface FederatedConnectorSummary {
id: number;
name: string;
source: string;
entities: Record<string, any>;
}
export interface DocumentSetSummary {
id: number;
name: string;
description: string;
cc_pair_descriptors: CCPairDescriptor<any, any>[];
cc_pair_summaries: CCPairSummary[];
is_up_to_date: boolean;
is_public: boolean;
users: string[];
@@ -339,7 +354,7 @@ export interface UserGroup {
users: User[];
curator_ids: string[];
cc_pairs: CCPairDescriptor<any, any>[];
document_sets: DocumentSet[];
document_sets: DocumentSetSummary[];
personas: Persona[];
is_up_to_date: boolean;
is_up_for_deletion: boolean;