Compare commits

..

5 Commits

Author SHA1 Message Date
pablodanswer
45d1eba580 update 2025-02-04 20:20:57 -08:00
pablodanswer
3193f4720d finalize 2025-02-04 20:20:48 -08:00
pablodanswer
74a497bbd0 improvement 2025-02-04 19:02:52 -08:00
pablodanswer
d871376144 various minor improvements 2025-02-04 17:58:52 -08:00
pablodanswer
828a2aef13 pop 2025-02-04 17:39:18 -08:00
110 changed files with 847 additions and 1214 deletions

View File

@@ -67,6 +67,7 @@ jobs:
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
NEXT_PUBLIC_GTM_ENABLED=true
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=true
NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK=true
NODE_OPTIONS=--max-old-space-size=8192
# needed due to weird interactions with the builds for different platforms
no-cache: true

View File

@@ -1,76 +0,0 @@
"""add default slack channel config
Revision ID: eaa3b5593925
Revises: 98a5008d8711
Create Date: 2025-02-03 18:07:56.552526
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "eaa3b5593925"
down_revision = "98a5008d8711"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Add is_default column
op.add_column(
"slack_channel_config",
sa.Column("is_default", sa.Boolean(), nullable=False, server_default="false"),
)
op.create_index(
"ix_slack_channel_config_slack_bot_id_default",
"slack_channel_config",
["slack_bot_id", "is_default"],
unique=True,
postgresql_where=sa.text("is_default IS TRUE"),
)
# Create default channel configs for existing slack bots without one
conn = op.get_bind()
slack_bots = conn.execute(sa.text("SELECT id FROM slack_bot")).fetchall()
for slack_bot in slack_bots:
slack_bot_id = slack_bot[0]
existing_default = conn.execute(
sa.text(
"SELECT id FROM slack_channel_config WHERE slack_bot_id = :bot_id AND is_default = TRUE"
),
{"bot_id": slack_bot_id},
).fetchone()
if not existing_default:
conn.execute(
sa.text(
"""
INSERT INTO slack_channel_config (
slack_bot_id, persona_id, channel_config, enable_auto_filters, is_default
) VALUES (
:bot_id, NULL,
'{"channel_name": null, "respond_member_group_list": [], "answer_filters": [], "follow_up_tags": []}',
FALSE, TRUE
)
"""
),
{"bot_id": slack_bot_id},
)
def downgrade() -> None:
# Delete default slack channel configs
conn = op.get_bind()
conn.execute(sa.text("DELETE FROM slack_channel_config WHERE is_default = TRUE"))
# Remove index
op.drop_index(
"ix_slack_channel_config_slack_bot_id_default",
table_name="slack_channel_config",
)
# Remove is_default column
op.drop_column("slack_channel_config", "is_default")

View File

@@ -80,7 +80,7 @@ def oneoff_standard_answers(
def _handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_channel_config: SlackChannelConfig,
slack_channel_config: SlackChannelConfig | None,
prompt: Prompt | None,
logger: OnyxLoggingAdapter,
client: WebClient,
@@ -94,10 +94,13 @@ def _handle_standard_answers(
Returns True if standard answers are found to match the user's message and therefore,
we still need to respond to the users.
"""
# if no channel config, then no standard answers are configured
if not slack_channel_config:
return False
slack_thread_id = message_info.thread_to_respond
configured_standard_answer_categories = (
slack_channel_config.standard_answer_categories
slack_channel_config.standard_answer_categories if slack_channel_config else []
)
configured_standard_answers = set(
[

View File

@@ -10,7 +10,6 @@ from fastapi import Response
from ee.onyx.auth.users import decode_anonymous_user_jwt_token
from ee.onyx.configs.app_configs import ANONYMOUS_USER_COOKIE_NAME
from onyx.auth.api_key import extract_tenant_from_api_key_header
from onyx.configs.constants import TENANT_ID_COOKIE_NAME
from onyx.db.engine import is_valid_schema_name
from onyx.redis.redis_pool import retrieve_auth_token_data_from_redis
from shared_configs.configs import MULTI_TENANT
@@ -44,7 +43,6 @@ async def _get_tenant_id_from_request(
Attempt to extract tenant_id from:
1) The API key header
2) The Redis-based token (stored in Cookie: fastapiusersauth)
3) Reset token cookie
Fallback: POSTGRES_DEFAULT_SCHEMA
"""
# Check for API key
@@ -92,12 +90,3 @@ async def _get_tenant_id_from_request(
except Exception as e:
logger.error(f"Unexpected error in _get_tenant_id_from_request: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
finally:
# As a final step, check for explicit tenant_id cookie
tenant_id_cookie = request.cookies.get(TENANT_ID_COOKIE_NAME)
if tenant_id_cookie and is_valid_schema_name(tenant_id_cookie):
return tenant_id_cookie
# If we've reached this point, return the default schema
return POSTGRES_DEFAULT_SCHEMA

View File

@@ -34,7 +34,6 @@ from onyx.auth.users import get_redis_strategy
from onyx.auth.users import optional_user
from onyx.auth.users import User
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.configs.constants import FASTAPI_USERS_AUTH_COOKIE_NAME
from onyx.db.auth import get_user_count
from onyx.db.engine import get_current_tenant_id
from onyx.db.engine import get_session
@@ -112,7 +111,7 @@ async def login_as_anonymous_user(
token = generate_anonymous_user_jwt_token(tenant_id)
response = Response()
response.delete_cookie(FASTAPI_USERS_AUTH_COOKIE_NAME)
response.delete_cookie("fastapiusersauth")
response.set_cookie(
key=ANONYMOUS_USER_COOKIE_NAME,
value=token,

View File

@@ -7,7 +7,6 @@ from langgraph.types import StreamWriter
from onyx.agents.agent_search.shared_graph_utils.utils import write_custom_event
from onyx.chat.models import LlmDoc
from onyx.chat.models import OnyxContext
from onyx.chat.stream_processing.answer_response_handler import AnswerResponseHandler
from onyx.chat.stream_processing.answer_response_handler import CitationResponseHandler
from onyx.chat.stream_processing.answer_response_handler import (
@@ -24,7 +23,7 @@ def process_llm_stream(
should_stream_answer: bool,
writer: StreamWriter,
final_search_results: list[LlmDoc] | None = None,
displayed_search_results: list[OnyxContext] | list[LlmDoc] | None = None,
displayed_search_results: list[LlmDoc] | None = None,
) -> AIMessageChunk:
tool_call_chunk = AIMessageChunk(content="")

View File

@@ -9,7 +9,6 @@ from onyx.agents.agent_search.basic.states import BasicState
from onyx.agents.agent_search.basic.utils import process_llm_stream
from onyx.agents.agent_search.models import GraphConfig
from onyx.chat.models import LlmDoc
from onyx.chat.models import OnyxContexts
from onyx.tools.tool_implementations.search.search_tool import (
SEARCH_DOC_CONTENT_ID,
)
@@ -51,11 +50,13 @@ def basic_use_tool_response(
if yield_item.id == FINAL_CONTEXT_DOCUMENTS_ID:
final_search_results = cast(list[LlmDoc], yield_item.response)
elif yield_item.id == SEARCH_DOC_CONTENT_ID:
search_contexts = cast(OnyxContexts, yield_item.response).contexts
search_contexts = yield_item.response.contexts
for doc in search_contexts:
if doc.document_id not in initial_search_results:
initial_search_results.append(doc)
initial_search_results = cast(list[LlmDoc], initial_search_results)
new_tool_call_chunk = AIMessageChunk(content="")
if not agent_config.behavior.skip_gen_ai_answer_generation:
stream = llm.stream(
@@ -69,9 +70,7 @@ def basic_use_tool_response(
True,
writer,
final_search_results=final_search_results,
# when the search tool is called with specific doc ids, initial search
# results are not output. But, we still want i.e. citations to be processed.
displayed_search_results=initial_search_results or final_search_results,
displayed_search_results=initial_search_results,
)
return BasicOutput(tool_call_chunk=new_tool_call_chunk)

View File

@@ -10,7 +10,6 @@ from onyx.configs.app_configs import SMTP_PORT
from onyx.configs.app_configs import SMTP_SERVER
from onyx.configs.app_configs import SMTP_USER
from onyx.configs.app_configs import WEB_DOMAIN
from onyx.configs.constants import TENANT_ID_COOKIE_NAME
from onyx.db.models import User
@@ -66,13 +65,9 @@ def send_forgot_password_email(
user_email: str,
token: str,
mail_from: str = EMAIL_FROM,
tenant_id: str | None = None,
) -> None:
subject = "Onyx Forgot Password"
link = f"{WEB_DOMAIN}/auth/reset-password?token={token}"
if tenant_id:
link += f"&{TENANT_ID_COOKIE_NAME}={tenant_id}"
# Keep search param same name as cookie for simplicity
body = f"Click the following link to reset your password: {link}"
send_email(user_email, subject, body, mail_from)

View File

@@ -73,7 +73,6 @@ from onyx.configs.app_configs import WEB_DOMAIN
from onyx.configs.constants import AuthType
from onyx.configs.constants import DANSWER_API_KEY_DUMMY_EMAIL_DOMAIN
from onyx.configs.constants import DANSWER_API_KEY_PREFIX
from onyx.configs.constants import FASTAPI_USERS_AUTH_COOKIE_NAME
from onyx.configs.constants import MilestoneRecordType
from onyx.configs.constants import OnyxRedisLocks
from onyx.configs.constants import PASSWORD_SPECIAL_CHARS
@@ -219,24 +218,6 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
verification_token_lifetime_seconds = AUTH_COOKIE_EXPIRE_TIME_SECONDS
user_db: SQLAlchemyUserDatabase[User, uuid.UUID]
async def get_by_email(self, user_email: str) -> User:
tenant_id = fetch_ee_implementation_or_noop(
"onyx.server.tenants.user_mapping", "get_tenant_id_for_email", None
)(user_email)
async with get_async_session_with_tenant(tenant_id) as db_session:
if MULTI_TENANT:
tenant_user_db = SQLAlchemyUserAdminDB[User, uuid.UUID](
db_session, User, OAuthAccount
)
user = await tenant_user_db.get_by_email(user_email)
else:
user = await self.user_db.get_by_email(user_email)
if not user:
raise exceptions.UserNotExists()
return user
async def create(
self,
user_create: schemas.UC | UserCreate,
@@ -523,15 +504,9 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
"Your admin has not enabled this feature.",
"Your admin has not enbaled this feature.",
)
tenant_id = await fetch_ee_implementation_or_noop(
"onyx.server.tenants.provisioning",
"get_or_provision_tenant",
async_return_default_schema,
)(email=user.email)
send_forgot_password_email(user.email, token, tenant_id=tenant_id)
send_forgot_password_email(user.email, token)
async def on_after_request_verify(
self, user: User, token: str, request: Optional[Request] = None
@@ -605,7 +580,6 @@ async def get_user_manager(
cookie_transport = CookieTransport(
cookie_max_age=SESSION_EXPIRE_TIME_SECONDS,
cookie_secure=WEB_DOMAIN.startswith("https"),
cookie_name=FASTAPI_USERS_AUTH_COOKIE_NAME,
)

View File

@@ -659,8 +659,7 @@ def validate_permission_sync_fence(
f"tasks_scanned={tasks_scanned} tasks_not_in_celery={tasks_not_in_celery}"
)
# we're only active if tasks_scanned > 0 and tasks_not_in_celery == 0
if tasks_scanned > 0 and tasks_not_in_celery == 0:
if tasks_not_in_celery == 0:
redis_connector.permissions.set_active()
return

View File

@@ -728,10 +728,6 @@ def cloud_check_alembic() -> bool | None:
TODO: have the cloud migration script set an activity signal that this check
uses to know it doesn't make sense to run a check at the present time.
"""
# Used as a placeholder if the alembic revision cannot be retrieved
ALEMBIC_NULL_REVISION = "000000000000"
time_start = time.monotonic()
redis_client = get_redis_client(tenant_id=ONYX_CLOUD_TENANT_ID)
@@ -747,14 +743,14 @@ def cloud_check_alembic() -> bool | None:
last_lock_time = time.monotonic()
tenant_to_revision: dict[str, str] = {}
tenant_to_revision: dict[str, str | None] = {}
revision_counts: dict[str, int] = {}
out_of_date_tenants: dict[str, str] = {}
out_of_date_tenants: dict[str, str | None] = {}
top_revision: str = ""
tenant_ids: list[str] | list[None] = []
try:
# map tenant_id to revision (or ALEMBIC_NULL_REVISION if the query fails)
# map each tenant_id to its revision
tenant_ids = get_all_tenant_ids()
for tenant_id in tenant_ids:
current_time = time.monotonic()
@@ -766,28 +762,19 @@ def cloud_check_alembic() -> bool | None:
continue
with get_session_with_tenant(tenant_id=None) as session:
try:
result = session.execute(
text(f'SELECT * FROM "{tenant_id}".alembic_version LIMIT 1')
)
result = session.execute(
text(f'SELECT * FROM "{tenant_id}".alembic_version LIMIT 1')
)
result_scalar: str | None = result.scalar_one_or_none()
if result_scalar is None:
raise ValueError("Alembic version should not be None.")
tenant_to_revision[tenant_id] = result_scalar
except Exception:
task_logger.warning(f"Tenant {tenant_id} has no revision!")
tenant_to_revision[tenant_id] = ALEMBIC_NULL_REVISION
result_scalar: str | None = result.scalar_one_or_none()
tenant_to_revision[tenant_id] = result_scalar
# get the total count of each revision
for k, v in tenant_to_revision.items():
revision_counts[v] = revision_counts.get(v, 0) + 1
if v is None:
continue
# error if any null revision tenants are found
if ALEMBIC_NULL_REVISION in revision_counts:
num_null_revisions = revision_counts[ALEMBIC_NULL_REVISION]
raise ValueError(f"No revision was found for {num_null_revisions} tenants!")
revision_counts[v] = revision_counts.get(v, 0) + 1
# get the revision with the most counts
sorted_revision_counts = sorted(
@@ -795,24 +782,23 @@ def cloud_check_alembic() -> bool | None:
)
if len(sorted_revision_counts) == 0:
raise ValueError(
task_logger.error(
f"cloud_check_alembic - No revisions found for {len(tenant_ids)} tenant ids!"
)
else:
top_revision, _ = sorted_revision_counts[0]
top_revision, _ = sorted_revision_counts[0]
# build a list of out of date tenants
for k, v in tenant_to_revision.items():
if v == top_revision:
continue
# build a list of out of date tenants
for k, v in tenant_to_revision.items():
if v == top_revision:
continue
out_of_date_tenants[k] = v
out_of_date_tenants[k] = v
except SoftTimeLimitExceeded:
task_logger.info(
"Soft time limit exceeded, task is being terminated gracefully."
)
raise
except Exception:
task_logger.exception("Unexpected exception during cloud alembic check")
raise
@@ -830,11 +816,6 @@ def cloud_check_alembic() -> bool | None:
f"num_tenants={len(tenant_ids)} "
f"revision={top_revision}"
)
num_to_log = min(5, len(out_of_date_tenants))
task_logger.info(
f"Logging {num_to_log}/{len(out_of_date_tenants)} out of date tenants."
)
for k, v in islice(out_of_date_tenants.items(), 5):
task_logger.info(f"Out of date tenant: tenant={k} revision={v}")
else:

