mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-26 17:51:54 +01:00
add default slack channel config
This commit is contained in:
parent
78153e5012
commit
7153cb09f1
@ -0,0 +1,76 @@
|
||||
"""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")
|
@ -80,7 +80,7 @@ def oneoff_standard_answers(
|
||||
def _handle_standard_answers(
|
||||
message_info: SlackMessageInfo,
|
||||
receiver_ids: list[str] | None,
|
||||
slack_channel_config: SlackChannelConfig | None,
|
||||
slack_channel_config: SlackChannelConfig,
|
||||
prompt: Prompt | None,
|
||||
logger: OnyxLoggingAdapter,
|
||||
client: WebClient,
|
||||
@ -94,13 +94,10 @@ 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 if slack_channel_config else []
|
||||
slack_channel_config.standard_answer_categories
|
||||
)
|
||||
configured_standard_answers = set(
|
||||
[
|
||||
|
@ -1 +1,2 @@
|
||||
SLACK_BOT_PERSONA_PREFIX = "__slack_bot_persona__"
|
||||
DEFAULT_PERSONA_SLACK_CHANNEL_NAME = "DEFAULT_SLACK_CHANNEL"
|
||||
|
@ -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
|
||||
channel_name: str | None # None for default channel config
|
||||
respond_tag_only: NotRequired[bool] # defaults to False
|
||||
respond_to_bots: NotRequired[bool] # defaults to False
|
||||
respond_member_group_list: NotRequired[list[str]]
|
||||
@ -1737,7 +1737,6 @@ 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
|
||||
)
|
||||
@ -1746,6 +1745,8 @@ 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",
|
||||
@ -1757,6 +1758,21 @@ 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"
|
||||
|
@ -74,3 +74,15 @@ 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,
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ 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
|
||||
@ -22,8 +23,8 @@ from onyx.utils.variable_functionality import (
|
||||
)
|
||||
|
||||
|
||||
def _build_persona_name(channel_name: str) -> str:
|
||||
return f"{SLACK_BOT_PERSONA_PREFIX}{channel_name}"
|
||||
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 _cleanup_relationships(db_session: Session, persona_id: int) -> None:
|
||||
@ -40,7 +41,7 @@ def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
|
||||
|
||||
def create_slack_channel_persona(
|
||||
db_session: Session,
|
||||
channel_name: str,
|
||||
channel_name: str | None,
|
||||
document_set_ids: list[int],
|
||||
existing_persona_id: int | None = None,
|
||||
num_chunks: float = MAX_CHUNKS_FED_TO_CHAT,
|
||||
@ -90,6 +91,7 @@ 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(
|
||||
@ -115,12 +117,26 @@ 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()
|
||||
@ -164,12 +180,7 @@ 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(
|
||||
@ -177,20 +188,6 @@ 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
|
||||
@ -253,3 +250,32 @@ 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
|
||||
|
@ -3,9 +3,11 @@ 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",
|
||||
@ -17,18 +19,16 @@ def get_slack_channel_config_for_bot_and_channel(
|
||||
db_session: Session,
|
||||
slack_bot_id: int,
|
||||
channel_name: str | None,
|
||||
) -> SlackChannelConfig | None:
|
||||
if not channel_name:
|
||||
return None
|
||||
|
||||
slack_bot_configs = fetch_slack_channel_configs(
|
||||
db_session=db_session, slack_bot_id=slack_bot_id
|
||||
) -> 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
|
||||
)
|
||||
for config in slack_bot_configs:
|
||||
if channel_name in config.channel_config["channel_name"]:
|
||||
return config
|
||||
if not slack_bot_config:
|
||||
raise ValueError(
|
||||
"No default configuration has been set for this Slack bot. This should not be possible."
|
||||
)
|
||||
|
||||
return None
|
||||
return slack_bot_config
|
||||
|
||||
|
||||
def validate_channel_name(
|
||||
|
@ -106,7 +106,7 @@ def remove_scheduled_feedback_reminder(
|
||||
|
||||
def handle_message(
|
||||
message_info: SlackMessageInfo,
|
||||
slack_channel_config: SlackChannelConfig | None,
|
||||
slack_channel_config: SlackChannelConfig,
|
||||
client: WebClient,
|
||||
feedback_reminder_id: str | None,
|
||||
tenant_id: str | None,
|
||||
|
@ -64,7 +64,7 @@ def rate_limits(
|
||||
|
||||
def handle_regular_answer(
|
||||
message_info: SlackMessageInfo,
|
||||
slack_channel_config: SlackChannelConfig | None,
|
||||
slack_channel_config: SlackChannelConfig,
|
||||
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 if slack_channel_config else None
|
||||
channel_conf = slack_channel_config.channel_config
|
||||
|
||||
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 if slack_channel_config else None
|
||||
persona = slack_channel_config.persona
|
||||
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,11 +134,7 @@ def handle_regular_answer(
|
||||
single_message_history = slackify_message_thread(history_messages) or None
|
||||
|
||||
bypass_acl = False
|
||||
if (
|
||||
slack_channel_config
|
||||
and slack_channel_config.persona
|
||||
and slack_channel_config.persona.document_sets
|
||||
):
|
||||
if 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
|
||||
@ -190,11 +186,7 @@ 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
|
||||
if slack_channel_config is not None
|
||||
else False
|
||||
)
|
||||
auto_detect_filters = slack_channel_config.enable_auto_filters
|
||||
retrieval_details = RetrievalDetails(
|
||||
run_search=OptionalSearchSetting.ALWAYS,
|
||||
real_time=False,
|
||||
|
@ -14,7 +14,7 @@ logger = setup_logger()
|
||||
def handle_standard_answers(
|
||||
message_info: SlackMessageInfo,
|
||||
receiver_ids: list[str] | None,
|
||||
slack_channel_config: SlackChannelConfig | None,
|
||||
slack_channel_config: SlackChannelConfig,
|
||||
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 | None,
|
||||
slack_channel_config: SlackChannelConfig,
|
||||
prompt: Prompt | None,
|
||||
logger: OnyxLoggingAdapter,
|
||||
client: WebClient,
|
||||
|
@ -790,8 +790,7 @@ 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 (
|
||||
slack_channel_config is None
|
||||
and not respond_every_channel
|
||||
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)
|
||||
@ -801,8 +800,7 @@ def process_message(
|
||||
return
|
||||
|
||||
follow_up = bool(
|
||||
slack_channel_config
|
||||
and slack_channel_config.channel_config
|
||||
slack_channel_config.channel_config
|
||||
and slack_channel_config.channel_config.get("follow_up_tags")
|
||||
is not None
|
||||
)
|
||||
|
@ -215,6 +215,7 @@ 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(
|
||||
@ -237,6 +238,7 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
@ -279,3 +281,8 @@ class AllUsersResponse(BaseModel):
|
||||
accepted_pages: int
|
||||
invited_pages: int
|
||||
slack_users_pages: int
|
||||
|
||||
|
||||
class SlackChannel(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
|
@ -1,6 +1,10 @@
|
||||
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
|
||||
@ -12,6 +16,7 @@ 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
|
||||
@ -25,6 +30,7 @@ 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
|
||||
@ -48,12 +54,6 @@ 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,6 +108,12 @@ 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
|
||||
@ -120,11 +126,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)
|
||||
@ -235,6 +241,23 @@ 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",
|
||||
@ -315,3 +338,48 @@ 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)}"
|
||||
)
|
||||
|
@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { SlackBot } from "@/lib/types";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FiCheck, FiEdit, FiXCircle } from "react-icons/fi";
|
||||
import { FiEdit } from "react-icons/fi";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@ -13,6 +12,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { SlackBot } from "@/lib/types";
|
||||
|
||||
const NUM_IN_PAGE = 20;
|
||||
|
||||
@ -42,7 +43,7 @@ function ClickableTableRow({
|
||||
);
|
||||
}
|
||||
|
||||
export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
|
||||
export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
// sort by id for consistent ordering
|
||||
@ -67,8 +68,9 @@ export function 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>
|
||||
@ -85,21 +87,27 @@ export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
|
||||
{slackBot.name}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{slackBot.configs_count}</TableCell>
|
||||
<TableCell>
|
||||
{slackBot.enabled ? (
|
||||
<FiCheck className="text-emerald-600" size="18" />
|
||||
<Badge variant="success">Enabled</Badge>
|
||||
) : (
|
||||
<FiXCircle className="text-red-600" size="18" />
|
||||
<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 */}
|
||||
</TableCell>
|
||||
</ClickableTableRow>
|
||||
);
|
||||
})}
|
||||
{slackBots.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
colSpan={5}
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
Please add a New Slack Bot to begin chatting with Danswer!
|
||||
@ -128,4 +136,4 @@ export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ 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,
|
||||
@ -33,7 +34,9 @@ export const SlackTokensForm = ({
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
initialValues={{
|
||||
...initialValues,
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
bot_token: Yup.string().required(),
|
||||
app_token: Yup.string().required(),
|
||||
|
@ -14,8 +14,10 @@ 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;
|
||||
|
||||
@ -32,128 +34,147 @@ export function SlackChannelConfigsTable({
|
||||
}) {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
const defaultConfig = slackChannelConfigs.find((config) => config.is_default);
|
||||
const channelConfigs = slackChannelConfigs.filter(
|
||||
(config) => !config.is_default
|
||||
);
|
||||
|
||||
return (
|
||||
<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 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="mt-3 flex">
|
||||
<div className="mx-auto">
|
||||
<PageSelector
|
||||
totalPages={Math.ceil(slackChannelConfigs.length / numToDisplay)}
|
||||
currentPage={page}
|
||||
onPageChange={(newPage) => setPage(newPage)}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,21 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import { Formik } from "formik";
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import { Formik, Form, Field } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { DocumentSet, SlackChannelConfig } from "@/lib/types";
|
||||
import {
|
||||
DocumentSet,
|
||||
SlackChannelConfig,
|
||||
SlackBotResponseType,
|
||||
} 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 } from "./SlackChannelConfigFormFields";
|
||||
import {
|
||||
SlackChannelConfigFormFields,
|
||||
SlackChannelConfigFormFieldsProps,
|
||||
} from "./SlackChannelConfigFormFields";
|
||||
|
||||
export const SlackChannelConfigCreationForm = ({
|
||||
slack_bot_id,
|
||||
@ -33,6 +41,7 @@ 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;
|
||||
@ -46,13 +55,16 @@ export const SlackChannelConfigCreationForm = ({
|
||||
}, [personas]);
|
||||
|
||||
return (
|
||||
<CardSection className="max-w-4xl">
|
||||
<CardSection className="!px-12 max-w-4xl">
|
||||
{popup}
|
||||
|
||||
<Formik
|
||||
initialValues={{
|
||||
slack_bot_id: slack_bot_id,
|
||||
channel_name:
|
||||
existingSlackChannelConfig?.channel_config.channel_name || "",
|
||||
channel_name: isDefault
|
||||
? ""
|
||||
: existingSlackChannelConfig?.channel_config.channel_name || "",
|
||||
response_type: "citations" as SlackBotResponseType,
|
||||
answer_validity_check_enabled: (
|
||||
existingSlackChannelConfig?.channel_config?.answer_filters || []
|
||||
).includes("well_answered_postfilter"),
|
||||
@ -90,8 +102,6 @@ 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
|
||||
@ -102,10 +112,12 @@ export const SlackChannelConfigCreationForm = ({
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
slack_bot_id: Yup.number().required(),
|
||||
channel_name: Yup.string().required("Channel Name is required"),
|
||||
response_type: Yup.string()
|
||||
channel_name: isDefault
|
||||
? Yup.string()
|
||||
: Yup.string().required("Channel Name is required"),
|
||||
response_type: Yup.mixed<SlackBotResponseType>()
|
||||
.oneOf(["quotes", "citations"])
|
||||
.required("Response type is required"),
|
||||
.required(),
|
||||
answer_validity_check_enabled: Yup.boolean().required(),
|
||||
questionmark_prefilter_enabled: Yup.boolean().required(),
|
||||
respond_tag_only: Yup.boolean().required(),
|
||||
@ -159,6 +171,7 @@ 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) {
|
||||
@ -191,13 +204,22 @@ export const SlackChannelConfigCreationForm = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SlackChannelConfigFormFields
|
||||
isUpdate={isUpdate}
|
||||
documentSets={documentSets}
|
||||
searchEnabledAssistants={searchEnabledAssistants}
|
||||
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
{({ 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>
|
||||
)}
|
||||
</Formik>
|
||||
</CardSection>
|
||||
);
|
||||
|
@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { FieldArray, Form, useFormikContext, ErrorMessage } from "formik";
|
||||
import {
|
||||
FieldArray,
|
||||
Form,
|
||||
useFormikContext,
|
||||
ErrorMessage,
|
||||
Field,
|
||||
} from "formik";
|
||||
import { CCPairDescriptor, DocumentSet } from "@/lib/types";
|
||||
import {
|
||||
BooleanFormField,
|
||||
@ -31,9 +37,15 @@ 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";
|
||||
|
||||
interface SlackChannelConfigFormFieldsProps {
|
||||
export interface SlackChannelConfigFormFieldsProps {
|
||||
isUpdate: boolean;
|
||||
isDefault: boolean;
|
||||
documentSets: DocumentSet[];
|
||||
searchEnabledAssistants: Persona[];
|
||||
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
|
||||
@ -41,19 +53,23 @@ 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);
|
||||
|
||||
@ -152,11 +168,54 @@ export function SlackChannelConfigFormFields({
|
||||
);
|
||||
}, [documentSets]);
|
||||
|
||||
return (
|
||||
<Form className="px-6 max-w-4xl">
|
||||
<div className="pt-4 w-full">
|
||||
<TextFormField name="channel_name" label="Slack Channel Name:" />
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
<div className="space-y-2 mt-4">
|
||||
<Label>Knowledge Source</Label>
|
||||
<RadioGroup
|
||||
@ -170,7 +229,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
|
||||
@ -188,7 +247,6 @@ export function SlackChannelConfigFormFields({
|
||||
/>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{values.knowledge_source === "document_sets" &&
|
||||
documentSets.length > 0 && (
|
||||
<div className="mt-4">
|
||||
@ -281,7 +339,6 @@ export function SlackChannelConfigFormFields({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{values.knowledge_source === "assistant" && (
|
||||
<div className="mt-4">
|
||||
<SubLabel>
|
||||
@ -353,15 +410,15 @@ export function SlackChannelConfigFormFields({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<div className="mt-6">
|
||||
<AdvancedOptionsToggle
|
||||
showAdvancedOptions={showAdvancedOptions}
|
||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||
/>
|
||||
</div>
|
||||
{showAdvancedOptions && (
|
||||
<div className="mt-4">
|
||||
<div className="w-64 mb-4">
|
||||
<div className="mt-2 space-y-4">
|
||||
<div className="w-64">
|
||||
<SelectorFormField
|
||||
name="response_type"
|
||||
label="Answer Type"
|
||||
@ -380,83 +437,79 @@ 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"
|
||||
/>
|
||||
|
||||
<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 "Still need help?" 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">
|
||||
<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="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."
|
||||
}
|
||||
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 "Still need help?" 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..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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..."
|
||||
/>
|
||||
|
||||
<StandardAnswerCategoryDropdownField
|
||||
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
||||
@ -468,7 +521,7 @@ export function SlackChannelConfigFormFields({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex mt-2 gap-x-2 w-full justify-end flex">
|
||||
<div className="flex mt-8 gap-x-2 w-full justify-end">
|
||||
{shouldShowPrivacyAlert && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@ -518,13 +571,11 @@ export function SlackChannelConfigFormFields({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
<Button onClick={() => {}} type="submit">
|
||||
{isUpdate ? "Update" : "Create"}
|
||||
</Button>
|
||||
<Button type="submit">{isUpdate ? "Update" : "Create"}</Button>
|
||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -94,3 +94,17 @@ 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();
|
||||
});
|
||||
};
|
||||
|
@ -78,30 +78,6 @@ 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}
|
||||
|
@ -52,15 +52,19 @@ 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("");
|
||||
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleSelect = (option: StringOrNumberOption) => {
|
||||
@ -89,6 +93,10 @@ export function SearchMultiSelectDropdown({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm(initialSearchTerm);
|
||||
}, [initialSearchTerm]);
|
||||
|
||||
return (
|
||||
<div className="relative text-left w-full" ref={dropdownRef}>
|
||||
<div>
|
||||
@ -105,21 +113,21 @@ export function SearchMultiSelectDropdown({
|
||||
}
|
||||
}}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
className="inline-flex justify-between w-full px-4 py-2 text-sm bg-background border border-border rounded-md shadow-sm"
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-0 right-0 text-sm h-full px-2 border-l border-border"
|
||||
className="absolute top-0 right-0 text-sm h-full px-2 border-l border-gray-300"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<ChevronDownIcon className="my-auto w-4 h-4" />
|
||||
<ChevronDownIcon className="my-auto w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<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 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
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
@ -152,9 +160,9 @@ export function SearchMultiSelectDropdown({
|
||||
option.name.toLowerCase() === searchTerm.toLowerCase()
|
||||
) && (
|
||||
<>
|
||||
<div className="border-t border-border"></div>
|
||||
<div className="border-t border-gray-300"></div>
|
||||
<button
|
||||
className="w-full text-left flex items-center px-4 py-2 text-sm hover:bg-hover"
|
||||
className="w-full text-left flex items-center px-4 py-2 text-sm text-gray-800 hover:bg-gray-100"
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
onCreate(searchTerm);
|
||||
@ -162,7 +170,7 @@ export function SearchMultiSelectDropdown({
|
||||
setSearchTerm("");
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<PlusIcon className="w-4 h-4 mr-2 text-gray-600" />
|
||||
Create label "{searchTerm}"
|
||||
</button>
|
||||
</>
|
||||
@ -170,7 +178,7 @@ export function SearchMultiSelectDropdown({
|
||||
|
||||
{filteredOptions.length === 0 &&
|
||||
(!onCreate || searchTerm.trim() === "") && (
|
||||
<div className="px-4 py-2.5 text-sm text-text-muted">
|
||||
<div className="px-4 py-2.5 text-sm text-gray-500">
|
||||
No matches found
|
||||
</div>
|
||||
)}
|
||||
|
@ -10,7 +10,9 @@ const badgeVariants = cva(
|
||||
variant: {
|
||||
"agent-faded":
|
||||
"border-neutral-200 bg-neutral-100 text-neutral-600 hover:bg-neutral-200",
|
||||
agent: "border-agent bg-agent text-white hover:bg-agent-hover",
|
||||
agent:
|
||||
"border-orange-200 bg-orange-50 text-orange-600 hover:bg-orange-75 dark:bg-orange-900 dark:text-neutral-50 dark:hover:bg-orange-850",
|
||||
|
||||
canceled:
|
||||
"border-gray-200 bg-gray-50 text-gray-600 hover:bg-gray-75 dark:bg-gray-900 dark:text-neutral-50 dark:hover:bg-gray-850",
|
||||
orange:
|
||||
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
|
||||
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@ -58,8 +58,8 @@ const buttonVariants = cva(
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
xs: "h-8 px-3 py-1",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
sm: "h-9 px-3",
|
||||
lg: "h-11 px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
reverse: {
|
||||
@ -118,7 +118,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
return (
|
||||
<div className="relative group">
|
||||
{button}
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-neutral-800 text-white text-sm rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-neutral-800 text-white text-sm rounded-sm opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
|
||||
{tooltip}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -258,23 +258,34 @@ export type SlackBotResponseType = "quotes" | "citations";
|
||||
export interface SlackChannelConfig {
|
||||
id: number;
|
||||
slack_bot_id: number;
|
||||
persona_id: number | null;
|
||||
persona: Persona | null;
|
||||
channel_config: ChannelConfig;
|
||||
response_type: SlackBotResponseType;
|
||||
standard_answer_categories: StandardAnswerCategory[];
|
||||
enable_auto_filters: boolean;
|
||||
standard_answer_categories: StandardAnswerCategory[];
|
||||
is_default: boolean;
|
||||
}
|
||||
|
||||
export interface SlackBot {
|
||||
export interface SlackChannelDescriptor {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type SlackBot = {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
configs_count: number;
|
||||
|
||||
// tokens
|
||||
slack_channel_configs: Array<{
|
||||
id: number;
|
||||
is_default: boolean;
|
||||
channel_config: {
|
||||
channel_name: string;
|
||||
};
|
||||
}>;
|
||||
bot_token: string;
|
||||
app_token: string;
|
||||
}
|
||||
};
|
||||
|
||||
export interface SlackBotTokens {
|
||||
bot_token: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user