mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-30 04:31:49 +02: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(
|
def _handle_standard_answers(
|
||||||
message_info: SlackMessageInfo,
|
message_info: SlackMessageInfo,
|
||||||
receiver_ids: list[str] | None,
|
receiver_ids: list[str] | None,
|
||||||
slack_channel_config: SlackChannelConfig | None,
|
slack_channel_config: SlackChannelConfig,
|
||||||
prompt: Prompt | None,
|
prompt: Prompt | None,
|
||||||
logger: OnyxLoggingAdapter,
|
logger: OnyxLoggingAdapter,
|
||||||
client: WebClient,
|
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,
|
Returns True if standard answers are found to match the user's message and therefore,
|
||||||
we still need to respond to the users.
|
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
|
slack_thread_id = message_info.thread_to_respond
|
||||||
configured_standard_answer_categories = (
|
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(
|
configured_standard_answers = set(
|
||||||
[
|
[
|
||||||
|
@ -1 +1,2 @@
|
|||||||
SLACK_BOT_PERSONA_PREFIX = "__slack_bot_persona__"
|
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
|
"""NOTE: is a `TypedDict` so it can be used as a type hint for a JSONB column
|
||||||
in Postgres"""
|
in Postgres"""
|
||||||
|
|
||||||
channel_name: str
|
channel_name: str | None # None for default channel config
|
||||||
respond_tag_only: NotRequired[bool] # defaults to False
|
respond_tag_only: NotRequired[bool] # defaults to False
|
||||||
respond_to_bots: NotRequired[bool] # defaults to False
|
respond_to_bots: NotRequired[bool] # defaults to False
|
||||||
respond_member_group_list: NotRequired[list[str]]
|
respond_member_group_list: NotRequired[list[str]]
|
||||||
@ -1737,7 +1737,6 @@ class SlackChannelConfig(Base):
|
|||||||
persona_id: Mapped[int | None] = mapped_column(
|
persona_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey("persona.id"), nullable=True
|
ForeignKey("persona.id"), nullable=True
|
||||||
)
|
)
|
||||||
# JSON for flexibility. Contains things like: channel name, team members, etc.
|
|
||||||
channel_config: Mapped[ChannelConfig] = mapped_column(
|
channel_config: Mapped[ChannelConfig] = mapped_column(
|
||||||
postgresql.JSONB(), nullable=False
|
postgresql.JSONB(), nullable=False
|
||||||
)
|
)
|
||||||
@ -1746,6 +1745,8 @@ class SlackChannelConfig(Base):
|
|||||||
Boolean, nullable=False, default=False
|
Boolean, nullable=False, default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
persona: Mapped[Persona | None] = relationship("Persona")
|
persona: Mapped[Persona | None] = relationship("Persona")
|
||||||
slack_bot: Mapped["SlackBot"] = relationship(
|
slack_bot: Mapped["SlackBot"] = relationship(
|
||||||
"SlackBot",
|
"SlackBot",
|
||||||
@ -1757,6 +1758,21 @@ class SlackChannelConfig(Base):
|
|||||||
back_populates="slack_channel_configs",
|
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):
|
class SlackBot(Base):
|
||||||
__tablename__ = "slack_bot"
|
__tablename__ = "slack_bot"
|
||||||
|
@ -74,3 +74,15 @@ def remove_slack_bot(
|
|||||||
|
|
||||||
def fetch_slack_bots(db_session: Session) -> Sequence[SlackBot]:
|
def fetch_slack_bots(db_session: Session) -> Sequence[SlackBot]:
|
||||||
return db_session.scalars(select(SlackBot)).all()
|
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.configs.chat_configs import MAX_CHUNKS_FED_TO_CHAT
|
||||||
from onyx.context.search.enums import RecencyBiasSetting
|
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.constants import SLACK_BOT_PERSONA_PREFIX
|
||||||
from onyx.db.models import ChannelConfig
|
from onyx.db.models import ChannelConfig
|
||||||
from onyx.db.models import Persona
|
from onyx.db.models import Persona
|
||||||
@ -22,8 +23,8 @@ from onyx.utils.variable_functionality import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_persona_name(channel_name: str) -> str:
|
def _build_persona_name(channel_name: str | None) -> str:
|
||||||
return f"{SLACK_BOT_PERSONA_PREFIX}{channel_name}"
|
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:
|
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(
|
def create_slack_channel_persona(
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
channel_name: str,
|
channel_name: str | None,
|
||||||
document_set_ids: list[int],
|
document_set_ids: list[int],
|
||||||
existing_persona_id: int | None = None,
|
existing_persona_id: int | None = None,
|
||||||
num_chunks: float = MAX_CHUNKS_FED_TO_CHAT,
|
num_chunks: float = MAX_CHUNKS_FED_TO_CHAT,
|
||||||
@ -90,6 +91,7 @@ def insert_slack_channel_config(
|
|||||||
channel_config: ChannelConfig,
|
channel_config: ChannelConfig,
|
||||||
standard_answer_category_ids: list[int],
|
standard_answer_category_ids: list[int],
|
||||||
enable_auto_filters: bool,
|
enable_auto_filters: bool,
|
||||||
|
is_default: bool = False,
|
||||||
) -> SlackChannelConfig:
|
) -> SlackChannelConfig:
|
||||||
versioned_fetch_standard_answer_categories_by_ids = (
|
versioned_fetch_standard_answer_categories_by_ids = (
|
||||||
fetch_versioned_implementation_with_fallback(
|
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"
|
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_channel_config = SlackChannelConfig(
|
||||||
slack_bot_id=slack_bot_id,
|
slack_bot_id=slack_bot_id,
|
||||||
persona_id=persona_id,
|
persona_id=persona_id,
|
||||||
channel_config=channel_config,
|
channel_config=channel_config,
|
||||||
standard_answer_categories=existing_standard_answer_categories,
|
standard_answer_categories=existing_standard_answer_categories,
|
||||||
enable_auto_filters=enable_auto_filters,
|
enable_auto_filters=enable_auto_filters,
|
||||||
|
is_default=is_default,
|
||||||
)
|
)
|
||||||
db_session.add(slack_channel_config)
|
db_session.add(slack_channel_config)
|
||||||
db_session.commit()
|
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"
|
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
|
# 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.persona_id = persona_id
|
||||||
slack_channel_config.channel_config = channel_config
|
slack_channel_config.channel_config = channel_config
|
||||||
slack_channel_config.standard_answer_categories = list(
|
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
|
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()
|
db_session.commit()
|
||||||
|
|
||||||
return slack_channel_config
|
return slack_channel_config
|
||||||
@ -253,3 +250,32 @@ def fetch_slack_channel_config(
|
|||||||
SlackChannelConfig.id == slack_channel_config_id
|
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 sqlalchemy.orm import Session
|
||||||
|
|
||||||
from onyx.db.models import SlackChannelConfig
|
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
|
from onyx.db.slack_channel_config import fetch_slack_channel_configs
|
||||||
|
|
||||||
|
|
||||||
VALID_SLACK_FILTERS = [
|
VALID_SLACK_FILTERS = [
|
||||||
"answerable_prefilter",
|
"answerable_prefilter",
|
||||||
"well_answered_postfilter",
|
"well_answered_postfilter",
|
||||||
@ -17,18 +19,16 @@ def get_slack_channel_config_for_bot_and_channel(
|
|||||||
db_session: Session,
|
db_session: Session,
|
||||||
slack_bot_id: int,
|
slack_bot_id: int,
|
||||||
channel_name: str | None,
|
channel_name: str | None,
|
||||||
) -> SlackChannelConfig | None:
|
) -> SlackChannelConfig:
|
||||||
if not channel_name:
|
slack_bot_config = fetch_slack_channel_config_for_channel_or_default(
|
||||||
return None
|
db_session=db_session, slack_bot_id=slack_bot_id, channel_name=channel_name
|
||||||
|
|
||||||
slack_bot_configs = fetch_slack_channel_configs(
|
|
||||||
db_session=db_session, slack_bot_id=slack_bot_id
|
|
||||||
)
|
)
|
||||||
for config in slack_bot_configs:
|
if not slack_bot_config:
|
||||||
if channel_name in config.channel_config["channel_name"]:
|
raise ValueError(
|
||||||
return config
|
"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(
|
def validate_channel_name(
|
||||||
|
@ -106,7 +106,7 @@ def remove_scheduled_feedback_reminder(
|
|||||||
|
|
||||||
def handle_message(
|
def handle_message(
|
||||||
message_info: SlackMessageInfo,
|
message_info: SlackMessageInfo,
|
||||||
slack_channel_config: SlackChannelConfig | None,
|
slack_channel_config: SlackChannelConfig,
|
||||||
client: WebClient,
|
client: WebClient,
|
||||||
feedback_reminder_id: str | None,
|
feedback_reminder_id: str | None,
|
||||||
tenant_id: str | None,
|
tenant_id: str | None,
|
||||||
|
@ -64,7 +64,7 @@ def rate_limits(
|
|||||||
|
|
||||||
def handle_regular_answer(
|
def handle_regular_answer(
|
||||||
message_info: SlackMessageInfo,
|
message_info: SlackMessageInfo,
|
||||||
slack_channel_config: SlackChannelConfig | None,
|
slack_channel_config: SlackChannelConfig,
|
||||||
receiver_ids: list[str] | None,
|
receiver_ids: list[str] | None,
|
||||||
client: WebClient,
|
client: WebClient,
|
||||||
channel: str,
|
channel: str,
|
||||||
@ -76,7 +76,7 @@ def handle_regular_answer(
|
|||||||
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
|
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
|
||||||
disable_docs_only_answer: bool = DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER,
|
disable_docs_only_answer: bool = DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER,
|
||||||
) -> bool:
|
) -> 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
|
messages = message_info.thread_messages
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ def handle_regular_answer(
|
|||||||
prompt = None
|
prompt = None
|
||||||
# If no persona is specified, use the default search based persona
|
# If no persona is specified, use the default search based persona
|
||||||
# This way slack flow always has a 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:
|
if not persona:
|
||||||
with get_session_with_tenant(tenant_id) as db_session:
|
with get_session_with_tenant(tenant_id) as db_session:
|
||||||
persona = get_persona_by_id(DEFAULT_PERSONA_ID, user, 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
|
single_message_history = slackify_message_thread(history_messages) or None
|
||||||
|
|
||||||
bypass_acl = False
|
bypass_acl = False
|
||||||
if (
|
if slack_channel_config.persona and slack_channel_config.persona.document_sets:
|
||||||
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
|
# For Slack channels, use the full document set, admin will be warned when configuring it
|
||||||
# with non-public document sets
|
# with non-public document sets
|
||||||
bypass_acl = True
|
bypass_acl = True
|
||||||
@ -190,11 +186,7 @@ def handle_regular_answer(
|
|||||||
# auto_detect_filters = (
|
# auto_detect_filters = (
|
||||||
# persona.llm_filter_extraction if persona is not None else True
|
# persona.llm_filter_extraction if persona is not None else True
|
||||||
# )
|
# )
|
||||||
auto_detect_filters = (
|
auto_detect_filters = slack_channel_config.enable_auto_filters
|
||||||
slack_channel_config.enable_auto_filters
|
|
||||||
if slack_channel_config is not None
|
|
||||||
else False
|
|
||||||
)
|
|
||||||
retrieval_details = RetrievalDetails(
|
retrieval_details = RetrievalDetails(
|
||||||
run_search=OptionalSearchSetting.ALWAYS,
|
run_search=OptionalSearchSetting.ALWAYS,
|
||||||
real_time=False,
|
real_time=False,
|
||||||
|
@ -14,7 +14,7 @@ logger = setup_logger()
|
|||||||
def handle_standard_answers(
|
def handle_standard_answers(
|
||||||
message_info: SlackMessageInfo,
|
message_info: SlackMessageInfo,
|
||||||
receiver_ids: list[str] | None,
|
receiver_ids: list[str] | None,
|
||||||
slack_channel_config: SlackChannelConfig | None,
|
slack_channel_config: SlackChannelConfig,
|
||||||
prompt: Prompt | None,
|
prompt: Prompt | None,
|
||||||
logger: OnyxLoggingAdapter,
|
logger: OnyxLoggingAdapter,
|
||||||
client: WebClient,
|
client: WebClient,
|
||||||
@ -40,7 +40,7 @@ def handle_standard_answers(
|
|||||||
def _handle_standard_answers(
|
def _handle_standard_answers(
|
||||||
message_info: SlackMessageInfo,
|
message_info: SlackMessageInfo,
|
||||||
receiver_ids: list[str] | None,
|
receiver_ids: list[str] | None,
|
||||||
slack_channel_config: SlackChannelConfig | None,
|
slack_channel_config: SlackChannelConfig,
|
||||||
prompt: Prompt | None,
|
prompt: Prompt | None,
|
||||||
logger: OnyxLoggingAdapter,
|
logger: OnyxLoggingAdapter,
|
||||||
client: WebClient,
|
client: WebClient,
|
||||||
|
@ -790,8 +790,7 @@ def process_message(
|
|||||||
# Be careful about this default, don't want to accidentally spam every channel
|
# 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
|
# Users should be able to DM slack bot in their private channels though
|
||||||
if (
|
if (
|
||||||
slack_channel_config is None
|
not respond_every_channel
|
||||||
and not respond_every_channel
|
|
||||||
# Can't have configs for DMs so don't toss them out
|
# Can't have configs for DMs so don't toss them out
|
||||||
and not is_dm
|
and not is_dm
|
||||||
# If /OnyxBot (is_bot_msg) or @OnyxBot (bypass_filters)
|
# If /OnyxBot (is_bot_msg) or @OnyxBot (bypass_filters)
|
||||||
@ -801,8 +800,7 @@ def process_message(
|
|||||||
return
|
return
|
||||||
|
|
||||||
follow_up = bool(
|
follow_up = bool(
|
||||||
slack_channel_config
|
slack_channel_config.channel_config
|
||||||
and slack_channel_config.channel_config
|
|
||||||
and slack_channel_config.channel_config.get("follow_up_tags")
|
and slack_channel_config.channel_config.get("follow_up_tags")
|
||||||
is not None
|
is not None
|
||||||
)
|
)
|
||||||
|
@ -215,6 +215,7 @@ class SlackChannelConfig(BaseModel):
|
|||||||
# XXX this is going away soon
|
# XXX this is going away soon
|
||||||
standard_answer_categories: list[StandardAnswerCategory]
|
standard_answer_categories: list[StandardAnswerCategory]
|
||||||
enable_auto_filters: bool
|
enable_auto_filters: bool
|
||||||
|
is_default: bool
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_model(
|
def from_model(
|
||||||
@ -237,6 +238,7 @@ class SlackChannelConfig(BaseModel):
|
|||||||
for standard_answer_category_model in slack_channel_config_model.standard_answer_categories
|
for standard_answer_category_model in slack_channel_config_model.standard_answer_categories
|
||||||
],
|
],
|
||||||
enable_auto_filters=slack_channel_config_model.enable_auto_filters,
|
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
|
accepted_pages: int
|
||||||
invited_pages: int
|
invited_pages: int
|
||||||
slack_users_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 APIRouter
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
from slack_sdk import WebClient
|
||||||
|
from slack_sdk.errors import SlackApiError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from onyx.auth.users import current_admin_user
|
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.models import User
|
||||||
from onyx.db.persona import get_persona_by_id
|
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
|
||||||
|
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 fetch_slack_bots
|
||||||
from onyx.db.slack_bot import insert_slack_bot
|
from onyx.db.slack_bot import insert_slack_bot
|
||||||
from onyx.db.slack_bot import remove_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.onyxbot.slack.config import validate_channel_name
|
||||||
from onyx.server.manage.models import SlackBot
|
from onyx.server.manage.models import SlackBot
|
||||||
from onyx.server.manage.models import SlackBotCreationRequest
|
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 SlackChannelConfig
|
||||||
from onyx.server.manage.models import SlackChannelConfigCreationRequest
|
from onyx.server.manage.models import SlackChannelConfigCreationRequest
|
||||||
from onyx.server.manage.validate_tokens import validate_app_token
|
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
|
answer_filters = slack_channel_config_creation_request.answer_filters
|
||||||
follow_up_tags = slack_channel_config_creation_request.follow_up_tags
|
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:
|
try:
|
||||||
cleaned_channel_name = validate_channel_name(
|
cleaned_channel_name = validate_channel_name(
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
@ -108,6 +108,12 @@ def create_slack_channel_config(
|
|||||||
current_slack_channel_config_id=None,
|
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
|
persona_id = None
|
||||||
if slack_channel_config_creation_request.persona_id is not None:
|
if slack_channel_config_creation_request.persona_id is not None:
|
||||||
persona_id = slack_channel_config_creation_request.persona_id
|
persona_id = slack_channel_config_creation_request.persona_id
|
||||||
@ -120,11 +126,11 @@ def create_slack_channel_config(
|
|||||||
).id
|
).id
|
||||||
|
|
||||||
slack_channel_config_model = insert_slack_channel_config(
|
slack_channel_config_model = insert_slack_channel_config(
|
||||||
|
db_session=db_session,
|
||||||
slack_bot_id=slack_channel_config_creation_request.slack_bot_id,
|
slack_bot_id=slack_channel_config_creation_request.slack_bot_id,
|
||||||
persona_id=persona_id,
|
persona_id=persona_id,
|
||||||
channel_config=channel_config,
|
channel_config=channel_config,
|
||||||
standard_answer_category_ids=slack_channel_config_creation_request.standard_answer_categories,
|
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,
|
enable_auto_filters=slack_channel_config_creation_request.enable_auto_filters,
|
||||||
)
|
)
|
||||||
return SlackChannelConfig.from_model(slack_channel_config_model)
|
return SlackChannelConfig.from_model(slack_channel_config_model)
|
||||||
@ -235,6 +241,23 @@ def create_bot(
|
|||||||
app_token=slack_bot_creation_request.app_token,
|
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(
|
create_milestone_and_report(
|
||||||
user=None,
|
user=None,
|
||||||
distinct_id=tenant_id or "N/A",
|
distinct_id=tenant_id or "N/A",
|
||||||
@ -315,3 +338,48 @@ def list_bot_configs(
|
|||||||
SlackChannelConfig.from_model(slack_bot_config_model)
|
SlackChannelConfig.from_model(slack_bot_config_model)
|
||||||
for slack_bot_config_model in slack_bot_config_models
|
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";
|
"use client";
|
||||||
|
|
||||||
import { PageSelector } from "@/components/PageSelector";
|
import { PageSelector } from "@/components/PageSelector";
|
||||||
import { SlackBot } from "@/lib/types";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FiCheck, FiEdit, FiXCircle } from "react-icons/fi";
|
import { FiEdit } from "react-icons/fi";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@ -13,6 +12,8 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { SlackBot } from "@/lib/types";
|
||||||
|
|
||||||
const NUM_IN_PAGE = 20;
|
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);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
// sort by id for consistent ordering
|
// sort by id for consistent ordering
|
||||||
@ -67,8 +68,9 @@ export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Default Config</TableHead>
|
||||||
<TableHead>Channel Count</TableHead>
|
<TableHead>Channel Count</TableHead>
|
||||||
<TableHead>Enabled</TableHead>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -85,21 +87,27 @@ export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
|
|||||||
{slackBot.name}
|
{slackBot.name}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{slackBot.configs_count}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{slackBot.enabled ? (
|
{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>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant="secondary">Default Set</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{slackBot.configs_count}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{/* Add any action buttons here if needed */}
|
||||||
|
</TableCell>
|
||||||
</ClickableTableRow>
|
</ClickableTableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{slackBots.length === 0 && (
|
{slackBots.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={4}
|
colSpan={5}
|
||||||
className="text-center text-muted-foreground"
|
className="text-center text-muted-foreground"
|
||||||
>
|
>
|
||||||
Please add a New Slack Bot to begin chatting with Danswer!
|
Please add a New Slack Bot to begin chatting with Danswer!
|
||||||
@ -128,4 +136,4 @@ export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
@ -7,6 +7,7 @@ import { createSlackBot, updateSlackBot } from "./new/lib";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
export const SlackTokensForm = ({
|
export const SlackTokensForm = ({
|
||||||
isUpdate,
|
isUpdate,
|
||||||
@ -33,7 +34,9 @@ export const SlackTokensForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={{
|
||||||
|
...initialValues,
|
||||||
|
}}
|
||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
bot_token: Yup.string().required(),
|
bot_token: Yup.string().required(),
|
||||||
app_token: Yup.string().required(),
|
app_token: Yup.string().required(),
|
||||||
|
@ -14,8 +14,10 @@ import {
|
|||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FiArrowUpRight } from "react-icons/fi";
|
|
||||||
import { deleteSlackChannelConfig, isPersonaASlackBotPersona } from "./lib";
|
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;
|
const numToDisplay = 50;
|
||||||
|
|
||||||
@ -32,128 +34,147 @@ export function SlackChannelConfigsTable({
|
|||||||
}) {
|
}) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
// sort by name for consistent ordering
|
const defaultConfig = slackChannelConfigs.find((config) => config.is_default);
|
||||||
slackChannelConfigs.sort((a, b) => {
|
const channelConfigs = slackChannelConfigs.filter(
|
||||||
if (a.id < b.id) {
|
(config) => !config.is_default
|
||||||
return -1;
|
);
|
||||||
} else if (a.id > b.id) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-8">
|
||||||
<div className="rounded-md border">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<Table>
|
<Button
|
||||||
<TableHeader>
|
variant="outline"
|
||||||
<TableRow>
|
onClick={() => {
|
||||||
<TableHead>Channel</TableHead>
|
window.location.href = `/admin/bots/${slackBotId}/channels/${defaultConfig?.id}`;
|
||||||
<TableHead>Assistant</TableHead>
|
}}
|
||||||
<TableHead>Document Sets</TableHead>
|
>
|
||||||
<TableHead>Delete</TableHead>
|
<FiSettings />
|
||||||
</TableRow>
|
Edit Default Config
|
||||||
</TableHeader>
|
</Button>
|
||||||
<TableBody>
|
<Link href={`/admin/bots/${slackBotId}/channels/new`}>
|
||||||
{slackChannelConfigs
|
<Button variant="outline">
|
||||||
.slice(numToDisplay * (page - 1), numToDisplay * page)
|
<FiPlusSquare />
|
||||||
.map((slackChannelConfig) => {
|
New Channel Configuration
|
||||||
return (
|
</Button>
|
||||||
<TableRow
|
</Link>
|
||||||
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>
|
||||||
|
|
||||||
<div className="mt-3 flex">
|
<div>
|
||||||
<div className="mx-auto">
|
<h2 className="text-2xl font- mb-4">Channel-Specific Configurations</h2>
|
||||||
<PageSelector
|
<Card>
|
||||||
totalPages={Math.ceil(slackChannelConfigs.length / numToDisplay)}
|
<Table>
|
||||||
currentPage={page}
|
<TableHeader>
|
||||||
onPageChange={(newPage) => setPage(newPage)}
|
<TableRow>
|
||||||
/>
|
<TableHead>Channel</TableHead>
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,21 +1,29 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo, useState, useEffect } from "react";
|
||||||
import { Formik } from "formik";
|
import { Formik, Form, Field } from "formik";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
import { DocumentSet, SlackChannelConfig } from "@/lib/types";
|
import {
|
||||||
|
DocumentSet,
|
||||||
|
SlackChannelConfig,
|
||||||
|
SlackBotResponseType,
|
||||||
|
} from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
createSlackChannelConfig,
|
createSlackChannelConfig,
|
||||||
isPersonaASlackBotPersona,
|
isPersonaASlackBotPersona,
|
||||||
updateSlackChannelConfig,
|
updateSlackChannelConfig,
|
||||||
|
fetchSlackChannels,
|
||||||
} from "../lib";
|
} from "../lib";
|
||||||
import CardSection from "@/components/admin/CardSection";
|
import CardSection from "@/components/admin/CardSection";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||||
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
|
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
|
||||||
import { SEARCH_TOOL_ID, SEARCH_TOOL_NAME } from "@/app/chat/tools/constants";
|
import { SEARCH_TOOL_ID, SEARCH_TOOL_NAME } from "@/app/chat/tools/constants";
|
||||||
import { SlackChannelConfigFormFields } from "./SlackChannelConfigFormFields";
|
import {
|
||||||
|
SlackChannelConfigFormFields,
|
||||||
|
SlackChannelConfigFormFieldsProps,
|
||||||
|
} from "./SlackChannelConfigFormFields";
|
||||||
|
|
||||||
export const SlackChannelConfigCreationForm = ({
|
export const SlackChannelConfigCreationForm = ({
|
||||||
slack_bot_id,
|
slack_bot_id,
|
||||||
@ -33,6 +41,7 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
const { popup, setPopup } = usePopup();
|
const { popup, setPopup } = usePopup();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isUpdate = Boolean(existingSlackChannelConfig);
|
const isUpdate = Boolean(existingSlackChannelConfig);
|
||||||
|
const isDefault = existingSlackChannelConfig?.is_default || false;
|
||||||
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
|
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
|
||||||
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
|
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
|
||||||
: false;
|
: false;
|
||||||
@ -46,13 +55,16 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
}, [personas]);
|
}, [personas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardSection className="max-w-4xl">
|
<CardSection className="!px-12 max-w-4xl">
|
||||||
{popup}
|
{popup}
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
slack_bot_id: slack_bot_id,
|
slack_bot_id: slack_bot_id,
|
||||||
channel_name:
|
channel_name: isDefault
|
||||||
existingSlackChannelConfig?.channel_config.channel_name || "",
|
? ""
|
||||||
|
: existingSlackChannelConfig?.channel_config.channel_name || "",
|
||||||
|
response_type: "citations" as SlackBotResponseType,
|
||||||
answer_validity_check_enabled: (
|
answer_validity_check_enabled: (
|
||||||
existingSlackChannelConfig?.channel_config?.answer_filters || []
|
existingSlackChannelConfig?.channel_config?.answer_filters || []
|
||||||
).includes("well_answered_postfilter"),
|
).includes("well_answered_postfilter"),
|
||||||
@ -90,8 +102,6 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
!isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
|
!isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
|
||||||
? existingSlackChannelConfig.persona.id
|
? existingSlackChannelConfig.persona.id
|
||||||
: null,
|
: null,
|
||||||
response_type:
|
|
||||||
existingSlackChannelConfig?.response_type || "citations",
|
|
||||||
standard_answer_categories:
|
standard_answer_categories:
|
||||||
existingSlackChannelConfig?.standard_answer_categories || [],
|
existingSlackChannelConfig?.standard_answer_categories || [],
|
||||||
knowledge_source: existingSlackBotUsesPersona
|
knowledge_source: existingSlackBotUsesPersona
|
||||||
@ -102,10 +112,12 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
}}
|
}}
|
||||||
validationSchema={Yup.object().shape({
|
validationSchema={Yup.object().shape({
|
||||||
slack_bot_id: Yup.number().required(),
|
slack_bot_id: Yup.number().required(),
|
||||||
channel_name: Yup.string().required("Channel Name is required"),
|
channel_name: isDefault
|
||||||
response_type: Yup.string()
|
? Yup.string()
|
||||||
|
: Yup.string().required("Channel Name is required"),
|
||||||
|
response_type: Yup.mixed<SlackBotResponseType>()
|
||||||
.oneOf(["quotes", "citations"])
|
.oneOf(["quotes", "citations"])
|
||||||
.required("Response type is required"),
|
.required(),
|
||||||
answer_validity_check_enabled: Yup.boolean().required(),
|
answer_validity_check_enabled: Yup.boolean().required(),
|
||||||
questionmark_prefilter_enabled: Yup.boolean().required(),
|
questionmark_prefilter_enabled: Yup.boolean().required(),
|
||||||
respond_tag_only: Yup.boolean().required(),
|
respond_tag_only: Yup.boolean().required(),
|
||||||
@ -159,6 +171,7 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
standard_answer_categories: values.standard_answer_categories.map(
|
standard_answer_categories: values.standard_answer_categories.map(
|
||||||
(category: any) => category.id
|
(category: any) => category.id
|
||||||
),
|
),
|
||||||
|
response_type: values.response_type as SlackBotResponseType,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!cleanedValues.still_need_help_enabled) {
|
if (!cleanedValues.still_need_help_enabled) {
|
||||||
@ -191,13 +204,22 @@ export const SlackChannelConfigCreationForm = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SlackChannelConfigFormFields
|
{({ isSubmitting, values, setFieldValue }) => (
|
||||||
isUpdate={isUpdate}
|
<Form>
|
||||||
documentSets={documentSets}
|
<div className="pb-6 w-full">
|
||||||
searchEnabledAssistants={searchEnabledAssistants}
|
<SlackChannelConfigFormFields
|
||||||
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
{...values}
|
||||||
setPopup={setPopup}
|
isUpdate={isUpdate}
|
||||||
/>
|
isDefault={isDefault}
|
||||||
|
documentSets={documentSets}
|
||||||
|
searchEnabledAssistants={searchEnabledAssistants}
|
||||||
|
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
||||||
|
setPopup={setPopup}
|
||||||
|
slack_bot_id={slack_bot_id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardSection>
|
</CardSection>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
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 { CCPairDescriptor, DocumentSet } from "@/lib/types";
|
||||||
import {
|
import {
|
||||||
BooleanFormField,
|
BooleanFormField,
|
||||||
@ -31,9 +37,15 @@ import { TooltipProvider } from "@radix-ui/react-tooltip";
|
|||||||
import { SourceIcon } from "@/components/SourceIcon";
|
import { SourceIcon } from "@/components/SourceIcon";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
|
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;
|
isUpdate: boolean;
|
||||||
|
isDefault: boolean;
|
||||||
documentSets: DocumentSet[];
|
documentSets: DocumentSet[];
|
||||||
searchEnabledAssistants: Persona[];
|
searchEnabledAssistants: Persona[];
|
||||||
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
|
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
|
||||||
@ -41,19 +53,23 @@ interface SlackChannelConfigFormFieldsProps {
|
|||||||
message: string;
|
message: string;
|
||||||
type: "error" | "success" | "warning";
|
type: "error" | "success" | "warning";
|
||||||
}) => void;
|
}) => void;
|
||||||
|
slack_bot_id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SlackChannelConfigFormFields({
|
export function SlackChannelConfigFormFields({
|
||||||
isUpdate,
|
isUpdate,
|
||||||
|
isDefault,
|
||||||
documentSets,
|
documentSets,
|
||||||
searchEnabledAssistants,
|
searchEnabledAssistants,
|
||||||
standardAnswerCategoryResponse,
|
standardAnswerCategoryResponse,
|
||||||
setPopup,
|
setPopup,
|
||||||
|
slack_bot_id,
|
||||||
}: SlackChannelConfigFormFieldsProps) {
|
}: SlackChannelConfigFormFieldsProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { values, setFieldValue } = useFormikContext<any>();
|
const { values, setFieldValue } = useFormikContext<any>();
|
||||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||||
const [viewUnselectableSets, setViewUnselectableSets] = useState(false);
|
const [viewUnselectableSets, setViewUnselectableSets] = useState(false);
|
||||||
|
const [currentSearchTerm, setCurrentSearchTerm] = useState("");
|
||||||
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
|
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@ -152,11 +168,54 @@ export function SlackChannelConfigFormFields({
|
|||||||
);
|
);
|
||||||
}, [documentSets]);
|
}, [documentSets]);
|
||||||
|
|
||||||
return (
|
const { data: channelOptions, isLoading } = useSWR(
|
||||||
<Form className="px-6 max-w-4xl">
|
`/api/manage/admin/slack-app/bots/${slack_bot_id}/channels`,
|
||||||
<div className="pt-4 w-full">
|
async (url: string) => {
|
||||||
<TextFormField name="channel_name" label="Slack Channel Name:" />
|
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">
|
<div className="space-y-2 mt-4">
|
||||||
<Label>Knowledge Source</Label>
|
<Label>Knowledge Source</Label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
@ -170,7 +229,7 @@ export function SlackChannelConfigFormFields({
|
|||||||
value="all_public"
|
value="all_public"
|
||||||
id="all_public"
|
id="all_public"
|
||||||
label="All Public Knowledge"
|
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 && (
|
{selectableSets.length + unselectableSets.length > 0 && (
|
||||||
<RadioGroupItemField
|
<RadioGroupItemField
|
||||||
@ -188,7 +247,6 @@ export function SlackChannelConfigFormFields({
|
|||||||
/>
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{values.knowledge_source === "document_sets" &&
|
{values.knowledge_source === "document_sets" &&
|
||||||
documentSets.length > 0 && (
|
documentSets.length > 0 && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@ -281,7 +339,6 @@ export function SlackChannelConfigFormFields({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{values.knowledge_source === "assistant" && (
|
{values.knowledge_source === "assistant" && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<SubLabel>
|
<SubLabel>
|
||||||
@ -353,15 +410,15 @@ export function SlackChannelConfigFormFields({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2">
|
<div className="mt-6">
|
||||||
<AdvancedOptionsToggle
|
<AdvancedOptionsToggle
|
||||||
showAdvancedOptions={showAdvancedOptions}
|
showAdvancedOptions={showAdvancedOptions}
|
||||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{showAdvancedOptions && (
|
{showAdvancedOptions && (
|
||||||
<div className="mt-4">
|
<div className="mt-2 space-y-4">
|
||||||
<div className="w-64 mb-4">
|
<div className="w-64">
|
||||||
<SelectorFormField
|
<SelectorFormField
|
||||||
name="response_type"
|
name="response_type"
|
||||||
label="Answer 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"
|
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
|
||||||
<BooleanFormField
|
name="still_need_help_enabled"
|
||||||
name="still_need_help_enabled"
|
removeIndent
|
||||||
removeIndent
|
onChange={(checked: boolean) => {
|
||||||
onChange={(checked: boolean) => {
|
setFieldValue("still_need_help_enabled", checked);
|
||||||
setFieldValue("still_need_help_enabled", checked);
|
if (!checked) {
|
||||||
if (!checked) {
|
setFieldValue("follow_up_tags", []);
|
||||||
setFieldValue("follow_up_tags", []);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
label={'Give a "Still need help?" button'}
|
||||||
label={'Give a "Still need help?" button'}
|
tooltip={`OnyxBot's response will include a button at the bottom
|
||||||
tooltip={`OnyxBot's response will include a button at the bottom
|
of the response that asks the user if they still need help.`}
|
||||||
of the response that asks the user if they still need help.`}
|
/>
|
||||||
/>
|
{values.still_need_help_enabled && (
|
||||||
{values.still_need_help_enabled && (
|
<CollapsibleSection prompt="Configure Still Need Help Button">
|
||||||
<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">
|
|
||||||
<TextArrayField
|
<TextArrayField
|
||||||
name="respond_member_group_list"
|
name="follow_up_tags"
|
||||||
label="(Optional) Respond to Certain Users / Groups"
|
label="(Optional) Users / Groups to Tag"
|
||||||
subtext={
|
|
||||||
"If specified, OnyxBot responses will only " +
|
|
||||||
"be visible to the members or groups in this list."
|
|
||||||
}
|
|
||||||
values={values}
|
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..."
|
placeholder="User email or user group name..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</CollapsibleSection>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
<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
|
<StandardAnswerCategoryDropdownField
|
||||||
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
|
||||||
@ -468,7 +521,7 @@ export function SlackChannelConfigFormFields({
|
|||||||
</div>
|
</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 && (
|
{shouldShowPrivacyAlert && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -518,13 +571,11 @@ export function SlackChannelConfigFormFields({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
<Button onClick={() => {}} type="submit">
|
<Button type="submit">{isUpdate ? "Update" : "Create"}</Button>
|
||||||
{isUpdate ? "Update" : "Create"}
|
|
||||||
</Button>
|
|
||||||
<Button type="button" variant="outline" onClick={() => router.back()}>
|
<Button type="button" variant="outline" onClick={() => router.back()}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -94,3 +94,17 @@ export const deleteSlackChannelConfig = async (id: number) => {
|
|||||||
export function isPersonaASlackBotPersona(persona: Persona) {
|
export function isPersonaASlackBotPersona(persona: Persona) {
|
||||||
return persona.name.startsWith("__slack_bot_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 />
|
<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">
|
<div className="mt-8">
|
||||||
<SlackChannelConfigsTable
|
<SlackChannelConfigsTable
|
||||||
slackBotId={slackBot.id}
|
slackBotId={slackBot.id}
|
||||||
|
@ -52,15 +52,19 @@ export function SearchMultiSelectDropdown({
|
|||||||
itemComponent,
|
itemComponent,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onSearchTermChange,
|
||||||
|
initialSearchTerm = "",
|
||||||
}: {
|
}: {
|
||||||
options: StringOrNumberOption[];
|
options: StringOrNumberOption[];
|
||||||
onSelect: (selected: StringOrNumberOption) => void;
|
onSelect: (selected: StringOrNumberOption) => void;
|
||||||
itemComponent?: FC<{ option: StringOrNumberOption }>;
|
itemComponent?: FC<{ option: StringOrNumberOption }>;
|
||||||
onCreate?: (name: string) => void;
|
onCreate?: (name: string) => void;
|
||||||
onDelete?: (name: string) => void;
|
onDelete?: (name: string) => void;
|
||||||
|
onSearchTermChange?: (term: string) => void;
|
||||||
|
initialSearchTerm?: string;
|
||||||
}) {
|
}) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const handleSelect = (option: StringOrNumberOption) => {
|
const handleSelect = (option: StringOrNumberOption) => {
|
||||||
@ -89,6 +93,10 @@ export function SearchMultiSelectDropdown({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSearchTerm(initialSearchTerm);
|
||||||
|
}, [initialSearchTerm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative text-left w-full" ref={dropdownRef}>
|
<div className="relative text-left w-full" ref={dropdownRef}>
|
||||||
<div>
|
<div>
|
||||||
@ -105,21 +113,21 @@ export function SearchMultiSelectDropdown({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={() => setIsOpen(true)}
|
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
|
<button
|
||||||
type="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-expanded={isOpen}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon className="my-auto w-4 h-4" />
|
<ChevronDownIcon className="my-auto w-4 h-4 text-gray-600" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOpen && (
|
{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
|
<div
|
||||||
role="menu"
|
role="menu"
|
||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
@ -152,9 +160,9 @@ export function SearchMultiSelectDropdown({
|
|||||||
option.name.toLowerCase() === searchTerm.toLowerCase()
|
option.name.toLowerCase() === searchTerm.toLowerCase()
|
||||||
) && (
|
) && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t border-border"></div>
|
<div className="border-t border-gray-300"></div>
|
||||||
<button
|
<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"
|
role="menuitem"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onCreate(searchTerm);
|
onCreate(searchTerm);
|
||||||
@ -162,7 +170,7 @@ export function SearchMultiSelectDropdown({
|
|||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="w-4 h-4 mr-2" />
|
<PlusIcon className="w-4 h-4 mr-2 text-gray-600" />
|
||||||
Create label "{searchTerm}"
|
Create label "{searchTerm}"
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
@ -170,7 +178,7 @@ export function SearchMultiSelectDropdown({
|
|||||||
|
|
||||||
{filteredOptions.length === 0 &&
|
{filteredOptions.length === 0 &&
|
||||||
(!onCreate || searchTerm.trim() === "") && (
|
(!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
|
No matches found
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -10,7 +10,9 @@ const badgeVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
"agent-faded":
|
"agent-faded":
|
||||||
"border-neutral-200 bg-neutral-100 text-neutral-600 hover:bg-neutral-200",
|
"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:
|
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",
|
"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:
|
orange:
|
||||||
|
@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@ -58,8 +58,8 @@ const buttonVariants = cva(
|
|||||||
size: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-10 px-4 py-2",
|
||||||
xs: "h-8 px-3 py-1",
|
xs: "h-8 px-3 py-1",
|
||||||
sm: "h-9 rounded-md px-3",
|
sm: "h-9 px-3",
|
||||||
lg: "h-11 rounded-md px-8",
|
lg: "h-11 px-8",
|
||||||
icon: "h-10 w-10",
|
icon: "h-10 w-10",
|
||||||
},
|
},
|
||||||
reverse: {
|
reverse: {
|
||||||
@ -118,7 +118,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
{button}
|
{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}
|
{tooltip}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -258,23 +258,34 @@ export type SlackBotResponseType = "quotes" | "citations";
|
|||||||
export interface SlackChannelConfig {
|
export interface SlackChannelConfig {
|
||||||
id: number;
|
id: number;
|
||||||
slack_bot_id: number;
|
slack_bot_id: number;
|
||||||
|
persona_id: number | null;
|
||||||
persona: Persona | null;
|
persona: Persona | null;
|
||||||
channel_config: ChannelConfig;
|
channel_config: ChannelConfig;
|
||||||
response_type: SlackBotResponseType;
|
|
||||||
standard_answer_categories: StandardAnswerCategory[];
|
|
||||||
enable_auto_filters: boolean;
|
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;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
configs_count: number;
|
configs_count: number;
|
||||||
|
slack_channel_configs: Array<{
|
||||||
// tokens
|
id: number;
|
||||||
|
is_default: boolean;
|
||||||
|
channel_config: {
|
||||||
|
channel_name: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
bot_token: string;
|
bot_token: string;
|
||||||
app_token: string;
|
app_token: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface SlackBotTokens {
|
export interface SlackBotTokens {
|
||||||
bot_token: string;
|
bot_token: string;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user