View File

@@ -11,7 +11,6 @@ from onyx.background.indexing.checkpointing import get_time_windows_for_index_at
from onyx.background.indexing.tracer import OnyxTracer
from onyx.configs.app_configs import INDEXING_SIZE_WARNING_THRESHOLD
from onyx.configs.app_configs import INDEXING_TRACER_INTERVAL
from onyx.configs.app_configs import LEAVE_CONNECTOR_ACTIVE_ON_INITIALIZATION_FAILURE
from onyx.configs.app_configs import POLL_CONNECTOR_OFFSET
from onyx.configs.constants import DocumentSource
from onyx.configs.constants import MilestoneRecordType
@@ -56,7 +55,6 @@ def _get_connector_runner(
start_time: datetime,
end_time: datetime,
tenant_id: str | None,
leave_connector_active: bool = LEAVE_CONNECTOR_ACTIVE_ON_INITIALIZATION_FAILURE,
) -> ConnectorRunner:
"""
NOTE: `start_time` and `end_time` are only used for poll connectors
@@ -78,25 +76,20 @@ def _get_connector_runner(
)
except Exception as e:
logger.exception(f"Unable to instantiate connector due to {e}")
# since we failed to even instantiate the connector, we pause the CCPair since
# it will never succeed. Sometimes there are cases where the connector will
# intermittently fail to initialize in which case we should pass in
# leave_connector_active=True to allow it to continue.
# For example, if there is nightly maintenance on a Confluence Server instance,
# the connector will fail to initialize every night.
if not leave_connector_active:
cc_pair = get_connector_credential_pair_from_id(
# it will never succeed
cc_pair = get_connector_credential_pair_from_id(
db_session=db_session,
cc_pair_id=attempt.connector_credential_pair.id,
)
if cc_pair and cc_pair.status == ConnectorCredentialPairStatus.ACTIVE:
update_connector_credential_pair(
db_session=db_session,
cc_pair_id=attempt.connector_credential_pair.id,
connector_id=attempt.connector_credential_pair.connector.id,
credential_id=attempt.connector_credential_pair.credential.id,
status=ConnectorCredentialPairStatus.PAUSED,
)
if cc_pair and cc_pair.status == ConnectorCredentialPairStatus.ACTIVE:
update_connector_credential_pair(
db_session=db_session,
connector_id=attempt.connector_credential_pair.connector.id,
credential_id=attempt.connector_credential_pair.credential.id,
status=ConnectorCredentialPairStatus.PAUSED,
)
raise e
return ConnectorRunner(

View File

@@ -183,7 +183,6 @@ class Answer:
citations_by_subquestion: dict[
SubQuestionKey, list[CitationInfo]
] = defaultdict(list)
basic_subq_key = SubQuestionKey(level=BASIC_KEY[0], question_num=BASIC_KEY[1])
for packet in self.processed_streamed_output:
if isinstance(packet, CitationInfo):
if packet.level_question_num is not None and packet.level is not None:
@@ -193,7 +192,7 @@ class Answer:
)
].append(packet)
elif packet.level is None:
citations_by_subquestion[basic_subq_key].append(packet)
citations_by_subquestion[BASIC_SQ_KEY].append(packet)
return citations_by_subquestion
def is_cancelled(self) -> bool:

View File

@@ -3,7 +3,6 @@ from collections.abc import Sequence
from pydantic import BaseModel
from onyx.chat.models import LlmDoc
from onyx.chat.models import OnyxContext
from onyx.context.search.models import InferenceChunk
@@ -12,7 +11,7 @@ class DocumentIdOrderMapping(BaseModel):
def map_document_id_order(
chunks: Sequence[InferenceChunk | LlmDoc | OnyxContext], one_indexed: bool = True
chunks: Sequence[InferenceChunk | LlmDoc], one_indexed: bool = True
) -> DocumentIdOrderMapping:
order_mapping = {}
current = 1 if one_indexed else 0

View File

@@ -409,11 +409,6 @@ EXPERIMENTAL_CHECKPOINTING_ENABLED = (
os.environ.get("EXPERIMENTAL_CHECKPOINTING_ENABLED", "").lower() == "true"
)
LEAVE_CONNECTOR_ACTIVE_ON_INITIALIZATION_FAILURE = (
os.environ.get("LEAVE_CONNECTOR_ACTIVE_ON_INITIALIZATION_FAILURE", "").lower()
== "true"
)
PRUNING_DISABLED = -1
DEFAULT_PRUNING_FREQ = 60 * 60 * 24 # Once a day

View File

@@ -15,12 +15,6 @@ ID_SEPARATOR = ":;:"
DEFAULT_BOOST = 0
SESSION_KEY = "session"
# Cookies
FASTAPI_USERS_AUTH_COOKIE_NAME = (
"fastapiusersauth" # Currently a constant, but logic allows for configuration
)
TENANT_ID_COOKIE_NAME = "onyx_tid" # tenant id - for workaround cases
NO_AUTH_USER_ID = "__no_auth_user__"
NO_AUTH_USER_EMAIL = "anonymous@onyx.app"

View File

@@ -1,2 +1 @@
SLACK_BOT_PERSONA_PREFIX = "__slack_bot_persona__"
DEFAULT_PERSONA_SLACK_CHANNEL_NAME = "DEFAULT_SLACK_CHANNEL"

View File

@@ -198,7 +198,7 @@ def _check_if_cc_pairs_are_owned_by_groups(
ids=missing_cc_pair_ids,
)
for cc_pair in cc_pairs:
if cc_pair.access_type == AccessType.PRIVATE:
if cc_pair.access_type != AccessType.PUBLIC:
raise ValueError(
f"Connector Credential Pair with ID: '{cc_pair.id}'"
" is not owned by the specified groups"

View File

@@ -1716,7 +1716,7 @@ class ChannelConfig(TypedDict):
"""NOTE: is a `TypedDict` so it can be used as a type hint for a JSONB column
in Postgres"""
channel_name: str | None # None for default channel config
channel_name: str
respond_tag_only: NotRequired[bool] # defaults to False
respond_to_bots: NotRequired[bool] # defaults to False
respond_member_group_list: NotRequired[list[str]]
@@ -1737,6 +1737,7 @@ class SlackChannelConfig(Base):
persona_id: Mapped[int | None] = mapped_column(
ForeignKey("persona.id"), nullable=True
)
# JSON for flexibility. Contains things like: channel name, team members, etc.
channel_config: Mapped[ChannelConfig] = mapped_column(
postgresql.JSONB(), nullable=False
)
@@ -1745,8 +1746,6 @@ class SlackChannelConfig(Base):
Boolean, nullable=False, default=False
)
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
persona: Mapped[Persona | None] = relationship("Persona")
slack_bot: Mapped["SlackBot"] = relationship(
"SlackBot",
@@ -1758,21 +1757,6 @@ class SlackChannelConfig(Base):
back_populates="slack_channel_configs",
)
__table_args__ = (
UniqueConstraint(
"slack_bot_id",
"is_default",
name="uq_slack_channel_config_slack_bot_id_default",
),
Index(
"ix_slack_channel_config_slack_bot_id_default",
"slack_bot_id",
"is_default",
unique=True,
postgresql_where=(is_default is True), # type: ignore
),
)
class SlackBot(Base):
__tablename__ = "slack_bot"

View File

@@ -74,15 +74,3 @@ def remove_slack_bot(
def fetch_slack_bots(db_session: Session) -> Sequence[SlackBot]:
return db_session.scalars(select(SlackBot)).all()
def fetch_slack_bot_tokens(
db_session: Session, slack_bot_id: int
) -> dict[str, str] | None:
slack_bot = db_session.scalar(select(SlackBot).where(SlackBot.id == slack_bot_id))
if not slack_bot:
return None
return {
"app_token": slack_bot.app_token,
"bot_token": slack_bot.bot_token,
}

View File

@@ -6,7 +6,6 @@ from sqlalchemy.orm import Session
from onyx.configs.chat_configs import MAX_CHUNKS_FED_TO_CHAT
from onyx.context.search.enums import RecencyBiasSetting
from onyx.db.constants import DEFAULT_PERSONA_SLACK_CHANNEL_NAME
from onyx.db.constants import SLACK_BOT_PERSONA_PREFIX
from onyx.db.models import ChannelConfig
from onyx.db.models import Persona
@@ -23,8 +22,8 @@ from onyx.utils.variable_functionality import (
)
def _build_persona_name(channel_name: str | None) -> str:
return f"{SLACK_BOT_PERSONA_PREFIX}{channel_name if channel_name else DEFAULT_PERSONA_SLACK_CHANNEL_NAME}"
def _build_persona_name(channel_name: str) -> str:
return f"{SLACK_BOT_PERSONA_PREFIX}{channel_name}"
def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
@@ -41,7 +40,7 @@ def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
def create_slack_channel_persona(
db_session: Session,
channel_name: str | None,
channel_name: str,
document_set_ids: list[int],
existing_persona_id: int | None = None,
num_chunks: float = MAX_CHUNKS_FED_TO_CHAT,
@@ -91,7 +90,6 @@ def insert_slack_channel_config(
channel_config: ChannelConfig,
standard_answer_category_ids: list[int],
enable_auto_filters: bool,
is_default: bool = False,
) -> SlackChannelConfig:
versioned_fetch_standard_answer_categories_by_ids = (
fetch_versioned_implementation_with_fallback(
@@ -117,26 +115,12 @@ def insert_slack_channel_config(
f"Some or all categories with ids {standard_answer_category_ids} do not exist"
)
if is_default:
existing_default = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id,
SlackChannelConfig.is_default is True, # type: ignore
)
)
if existing_default:
raise ValueError("A default config already exists for this Slack bot.")
else:
if "channel_name" not in channel_config:
raise ValueError("Channel name is required for non-default configs.")
slack_channel_config = SlackChannelConfig(
slack_bot_id=slack_bot_id,
persona_id=persona_id,
channel_config=channel_config,
standard_answer_categories=existing_standard_answer_categories,
enable_auto_filters=enable_auto_filters,
is_default=is_default,
)
db_session.add(slack_channel_config)
db_session.commit()
@@ -180,7 +164,12 @@ def update_slack_channel_config(
f"Some or all categories with ids {standard_answer_category_ids} do not exist"
)
# get the existing persona id before updating the object
existing_persona_id = slack_channel_config.persona_id
# update the config
# NOTE: need to do this before cleaning up the old persona or else we
# will encounter `violates foreign key constraint` errors
slack_channel_config.persona_id = persona_id
slack_channel_config.channel_config = channel_config
slack_channel_config.standard_answer_categories = list(
@@ -188,6 +177,20 @@ def update_slack_channel_config(
)
slack_channel_config.enable_auto_filters = enable_auto_filters
# if the persona has changed, then clean up the old persona
if persona_id != existing_persona_id and existing_persona_id:
existing_persona = db_session.scalar(
select(Persona).where(Persona.id == existing_persona_id)
)
# if the existing persona was one created just for use with this Slack channel,
# then clean it up
if existing_persona and existing_persona.name.startswith(
SLACK_BOT_PERSONA_PREFIX
):
_cleanup_relationships(
db_session=db_session, persona_id=existing_persona_id
)
db_session.commit()
return slack_channel_config
@@ -250,32 +253,3 @@ def fetch_slack_channel_config(
SlackChannelConfig.id == slack_channel_config_id
)
)
def fetch_slack_channel_config_for_channel_or_default(
db_session: Session, slack_bot_id: int, channel_name: str | None
) -> SlackChannelConfig | None:
# attempt to find channel-specific config first
if channel_name:
sc_config = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id,
SlackChannelConfig.channel_config["channel_name"].astext
== channel_name,
)
)
else:
sc_config = None
if sc_config:
return sc_config
# if none found, see if there is a default
default_sc = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id,
SlackChannelConfig.is_default == True, # noqa: E712
)
)
return default_sc

View File

@@ -142,8 +142,6 @@ def get_uuid_from_chunk_info(
tenant_id: str | None,
large_chunk_id: int | None = None,
) -> UUID:
"""NOTE: be VERY carefuly about changing this function. If changed without a migration,
this can cause deletion/update/insertion to function incorrectly."""
doc_str = document_id
# Web parsing URL duplicate catching

View File

@@ -346,14 +346,6 @@ class VespaIndex(DocumentIndex):
# IMPORTANT: This must be done one index at a time, do not use secondary index here
cleaned_chunks = [clean_chunk_id_copy(chunk) for chunk in chunks]
# needed so the final DocumentInsertionRecord returned can have the original document ID
new_document_id_to_original_document_id: dict[str, str] = {}
for ind, chunk in enumerate(cleaned_chunks):
old_chunk = chunks[ind]
new_document_id_to_original_document_id[
chunk.source_document.id
] = old_chunk.source_document.id
existing_docs: set[str] = set()
# NOTE: using `httpx` here since `requests` doesn't support HTTP2. This is beneficial for
@@ -409,14 +401,14 @@ class VespaIndex(DocumentIndex):
executor=executor,
)
all_cleaned_doc_ids = {chunk.source_document.id for chunk in cleaned_chunks}
all_doc_ids = {chunk.source_document.id for chunk in cleaned_chunks}
return {
DocumentInsertionRecord(
document_id=new_document_id_to_original_document_id[cleaned_doc_id],
already_existed=cleaned_doc_id in existing_docs,
document_id=doc_id,
already_existed=doc_id in existing_docs,
)
for cleaned_doc_id in all_cleaned_doc_ids
for doc_id in all_doc_ids
}
@classmethod
@@ -549,7 +541,7 @@ class VespaIndex(DocumentIndex):
time.monotonic() - update_start,
)
def _update_single_chunk(
def update_single_chunk(
self,
doc_chunk_id: UUID,
index_name: str,
@@ -613,8 +605,6 @@ class VespaIndex(DocumentIndex):
"""
doc_chunk_count = 0
doc_id = replace_invalid_doc_id_characters(doc_id)
with self.httpx_client_context as httpx_client:
for (
index_name,
@@ -637,7 +627,7 @@ class VespaIndex(DocumentIndex):
doc_chunk_count += len(doc_chunk_ids)
for doc_chunk_id in doc_chunk_ids:
self._update_single_chunk(
self.update_single_chunk(
doc_chunk_id, index_name, fields, doc_id, httpx_client
)
@@ -699,18 +689,6 @@ class VespaIndex(DocumentIndex):
batch_retrieval: bool = False,
get_large_chunks: bool = False,
) -> list[InferenceChunkUncleaned]:
# make sure to use the vespa-afied document IDs
chunk_requests = [
VespaChunkRequest(
document_id=replace_invalid_doc_id_characters(
chunk_request.document_id
),
min_chunk_ind=chunk_request.min_chunk_ind,
max_chunk_ind=chunk_request.max_chunk_ind,
)
for chunk_request in chunk_requests
]
if batch_retrieval:
return batch_search_api_retrieval(
index_name=self.index_name,

View File

@@ -242,9 +242,9 @@ def batch_index_vespa_chunks(
def clean_chunk_id_copy(
chunk: DocMetadataAwareIndexChunk,
) -> DocMetadataAwareIndexChunk:
clean_chunk = chunk.model_copy(
clean_chunk = chunk.copy(
update={
"source_document": chunk.source_document.model_copy(
"source_document": chunk.source_document.copy(
update={
"id": replace_invalid_doc_id_characters(chunk.source_document.id)
}

View File

@@ -45,9 +45,7 @@ def is_text_character(codepoint: int) -> bool:
def replace_invalid_doc_id_characters(text: str) -> str:
"""Replaces invalid document ID characters in text.
NOTE: this must be called at the start of every vespa-related operation or else we
risk discrepancies -> silent failures on deletion/update/insertion."""
"""Replaces invalid document ID characters in text."""
# There may be a more complete set of replacements that need to be made but Vespa docs are unclear
# and users only seem to be running into this error with single quotes
return text.replace("'", "_")

View File

@@ -409,11 +409,7 @@ class DefaultMultiLLM(LLM):
# For now, we don't support parallel tool calls
# NOTE: we can't pass this in if tools are not specified
# or else OpenAI throws an error
**(
{"parallel_tool_calls": False}
if tools and self.config.model_name != "o3-mini"
else {}
), # TODO: remove once LITELLM has patched
**({"parallel_tool_calls": False} if tools else {}),
**(
{"response_format": structured_response_format}
if structured_response_format
@@ -473,7 +469,9 @@ class DefaultMultiLLM(LLM):
if LOG_DANSWER_MODEL_INTERACTIONS:
self.log_model_configs()
if DISABLE_LITELLM_STREAMING:
if (
DISABLE_LITELLM_STREAMING or self.config.model_name == "o1-2024-12-17"
): # TODO: remove once litellm supports streaming
yield self.invoke(prompt, tools, tool_choice, structured_response_format)
return

View File

@@ -27,7 +27,6 @@ class WellKnownLLMProviderDescriptor(BaseModel):
OPENAI_PROVIDER_NAME = "openai"
OPEN_AI_MODEL_NAMES = [
"o3-mini",
"o1-mini",
"o1-preview",
"o1-2024-12-17",
@@ -92,7 +91,7 @@ def fetch_available_well_known_llms() -> list[WellKnownLLMProviderDescriptor]:
api_version_required=False,
custom_config_keys=[],
llm_names=fetch_models_for_provider(OPENAI_PROVIDER_NAME),
default_model="gpt-4o",
default_model="gpt-4",
default_fast_model="gpt-4o-mini",
),
WellKnownLLMProviderDescriptor(

View File

@@ -3,11 +3,9 @@ import os
from sqlalchemy.orm import Session
from onyx.db.models import SlackChannelConfig
from onyx.db.slack_channel_config import (
fetch_slack_channel_config_for_channel_or_default,
)
from onyx.db.slack_channel_config import fetch_slack_channel_configs
VALID_SLACK_FILTERS = [
"answerable_prefilter",
"well_answered_postfilter",
@@ -19,16 +17,18 @@ def get_slack_channel_config_for_bot_and_channel(
db_session: Session,
slack_bot_id: int,
channel_name: str | None,
) -> SlackChannelConfig:
slack_bot_config = fetch_slack_channel_config_for_channel_or_default(
db_session=db_session, slack_bot_id=slack_bot_id, channel_name=channel_name
)
if not slack_bot_config:
raise ValueError(
"No default configuration has been set for this Slack bot. This should not be possible."
)
) -> SlackChannelConfig | None:
if not channel_name:
return None
return slack_bot_config
slack_bot_configs = fetch_slack_channel_configs(
db_session=db_session, slack_bot_id=slack_bot_id
)
for config in slack_bot_configs:
if channel_name in config.channel_config["channel_name"]:
return config
return None
def validate_channel_name(

View File

@@ -106,7 +106,7 @@ def remove_scheduled_feedback_reminder(
def handle_message(
message_info: SlackMessageInfo,
slack_channel_config: SlackChannelConfig,
slack_channel_config: SlackChannelConfig | None,
client: WebClient,
feedback_reminder_id: str | None,
tenant_id: str | None,

View File

@@ -64,7 +64,7 @@ def rate_limits(
def handle_regular_answer(
message_info: SlackMessageInfo,
slack_channel_config: SlackChannelConfig,
slack_channel_config: SlackChannelConfig | None,
receiver_ids: list[str] | None,
client: WebClient,
channel: str,
@@ -76,7 +76,7 @@ def handle_regular_answer(
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
disable_docs_only_answer: bool = DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER,
) -> bool:
channel_conf = slack_channel_config.channel_config
channel_conf = slack_channel_config.channel_config if slack_channel_config else None
messages = message_info.thread_messages
@@ -92,7 +92,7 @@ def handle_regular_answer(
prompt = None
# If no persona is specified, use the default search based persona
# This way slack flow always has a persona
persona = slack_channel_config.persona
persona = slack_channel_config.persona if slack_channel_config else None
if not persona:
with get_session_with_tenant(tenant_id) as db_session:
persona = get_persona_by_id(DEFAULT_PERSONA_ID, user, db_session)
@@ -134,7 +134,11 @@ def handle_regular_answer(
single_message_history = slackify_message_thread(history_messages) or None
bypass_acl = False
if slack_channel_config.persona and slack_channel_config.persona.document_sets:
if (
slack_channel_config
and slack_channel_config.persona
and slack_channel_config.persona.document_sets
):
# For Slack channels, use the full document set, admin will be warned when configuring it
# with non-public document sets
bypass_acl = True
@@ -186,7 +190,11 @@ def handle_regular_answer(
# auto_detect_filters = (
# persona.llm_filter_extraction if persona is not None else True
# )
auto_detect_filters = slack_channel_config.enable_auto_filters
auto_detect_filters = (
slack_channel_config.enable_auto_filters
if slack_channel_config is not None
else False
)
retrieval_details = RetrievalDetails(
run_search=OptionalSearchSetting.ALWAYS,
real_time=False,

View File

@@ -14,7 +14,7 @@ logger = setup_logger()
def handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_channel_config: SlackChannelConfig,
slack_channel_config: SlackChannelConfig | None,
prompt: Prompt | None,
logger: OnyxLoggingAdapter,
client: WebClient,
@@ -40,7 +40,7 @@ def handle_standard_answers(
def _handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_channel_config: SlackChannelConfig,
slack_channel_config: SlackChannelConfig | None,
prompt: Prompt | None,
logger: OnyxLoggingAdapter,
client: WebClient,

View File

@@ -410,27 +410,13 @@ class SlackbotHandler:
def start_socket_client(
self, slack_bot_id: int, tenant_id: str | None, slack_bot_tokens: SlackBotTokens
) -> None:
logger.info(
f"Starting socket client for tenant: {tenant_id}, app: {slack_bot_id}"
)
socket_client: TenantSocketModeClient = _get_socket_client(
slack_bot_tokens, tenant_id, slack_bot_id
)
try:
bot_info = socket_client.web_client.auth_test()
if bot_info["ok"]:
bot_user_id = bot_info["user_id"]
user_info = socket_client.web_client.users_info(user=bot_user_id)
if user_info["ok"]:
bot_name = (
user_info["user"]["real_name"] or user_info["user"]["name"]
)
logger.info(
f"Started socket client for Slackbot with name '{bot_name}' (tenant: {tenant_id}, app: {slack_bot_id})"
)
except Exception as e:
logger.warning(
f"Could not fetch bot name: {e} for tenant: {tenant_id}, app: {slack_bot_id}"
)
# Append the event handler
process_slack_event = create_process_slack_event()
socket_client.socket_mode_request_listeners.append(process_slack_event) # type: ignore
@@ -804,7 +790,8 @@ def process_message(
# Be careful about this default, don't want to accidentally spam every channel
# Users should be able to DM slack bot in their private channels though
if (
not respond_every_channel
slack_channel_config is None
and not respond_every_channel
# Can't have configs for DMs so don't toss them out
and not is_dm
# If /OnyxBot (is_bot_msg) or @OnyxBot (bypass_filters)
@@ -814,7 +801,8 @@ def process_message(
return
follow_up = bool(
slack_channel_config.channel_config
slack_channel_config
and slack_channel_config.channel_config
and slack_channel_config.channel_config.get("follow_up_tags")
is not None
)

View File

@@ -25,7 +25,6 @@ from onyx.configs.app_configs import REDIS_REPLICA_HOST
from onyx.configs.app_configs import REDIS_SSL
from onyx.configs.app_configs import REDIS_SSL_CA_CERTS
from onyx.configs.app_configs import REDIS_SSL_CERT_REQS
from onyx.configs.constants import FASTAPI_USERS_AUTH_COOKIE_NAME
from onyx.configs.constants import REDIS_SOCKET_KEEPALIVE_OPTIONS
from onyx.utils.logger import setup_logger
@@ -288,7 +287,7 @@ async def get_async_redis_connection() -> aioredis.Redis:
async def retrieve_auth_token_data_from_redis(request: Request) -> dict | None:
token = request.cookies.get(FASTAPI_USERS_AUTH_COOKIE_NAME)
token = request.cookies.get("fastapiusersauth")
if not token:
logger.debug("No auth token cookie found")
return None

View File

@@ -215,7 +215,6 @@ class SlackChannelConfig(BaseModel):
# XXX this is going away soon
standard_answer_categories: list[StandardAnswerCategory]
enable_auto_filters: bool
is_default: bool
@classmethod
def from_model(
@@ -238,7 +237,6 @@ class SlackChannelConfig(BaseModel):
for standard_answer_category_model in slack_channel_config_model.standard_answer_categories
],
enable_auto_filters=slack_channel_config_model.enable_auto_filters,
is_default=slack_channel_config_model.is_default,
)
@@ -281,8 +279,3 @@ class AllUsersResponse(BaseModel):
accepted_pages: int
invited_pages: int
slack_users_pages: int
class SlackChannel(BaseModel):
id: str
name: str

View File

@@ -1,10 +1,6 @@
from typing import Any
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
@@ -16,7 +12,6 @@ from onyx.db.models import ChannelConfig
from onyx.db.models import User
from onyx.db.persona import get_persona_by_id
from onyx.db.slack_bot import fetch_slack_bot
from onyx.db.slack_bot import fetch_slack_bot_tokens
from onyx.db.slack_bot import fetch_slack_bots
from onyx.db.slack_bot import insert_slack_bot
from onyx.db.slack_bot import remove_slack_bot
@@ -30,7 +25,6 @@ from onyx.db.slack_channel_config import update_slack_channel_config
from onyx.onyxbot.slack.config import validate_channel_name
from onyx.server.manage.models import SlackBot
from onyx.server.manage.models import SlackBotCreationRequest
from onyx.server.manage.models import SlackChannel
from onyx.server.manage.models import SlackChannelConfig
from onyx.server.manage.models import SlackChannelConfigCreationRequest
from onyx.server.manage.validate_tokens import validate_app_token
@@ -54,6 +48,12 @@ def _form_channel_config(
answer_filters = slack_channel_config_creation_request.answer_filters
follow_up_tags = slack_channel_config_creation_request.follow_up_tags
if not raw_channel_name:
raise HTTPException(
status_code=400,
detail="Must provide at least one channel name",
)
try:
cleaned_channel_name = validate_channel_name(
db_session=db_session,
@@ -108,12 +108,6 @@ def create_slack_channel_config(
current_slack_channel_config_id=None,
)
if channel_config["channel_name"] is None:
raise HTTPException(
status_code=400,
detail="Channel name is required",
)
persona_id = None
if slack_channel_config_creation_request.persona_id is not None:
persona_id = slack_channel_config_creation_request.persona_id
@@ -126,11 +120,11 @@ def create_slack_channel_config(
).id
slack_channel_config_model = insert_slack_channel_config(
db_session=db_session,
slack_bot_id=slack_channel_config_creation_request.slack_bot_id,
persona_id=persona_id,
channel_config=channel_config,
standard_answer_category_ids=slack_channel_config_creation_request.standard_answer_categories,
db_session=db_session,
enable_auto_filters=slack_channel_config_creation_request.enable_auto_filters,
)
return SlackChannelConfig.from_model(slack_channel_config_model)
@@ -241,23 +235,6 @@ def create_bot(
app_token=slack_bot_creation_request.app_token,
)
# Create a default Slack channel config
default_channel_config = ChannelConfig(
channel_name=None,
respond_member_group_list=[],
answer_filters=[],
follow_up_tags=[],
)
insert_slack_channel_config(
db_session=db_session,
slack_bot_id=slack_bot_model.id,
persona_id=None,
channel_config=default_channel_config,
standard_answer_category_ids=[],
enable_auto_filters=False,
is_default=True,
)
create_milestone_and_report(
user=None,
distinct_id=tenant_id or "N/A",
@@ -338,48 +315,3 @@ def list_bot_configs(
SlackChannelConfig.from_model(slack_bot_config_model)
for slack_bot_config_model in slack_bot_config_models
]
@router.get(
"/admin/slack-app/bots/{bot_id}/channels",
)
def get_all_channels_from_slack_api(
bot_id: int,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> list[SlackChannel]:
tokens = fetch_slack_bot_tokens(db_session, bot_id)
if not tokens or "bot_token" not in tokens:
raise HTTPException(
status_code=404, detail="Bot token not found for the given bot ID"
)
bot_token = tokens["bot_token"]
client = WebClient(token=bot_token)
try:
channels = []
cursor = None
while True:
response = client.conversations_list(
types="public_channel,private_channel",
exclude_archived=True,
limit=1000,
cursor=cursor,
)
for channel in response["channels"]:
channels.append(SlackChannel(id=channel["id"], name=channel["name"]))
response_metadata: dict[str, Any] = response.get("response_metadata", {})
if isinstance(response_metadata, dict):
cursor = response_metadata.get("next_cursor")
if not cursor:
break
else:
break
return channels
except SlackApiError as e:
raise HTTPException(
status_code=500, detail=f"Error fetching channels from Slack API: {str(e)}"
)

View File

@@ -38,7 +38,6 @@ from onyx.configs.app_configs import ENABLE_EMAIL_INVITES
from onyx.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
from onyx.configs.app_configs import VALID_EMAIL_DOMAINS
from onyx.configs.constants import AuthType
from onyx.configs.constants import FASTAPI_USERS_AUTH_COOKIE_NAME
from onyx.db.api_key import is_api_key_email_address
from onyx.db.auth import get_total_users_count
from onyx.db.engine import CURRENT_TENANT_ID_CONTEXTVAR
@@ -480,7 +479,7 @@ def get_current_token_expiration_jwt(
try:
# Get the JWT from the cookie
jwt_token = request.cookies.get(FASTAPI_USERS_AUTH_COOKIE_NAME)
jwt_token = request.cookies.get("fastapiusersauth")
if not jwt_token:
logger.error("No JWT token found in cookies")
return None

View File

@@ -717,6 +717,8 @@ def upload_files_for_chat(
else ChatFileType.PLAIN_TEXT
)
file_content = file.file.read() # Read the file content
if file_type == ChatFileType.IMAGE:
file_content_io = file.file
# NOTE: Image conversion to JPEG used to be enforced here.
@@ -725,7 +727,7 @@ def upload_files_for_chat(
# 2. Maintain transparency in formats like PNG
# 3. Ameliorate issue with file conversion
else:
file_content_io = io.BytesIO(file.file.read())
file_content_io = io.BytesIO(file_content)
new_content_type = file.content_type
@@ -743,7 +745,7 @@ def upload_files_for_chat(
# to re-extract it every time we send a message
if file_type == ChatFileType.DOC:
extracted_text = extract_file_text(
file=file_content_io, # use the bytes we already read
file=io.BytesIO(file_content), # use the bytes we already read
file_name=file.filename or "",
)
text_file_id = str(uuid.uuid4())

View File

@@ -7,6 +7,7 @@ from typing import cast
from sqlalchemy.orm import Session
from onyx.chat.chat_utils import llm_doc_from_inference_section
from onyx.chat.llm_response_handler import LLMCall
from onyx.chat.models import AnswerStyleConfig
from onyx.chat.models import ContextualPruningConfig
from onyx.chat.models import DocumentPruningConfig
@@ -370,6 +371,41 @@ class SearchTool(Tool):
prompt_config=self.prompt_config,
)
"""Other utility functions"""
@classmethod
def get_search_result(
cls, llm_call: LLMCall
) -> tuple[list[LlmDoc], list[LlmDoc]] | None:
"""
Returns the final search results and a map of docs to their original search rank (which is what is displayed to user)
"""
if not llm_call.tool_call_info:
return None
final_search_results = []
initial_search_results = []
for yield_item in llm_call.tool_call_info:
if (
isinstance(yield_item, ToolResponse)
and yield_item.id == FINAL_CONTEXT_DOCUMENTS_ID
):
final_search_results = cast(list[LlmDoc], yield_item.response)
elif (
isinstance(yield_item, ToolResponse)
and yield_item.id == SEARCH_DOC_CONTENT_ID
):
search_contexts = yield_item.response.contexts
# original_doc_search_rank = 1
for doc in search_contexts:
if doc.document_id not in initial_search_results:
initial_search_results.append(doc)
initial_search_results = cast(list[LlmDoc], initial_search_results)
return final_search_results, initial_search_results
# Allows yielding the same responses as a SearchTool without being a SearchTool.
# SearchTool passed in to allow for access to SearchTool properties.

View File

@@ -37,7 +37,7 @@ langchainhub==0.1.21
langgraph==0.2.59
langgraph-checkpoint==2.0.5
langgraph-sdk==0.1.44
litellm==1.60.2
litellm==1.55.4
lxml==5.3.0
lxml_html_clean==0.2.2
llama-index==0.9.45
@@ -46,7 +46,7 @@ msal==1.28.0
nltk==3.8.1
Office365-REST-Python-Client==2.5.9
oauthlib==3.2.2
openai==1.61.0
openai==1.55.3
openpyxl==3.1.2
playwright==1.41.2
psutil==5.9.5

View File

@@ -3,7 +3,7 @@ cohere==5.6.1
fastapi==0.109.2
google-cloud-aiplatform==1.58.0
numpy==1.26.4
openai==1.61.0
openai==1.55.3
pydantic==2.8.2
retry==0.9.2
safetensors==0.4.2
@@ -12,5 +12,5 @@ torch==2.2.0
transformers==4.39.2
uvicorn==0.21.1
voyageai==0.2.3
litellm==1.60.2
litellm==1.55.4
sentry-sdk[fastapi,celery,starlette]==2.14.0

View File

@@ -11,8 +11,6 @@ from typing import Optional
import requests
from onyx.configs.constants import FASTAPI_USERS_AUTH_COOKIE_NAME
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(parent_dir)
@@ -376,11 +374,7 @@ class SelectionAnalysis:
Returns:
dict: The Onyx API response content
"""
cookies = (
{FASTAPI_USERS_AUTH_COOKIE_NAME: self._auth_cookie}
if self._auth_cookie
else {}
)
cookies = {"fastapiusersauth": self._auth_cookie} if self._auth_cookie else {}
endpoint = f"http://127.0.0.1:{self._web_port}/api/direct-qa"
query_json = {

View File

@@ -7,7 +7,6 @@ import requests
from requests import HTTPError
from onyx.auth.schemas import UserRole
from onyx.configs.constants import FASTAPI_USERS_AUTH_COOKIE_NAME
from onyx.server.documents.models import PaginatedReturn
from onyx.server.models import FullUserSnapshot
from tests.integration.common_utils.constants import API_SERVER_URL
@@ -83,7 +82,7 @@ class UserManager:
response.raise_for_status()
cookies = response.cookies.get_dict()
session_cookie = cookies.get(FASTAPI_USERS_AUTH_COOKIE_NAME)
session_cookie = cookies.get("fastapiusersauth")
if not session_cookie:
raise Exception("Failed to login")

View File

@@ -1,5 +1,6 @@
import json
import pytest
import requests
from onyx.configs.constants import MessageType
@@ -65,6 +66,9 @@ def test_send_message_simple_with_history(reset: None) -> None:
assert found_doc["metadata"]["document_id"] == doc.id
@pytest.mark.xfail(
reason="agent search broke this",
)
def test_using_reference_docs_with_simple_with_history_api_flow(reset: None) -> None:
# Creating an admin user (first user created is automatically an admin)
admin_user: DATestUser = UserManager.create(name="admin_user")

View File

@@ -81,6 +81,9 @@ ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}
ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}
ARG NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK
ENV NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK=${NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK}
# Use NODE_OPTIONS in the build command
RUN NODE_OPTIONS="${NODE_OPTIONS}" npx next build
@@ -160,6 +163,9 @@ ENV NEXT_PUBLIC_GTM_ENABLED=${NEXT_PUBLIC_GTM_ENABLED}
ARG NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED
ENV NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED=${NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED}
ARG NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK
ENV NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK=${NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK}
# Note: Don't expose ports here, Compose will handle that for us if necessary.
# If you want to run this without compose, specify the ports to
# expose via cli

View File

@@ -28,6 +28,7 @@ import { Spinner } from "@/components/Spinner";
import { deleteApiKey, regenerateApiKey } from "./lib";
import { OnyxApiKeyForm } from "./OnyxApiKeyForm";
import { APIKey } from "./types";
import CreateButton from "@/components/ui/createButton";
const API_KEY_TEXT = `API Keys allow you to access Onyx APIs programmatically. Click the button below to generate a new API Key.`;
@@ -111,14 +112,7 @@ function Main() {
}
const newApiKeyButton = (
<Button
variant="navigate"
size="sm"
className="mt-3"
onClick={() => setShowCreateUpdateForm(true)}
>
Create API Key
</Button>
<CreateButton href="/admin/api-key/new" text="Create API Key" />
);
if (apiKeys.length === 0) {

View File

@@ -40,7 +40,12 @@ import * as Yup from "yup";
import CollapsibleSection from "./CollapsibleSection";
import { SuccessfulPersonaUpdateRedirectType } from "./enums";
import { Persona, PersonaLabel, StarterMessage } from "./interfaces";
import { PersonaUpsertParameters, createPersona, updatePersona } from "./lib";
import {
PersonaUpsertParameters,
createPersona,
updatePersona,
deletePersona,
} from "./lib";
import {
CameraIcon,
GroupsIconSkeleton,
@@ -71,7 +76,6 @@ import { LLMSelector } from "@/components/llm/LLMSelector";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { DeletePersonaButton } from "./[id]/DeletePersonaButton";
import Title from "@/components/ui/title";
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
@@ -322,10 +326,39 @@ export function AssistantEditor({
}));
};
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
if (!labels) {
return <></>;
}
const openDeleteModal = () => {
setDeleteModalOpen(true);
};
const closeDeleteModal = () => {
setDeleteModalOpen(false);
};
const handleDeletePersona = async () => {
if (existingPersona) {
const response = await deletePersona(existingPersona.id);
if (response.ok) {
await refreshAssistants();
router.push(
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN
? `/admin/assistants?u=${Date.now()}`
: `/chat`
);
} else {
setPopup({
type: "error",
message: `Failed to delete persona - ${await response.text()}`,
});
}
}
};
return (
<div className="mx-auto max-w-4xl">
<style>
@@ -364,6 +397,14 @@ export function AssistantEditor({
}}
/>
)}
{deleteModalOpen && existingPersona && (
<DeleteEntityModal
entityType="Persona"
entityName={existingPersona.name}
onClose={closeDeleteModal}
onSubmit={handleDeletePersona}
/>
)}
{popup}
<Formik
enableReinitialize={true}
@@ -1312,14 +1353,6 @@ export function AssistantEditor({
explanationLink="https://docs.onyx.app/guides/assistants"
className="[&_textarea]:placeholder:text-text-muted/50"
/>
<div className="flex justify-end">
{existingPersona && (
<DeletePersonaButton
personaId={existingPersona!.id}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>
)}
</div>
</>
)}
@@ -1338,6 +1371,18 @@ export function AssistantEditor({
Cancel
</Button>
</div>
<div className="flex justify-end">
{existingPersona && (
<Button
variant="destructive"
onClick={openDeleteModal}
type="button"
>
Delete
</Button>
)}
</div>
</Form>
);
}}

View File

@@ -17,6 +17,7 @@ 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 { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
if (persona.builtin_persona) {
@@ -53,6 +54,8 @@ export function PersonasTable() {
}, [editablePersonas]);
const [finalPersonas, setFinalPersonas] = useState<Persona[]>([]);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [personaToDelete, setPersonaToDelete] = useState<Persona | null>(null);
useEffect(() => {
const editable = editablePersonas.sort(personaComparator);
@@ -98,9 +101,42 @@ export function PersonasTable() {
await refreshUser();
};
const openDeleteModal = (persona: Persona) => {
setPersonaToDelete(persona);
setDeleteModalOpen(true);
};
const closeDeleteModal = () => {
setDeleteModalOpen(false);
setPersonaToDelete(null);
};
const handleDeletePersona = async () => {
if (personaToDelete) {
const response = await deletePersona(personaToDelete.id);
if (response.ok) {
await refreshAssistants();
closeDeleteModal();
} else {
setPopup({
type: "error",
message: `Failed to delete persona - ${await response.text()}`,
});
}
}
};
return (
<div>
{popup}
{deleteModalOpen && personaToDelete && (
<DeleteEntityModal
entityType="Persona"
entityName={personaToDelete.name}
onClose={closeDeleteModal}
onSubmit={handleDeletePersona}
/>
)}
<DraggableTable
headers={["Name", "Description", "Type", "Is Visible", "Delete"]}
@@ -170,16 +206,7 @@ export function PersonasTable() {
{!persona.builtin_persona && isEditable ? (
<div
className="hover:bg-hover rounded p-1 cursor-pointer"
onClick={async () => {
const response = await deletePersona(persona.id);
if (response.ok) {
await refreshAssistants();
} else {
alert(
`Failed to delete persona - ${await response.text()}`
);
}
}}
onClick={() => openDeleteModal(persona)}
>
<TrashIcon />
</div>

View File

@@ -1,15 +1,12 @@
"use client";
import { PersonasTable } from "./PersonaTable";
import { FiPlusSquare } from "react-icons/fi";
import Link from "next/link";
import Text from "@/components/ui/text";
import Title from "@/components/ui/title";
import { Separator } from "@/components/ui/separator";
import { AssistantsIcon } from "@/components/icons/icons";
import { AdminPageTitle } from "@/components/admin/Title";
import LabelManagement from "./LabelManagement";
import { SubLabel } from "@/components/admin/connectors/Field";
import CreateButton from "@/components/ui/createButton";
export default async function Page() {
return (
<div className="mx-auto container">
@@ -33,15 +30,7 @@ export default async function Page() {
<Separator />
<Title>Create an Assistant</Title>
<Link
href="/admin/assistants/new"
className="flex py-2 px-4 mt-2 border border-border h-fit cursor-pointer hover:bg-hover text-sm w-40"
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />
New Assistant
</div>
</Link>
<CreateButton href="/admin/assistants/new" text="New Assistant" />
<Separator />

View File

@@ -1,9 +1,10 @@
"use client";
import { PageSelector } from "@/components/PageSelector";
import { SlackBot } from "@/lib/types";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { FiEdit } from "react-icons/fi";
import { FiCheck, FiEdit, FiXCircle } from "react-icons/fi";
import {
Table,
TableBody,
@@ -12,8 +13,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { SlackBot } from "@/lib/types";
const NUM_IN_PAGE = 20;
@@ -43,7 +42,7 @@ function ClickableTableRow({
);
}
export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
const [page, setPage] = useState(1);
// sort by id for consistent ordering
@@ -68,9 +67,8 @@ export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Default Config</TableHead>
<TableHead>Channel Count</TableHead>
<TableHead>Enabled</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -87,19 +85,13 @@ export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
{slackBot.name}
</div>
</TableCell>
<TableCell>
{slackBot.enabled ? (
<Badge variant="success">Enabled</Badge>
) : (
<Badge variant="destructive">Disabled</Badge>
)}
</TableCell>
<TableCell>
<Badge variant="secondary">Default Set</Badge>
</TableCell>
<TableCell>{slackBot.configs_count}</TableCell>
<TableCell>
{/* Add any action buttons here if needed */}
{slackBot.enabled ? (
<FiCheck className="text-emerald-600" size="18" />
) : (
<FiXCircle className="text-red-600" size="18" />
)}
</TableCell>
</ClickableTableRow>
);
@@ -107,7 +99,7 @@ export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
{slackBots.length === 0 && (
<TableRow>
<TableCell
colSpan={5}
colSpan={4}
className="text-center text-muted-foreground"
>
Please add a New Slack Bot to begin chatting with Danswer!
@@ -136,4 +128,4 @@ export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
)}
</div>
);
};
}

View File

@@ -7,7 +7,6 @@ import { createSlackBot, updateSlackBot } from "./new/lib";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { useEffect } from "react";
import { Switch } from "@/components/ui/switch";
export const SlackTokensForm = ({
isUpdate,
@@ -34,9 +33,7 @@ export const SlackTokensForm = ({
return (
<Formik
initialValues={{
...initialValues,
}}
initialValues={initialValues}
validationSchema={Yup.object().shape({
bot_token: Yup.string().required(),
app_token: Yup.string().required(),

View File

@@ -14,10 +14,8 @@ import {
} from "@/components/ui/table";
import Link from "next/link";
import { useState } from "react";
import { FiArrowUpRight } from "react-icons/fi";
import { deleteSlackChannelConfig, isPersonaASlackBotPersona } from "./lib";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { FiPlusSquare, FiSettings } from "react-icons/fi";
const numToDisplay = 50;
@@ -34,147 +32,128 @@ export function SlackChannelConfigsTable({
}) {
const [page, setPage] = useState(1);
const defaultConfig = slackChannelConfigs.find((config) => config.is_default);
const channelConfigs = slackChannelConfigs.filter(
(config) => !config.is_default
);
// sort by name for consistent ordering
slackChannelConfigs.sort((a, b) => {
if (a.id < b.id) {
return -1;
} else if (a.id > b.id) {
return 1;
} else {
return 0;
}
});
return (
<div className="space-y-8">
<div className="flex justify-between items-center mb-6">
<Button
variant="outline"
onClick={() => {
window.location.href = `/admin/bots/${slackBotId}/channels/${defaultConfig?.id}`;
}}
>
<FiSettings />
Edit Default Config
</Button>
<Link href={`/admin/bots/${slackBotId}/channels/new`}>
<Button variant="outline">
<FiPlusSquare />
New Channel Configuration
</Button>
</Link>
<div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Channel</TableHead>
<TableHead>Assistant</TableHead>
<TableHead>Document Sets</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{slackChannelConfigs
.slice(numToDisplay * (page - 1), numToDisplay * page)
.map((slackChannelConfig) => {
return (
<TableRow
key={slackChannelConfig.id}
className="cursor-pointer hover:bg-gray-100 transition-colors"
onClick={() => {
window.location.href = `/admin/bots/${slackBotId}/channels/${slackChannelConfig.id}`;
}}
>
<TableCell>
<div className="flex gap-x-2">
<div className="my-auto">
<EditIcon />
</div>
<div className="my-auto">
{"#" + slackChannelConfig.channel_config.channel_name}
</div>
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
{slackChannelConfig.persona &&
!isPersonaASlackBotPersona(slackChannelConfig.persona) ? (
<Link
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
className="text-blue-500 flex hover:underline"
>
{slackChannelConfig.persona.name}
</Link>
) : (
"-"
)}
</TableCell>
<TableCell>
<div>
{slackChannelConfig.persona &&
slackChannelConfig.persona.document_sets.length > 0
? slackChannelConfig.persona.document_sets
.map((documentSet) => documentSet.name)
.join(", ")
: "-"}
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<div
className="cursor-pointer hover:text-destructive"
onClick={async (e) => {
e.stopPropagation();
const response = await deleteSlackChannelConfig(
slackChannelConfig.id
);
if (response.ok) {
setPopup({
message: `Slack bot config "${slackChannelConfig.id}" deleted`,
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete Slack bot config - ${errorMsg}`,
type: "error",
});
}
refresh();
}}
>
<TrashIcon />
</div>
</TableCell>
</TableRow>
);
})}
{/* Empty row with message when table has no data */}
{slackChannelConfigs.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
Please add a New Slack Bot Configuration to begin chatting
with Onyx!
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div>
<h2 className="text-2xl font- mb-4">Channel-Specific Configurations</h2>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Channel</TableHead>
<TableHead>Assistant</TableHead>
<TableHead>Document Sets</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channelConfigs
.slice(numToDisplay * (page - 1), numToDisplay * page)
.map((slackChannelConfig) => {
return (
<TableRow
key={slackChannelConfig.id}
className="cursor-pointer transition-colors"
onClick={() => {
window.location.href = `/admin/bots/${slackBotId}/channels/${slackChannelConfig.id}`;
}}
>
<TableCell>
<div className="flex gap-x-2">
<div className="my-auto">
<EditIcon className="text-muted-foreground" />
</div>
<div className="my-auto">
{"#" +
slackChannelConfig.channel_config.channel_name}
</div>
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
{slackChannelConfig.persona &&
!isPersonaASlackBotPersona(
slackChannelConfig.persona
) ? (
<Link
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
className="text-primary hover:underline"
>
{slackChannelConfig.persona.name}
</Link>
) : (
"-"
)}
</TableCell>
<TableCell>
<div>
{slackChannelConfig.persona &&
slackChannelConfig.persona.document_sets.length > 0
? slackChannelConfig.persona.document_sets
.map((documentSet) => documentSet.name)
.join(", ")
: "-"}
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<Button
variant="ghost"
size="sm"
className="hover:text-destructive"
onClick={async (e) => {
e.stopPropagation();
const response = await deleteSlackChannelConfig(
slackChannelConfig.id
);
if (response.ok) {
setPopup({
message: `Slack bot config "${slackChannelConfig.id}" deleted`,
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete Slack bot config - ${errorMsg}`,
type: "error",
});
}
refresh();
}}
>
<TrashIcon />
</Button>
</TableCell>
</TableRow>
);
})}
{channelConfigs.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
No channel-specific configurations. Add a new configuration
to customize behavior for specific channels.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
{channelConfigs.length > numToDisplay && (
<div className="mt-4 flex justify-center">
<PageSelector
totalPages={Math.ceil(channelConfigs.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
)}
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(slackChannelConfigs.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
</div>
</div>
);

View File

@@ -1,29 +1,21 @@
"use client";
import React, { useMemo, useState, useEffect } from "react";
import { Formik, Form, Field } from "formik";
import React, { useMemo } from "react";
import { Formik } from "formik";
import * as Yup from "yup";
import { usePopup } from "@/components/admin/connectors/Popup";
import {
DocumentSet,
SlackChannelConfig,
SlackBotResponseType,
} from "@/lib/types";
import { DocumentSet, SlackChannelConfig } from "@/lib/types";
import {
createSlackChannelConfig,
isPersonaASlackBotPersona,
updateSlackChannelConfig,
fetchSlackChannels,
} from "../lib";
import CardSection from "@/components/admin/CardSection";
import { useRouter } from "next/navigation";
import { Persona } from "@/app/admin/assistants/interfaces";
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { SEARCH_TOOL_ID, SEARCH_TOOL_NAME } from "@/app/chat/tools/constants";
import {
SlackChannelConfigFormFields,
SlackChannelConfigFormFieldsProps,
} from "./SlackChannelConfigFormFields";
import { SlackChannelConfigFormFields } from "./SlackChannelConfigFormFields";
export const SlackChannelConfigCreationForm = ({
slack_bot_id,
@@ -41,7 +33,6 @@ export const SlackChannelConfigCreationForm = ({
const { popup, setPopup } = usePopup();
const router = useRouter();
const isUpdate = Boolean(existingSlackChannelConfig);
const isDefault = existingSlackChannelConfig?.is_default || false;
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
: false;
@@ -55,16 +46,13 @@ export const SlackChannelConfigCreationForm = ({
}, [personas]);
return (
<CardSection className="!px-12 max-w-4xl">
<CardSection className="max-w-4xl">
{popup}
<Formik
initialValues={{
slack_bot_id: slack_bot_id,
channel_name: isDefault
? ""
: existingSlackChannelConfig?.channel_config.channel_name || "",
response_type: "citations" as SlackBotResponseType,
channel_name:
existingSlackChannelConfig?.channel_config.channel_name || "",
answer_validity_check_enabled: (
existingSlackChannelConfig?.channel_config?.answer_filters || []
).includes("well_answered_postfilter"),
@@ -102,6 +90,8 @@ export const SlackChannelConfigCreationForm = ({
!isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
? existingSlackChannelConfig.persona.id
: null,
response_type:
existingSlackChannelConfig?.response_type || "citations",
standard_answer_categories:
existingSlackChannelConfig?.standard_answer_categories || [],
knowledge_source: existingSlackBotUsesPersona
@@ -112,12 +102,10 @@ export const SlackChannelConfigCreationForm = ({
}}
validationSchema={Yup.object().shape({
slack_bot_id: Yup.number().required(),
channel_name: isDefault
? Yup.string()
: Yup.string().required("Channel Name is required"),
response_type: Yup.mixed<SlackBotResponseType>()
channel_name: Yup.string().required("Channel Name is required"),
response_type: Yup.string()
.oneOf(["quotes", "citations"])
.required(),
.required("Response type is required"),
answer_validity_check_enabled: Yup.boolean().required(),
questionmark_prefilter_enabled: Yup.boolean().required(),
respond_tag_only: Yup.boolean().required(),
@@ -171,7 +159,6 @@ export const SlackChannelConfigCreationForm = ({
standard_answer_categories: values.standard_answer_categories.map(
(category: any) => category.id
),
response_type: values.response_type as SlackBotResponseType,
};
if (!cleanedValues.still_need_help_enabled) {
@@ -204,22 +191,13 @@ export const SlackChannelConfigCreationForm = ({
}
}}
>
{({ isSubmitting, values, setFieldValue }) => (
<Form>
<div className="pb-6 w-full">
<SlackChannelConfigFormFields
{...values}
isUpdate={isUpdate}
isDefault={isDefault}
documentSets={documentSets}
searchEnabledAssistants={searchEnabledAssistants}
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
setPopup={setPopup}
slack_bot_id={slack_bot_id}
/>
</div>
</Form>
)}
<SlackChannelConfigFormFields
isUpdate={isUpdate}
documentSets={documentSets}
searchEnabledAssistants={searchEnabledAssistants}
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
setPopup={setPopup}
/>
</Formik>
</CardSection>
);

View File

@@ -1,13 +1,7 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import {
FieldArray,
Form,
useFormikContext,
ErrorMessage,
Field,
} from "formik";
import { FieldArray, Form, useFormikContext, ErrorMessage } from "formik";
import { CCPairDescriptor, DocumentSet } from "@/lib/types";
import {
BooleanFormField,
@@ -37,15 +31,9 @@ import { TooltipProvider } from "@radix-ui/react-tooltip";
import { SourceIcon } from "@/components/SourceIcon";
import Link from "next/link";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { SearchMultiSelectDropdown } from "@/components/Dropdown";
import { fetchSlackChannels } from "../lib";
import { Badge } from "@/components/ui/badge";
import useSWR from "swr";
import { ThreeDotsLoader } from "@/components/Loading";
export interface SlackChannelConfigFormFieldsProps {
interface SlackChannelConfigFormFieldsProps {
isUpdate: boolean;
isDefault: boolean;
documentSets: DocumentSet[];
searchEnabledAssistants: Persona[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
@@ -53,23 +41,19 @@ export interface SlackChannelConfigFormFieldsProps {
message: string;
type: "error" | "success" | "warning";
}) => void;
slack_bot_id: number;
}
export function SlackChannelConfigFormFields({
isUpdate,
isDefault,
documentSets,
searchEnabledAssistants,
standardAnswerCategoryResponse,
setPopup,
slack_bot_id,
}: SlackChannelConfigFormFieldsProps) {
const router = useRouter();
const { values, setFieldValue } = useFormikContext<any>();
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [viewUnselectableSets, setViewUnselectableSets] = useState(false);
const [currentSearchTerm, setCurrentSearchTerm] = useState("");
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
useState(false);
@@ -168,54 +152,11 @@ export function SlackChannelConfigFormFields({
);
}, [documentSets]);
const { data: channelOptions, isLoading } = useSWR(
`/api/manage/admin/slack-app/bots/${slack_bot_id}/channels`,
async (url: string) => {
const channels = await fetchSlackChannels(slack_bot_id);
return channels.map((channel: any) => ({
name: channel.name,
value: channel.id,
}));
}
);
if (isLoading) {
return <ThreeDotsLoader />;
}
return (
<>
<div className="w-full">
{isDefault && (
<Badge variant="agent" className="bg-blue-100 text-blue-800">
Default Configuration
</Badge>
)}
{!isDefault && (
<>
<label
htmlFor="channel_name"
className="block font-medium text-base mb-2"
>
Select A Slack Channel:
</label>{" "}
<Field name="channel_name">
{({ field, form }: { field: any; form: any }) => (
<SearchMultiSelectDropdown
options={channelOptions || []}
onSelect={(selected) => {
form.setFieldValue("channel_name", selected.name);
setCurrentSearchTerm(selected.name);
}}
initialSearchTerm={field.value}
onSearchTermChange={(term) => {
setCurrentSearchTerm(term);
form.setFieldValue("channel_name", term);
}}
/>
)}
</Field>
</>
)}
<Form className="px-6 max-w-4xl">
<div className="pt-4 w-full">
<TextFormField name="channel_name" label="Slack Channel Name:" />
<div className="space-y-2 mt-4">
<Label>Knowledge Source</Label>
<RadioGroup
@@ -229,7 +170,7 @@ export function SlackChannelConfigFormFields({
value="all_public"
id="all_public"
label="All Public Knowledge"
sublabel="Let OnyxBot respond based on information from all public connectors"
sublabel="Let OnyxBot respond based on information from all public connectors "
/>
{selectableSets.length + unselectableSets.length > 0 && (
<RadioGroupItemField
@@ -247,6 +188,7 @@ export function SlackChannelConfigFormFields({
/>
</RadioGroup>
</div>
{values.knowledge_source === "document_sets" &&
documentSets.length > 0 && (
<div className="mt-4">
@@ -339,6 +281,7 @@ export function SlackChannelConfigFormFields({
/>
</div>
)}
{values.knowledge_source === "assistant" && (
<div className="mt-4">
<SubLabel>
@@ -410,15 +353,15 @@ export function SlackChannelConfigFormFields({
)}
</div>
<div className="mt-6">
<div className="mt-2">
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}
/>
</div>
{showAdvancedOptions && (
<div className="mt-2 space-y-4">
<div className="w-64">
<div className="mt-4">
<div className="w-64 mb-4">
<SelectorFormField
name="response_type"
label="Answer Type"
@@ -437,79 +380,83 @@ export function SlackChannelConfigFormFields({
tooltip="If set, will show a button at the bottom of the response that allows the user to continue the conversation in the Onyx Web UI"
/>
<BooleanFormField
name="still_need_help_enabled"
removeIndent
onChange={(checked: boolean) => {
setFieldValue("still_need_help_enabled", checked);
if (!checked) {
setFieldValue("follow_up_tags", []);
}
}}
label={'Give a "Still need help?" button'}
tooltip={`OnyxBot's response will include a button at the bottom
of the response that asks the user if they still need help.`}
/>
{values.still_need_help_enabled && (
<CollapsibleSection prompt="Configure Still Need Help Button">
<TextArrayField
name="follow_up_tags"
label="(Optional) Users / Groups to Tag"
values={values}
subtext={
<div>
The Slack users / groups we should tag if the user clicks
the &quot;Still need help?&quot; button. If no emails are
provided, we will not tag anyone and will just react with a
🆘 emoji to the original message.
</div>
<div className="flex flex-col space-y-3 mt-2">
<BooleanFormField
name="still_need_help_enabled"
removeIndent
onChange={(checked: boolean) => {
setFieldValue("still_need_help_enabled", checked);
if (!checked) {
setFieldValue("follow_up_tags", []);
}
}}
label={'Give a "Still need help?" button'}
tooltip={`OnyxBot's response will include a button at the bottom
of the response that asks the user if they still need help.`}
/>
{values.still_need_help_enabled && (
<CollapsibleSection prompt="Configure Still Need Help Button">
<TextArrayField
name="follow_up_tags"
label="(Optional) Users / Groups to Tag"
values={values}
subtext={
<div>
The Slack users / groups we should tag if the user clicks
the &quot;Still need help?&quot; button. If no emails are
provided, we will not tag anyone and will just react with
a 🆘 emoji to the original message.
</div>
}
placeholder="User email or user group name..."
/>
</CollapsibleSection>
)}
<BooleanFormField
name="answer_validity_check_enabled"
removeIndent
label="Only respond if citations found"
tooltip="If set, will only answer questions where the model successfully produces citations"
/>
<BooleanFormField
name="questionmark_prefilter_enabled"
removeIndent
label="Only respond to questions"
tooltip="If set, OnyxBot will only respond to messages that contain a question mark"
/>
<BooleanFormField
name="respond_tag_only"
removeIndent
label="Respond to @OnyxBot Only"
tooltip="If set, OnyxBot will only respond when directly tagged"
/>
<BooleanFormField
name="respond_to_bots"
removeIndent
label="Respond to Bot messages"
tooltip="If not set, OnyxBot will always ignore messages from Bots"
/>
<BooleanFormField
name="enable_auto_filters"
removeIndent
label="Enable LLM Autofiltering"
tooltip="If set, the LLM will generate source and time filters based on the user's query"
/>
<div className="mt-12">
<TextArrayField
name="respond_member_group_list"
label="(Optional) Respond to Certain Users / Groups"
subtext={
"If specified, OnyxBot responses will only " +
"be visible to the members or groups in this list."
}
values={values}
placeholder="User email or user group name..."
/>
</CollapsibleSection>
)}
<BooleanFormField
name="answer_validity_check_enabled"
removeIndent
label="Only respond if citations found"
tooltip="If set, will only answer questions where the model successfully produces citations"
/>
<BooleanFormField
name="questionmark_prefilter_enabled"
removeIndent
label="Only respond to questions"
tooltip="If set, OnyxBot will only respond to messages that contain a question mark"
/>
<BooleanFormField
name="respond_tag_only"
removeIndent
label="Respond to @OnyxBot Only"
tooltip="If set, OnyxBot will only respond when directly tagged"
/>
<BooleanFormField
name="respond_to_bots"
removeIndent
label="Respond to Bot messages"
tooltip="If not set, OnyxBot will always ignore messages from Bots"
/>
<BooleanFormField
name="enable_auto_filters"
removeIndent
label="Enable LLM Autofiltering"
tooltip="If set, the LLM will generate source and time filters based on the user's query"
/>
<TextArrayField
name="respond_member_group_list"
label="(Optional) Respond to Certain Users / Groups"
subtext={
"If specified, OnyxBot responses will only " +
"be visible to the members or groups in this list."
}
values={values}
placeholder="User email or user group name..."
/>
</div>
</div>
<StandardAnswerCategoryDropdownField
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
@@ -521,7 +468,7 @@ export function SlackChannelConfigFormFields({
</div>
)}
<div className="flex mt-8 gap-x-2 w-full justify-end">
<div className="flex mt-2 gap-x-2 w-full justify-end flex">
{shouldShowPrivacyAlert && (
<TooltipProvider>
<Tooltip>
@@ -571,11 +518,13 @@ export function SlackChannelConfigFormFields({
</Tooltip>
</TooltipProvider>
)}
<Button type="submit">{isUpdate ? "Update" : "Create"}</Button>
<Button onClick={() => {}} type="submit">
{isUpdate ? "Update" : "Create"}
</Button>
<Button type="button" variant="outline" onClick={() => router.back()}>
Cancel
</Button>
</div>
</>
</Form>
);
}

View File

@@ -94,17 +94,3 @@ export const deleteSlackChannelConfig = async (id: number) => {
export function isPersonaASlackBotPersona(persona: Persona) {
return persona.name.startsWith("__slack_bot_persona__");
}
export const fetchSlackChannels = async (botId: number) => {
return fetch(`/api/manage/admin/slack-app/bots/${botId}/channels`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
}).then((response) => {
if (!response.ok) {
throw new Error("Failed to fetch Slack channels");
}
return response.json();
});
};

View File

@@ -78,6 +78,30 @@ function SlackBotEditPage({
/>
<Separator />
<div className="my-8" />
<Link
className="
flex
py-2
px-4
mt-2
border
border-border
h-fit
cursor-pointer
hover:bg-hover
text-sm
w-80
"
href={`/admin/bots/${unwrappedParams["bot-id"]}/channels/new`}
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />
New Slack Channel Configuration
</div>
</Link>
<div className="mt-8">
<SlackChannelConfigsTable
slackBotId={slackBot.id}

View File

@@ -11,6 +11,7 @@ import { SourceIcon } from "@/components/SourceIcon";
import { SlackBotTable } from "./SlackBotTable";
import { useSlackBots } from "./[bot-id]/hooks";
import { ValidSources } from "@/lib/types";
import CreateButton from "@/components/ui/createButton";
const Main = () => {
const {
@@ -71,27 +72,7 @@ const Main = () => {
found in the Onyx documentation to get started!
</p>
<Link
className="
flex
py-2
px-4
mt-2
border
border-border
h-fit
cursor-pointer
hover:bg-hover
text-sm
w-40
"
href="/admin/bots/new"
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />
New Slack Bot
</div>
</Link>
<CreateButton href="/admin/bots/new" text="New Slack Bot" />
<SlackBotTable slackBots={slackBots} />
</div>

View File

@@ -33,6 +33,11 @@ import EditPropertyModal from "@/components/modals/EditPropertyModal";
import * as Yup from "yup";
// since the uploaded files are cleaned up after some period of time
// re-indexing will not work for the file connector. Also, it would not
// make sense to re-index, since the files will not have changed.
const CONNECTOR_TYPES_THAT_CANT_REINDEX: ValidSources[] = [ValidSources.File];
// synchronize these validations with the SQLAlchemy connector class until we have a
// centralized schema for both frontend and backend
const RefreshFrequencySchema = Yup.object().shape({
@@ -263,18 +268,21 @@ function Main({ ccPairId }: { ccPairId: number }) {
{ccPair.is_editable_for_current_user && (
<div className="ml-auto flex gap-x-2">
<ReIndexButton
ccPairId={ccPair.id}
connectorId={ccPair.connector.id}
credentialId={ccPair.credential.id}
isDisabled={
ccPair.indexing ||
ccPair.status === ConnectorCredentialPairStatus.PAUSED
}
isIndexing={ccPair.indexing}
isDeleting={isDeleting}
/>
{!CONNECTOR_TYPES_THAT_CANT_REINDEX.includes(
ccPair.connector.source
) && (
<ReIndexButton
ccPairId={ccPair.id}
connectorId={ccPair.connector.id}
credentialId={ccPair.credential.id}
isDisabled={
ccPair.indexing ||
ccPair.status === ConnectorCredentialPairStatus.PAUSED
}
isIndexing={ccPair.indexing}
isDeleting={isDeleting}
/>
)}
{!isDeleting && <ModifyStatusButtonCluster ccPair={ccPair} />}
</div>
)}

View File

@@ -17,7 +17,7 @@ import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBad
import { DocumentSet } from "@/lib/types";
import { SourceIcon } from "@/components/SourceIcon";
import { Connector } from "@/lib/connectors/connectors";
import { HorizontalFilters } from "@/components/filters/SourceSelector";
import { HorizontalFilters } from "@/app/chat/shared_chat_search/Filters";
const DocumentDisplay = ({
document,

View File

@@ -40,6 +40,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import CreateButton from "@/components/ui/createButton";
const numToDisplay = 50;
@@ -305,9 +306,13 @@ const Main = () => {
<div className="mb-3"></div>
<div className="flex mb-6">
<Link href="/admin/documents/sets/new">
<CreateButton
href="/admin/documents/sets/new"
text="New Document Set"
/>
{/* <Link href="/admin/documents/sets/new">
<Button variant="navigate">New Document Set</Button>
</Link>
</Link> */}
</div>
{documentSets.length > 0 && (

View File

@@ -18,6 +18,7 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { CreateRateLimitModal } from "./CreateRateLimitModal";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { ShieldIcon } from "@/components/icons/icons";
import CreateButton from "@/components/ui/createButton";
const BASE_URL = "/api/admin/token-rate-limits";
const GLOBAL_TOKEN_FETCH_URL = `${BASE_URL}/global`;
@@ -138,15 +139,10 @@ function Main() {
</li>
</ul>
<Button
variant="navigate"
size="sm"
className="my-4"
onClick={() => setModalIsOpen(true)}
>
Create a Token Rate Limit
</Button>
<CreateButton
href="/admin/token-rate-limits/new"
text="Create a Token Rate Limit"
/>
{isPaidEnterpriseFeaturesEnabled && (
<Tabs
value={tabIndex.toString()}

View File

@@ -9,6 +9,7 @@ import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { AdminPageTitle } from "@/components/admin/Title";
import { ToolIcon } from "@/components/icons/icons";
import CreateButton from "@/components/ui/createButton";
export default async function Page() {
const toolResponse = await fetchSS("/tool");
@@ -39,27 +40,7 @@ export default async function Page() {
<Separator />
<Title>Create a Tool</Title>
<Link
href="/admin/tools/new"
className="
flex
py-2
px-4
mt-2
border
border-border
h-fit
cursor-pointer
hover:bg-hover
text-sm
w-40
"
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />
New Tool
</div>
</Link>
<CreateButton href="/admin/tools/new" text="New Tool" />
<Separator />

View File

@@ -10,17 +10,16 @@ import {
useRef,
useState,
} from "react";
import { useSidebarVisibility } from "@/components/chat/hooks";
import FunctionalHeader from "@/components/chat/Header";
import { useSidebarVisibility } from "@/components/chat_search/hooks";
import FunctionalHeader from "@/components/chat_search/Header";
import { useRouter } from "next/navigation";
import { pageType } from "../chat/sessionSidebar/types";
import FixedLogo from "../../components/logo/FixedLogo";
import FixedLogo from "../chat/shared_chat_search/FixedLogo";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useChatContext } from "@/components/context/ChatContext";
import { HistorySidebar } from "../chat/sessionSidebar/HistorySidebar";
import { useAssistants } from "@/components/context/AssistantsContext";
import AssistantModal from "./mine/AssistantModal";
import { useSidebarShortcut } from "@/lib/browserUtilities";
interface SidebarWrapperProps<T extends object> {
initiallyToggled: boolean;
@@ -72,8 +71,23 @@ export default function SidebarWrapper<T extends object>({
const [showAssistantsModal, setShowAssistantsModal] = useState(false);
const router = useRouter();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
switch (event.key.toLowerCase()) {
case "e":
event.preventDefault();
toggleSidebar();
break;
}
}
};
useSidebarShortcut(router, toggleSidebar);
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [router]);
return (
<div className="flex relative overflow-x-hidden overscroll-contain flex-col w-full h-screen">

View File

@@ -28,12 +28,6 @@ export const resetPassword = async (
});
if (!response.ok) {
const error = await response.json();
if (error?.detail?.code === "RESET_PASSWORD_INVALID_PASSWORD") {
throw new Error(error.detail.reason || "Invalid password");
}
const errorMessage =
error?.detail || "An error occurred during password reset.";
throw new Error(errorMessage);
throw new Error("Failed to reset password");
}
};

View File

@@ -1,5 +1,5 @@
"use client";
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { resetPassword } from "../forgot-password/utils";
import AuthFlowContainer from "@/components/auth/AuthFlowContainer";
import CardSection from "@/components/admin/CardSection";
@@ -13,28 +13,13 @@ import { TextFormField } from "@/components/admin/connectors/Field";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Spinner } from "@/components/Spinner";
import { redirect, useSearchParams } from "next/navigation";
import {
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED,
TENANT_ID_COOKIE_NAME,
} from "@/lib/constants";
import Cookies from "js-cookie";
import { NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED } from "@/lib/constants";
const ResetPasswordPage: React.FC = () => {
const { popup, setPopup } = usePopup();
const [isWorking, setIsWorking] = useState(false);
const searchParams = useSearchParams();
const token = searchParams.get("token");
const tenantId = searchParams.get(TENANT_ID_COOKIE_NAME);
// Keep search param same name as cookie for simplicity
useEffect(() => {
if (tenantId) {
Cookies.set(TENANT_ID_COOKIE_NAME, tenantId, {
path: "/",
expires: 1 / 24,
}); // Expires in 1 hour
}
}, [tenantId]);
if (!NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED) {
redirect("/auth/login");
@@ -78,18 +63,10 @@ const ResetPasswordPage: React.FC = () => {
redirect("/auth/login");
}, 1000);
} catch (error) {
if (error instanceof Error) {
setPopup({
type: "error",
message:
error.message || "An error occurred during password reset.",
});
} else {
setPopup({
type: "error",
message: "An unexpected error occurred. Please try again.",
});
}
setPopup({
type: "error",
message: "An error occurred. Please try again.",
});
} finally {
setIsWorking(false);
}

View File

@@ -3,7 +3,7 @@
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useContext, useState, useRef, useLayoutEffect } from "react";
import { ChevronDownIcon } from "@/components/icons/icons";
import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
export function ChatBanner() {
const settings = useContext(SettingsContext);

View File

@@ -89,16 +89,16 @@ import { useChatContext } from "@/components/context/ChatContext";
import { v4 as uuidv4 } from "uuid";
import { ChatPopup } from "./ChatPopup";
import FunctionalHeader from "@/components/chat/Header";
import { useSidebarVisibility } from "@/components/chat/hooks";
import FunctionalHeader from "@/components/chat_search/Header";
import { useSidebarVisibility } from "@/components/chat_search/hooks";
import {
PRO_SEARCH_TOGGLED_COOKIE_NAME,
SIDEBAR_TOGGLED_COOKIE_NAME,
} from "@/components/resizable/constants";
import FixedLogo from "../../components/logo/FixedLogo";
import FixedLogo from "./shared_chat_search/FixedLogo";
import { DeleteEntityModal } from "../../components/modals/DeleteEntityModal";
import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
import {
@@ -108,10 +108,10 @@ import {
} from "./tools/constants";
import { useUser } from "@/components/user/UserProvider";
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
import BlurBackground from "../../components/chat/BlurBackground";
import BlurBackground from "./shared_chat_search/BlurBackground";
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
import { useAssistants } from "@/components/context/AssistantsContext";
import TextView from "@/components/chat/TextView";
import TextView from "@/components/chat_search/TextView";
import { Modal } from "@/components/Modal";
import { useSendMessageToParent } from "@/lib/extension/utils";
import {
@@ -124,11 +124,6 @@ import { UserSettingsModal } from "./modal/UserSettingsModal";
import { AlignStartVertical } from "lucide-react";
import { AgenticMessage } from "./message/AgenticMessage";
import AssistantModal from "../assistants/mine/AssistantModal";
import {
OperatingSystem,
useOperatingSystem,
useSidebarShortcut,
} from "@/lib/browserUtilities";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -2057,7 +2052,24 @@ export function ChatPage({
llmOverrideManager.updateImageFilesPresent(imageFileInMessageHistory);
}, [imageFileInMessageHistory]);
useSidebarShortcut(router, toggleSidebar);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
switch (event.key.toLowerCase()) {
case "e":
event.preventDefault();
toggleSidebar();
break;
}
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router]);
const [sharedChatSession, setSharedChatSession] =
useState<ChatSession | null>();

View File

@@ -1,7 +1,7 @@
"use client";
import { useChatContext } from "@/components/context/ChatContext";
import { ChatPage } from "./ChatPage";
import FunctionalWrapper from "../../components/chat/FunctionalWrapper";
import FunctionalWrapper from "./shared_chat_search/FunctionalWrapper";
export default function WrappedChat({
firstMessage,

View File

@@ -39,6 +39,7 @@ export function DocumentMetadataBlock({
{metadataEntries.length > 0 && (
<>
<div className="mx-1 h-4 border-l border-border" />
<div className="flex items-center overflow-hidden">
{metadataEntries
.slice(0, MAX_METADATA_ITEMS)

View File

@@ -24,7 +24,7 @@ import {
} from "@/components/ui/tooltip";
import { Hoverable } from "@/components/Hoverable";
import { ChatState } from "../types";
import UnconfiguredProviderText from "@/components/chat/UnconfiguredProviderText";
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
import { useAssistants } from "@/components/context/AssistantsContext";
import { CalendarIcon, TagIcon, XIcon } from "lucide-react";
import { FilterPopup } from "@/components/search/filtering/FilterPopup";

View File

@@ -64,7 +64,9 @@ import { MemoizedAnchor, MemoizedParagraph } from "./MemoizedTextComponents";
import { extractCodeText, preprocessLaTeX } from "./codeUtils";
import ToolResult from "../../../components/tools/ToolResult";
import CsvContent from "../../../components/tools/CSVContent";
import SourceCard, { SeeMoreBlock } from "@/components/chat/sources/SourceCard";
import SourceCard, {
SeeMoreBlock,
} from "@/components/chat_search/sources/SourceCard";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";

View File

@@ -1,6 +1,9 @@
import React, { useState, useEffect } from "react";
import { OnyxDocument } from "@/lib/search/interfaces";
import { ResultIcon, SeeMoreBlock } from "@/components/chat/sources/SourceCard";
import {
ResultIcon,
SeeMoreBlock,
} from "@/components/chat_search/sources/SourceCard";
import { openDocument } from "@/lib/search/utils";
import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay";
import { ValidSources } from "@/lib/types";

View File

@@ -9,7 +9,7 @@ import { FiSearch } from "react-icons/fi";
import { OnyxDocument } from "@/lib/search/interfaces";
import { BaseQuestionIdentifier, SubQuestionDetail } from "../interfaces";
import { SourceChip2 } from "../input/ChatInputBar";
import { ResultIcon } from "@/components/chat/sources/SourceCard";
import { ResultIcon } from "@/components/chat_search/sources/SourceCard";
import { openDocument } from "@/lib/search/utils";
import { SourcesDisplay } from "./SourcesDisplay";
import ReactMarkdown from "react-markdown";

View File

@@ -17,11 +17,11 @@ import { OnyxInitializingLoader } from "@/components/OnyxInitializingLoader";
import { Persona } from "@/app/admin/assistants/interfaces";
import { Button } from "@/components/ui/button";
import { OnyxDocument } from "@/lib/search/interfaces";
import TextView from "@/components/chat/TextView";
import TextView from "@/components/chat_search/TextView";
import { DocumentResults } from "../../documentSidebar/DocumentResults";
import { Modal } from "@/components/Modal";
import FunctionalHeader from "@/components/chat/Header";
import FixedLogo from "../../../../components/logo/FixedLogo";
import FunctionalHeader from "@/components/chat_search/Header";
import FixedLogo from "../../shared_chat_search/FixedLogo";
import { useRouter } from "next/navigation";
function BackToOnyxButton({

View File

@@ -1,7 +1,14 @@
import React from "react";
import React, { useState } from "react";
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
import { SourceMetadata } from "@/lib/search/interfaces";
import { InfoIcon, defaultTailwindCSS } from "@/components/icons/icons";
import {
GearIcon,
InfoIcon,
MinusIcon,
PlusCircleIcon,
PlusIcon,
defaultTailwindCSS,
} from "@/components/icons/icons";
import { HoverPopup } from "@/components/HoverPopup";
import {
FiBook,
@@ -20,7 +27,7 @@ import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverTrigger } from "@/components/ui/popover";
import { PopoverContent } from "@radix-ui/react-popover";
import { CalendarIcon } from "lucide-react";
import { getTimeAgoString } from "@/lib/dateUtils";
import { buildDateString, getTimeAgoString } from "@/lib/dateUtils";
import { Separator } from "@/components/ui/separator";
import { FilterDropdown } from "@/components/search/filtering/FilterDropdown";

View File

@@ -0,0 +1,65 @@
"use client";
import React, { ReactNode, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
export default function FunctionalWrapper({
initiallyToggled,
content,
}: {
content: (
toggledSidebar: boolean,
toggle: (toggled?: boolean) => void
) => ReactNode;
initiallyToggled: boolean;
}) {
const router = useRouter();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
const newPage = event.shiftKey;
switch (event.key.toLowerCase()) {
case "d":
event.preventDefault();
if (newPage) {
window.open("/chat", "_blank");
} else {
router.push("/chat");
}
break;
case "s":
event.preventDefault();
if (newPage) {
window.open("/search", "_blank");
} else {
router.push("/search");
}
break;
}
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [router]);
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
const toggle = (value?: boolean) => {
setToggledSidebar((toggledSidebar) =>
value !== undefined ? value : !toggledSidebar
);
};
return (
<>
{" "}
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
{content(toggledSidebar, toggle)}
</div>
</>
);
}

View File

@@ -74,8 +74,8 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
.map((userGroupCCPair) => userGroupCCPair.id)
.includes(ccPair.cc_pair_id)
)
// remove public and synced docs, since they don't make sense as part of a group
.filter((ccPair) => ccPair.access_type === "private")
// remove public docs, since they don't make sense as part of a group
.filter((ccPair) => !(ccPair.access_type === "public"))
.map((ccPair) => {
return {
name: ccPair.name?.toString() || "",

View File

@@ -32,12 +32,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
import { DeleteButton } from "@/components/DeleteButton";
import { Bubble } from "@/components/Bubble";
@@ -296,29 +290,16 @@ export const GroupDisplay = ({
)}
</div>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Button
size="sm"
className={userGroup.is_up_to_date ? "" : "opacity-50"}
variant="submit"
onClick={() => {
if (userGroup.is_up_to_date) {
setAddMemberFormVisible(true);
}
}}
>
Add Users
</Button>
</TooltipTrigger>
{!userGroup.is_up_to_date && (
<TooltipContent>
<p>Cannot update group while sync is occurring</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<Button
className="mt-3"
size="sm"
variant="submit"
onClick={() => setAddMemberFormVisible(true)}
disabled={!userGroup.is_up_to_date}
>
Add Users
</Button>
{addMemberFormVisible && (
<AddMemberForm
users={users}
@@ -408,29 +389,15 @@ export const GroupDisplay = ({
)}
</div>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<Button
size="sm"
className={userGroup.is_up_to_date ? "" : "opacity-50"}
variant="submit"
onClick={() => {
if (userGroup.is_up_to_date) {
setAddConnectorFormVisible(true);
}
}}
>
Add Connectors
</Button>
</TooltipTrigger>
{!userGroup.is_up_to_date && (
<TooltipContent>
<p>Cannot update group while sync is occurring</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<Button
className="mt-3"
onClick={() => setAddConnectorFormVisible(true)}
size="sm"
variant="submit"
disabled={!userGroup.is_up_to_date}
>
Add Connectors
</Button>
{addConnectorFormVisible && (
<AddConnectorForm

View File

@@ -11,6 +11,7 @@ import { AdminPageTitle } from "@/components/admin/Title";
import { Button } from "@/components/ui/button";
import { useUser } from "@/components/user/UserProvider";
import CreateButton from "@/components/ui/createButton";
const Main = () => {
const { popup, setPopup } = usePopup();
@@ -52,18 +53,10 @@ const Main = () => {
<>
{popup}
{isAdmin && (
<div className="my-3">
<Button
size="sm"
variant="navigate"
onClick={() => setShowForm(true)}
>
Create New User Group
</Button>
</div>
<CreateButton href="/admin/groups/new" text="Create New User Group" />
)}
{data.length > 0 && (
<div>
<div className="mt-2">
<UserGroupsTable
userGroups={data}
setPopup={setPopup}

View File

@@ -29,6 +29,7 @@ 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";
const NUM_RESULTS_PER_PAGE = 10;
@@ -402,11 +403,10 @@ const Main = () => {
)}
<div className="mb-2"></div>
<Link className="flex mb-3 mt-2 w-fit" href="/admin/standard-answer/new">
<Button className="my-auto" variant="submit" size="sm">
New Standard Answer
</Button>
</Link>
<CreateButton
href="/admin/standard-answer/new"
text="New Standard Answer"
/>
<Separator />

View File

@@ -52,19 +52,15 @@ export function SearchMultiSelectDropdown({
itemComponent,
onCreate,
onDelete,
onSearchTermChange,
initialSearchTerm = "",
}: {
options: StringOrNumberOption[];
onSelect: (selected: StringOrNumberOption) => void;
itemComponent?: FC<{ option: StringOrNumberOption }>;
onCreate?: (name: string) => void;
onDelete?: (name: string) => void;
onSearchTermChange?: (term: string) => void;
initialSearchTerm?: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
const [searchTerm, setSearchTerm] = useState("");
const dropdownRef = useRef<HTMLDivElement>(null);
const handleSelect = (option: StringOrNumberOption) => {
@@ -93,10 +89,6 @@ export function SearchMultiSelectDropdown({
};
}, []);
useEffect(() => {
setSearchTerm(initialSearchTerm);
}, [initialSearchTerm]);
return (
<div className="relative text-left w-full" ref={dropdownRef}>
<div>
@@ -113,21 +105,21 @@ export function SearchMultiSelectDropdown({
}
}}
onFocus={() => setIsOpen(true)}
className="inline-flex justify-between w-full px-4 py-2 text-sm bg-white text-gray-800 border border-gray-300 rounded-md shadow-sm"
className="inline-flex justify-between w-full px-4 py-2 text-sm bg-background border border-border rounded-md shadow-sm"
/>
<button
type="button"
className="absolute top-0 right-0 text-sm h-full px-2 border-l border-gray-300"
className="absolute top-0 right-0 text-sm h-full px-2 border-l border-border"
aria-expanded={isOpen}
aria-haspopup="true"
onClick={() => setIsOpen(!isOpen)}
>
<ChevronDownIcon className="my-auto w-4 h-4 text-gray-600" />
<ChevronDownIcon className="my-auto w-4 h-4" />
</button>
</div>
{isOpen && (
<div className="absolute z-10 mt-1 w-full rounded-md shadow-lg bg-white border border-gray-300 max-h-60 overflow-y-auto">
<div className="absolute z-10 mt-1 w-full rounded-md shadow-lg bg-background border border-border max-h-60 overflow-y-auto">
<div
role="menu"
aria-orientation="vertical"
@@ -160,9 +152,9 @@ export function SearchMultiSelectDropdown({
option.name.toLowerCase() === searchTerm.toLowerCase()
) && (
<>
<div className="border-t border-gray-300"></div>
<div className="border-t border-border"></div>
<button
className="w-full text-left flex items-center px-4 py-2 text-sm text-gray-800 hover:bg-gray-100"
className="w-full text-left flex items-center px-4 py-2 text-sm hover:bg-hover"
role="menuitem"
onClick={() => {
onCreate(searchTerm);
@@ -170,7 +162,7 @@ export function SearchMultiSelectDropdown({
setSearchTerm("");
}}
>
<PlusIcon className="w-4 h-4 mr-2 text-gray-600" />
<PlusIcon className="w-4 h-4 mr-2" />
Create label &quot;{searchTerm}&quot;
</button>
</>
@@ -178,7 +170,7 @@ export function SearchMultiSelectDropdown({
{filteredOptions.length === 0 &&
(!onCreate || searchTerm.trim() === "") && (
<div className="px-4 py-2.5 text-sm text-gray-500">
<div className="px-4 py-2.5 text-sm text-text-muted">
No matches found
</div>
)}

View File

@@ -85,7 +85,7 @@ export function Modal({
ease-in-out
relative
${width ?? "w-11/12 max-w-4xl"}
${noPadding ? "" : removeBottomPadding ? "pt-10 px-10" : "p-10"}
${noPadding ? "" : removeBottomPadding ? "pt-8 px-8" : "p-8"}
${className || ""}
flex
flex-col

View File

@@ -15,7 +15,7 @@ import { NavigationItem, Notification } from "@/app/admin/settings/interfaces";
import DynamicFaIcon, { preloadIcons } from "./icons/DynamicFaIcon";
import { useUser } from "./user/UserProvider";
import { usePaidEnterpriseFeaturesEnabled } from "./settings/usePaidEnterpriseFeaturesEnabled";
import { Notifications } from "./chat/Notifications";
import { Notifications } from "./chat_search/Notifications";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";

View File

@@ -15,7 +15,7 @@ import {
} from "@/components/ui/tooltip";
import { CgArrowsExpandUpLeft } from "react-icons/cg";
import LogoWithText from "@/components/header/LogoWithText";
import { LogoComponent } from "@/components/logo/FixedLogo";
import { LogoComponent } from "@/app/chat/shared_chat_search/FixedLogo";
interface Item {
name: string | JSX.Element;

View File

@@ -3,6 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Check, CheckCircle, XCircle } from "lucide-react";
import { Warning } from "@phosphor-icons/react";
import { NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK } from "@/lib/constants";
const popupVariants = cva(
"fixed bottom-4 left-4 p-4 rounded-lg shadow-xl text-white z-[10000] flex items-center space-x-3 transition-all duration-300 ease-in-out",
{
@@ -59,7 +60,23 @@ export const Popup: React.FC<PopupSpec> = ({ message, type }) => (
/>
</svg>
)}
<span className="font-medium">{message}</span>
<div className="flex flex-col justify-center items-start">
<p className="font-medium">{message}</p>
{type === "error" && NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK && (
<p className="text-xs">
Need help?{" "}
<a
href="https://join.slack.com/t/onyx-dot-app/shared_invite/zt-2twesxdr6-5iQitKZQpgq~hYIZ~dv3KA"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-red-100"
>
Join our community
</a>{" "}
for support!
</p>
)}
</div>
</div>
);

View File

@@ -1,30 +0,0 @@
"use client";
import React, { ReactNode, useState } from "react";
export default function FunctionalWrapper({
initiallyToggled,
content,
}: {
content: (
toggledSidebar: boolean,
toggle: (toggled?: boolean) => void
) => ReactNode;
initiallyToggled: boolean;
}) {
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
const toggle = (value?: boolean) => {
setToggledSidebar((toggledSidebar) =>
value !== undefined ? value : !toggledSidebar
);
};
return (
<>
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
{content(toggledSidebar, toggle)}
</div>
</>
);
}

View File

@@ -1,6 +1,6 @@
import { CombinedSettings } from "@/app/admin/settings/interfaces";
import { UserProvider } from "../user/UserProvider";
import { ProviderContextProvider } from "../chat/ProviderContext";
import { ProviderContextProvider } from "../chat_search/ProviderContext";
import { SettingsProvider } from "../settings/SettingsProvider";
import { AssistantsProvider } from "./AssistantsContext";
import { Persona } from "@/app/admin/assistants/interfaces";

View File

@@ -12,7 +12,7 @@ import {
import { pageType } from "@/app/chat/sessionSidebar/types";
import { Logo } from "../logo/Logo";
import Link from "next/link";
import { LogoComponent } from "@/components/logo/FixedLogo";
import { LogoComponent } from "@/app/chat/shared_chat_search/FixedLogo";
export default function LogoWithText({
toggleSidebar,

View File

@@ -11,7 +11,7 @@ import { ApiKeyForm } from "@/components/llm/ApiKeyForm";
import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { checkLlmProvider } from "./lib";
import { User } from "@/lib/types";
import { useProviderStatus } from "@/components/chat/ProviderContext";
import { useProviderStatus } from "@/components/chat_search/ProviderContext";
import { usePopup } from "@/components/admin/connectors/Popup";

View File

@@ -3,7 +3,7 @@
import { ApiKeyForm } from "./ApiKeyForm";
import { Modal } from "../Modal";
import { useRouter } from "next/navigation";
import { useProviderStatus } from "../chat/ProviderContext";
import { useProviderStatus } from "../chat_search/ProviderContext";
import { PopupSpec } from "../admin/connectors/Popup";
export const ApiKeyModal = ({

Some files were not shown because too many files have changed in this diff Show More