multiple slackbot support (#3077)

* multiple slackbot support

* app_id + tenant_id key

* removed kv store stuff

* fixed up mypy and migration

* got frontend working for multiple slack bots

* some frontend stuff

* alembic fix

* might be valid

* refactor dun

* alembic stuff

* temp frontend stuff

* alembic stuff

* maybe fixed alembic

* maybe dis fix

* im getting mad

* api names changed

* tested

* almost done

* done

* routing nonsense

* done!

* done!!

* fr done

* doneski

* fix alembic migration

* getting mad again

* PLEASE IM BEGGING YOU
This commit is contained in:
hagen-danswer 2024-11-19 17:49:43 -08:00 committed by GitHub
parent b712877701
commit 9209fc804b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 2422 additions and 1026 deletions

View File

@ -135,7 +135,7 @@ Looking to contribute? Please check out the [Contribution Guide](CONTRIBUTING.md
## ✨Contributors
<a href="https://github.com/aryn-ai/sycamore/graphs/contributors">
<a href="https://github.com/danswer-ai/danswer/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=danswer-ai/danswer"/>
</a>

View File

@ -0,0 +1,280 @@
"""add_multiple_slack_bot_support
Revision ID: 4ee1287bd26a
Revises: 47e5bef3a1d7
Create Date: 2024-11-06 13:15:53.302644
"""
import logging
from typing import cast
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import Session
from danswer.key_value_store.factory import get_kv_store
from danswer.db.models import SlackBot
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "4ee1287bd26a"
down_revision = "47e5bef3a1d7"
branch_labels: None = None
depends_on: None = None
# Configure logging
logger = logging.getLogger("alembic.runtime.migration")
logger.setLevel(logging.INFO)
def upgrade() -> None:
logger.info(f"{revision}: create_table: slack_bot")
# Create new slack_bot table
op.create_table(
"slack_bot",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default="true"),
sa.Column("bot_token", sa.LargeBinary(), nullable=False),
sa.Column("app_token", sa.LargeBinary(), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("bot_token"),
sa.UniqueConstraint("app_token"),
)
# # Create new slack_channel_config table
op.create_table(
"slack_channel_config",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("slack_bot_id", sa.Integer(), nullable=True),
sa.Column("persona_id", sa.Integer(), nullable=True),
sa.Column("channel_config", postgresql.JSONB(), nullable=False),
sa.Column("response_type", sa.String(), nullable=False),
sa.Column(
"enable_auto_filters", sa.Boolean(), nullable=False, server_default="false"
),
sa.ForeignKeyConstraint(
["slack_bot_id"],
["slack_bot.id"],
),
sa.ForeignKeyConstraint(
["persona_id"],
["persona.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# Handle existing Slack bot tokens first
logger.info(f"{revision}: Checking for existing Slack bot.")
bot_token = None
app_token = None
first_row_id = None
try:
tokens = cast(dict, get_kv_store().load("slack_bot_tokens_config_key"))
except Exception:
logger.warning("No existing Slack bot tokens found.")
tokens = {}
bot_token = tokens.get("bot_token")
app_token = tokens.get("app_token")
if bot_token and app_token:
logger.info(f"{revision}: Found bot and app tokens.")
session = Session(bind=op.get_bind())
new_slack_bot = SlackBot(
name="Slack Bot (Migrated)",
enabled=True,
bot_token=bot_token,
app_token=app_token,
)
session.add(new_slack_bot)
session.commit()
first_row_id = new_slack_bot.id
# Create a default bot if none exists
# This is in case there are no slack tokens but there are channels configured
op.execute(
sa.text(
"""
INSERT INTO slack_bot (name, enabled, bot_token, app_token)
SELECT 'Default Bot', true, '', ''
WHERE NOT EXISTS (SELECT 1 FROM slack_bot)
RETURNING id;
"""
)
)
# Get the bot ID to use (either from existing migration or newly created)
bot_id_query = sa.text(
"""
SELECT COALESCE(
:first_row_id,
(SELECT id FROM slack_bot ORDER BY id ASC LIMIT 1)
) as bot_id;
"""
)
result = op.get_bind().execute(bot_id_query, {"first_row_id": first_row_id})
bot_id = result.scalar()
# CTE (Common Table Expression) that transforms the old slack_bot_config table data
# This splits up the channel_names into their own rows
channel_names_cte = """
WITH channel_names AS (
SELECT
sbc.id as config_id,
sbc.persona_id,
sbc.response_type,
sbc.enable_auto_filters,
jsonb_array_elements_text(sbc.channel_config->'channel_names') as channel_name,
sbc.channel_config->>'respond_tag_only' as respond_tag_only,
sbc.channel_config->>'respond_to_bots' as respond_to_bots,
sbc.channel_config->'respond_member_group_list' as respond_member_group_list,
sbc.channel_config->'answer_filters' as answer_filters,
sbc.channel_config->'follow_up_tags' as follow_up_tags
FROM slack_bot_config sbc
)
"""
# Insert the channel names into the new slack_channel_config table
insert_statement = """
INSERT INTO slack_channel_config (
slack_bot_id,
persona_id,
channel_config,
response_type,
enable_auto_filters
)
SELECT
:bot_id,
channel_name.persona_id,
jsonb_build_object(
'channel_name', channel_name.channel_name,
'respond_tag_only',
COALESCE((channel_name.respond_tag_only)::boolean, false),
'respond_to_bots',
COALESCE((channel_name.respond_to_bots)::boolean, false),
'respond_member_group_list',
COALESCE(channel_name.respond_member_group_list, '[]'::jsonb),
'answer_filters',
COALESCE(channel_name.answer_filters, '[]'::jsonb),
'follow_up_tags',
COALESCE(channel_name.follow_up_tags, '[]'::jsonb)
),
channel_name.response_type,
channel_name.enable_auto_filters
FROM channel_names channel_name;
"""
op.execute(sa.text(channel_names_cte + insert_statement).bindparams(bot_id=bot_id))
# Clean up old tokens if they existed
try:
if bot_token and app_token:
logger.info(f"{revision}: Removing old bot and app tokens.")
get_kv_store().delete("slack_bot_tokens_config_key")
except Exception:
logger.warning("tried to delete tokens in dynamic config but failed")
# Rename the table
op.rename_table(
"slack_bot_config__standard_answer_category",
"slack_channel_config__standard_answer_category",
)
# Rename the column
op.alter_column(
"slack_channel_config__standard_answer_category",
"slack_bot_config_id",
new_column_name="slack_channel_config_id",
)
# Drop the table with CASCADE to handle dependent objects
op.execute("DROP TABLE slack_bot_config CASCADE")
logger.info(f"{revision}: Migration complete.")
def downgrade() -> None:
# Recreate the old slack_bot_config table
op.create_table(
"slack_bot_config",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("persona_id", sa.Integer(), nullable=True),
sa.Column("channel_config", postgresql.JSONB(), nullable=False),
sa.Column("response_type", sa.String(), nullable=False),
sa.Column("enable_auto_filters", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(
["persona_id"],
["persona.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# Migrate data back to the old format
# Group by persona_id to combine channel names back into arrays
op.execute(
sa.text(
"""
INSERT INTO slack_bot_config (
persona_id,
channel_config,
response_type,
enable_auto_filters
)
SELECT DISTINCT ON (persona_id)
persona_id,
jsonb_build_object(
'channel_names', (
SELECT jsonb_agg(c.channel_config->>'channel_name')
FROM slack_channel_config c
WHERE c.persona_id = scc.persona_id
),
'respond_tag_only', (channel_config->>'respond_tag_only')::boolean,
'respond_to_bots', (channel_config->>'respond_to_bots')::boolean,
'respond_member_group_list', channel_config->'respond_member_group_list',
'answer_filters', channel_config->'answer_filters',
'follow_up_tags', channel_config->'follow_up_tags'
),
response_type,
enable_auto_filters
FROM slack_channel_config scc
WHERE persona_id IS NOT NULL;
"""
)
)
# Rename the table back
op.rename_table(
"slack_channel_config__standard_answer_category",
"slack_bot_config__standard_answer_category",
)
# Rename the column back
op.alter_column(
"slack_bot_config__standard_answer_category",
"slack_channel_config_id",
new_column_name="slack_bot_config_id",
)
# Try to save the first bot's tokens back to KV store
try:
first_bot = (
op.get_bind()
.execute(
sa.text(
"SELECT bot_token, app_token FROM slack_bot ORDER BY id LIMIT 1"
)
)
.first()
)
if first_bot and first_bot.bot_token and first_bot.app_token:
tokens = {
"bot_token": first_bot.bot_token,
"app_token": first_bot.app_token,
}
get_kv_store().store("slack_bot_tokens_config_key", tokens)
except Exception:
logger.warning("Failed to save tokens back to KV store")
# Drop the new tables in reverse order
op.drop_table("slack_channel_config")
op.drop_table("slack_bot")

View File

@ -7,6 +7,7 @@ Create Date: 2024-10-26 13:06:06.937969
"""
from alembic import op
from sqlalchemy.orm import Session
from sqlalchemy import text
# Import your models and constants
from danswer.db.models import (
@ -15,7 +16,6 @@ from danswer.db.models import (
Credential,
IndexAttempt,
)
from danswer.configs.constants import DocumentSource
# revision identifiers, used by Alembic.
@ -30,13 +30,11 @@ def upgrade() -> None:
bind = op.get_bind()
session = Session(bind=bind)
connectors_to_delete = (
session.query(Connector)
.filter(Connector.source == DocumentSource.REQUESTTRACKER)
.all()
# Get connectors using raw SQL
result = bind.execute(
text("SELECT id FROM connector WHERE source = 'requesttracker'")
)
connector_ids = [connector.id for connector in connectors_to_delete]
connector_ids = [row[0] for row in result]
if connector_ids:
cc_pairs_to_delete = (

View File

@ -1,7 +1,7 @@
"""add creator to cc pair
Revision ID: 9cf5c00f72fe
Revises: c0fd6e4da83a
Revises: 26b931506ecb
Create Date: 2024-11-12 15:16:42.682902
"""

View File

@ -60,7 +60,6 @@ KV_GMAIL_CRED_KEY = "gmail_app_credential"
KV_GMAIL_SERVICE_ACCOUNT_KEY = "gmail_service_account_key"
KV_GOOGLE_DRIVE_CRED_KEY = "google_drive_app_credential"
KV_GOOGLE_DRIVE_SERVICE_ACCOUNT_KEY = "google_drive_service_account_key"
KV_SLACK_BOT_TOKENS_CONFIG_KEY = "slack_bot_tokens_config_key"
KV_GEN_AI_KEY_CHECK_TIME = "genai_api_key_last_check_time"
KV_SETTINGS_KEY = "danswer_settings"
KV_CUSTOMER_UUID_KEY = "customer_uuid"

View File

@ -5,9 +5,9 @@ from io import BytesIO
from typing import Any
from typing import Optional
import boto3
from botocore.client import Config
from mypy_boto3_s3 import S3Client
import boto3 # type: ignore
from botocore.client import Config # type: ignore
from mypy_boto3_s3 import S3Client # type: ignore
from danswer.configs.app_configs import INDEX_BATCH_SIZE
from danswer.configs.constants import BlobType

View File

@ -2,8 +2,8 @@ import os
from sqlalchemy.orm import Session
from danswer.db.models import SlackBotConfig
from danswer.db.slack_bot_config import fetch_slack_bot_configs
from danswer.db.models import SlackChannelConfig
from danswer.db.slack_channel_config import fetch_slack_channel_configs
VALID_SLACK_FILTERS = [
@ -13,46 +13,52 @@ VALID_SLACK_FILTERS = [
]
def get_slack_bot_config_for_channel(
channel_name: str | None, db_session: Session
) -> SlackBotConfig | None:
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_bot_configs(db_session=db_session)
slack_bot_configs = fetch_slack_channel_configs(
db_session=db_session, slack_bot_id=slack_bot_id
)
for config in slack_bot_configs:
if channel_name in config.channel_config["channel_names"]:
if channel_name in config.channel_config["channel_name"]:
return config
return None
def validate_channel_names(
channel_names: list[str],
current_slack_bot_config_id: int | None,
def validate_channel_name(
db_session: Session,
) -> list[str]:
"""Make sure that these channel_names don't exist in other slack bot configs.
Returns a list of cleaned up channel names (e.g. '#' removed if present)"""
slack_bot_configs = fetch_slack_bot_configs(db_session=db_session)
cleaned_channel_names = [
channel_name.lstrip("#").lower() for channel_name in channel_names
]
for slack_bot_config in slack_bot_configs:
if slack_bot_config.id == current_slack_bot_config_id:
current_slack_bot_id: int,
channel_name: str,
current_slack_channel_config_id: int | None,
) -> str:
"""Make sure that this channel_name does not exist in other Slack channel configs.
Returns a cleaned up channel name (e.g. '#' removed if present)"""
slack_bot_configs = fetch_slack_channel_configs(
db_session=db_session,
slack_bot_id=current_slack_bot_id,
)
cleaned_channel_name = channel_name.lstrip("#").lower()
for slack_channel_config in slack_bot_configs:
if slack_channel_config.id == current_slack_channel_config_id:
continue
for channel_name in cleaned_channel_names:
if channel_name in slack_bot_config.channel_config["channel_names"]:
raise ValueError(
f"Channel name '{channel_name}' already exists in "
"another slack bot config"
)
if cleaned_channel_name == slack_channel_config.channel_config["channel_name"]:
raise ValueError(
f"Channel name '{channel_name}' already exists in "
"another Slack channel config with in Slack Bot with name: "
f"{slack_channel_config.slack_bot.name}"
)
return cleaned_channel_names
return cleaned_channel_name
# Scaling configurations for multi-tenant Slack bot handling
# Scaling configurations for multi-tenant Slack channel handling
TENANT_LOCK_EXPIRATION = 1800 # How long a pod can hold exclusive access to a tenant before other pods can acquire it
TENANT_HEARTBEAT_INTERVAL = (
15 # How often pods send heartbeats to indicate they are still processing a tenant

View File

@ -13,7 +13,7 @@ from danswer.connectors.slack.utils import expert_info_from_slack_id
from danswer.connectors.slack.utils import make_slack_api_rate_limited
from danswer.danswerbot.slack.blocks import build_follow_up_resolved_blocks
from danswer.danswerbot.slack.blocks import get_document_feedback_blocks
from danswer.danswerbot.slack.config import get_slack_bot_config_for_channel
from danswer.danswerbot.slack.config import get_slack_channel_config_for_bot_and_channel
from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID
from danswer.danswerbot.slack.constants import FeedbackVisibility
from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID
@ -117,8 +117,10 @@ def handle_generate_answer_button(
)
with get_session_with_tenant(client.tenant_id) as db_session:
slack_bot_config = get_slack_bot_config_for_channel(
channel_name=channel_name, db_session=db_session
slack_channel_config = get_slack_channel_config_for_bot_and_channel(
db_session=db_session,
slack_bot_id=client.slack_bot_id,
channel_name=channel_name,
)
handle_regular_answer(
@ -133,7 +135,7 @@ def handle_generate_answer_button(
is_bot_msg=False,
is_bot_dm=False,
),
slack_bot_config=slack_bot_config,
slack_channel_config=slack_channel_config,
receiver_ids=None,
client=client.web_client,
tenant_id=client.tenant_id,
@ -256,11 +258,13 @@ def handle_followup_button(
channel_name, is_dm = get_channel_name_from_id(
client=client.web_client, channel_id=channel_id
)
slack_bot_config = get_slack_bot_config_for_channel(
channel_name=channel_name, db_session=db_session
slack_channel_config = get_slack_channel_config_for_bot_and_channel(
db_session=db_session,
slack_bot_id=client.slack_bot_id,
channel_name=channel_name,
)
if slack_bot_config:
tag_names = slack_bot_config.channel_config.get("follow_up_tags")
if slack_channel_config:
tag_names = slack_channel_config.channel_config.get("follow_up_tags")
remaining = None
if tag_names:
tag_ids, remaining = fetch_user_ids_from_emails(

View File

@ -19,7 +19,7 @@ from danswer.danswerbot.slack.utils import respond_in_thread
from danswer.danswerbot.slack.utils import slack_usage_report
from danswer.danswerbot.slack.utils import update_emote_react
from danswer.db.engine import get_session_with_tenant
from danswer.db.models import SlackBotConfig
from danswer.db.models import SlackChannelConfig
from danswer.db.users import add_slack_user_if_not_exists
from danswer.utils.logger import setup_logger
from shared_configs.configs import SLACK_CHANNEL_ID
@ -106,7 +106,7 @@ def remove_scheduled_feedback_reminder(
def handle_message(
message_info: SlackMessageInfo,
slack_bot_config: SlackBotConfig | None,
slack_channel_config: SlackChannelConfig | None,
client: WebClient,
feedback_reminder_id: str | None,
tenant_id: str | None,
@ -140,7 +140,7 @@ def handle_message(
)
document_set_names: list[str] | None = None
persona = slack_bot_config.persona if slack_bot_config else None
persona = slack_channel_config.persona if slack_channel_config else None
prompt = None
if persona:
document_set_names = [
@ -152,8 +152,8 @@ def handle_message(
respond_member_group_list = None
channel_conf = None
if slack_bot_config and slack_bot_config.channel_config:
channel_conf = slack_bot_config.channel_config
if slack_channel_config and slack_channel_config.channel_config:
channel_conf = slack_channel_config.channel_config
if not bypass_filters and "answer_filters" in channel_conf:
if (
"questionmark_prefilter" in channel_conf["answer_filters"]
@ -219,7 +219,7 @@ def handle_message(
used_standard_answer = handle_standard_answers(
message_info=message_info,
receiver_ids=send_to,
slack_bot_config=slack_bot_config,
slack_channel_config=slack_channel_config,
prompt=prompt,
logger=logger,
client=client,
@ -231,7 +231,7 @@ def handle_message(
# if no standard answer applies, try a regular answer
issue_with_regular_answer = handle_regular_answer(
message_info=message_info,
slack_bot_config=slack_bot_config,
slack_channel_config=slack_channel_config,
receiver_ids=send_to,
client=client,
channel=channel,

View File

@ -34,8 +34,8 @@ from danswer.danswerbot.slack.utils import SlackRateLimiter
from danswer.danswerbot.slack.utils import update_emote_react
from danswer.db.engine import get_session_with_tenant
from danswer.db.models import Persona
from danswer.db.models import SlackBotConfig
from danswer.db.models import SlackBotResponseType
from danswer.db.models import SlackChannelConfig
from danswer.db.persona import fetch_persona_by_id
from danswer.db.search_settings import get_current_search_settings
from danswer.db.users import get_user_by_email
@ -81,7 +81,7 @@ def rate_limits(
def handle_regular_answer(
message_info: SlackMessageInfo,
slack_bot_config: SlackBotConfig | None,
slack_channel_config: SlackChannelConfig | None,
receiver_ids: list[str] | None,
client: WebClient,
channel: str,
@ -96,7 +96,7 @@ def handle_regular_answer(
disable_cot: bool = DANSWER_BOT_DISABLE_COT,
reflexion: bool = ENABLE_DANSWERBOT_REFLEXION,
) -> bool:
channel_conf = slack_bot_config.channel_config if slack_bot_config else None
channel_conf = slack_channel_config.channel_config if slack_channel_config else None
messages = message_info.thread_messages
message_ts_to_respond_to = message_info.msg_to_respond
@ -108,7 +108,7 @@ def handle_regular_answer(
user = get_user_by_email(message_info.email, db_session)
document_set_names: list[str] | None = None
persona = slack_bot_config.persona if slack_bot_config else None
persona = slack_channel_config.persona if slack_channel_config else None
prompt = None
if persona:
document_set_names = [
@ -120,9 +120,9 @@ def handle_regular_answer(
bypass_acl = False
if (
slack_bot_config
and slack_bot_config.persona
and slack_bot_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
# with non-public document sets
@ -131,8 +131,8 @@ def handle_regular_answer(
# figure out if we want to use citations or quotes
use_citations = (
not DANSWER_BOT_USE_QUOTES
if slack_bot_config is None
else slack_bot_config.response_type == SlackBotResponseType.CITATIONS
if slack_channel_config is None
else slack_channel_config.response_type == SlackBotResponseType.CITATIONS
)
if not message_ts_to_respond_to and not is_bot_msg:
@ -234,8 +234,8 @@ def handle_regular_answer(
# persona.llm_filter_extraction if persona is not None else True
# )
auto_detect_filters = (
slack_bot_config.enable_auto_filters
if slack_bot_config is not None
slack_channel_config.enable_auto_filters
if slack_channel_config is not None
else False
)
retrieval_details = RetrievalDetails(

View File

@ -3,7 +3,7 @@ from sqlalchemy.orm import Session
from danswer.danswerbot.slack.models import SlackMessageInfo
from danswer.db.models import Prompt
from danswer.db.models import SlackBotConfig
from danswer.db.models import SlackChannelConfig
from danswer.utils.logger import DanswerLoggingAdapter
from danswer.utils.logger import setup_logger
from danswer.utils.variable_functionality import fetch_versioned_implementation
@ -14,7 +14,7 @@ logger = setup_logger()
def handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_bot_config: SlackBotConfig | None,
slack_channel_config: SlackChannelConfig | None,
prompt: Prompt | None,
logger: DanswerLoggingAdapter,
client: WebClient,
@ -29,7 +29,7 @@ def handle_standard_answers(
return versioned_handle_standard_answers(
message_info=message_info,
receiver_ids=receiver_ids,
slack_bot_config=slack_bot_config,
slack_channel_config=slack_channel_config,
prompt=prompt,
logger=logger,
client=client,
@ -40,7 +40,7 @@ def handle_standard_answers(
def _handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_bot_config: SlackBotConfig | None,
slack_channel_config: SlackChannelConfig | None,
prompt: Prompt | None,
logger: DanswerLoggingAdapter,
client: WebClient,

View File

@ -4,6 +4,7 @@ import signal
import sys
import threading
import time
from collections.abc import Callable
from threading import Event
from types import FrameType
from typing import Any
@ -16,6 +17,7 @@ from prometheus_client import start_http_server
from slack_sdk import WebClient
from slack_sdk.socket_mode.request import SocketModeRequest
from slack_sdk.socket_mode.response import SocketModeResponse
from sqlalchemy.orm import Session
from danswer.configs.app_configs import POD_NAME
from danswer.configs.app_configs import POD_NAMESPACE
@ -25,7 +27,7 @@ from danswer.configs.danswerbot_configs import DANSWER_BOT_REPHRASE_MESSAGE
from danswer.configs.danswerbot_configs import DANSWER_BOT_RESPOND_EVERY_CHANNEL
from danswer.configs.danswerbot_configs import NOTIFY_SLACKBOT_NO_ANSWER
from danswer.connectors.slack.utils import expert_info_from_slack_id
from danswer.danswerbot.slack.config import get_slack_bot_config_for_channel
from danswer.danswerbot.slack.config import get_slack_channel_config_for_bot_and_channel
from danswer.danswerbot.slack.config import MAX_TENANTS_PER_POD
from danswer.danswerbot.slack.config import TENANT_ACQUISITION_INTERVAL
from danswer.danswerbot.slack.config import TENANT_HEARTBEAT_EXPIRATION
@ -54,20 +56,20 @@ from danswer.danswerbot.slack.handlers.handle_message import (
)
from danswer.danswerbot.slack.handlers.handle_message import schedule_feedback_reminder
from danswer.danswerbot.slack.models import SlackMessageInfo
from danswer.danswerbot.slack.tokens import fetch_tokens
from danswer.danswerbot.slack.utils import check_message_limit
from danswer.danswerbot.slack.utils import decompose_action_id
from danswer.danswerbot.slack.utils import get_channel_name_from_id
from danswer.danswerbot.slack.utils import get_danswer_bot_app_id
from danswer.danswerbot.slack.utils import get_danswer_bot_slack_bot_id
from danswer.danswerbot.slack.utils import read_slack_thread
from danswer.danswerbot.slack.utils import remove_danswer_bot_tag
from danswer.danswerbot.slack.utils import rephrase_slack_message
from danswer.danswerbot.slack.utils import respond_in_thread
from danswer.danswerbot.slack.utils import TenantSocketModeClient
from danswer.db.engine import CURRENT_TENANT_ID_CONTEXTVAR
from danswer.db.engine import get_all_tenant_ids
from danswer.db.engine import get_session_with_tenant
from danswer.db.models import SlackBot
from danswer.db.search_settings import get_current_search_settings
from danswer.db.slack_bot import fetch_slack_bots
from danswer.key_value_store.interface import KvKeyNotFoundError
from danswer.natural_language_processing.search_nlp_models import EmbeddingModel
from danswer.natural_language_processing.search_nlp_models import warm_up_bi_encoder
@ -82,6 +84,8 @@ from shared_configs.configs import MODEL_SERVER_HOST
from shared_configs.configs import MODEL_SERVER_PORT
from shared_configs.configs import POSTGRES_DEFAULT_SCHEMA
from shared_configs.configs import SLACK_CHANNEL_ID
from shared_configs.contextvars import CURRENT_TENANT_ID_CONTEXTVAR
logger = setup_logger()
@ -113,8 +117,10 @@ class SlackbotHandler:
def __init__(self) -> None:
logger.info("Initializing SlackbotHandler")
self.tenant_ids: Set[str | None] = set()
self.socket_clients: Dict[str | None, TenantSocketModeClient] = {}
self.slack_bot_tokens: Dict[str | None, SlackBotTokens] = {}
# The keys for these dictionaries are tuples of (tenant_id, slack_bot_id)
self.socket_clients: Dict[tuple[str | None, int], TenantSocketModeClient] = {}
self.slack_bot_tokens: Dict[tuple[str | None, int], SlackBotTokens] = {}
self.running = True
self.pod_id = self.get_pod_id()
self._shutdown_event = Event()
@ -169,6 +175,50 @@ class SlackbotHandler:
logger.exception(f"Error in heartbeat loop: {e}")
self._shutdown_event.wait(timeout=TENANT_HEARTBEAT_INTERVAL)
def _manage_clients_per_tenant(
self, db_session: Session, tenant_id: str | None, bot: SlackBot
) -> None:
slack_bot_tokens = SlackBotTokens(
bot_token=bot.bot_token,
app_token=bot.app_token,
)
tenant_bot_pair = (tenant_id, bot.id)
# If the tokens are not set, we need to close the socket client and delete the tokens
# for the tenant and app
if not slack_bot_tokens:
logger.debug(
f"No Slack bot token found for tenant {tenant_id}, bot {bot.id}"
)
if tenant_bot_pair in self.socket_clients:
asyncio.run(self.socket_clients[tenant_bot_pair].close())
del self.socket_clients[tenant_bot_pair]
del self.slack_bot_tokens[tenant_bot_pair]
return
tokens_exist = tenant_bot_pair in self.slack_bot_tokens
tokens_changed = slack_bot_tokens != self.slack_bot_tokens[tenant_bot_pair]
if not tokens_exist or tokens_changed:
if tokens_exist:
logger.info(
f"Slack Bot tokens have changed for tenant {tenant_id}, bot {bot.id} - reconnecting"
)
else:
search_settings = get_current_search_settings(db_session)
embedding_model = EmbeddingModel.from_db_model(
search_settings=search_settings,
server_host=MODEL_SERVER_HOST,
server_port=MODEL_SERVER_PORT,
)
warm_up_bi_encoder(embedding_model=embedding_model)
self.slack_bot_tokens[tenant_bot_pair] = slack_bot_tokens
if tenant_bot_pair in self.socket_clients:
asyncio.run(self.socket_clients[tenant_bot_pair].close())
self.start_socket_client(bot.id, tenant_id, slack_bot_tokens)
def acquire_tenants(self) -> None:
tenant_ids = get_all_tenant_ids()
@ -203,6 +253,7 @@ class SlackbotHandler:
continue
logger.debug(f"Acquired lock for tenant {tenant_id}")
self.tenant_ids.add(tenant_id)
for tenant_id in self.tenant_ids:
@ -212,57 +263,20 @@ class SlackbotHandler:
try:
with get_session_with_tenant(tenant_id) as db_session:
try:
logger.debug(
f"Setting tenant ID context variable for tenant {tenant_id}"
)
slack_bot_tokens = fetch_tokens()
logger.debug(f"Fetched Slack bot tokens for tenant {tenant_id}")
logger.debug(
f"Reset tenant ID context variable for tenant {tenant_id}"
)
if not slack_bot_tokens:
logger.debug(
f"No Slack bot token found for tenant {tenant_id}"
bots = fetch_slack_bots(db_session=db_session)
for bot in bots:
self._manage_clients_per_tenant(
db_session=db_session,
tenant_id=tenant_id,
bot=bot,
)
if tenant_id in self.socket_clients:
asyncio.run(self.socket_clients[tenant_id].close())
del self.socket_clients[tenant_id]
del self.slack_bot_tokens[tenant_id]
continue
if (
tenant_id not in self.slack_bot_tokens
or slack_bot_tokens != self.slack_bot_tokens[tenant_id]
):
if tenant_id in self.slack_bot_tokens:
logger.info(
f"Slack Bot tokens have changed for tenant {tenant_id} - reconnecting"
)
else:
search_settings = get_current_search_settings(
db_session
)
embedding_model = EmbeddingModel.from_db_model(
search_settings=search_settings,
server_host=MODEL_SERVER_HOST,
server_port=MODEL_SERVER_PORT,
)
warm_up_bi_encoder(embedding_model=embedding_model)
self.slack_bot_tokens[tenant_id] = slack_bot_tokens
if self.socket_clients.get(tenant_id):
asyncio.run(self.socket_clients[tenant_id].close())
self.start_socket_client(tenant_id, slack_bot_tokens)
except KvKeyNotFoundError:
logger.debug(f"Missing Slack Bot tokens for tenant {tenant_id}")
if self.socket_clients.get(tenant_id):
asyncio.run(self.socket_clients[tenant_id].close())
del self.socket_clients[tenant_id]
del self.slack_bot_tokens[tenant_id]
if (tenant_id, bot.id) in self.socket_clients:
asyncio.run(self.socket_clients[tenant_id, bot.id].close())
del self.socket_clients[tenant_id, bot.id]
del self.slack_bot_tokens[tenant_id, bot.id]
except Exception as e:
logger.exception(f"Error handling tenant {tenant_id}: {e}")
finally:
@ -281,26 +295,37 @@ class SlackbotHandler:
)
def start_socket_client(
self, tenant_id: str | None, slack_bot_tokens: SlackBotTokens
self, slack_bot_id: int, tenant_id: str | None, slack_bot_tokens: SlackBotTokens
) -> None:
logger.info(f"Starting socket client for tenant {tenant_id}")
socket_client = _get_socket_client(slack_bot_tokens, tenant_id)
logger.info(
f"Starting socket client for tenant: {tenant_id}, app: {slack_bot_id}"
)
socket_client: TenantSocketModeClient = _get_socket_client(
slack_bot_tokens, tenant_id, slack_bot_id
)
# Append the event handler
process_slack_event = create_process_slack_event()
socket_client.socket_mode_request_listeners.append(process_slack_event) # type: ignore
# Establish a WebSocket connection to the Socket Mode servers
logger.info(f"Connecting socket client for tenant {tenant_id}")
logger.info(
f"Connecting socket client for tenant: {tenant_id}, app: {slack_bot_id}"
)
socket_client.connect()
self.socket_clients[tenant_id] = socket_client
logger.info(f"Started SocketModeClient for tenant {tenant_id}")
self.socket_clients[tenant_id, slack_bot_id] = socket_client
self.tenant_ids.add(tenant_id)
logger.info(
f"Started SocketModeClient for tenant: {tenant_id}, app: {slack_bot_id}"
)
def stop_socket_clients(self) -> None:
logger.info(f"Stopping {len(self.socket_clients)} socket clients")
for tenant_id, client in self.socket_clients.items():
if client:
asyncio.run(client.close())
logger.info(f"Stopped SocketModeClient for tenant {tenant_id}")
for (tenant_id, slack_bot_id), client in self.socket_clients.items():
asyncio.run(client.close())
logger.info(
f"Stopped SocketModeClient for tenant: {tenant_id}, app: {slack_bot_id}"
)
def shutdown(self, signum: int | None, frame: FrameType | None) -> None:
if not self.running:
@ -384,7 +409,7 @@ def prefilter_requests(req: SocketModeRequest, client: TenantSocketModeClient) -
)
return False
bot_tag_id = get_danswer_bot_app_id(client.web_client)
bot_tag_id = get_danswer_bot_slack_bot_id(client.web_client)
if event_type == "message":
is_dm = event.get("channel_type") == "im"
is_tagged = bot_tag_id and bot_tag_id in msg
@ -407,13 +432,15 @@ def prefilter_requests(req: SocketModeRequest, client: TenantSocketModeClient) -
)
with get_session_with_tenant(client.tenant_id) as db_session:
slack_bot_config = get_slack_bot_config_for_channel(
channel_name=channel_name, db_session=db_session
slack_channel_config = get_slack_channel_config_for_bot_and_channel(
db_session=db_session,
slack_bot_id=client.slack_bot_id,
channel_name=channel_name,
)
# If DanswerBot is not specifically tagged and the channel is not set to respond to bots, ignore the message
if (not bot_tag_id or bot_tag_id not in msg) and (
not slack_bot_config
or not slack_bot_config.channel_config.get("respond_to_bots")
not slack_channel_config
or not slack_channel_config.channel_config.get("respond_to_bots")
):
channel_specific_logger.info("Ignoring message from bot")
return False
@ -618,14 +645,16 @@ def process_message(
token = CURRENT_TENANT_ID_CONTEXTVAR.set(client.tenant_id)
try:
with get_session_with_tenant(client.tenant_id) as db_session:
slack_bot_config = get_slack_bot_config_for_channel(
channel_name=channel_name, db_session=db_session
slack_channel_config = get_slack_channel_config_for_bot_and_channel(
db_session=db_session,
slack_bot_id=client.slack_bot_id,
channel_name=channel_name,
)
# 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_bot_config is None
slack_channel_config is None
and not respond_every_channel
# Can't have configs for DMs so don't toss them out
and not is_dm
@ -636,9 +665,10 @@ def process_message(
return
follow_up = bool(
slack_bot_config
and slack_bot_config.channel_config
and slack_bot_config.channel_config.get("follow_up_tags") is not None
slack_channel_config
and slack_channel_config.channel_config
and slack_channel_config.channel_config.get("follow_up_tags")
is not None
)
feedback_reminder_id = schedule_feedback_reminder(
details=details, client=client.web_client, include_followup=follow_up
@ -646,7 +676,7 @@ def process_message(
failed = handle_message(
message_info=details,
slack_bot_config=slack_bot_config,
slack_channel_config=slack_channel_config,
client=client.web_client,
feedback_reminder_id=feedback_reminder_id,
tenant_id=client.tenant_id,
@ -698,26 +728,32 @@ def view_routing(req: SocketModeRequest, client: TenantSocketModeClient) -> None
return process_feedback(req, client)
def process_slack_event(client: TenantSocketModeClient, req: SocketModeRequest) -> None:
# Always respond right away, if Slack doesn't receive these frequently enough
# it will assume the Bot is DEAD!!! :(
acknowledge_message(req, client)
def create_process_slack_event() -> (
Callable[[TenantSocketModeClient, SocketModeRequest], None]
):
def process_slack_event(
client: TenantSocketModeClient, req: SocketModeRequest
) -> None:
# Always respond right away, if Slack doesn't receive these frequently enough
# it will assume the Bot is DEAD!!! :(
acknowledge_message(req, client)
try:
if req.type == "interactive":
if req.payload.get("type") == "block_actions":
return action_routing(req, client)
elif req.payload.get("type") == "view_submission":
return view_routing(req, client)
elif req.type == "events_api" or req.type == "slash_commands":
return process_message(req, client)
except Exception as e:
logger.exception(f"Failed to process slack event. Error: {e}")
logger.error(f"Slack request payload: {req.payload}")
try:
if req.type == "interactive":
if req.payload.get("type") == "block_actions":
return action_routing(req, client)
elif req.payload.get("type") == "view_submission":
return view_routing(req, client)
elif req.type == "events_api" or req.type == "slash_commands":
return process_message(req, client)
except Exception:
logger.exception("Failed to process slack event")
return process_slack_event
def _get_socket_client(
slack_bot_tokens: SlackBotTokens, tenant_id: str | None
slack_bot_tokens: SlackBotTokens, tenant_id: str | None, slack_bot_id: int
) -> TenantSocketModeClient:
# For more info on how to set this up, checkout the docs:
# https://docs.danswer.dev/slack_bot_setup
@ -726,6 +762,7 @@ def _get_socket_client(
app_token=slack_bot_tokens.app_token,
web_client=WebClient(token=slack_bot_tokens.bot_token),
tenant_id=tenant_id,
slack_bot_id=slack_bot_id,
)

View File

@ -1,28 +0,0 @@
import os
from typing import cast
from danswer.configs.constants import KV_SLACK_BOT_TOKENS_CONFIG_KEY
from danswer.key_value_store.factory import get_kv_store
from danswer.server.manage.models import SlackBotTokens
def fetch_tokens() -> SlackBotTokens:
# first check env variables
app_token = os.environ.get("DANSWER_BOT_SLACK_APP_TOKEN")
bot_token = os.environ.get("DANSWER_BOT_SLACK_BOT_TOKEN")
if app_token and bot_token:
return SlackBotTokens(app_token=app_token, bot_token=bot_token)
dynamic_config_store = get_kv_store()
return SlackBotTokens(
**cast(dict, dynamic_config_store.load(key=KV_SLACK_BOT_TOKENS_CONFIG_KEY))
)
def save_tokens(
tokens: SlackBotTokens,
) -> None:
dynamic_config_store = get_kv_store()
dynamic_config_store.store(
key=KV_SLACK_BOT_TOKENS_CONFIG_KEY, val=dict(tokens), encrypt=True
)

View File

@ -30,7 +30,6 @@ from danswer.configs.danswerbot_configs import (
from danswer.connectors.slack.utils import make_slack_api_rate_limited
from danswer.connectors.slack.utils import SlackTextCleaner
from danswer.danswerbot.slack.constants import FeedbackVisibility
from danswer.danswerbot.slack.tokens import fetch_tokens
from danswer.db.engine import get_session_with_tenant
from danswer.db.users import get_user_by_email
from danswer.llm.exceptions import GenAIDisabledException
@ -47,16 +46,16 @@ from danswer.utils.text_processing import replace_whitespaces_w_space
logger = setup_logger()
_DANSWER_BOT_APP_ID: str | None = None
_DANSWER_BOT_SLACK_BOT_ID: str | None = None
_DANSWER_BOT_MESSAGE_COUNT: int = 0
_DANSWER_BOT_COUNT_START_TIME: float = time.time()
def get_danswer_bot_app_id(web_client: WebClient) -> Any:
global _DANSWER_BOT_APP_ID
if _DANSWER_BOT_APP_ID is None:
_DANSWER_BOT_APP_ID = web_client.auth_test().get("user_id")
return _DANSWER_BOT_APP_ID
def get_danswer_bot_slack_bot_id(web_client: WebClient) -> Any:
global _DANSWER_BOT_SLACK_BOT_ID
if _DANSWER_BOT_SLACK_BOT_ID is None:
_DANSWER_BOT_SLACK_BOT_ID = web_client.auth_test().get("user_id")
return _DANSWER_BOT_SLACK_BOT_ID
def check_message_limit() -> bool:
@ -137,15 +136,10 @@ def update_emote_react(
def remove_danswer_bot_tag(message_str: str, client: WebClient) -> str:
bot_tag_id = get_danswer_bot_app_id(web_client=client)
bot_tag_id = get_danswer_bot_slack_bot_id(web_client=client)
return re.sub(rf"<@{bot_tag_id}>\s", "", message_str)
def get_web_client() -> WebClient:
slack_tokens = fetch_tokens()
return WebClient(token=slack_tokens.bot_token)
@retry(
tries=DANSWER_BOT_NUM_RETRIES,
delay=0.25,
@ -437,9 +431,9 @@ def read_slack_thread(
)
message_type = MessageType.USER
else:
self_app_id = get_danswer_bot_app_id(client)
self_slack_bot_id = get_danswer_bot_slack_bot_id(client)
if reply.get("user") == self_app_id:
if reply.get("user") == self_slack_bot_id:
# DanswerBot response
message_type = MessageType.ASSISTANT
user_sem_id = "Assistant"
@ -582,6 +576,9 @@ def get_feedback_visibility() -> FeedbackVisibility:
class TenantSocketModeClient(SocketModeClient):
def __init__(self, tenant_id: str | None, *args: Any, **kwargs: Any):
def __init__(
self, tenant_id: str | None, slack_bot_id: int, *args: Any, **kwargs: Any
):
super().__init__(*args, **kwargs)
self.tenant_id = tenant_id
self.slack_bot_id = slack_bot_id

View File

@ -350,11 +350,11 @@ class StandardAnswer__StandardAnswerCategory(Base):
)
class SlackBotConfig__StandardAnswerCategory(Base):
__tablename__ = "slack_bot_config__standard_answer_category"
class SlackChannelConfig__StandardAnswerCategory(Base):
__tablename__ = "slack_channel_config__standard_answer_category"
slack_bot_config_id: Mapped[int] = mapped_column(
ForeignKey("slack_bot_config.id"), primary_key=True
slack_channel_config_id: Mapped[int] = mapped_column(
ForeignKey("slack_channel_config.id"), primary_key=True
)
standard_answer_category_id: Mapped[int] = mapped_column(
ForeignKey("standard_answer_category.id"), primary_key=True
@ -1472,7 +1472,7 @@ class ChannelConfig(TypedDict):
"""NOTE: is a `TypedDict` so it can be used as a type hint for a JSONB column
in Postgres"""
channel_names: list[str]
channel_name: str
respond_tag_only: NotRequired[bool] # defaults to False
respond_to_bots: NotRequired[bool] # defaults to False
respond_member_group_list: NotRequired[list[str]]
@ -1487,10 +1487,11 @@ class SlackBotResponseType(str, PyEnum):
CITATIONS = "citations"
class SlackBotConfig(Base):
__tablename__ = "slack_bot_config"
class SlackChannelConfig(Base):
__tablename__ = "slack_channel_config"
id: Mapped[int] = mapped_column(primary_key=True)
slack_bot_id: Mapped[int] = mapped_column(ForeignKey("slack_bot.id"), nullable=True)
persona_id: Mapped[int | None] = mapped_column(
ForeignKey("persona.id"), nullable=True
)
@ -1507,10 +1508,30 @@ class SlackBotConfig(Base):
)
persona: Mapped[Persona | None] = relationship("Persona")
slack_bot: Mapped["SlackBot"] = relationship(
"SlackBot",
back_populates="slack_channel_configs",
)
standard_answer_categories: Mapped[list["StandardAnswerCategory"]] = relationship(
"StandardAnswerCategory",
secondary=SlackBotConfig__StandardAnswerCategory.__table__,
back_populates="slack_bot_configs",
secondary=SlackChannelConfig__StandardAnswerCategory.__table__,
back_populates="slack_channel_configs",
)
class SlackBot(Base):
__tablename__ = "slack_bot"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String)
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
bot_token: Mapped[str] = mapped_column(EncryptedString(), unique=True)
app_token: Mapped[str] = mapped_column(EncryptedString(), unique=True)
slack_channel_configs: Mapped[list[SlackChannelConfig]] = relationship(
"SlackChannelConfig",
back_populates="slack_bot",
)
@ -1749,9 +1770,9 @@ class StandardAnswerCategory(Base):
secondary=StandardAnswer__StandardAnswerCategory.__table__,
back_populates="categories",
)
slack_bot_configs: Mapped[list["SlackBotConfig"]] = relationship(
"SlackBotConfig",
secondary=SlackBotConfig__StandardAnswerCategory.__table__,
slack_channel_configs: Mapped[list["SlackChannelConfig"]] = relationship(
"SlackChannelConfig",
secondary=SlackChannelConfig__StandardAnswerCategory.__table__,
back_populates="standard_answer_categories",
)

View File

@ -0,0 +1,76 @@
from collections.abc import Sequence
from sqlalchemy import select
from sqlalchemy.orm import Session
from danswer.db.models import SlackBot
def insert_slack_bot(
db_session: Session,
name: str,
enabled: bool,
bot_token: str,
app_token: str,
) -> SlackBot:
slack_bot = SlackBot(
name=name,
enabled=enabled,
bot_token=bot_token,
app_token=app_token,
)
db_session.add(slack_bot)
db_session.commit()
return slack_bot
def update_slack_bot(
db_session: Session,
slack_bot_id: int,
name: str,
enabled: bool,
bot_token: str,
app_token: str,
) -> SlackBot:
slack_bot = db_session.scalar(select(SlackBot).where(SlackBot.id == slack_bot_id))
if slack_bot is None:
raise ValueError(f"Unable to find Slack Bot with ID {slack_bot_id}")
# update the app
slack_bot.name = name
slack_bot.enabled = enabled
slack_bot.bot_token = bot_token
slack_bot.app_token = app_token
db_session.commit()
return slack_bot
def fetch_slack_bot(
db_session: Session,
slack_bot_id: int,
) -> SlackBot:
slack_bot = db_session.scalar(select(SlackBot).where(SlackBot.id == slack_bot_id))
if slack_bot is None:
raise ValueError(f"Unable to find Slack Bot with ID {slack_bot_id}")
return slack_bot
def remove_slack_bot(
db_session: Session,
slack_bot_id: int,
) -> None:
slack_bot = fetch_slack_bot(
db_session=db_session,
slack_bot_id=slack_bot_id,
)
db_session.delete(slack_bot)
db_session.commit()
def fetch_slack_bots(db_session: Session) -> Sequence[SlackBot]:
return db_session.scalars(select(SlackBot)).all()

View File

@ -9,8 +9,8 @@ from danswer.db.constants import SLACK_BOT_PERSONA_PREFIX
from danswer.db.models import ChannelConfig
from danswer.db.models import Persona
from danswer.db.models import Persona__DocumentSet
from danswer.db.models import SlackBotConfig
from danswer.db.models import SlackBotResponseType
from danswer.db.models import SlackChannelConfig
from danswer.db.models import User
from danswer.db.persona import get_default_prompt
from danswer.db.persona import mark_persona_as_deleted
@ -22,8 +22,8 @@ from danswer.utils.variable_functionality import (
)
def _build_persona_name(channel_names: list[str]) -> str:
return f"{SLACK_BOT_PERSONA_PREFIX}{'-'.join(channel_names)}"
def _build_persona_name(channel_name: str) -> str:
return f"{SLACK_BOT_PERSONA_PREFIX}{channel_name}"
def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
@ -38,9 +38,9 @@ def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
db_session.delete(rel)
def create_slack_bot_persona(
def create_slack_channel_persona(
db_session: Session,
channel_names: list[str],
channel_name: str,
document_set_ids: list[int],
existing_persona_id: int | None = None,
num_chunks: float = MAX_CHUNKS_FED_TO_CHAT,
@ -48,11 +48,11 @@ def create_slack_bot_persona(
) -> Persona:
"""NOTE: does not commit changes"""
# create/update persona associated with the slack bot
persona_name = _build_persona_name(channel_names)
# create/update persona associated with the Slack channel
persona_name = _build_persona_name(channel_name)
default_prompt = get_default_prompt(db_session)
persona = upsert_persona(
user=None, # Slack Bot Personas are not attached to users
user=None, # Slack channel Personas are not attached to users
persona_id=existing_persona_id,
name=persona_name,
description="",
@ -78,14 +78,15 @@ def _no_ee_standard_answer_categories(*args: Any, **kwargs: Any) -> list:
return []
def insert_slack_bot_config(
def insert_slack_channel_config(
db_session: Session,
slack_bot_id: int,
persona_id: int | None,
channel_config: ChannelConfig,
response_type: SlackBotResponseType,
standard_answer_category_ids: list[int],
enable_auto_filters: bool,
db_session: Session,
) -> SlackBotConfig:
) -> SlackChannelConfig:
versioned_fetch_standard_answer_categories_by_ids = (
fetch_versioned_implementation_with_fallback(
"danswer.db.standard_answer",
@ -110,34 +111,37 @@ def insert_slack_bot_config(
f"Some or all categories with ids {standard_answer_category_ids} do not exist"
)
slack_bot_config = SlackBotConfig(
slack_channel_config = SlackChannelConfig(
slack_bot_id=slack_bot_id,
persona_id=persona_id,
channel_config=channel_config,
response_type=response_type,
standard_answer_categories=existing_standard_answer_categories,
enable_auto_filters=enable_auto_filters,
)
db_session.add(slack_bot_config)
db_session.add(slack_channel_config)
db_session.commit()
return slack_bot_config
return slack_channel_config
def update_slack_bot_config(
slack_bot_config_id: int,
def update_slack_channel_config(
db_session: Session,
slack_channel_config_id: int,
persona_id: int | None,
channel_config: ChannelConfig,
response_type: SlackBotResponseType,
standard_answer_category_ids: list[int],
enable_auto_filters: bool,
db_session: Session,
) -> SlackBotConfig:
slack_bot_config = db_session.scalar(
select(SlackBotConfig).where(SlackBotConfig.id == slack_bot_config_id)
) -> SlackChannelConfig:
slack_channel_config = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.id == slack_channel_config_id
)
)
if slack_bot_config is None:
if slack_channel_config is None:
raise ValueError(
f"Unable to find slack bot config with ID {slack_bot_config_id}"
f"Unable to find Slack channel config with ID {slack_channel_config_id}"
)
versioned_fetch_standard_answer_categories_by_ids = (
@ -159,25 +163,25 @@ def update_slack_bot_config(
)
# get the existing persona id before updating the object
existing_persona_id = slack_bot_config.persona_id
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_bot_config.persona_id = persona_id
slack_bot_config.channel_config = channel_config
slack_bot_config.response_type = response_type
slack_bot_config.standard_answer_categories = list(
slack_channel_config.persona_id = persona_id
slack_channel_config.channel_config = channel_config
slack_channel_config.response_type = response_type
slack_channel_config.standard_answer_categories = list(
existing_standard_answer_categories
)
slack_bot_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 Bot,
# 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
@ -188,28 +192,30 @@ def update_slack_bot_config(
db_session.commit()
return slack_bot_config
return slack_channel_config
def remove_slack_bot_config(
slack_bot_config_id: int,
user: User | None,
def remove_slack_channel_config(
db_session: Session,
slack_channel_config_id: int,
user: User | None,
) -> None:
slack_bot_config = db_session.scalar(
select(SlackBotConfig).where(SlackBotConfig.id == slack_bot_config_id)
slack_channel_config = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.id == slack_channel_config_id
)
)
if slack_bot_config is None:
if slack_channel_config is None:
raise ValueError(
f"Unable to find slack bot config with ID {slack_bot_config_id}"
f"Unable to find Slack channel config with ID {slack_channel_config_id}"
)
existing_persona_id = slack_bot_config.persona_id
existing_persona_id = slack_channel_config.persona_id
if 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 Bot,
# 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
@ -221,17 +227,28 @@ def remove_slack_bot_config(
persona_id=existing_persona_id, user=user, db_session=db_session
)
db_session.delete(slack_bot_config)
db_session.delete(slack_channel_config)
db_session.commit()
def fetch_slack_bot_config(
db_session: Session, slack_bot_config_id: int
) -> SlackBotConfig | None:
def fetch_slack_channel_configs(
db_session: Session, slack_bot_id: int | None = None
) -> Sequence[SlackChannelConfig]:
if not slack_bot_id:
return db_session.scalars(select(SlackChannelConfig)).all()
return db_session.scalars(
select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id
)
).all()
def fetch_slack_channel_config(
db_session: Session, slack_channel_config_id: int
) -> SlackChannelConfig | None:
return db_session.scalar(
select(SlackBotConfig).where(SlackBotConfig.id == slack_bot_config_id)
select(SlackChannelConfig).where(
SlackChannelConfig.id == slack_channel_config_id
)
)
def fetch_slack_bot_configs(db_session: Session) -> Sequence[SlackBotConfig]:
return db_session.scalars(select(SlackBotConfig)).all()

View File

@ -13,8 +13,9 @@ from danswer.configs.constants import AuthType
from danswer.danswerbot.slack.config import VALID_SLACK_FILTERS
from danswer.db.models import AllowedAnswerFilters
from danswer.db.models import ChannelConfig
from danswer.db.models import SlackBotConfig as SlackBotConfigModel
from danswer.db.models import SlackBot as SlackAppModel
from danswer.db.models import SlackBotResponseType
from danswer.db.models import SlackChannelConfig as SlackChannelConfigModel
from danswer.db.models import User
from danswer.search.models import SavedSearchSettings
from danswer.server.features.persona.models import PersonaSnapshot
@ -127,22 +128,32 @@ class HiddenUpdateRequest(BaseModel):
hidden: bool
class SlackBotCreationRequest(BaseModel):
name: str
enabled: bool
bot_token: str
app_token: str
class SlackBotTokens(BaseModel):
bot_token: str
app_token: str
model_config = ConfigDict(frozen=True)
class SlackBotConfigCreationRequest(BaseModel):
# currently, a persona is created for each slack bot config
class SlackChannelConfigCreationRequest(BaseModel):
slack_bot_id: int
# currently, a persona is created for each Slack channel config
# in the future, `document_sets` will probably be replaced
# by an optional `PersonaSnapshot` object. Keeping it like this
# for now for simplicity / speed of development
document_sets: list[int] | None = None
persona_id: (
int | None
) = None # NOTE: only one of `document_sets` / `persona_id` should be set
channel_names: list[str]
# NOTE: only one of `document_sets` / `persona_id` should be set
persona_id: int | None = None
channel_name: str
respond_tag_only: bool = False
respond_to_bots: bool = False
enable_auto_filters: bool = False
@ -165,14 +176,17 @@ class SlackBotConfigCreationRequest(BaseModel):
return value
@model_validator(mode="after")
def validate_document_sets_and_persona_id(self) -> "SlackBotConfigCreationRequest":
def validate_document_sets_and_persona_id(
self,
) -> "SlackChannelConfigCreationRequest":
if self.document_sets and self.persona_id:
raise ValueError("Only one of `document_sets` / `persona_id` should be set")
return self
class SlackBotConfig(BaseModel):
class SlackChannelConfig(BaseModel):
slack_bot_id: int
id: int
persona: PersonaSnapshot | None
channel_config: ChannelConfig
@ -183,25 +197,53 @@ class SlackBotConfig(BaseModel):
@classmethod
def from_model(
cls, slack_bot_config_model: SlackBotConfigModel
) -> "SlackBotConfig":
cls, slack_channel_config_model: SlackChannelConfigModel
) -> "SlackChannelConfig":
return cls(
id=slack_bot_config_model.id,
id=slack_channel_config_model.id,
slack_bot_id=slack_channel_config_model.slack_bot_id,
persona=(
PersonaSnapshot.from_model(
slack_bot_config_model.persona, allow_deleted=True
slack_channel_config_model.persona, allow_deleted=True
)
if slack_bot_config_model.persona
if slack_channel_config_model.persona
else None
),
channel_config=slack_bot_config_model.channel_config,
response_type=slack_bot_config_model.response_type,
channel_config=slack_channel_config_model.channel_config,
response_type=slack_channel_config_model.response_type,
# XXX this is going away soon
standard_answer_categories=[
StandardAnswerCategory.from_model(standard_answer_category_model)
for standard_answer_category_model in slack_bot_config_model.standard_answer_categories
for standard_answer_category_model in slack_channel_config_model.standard_answer_categories
],
enable_auto_filters=slack_bot_config_model.enable_auto_filters,
enable_auto_filters=slack_channel_config_model.enable_auto_filters,
)
class SlackBot(BaseModel):
"""
This model is identical to the SlackAppModel, but it contains
a `configs_count` field to make it easier to fetch the number
of SlackChannelConfigs associated with a SlackBot.
"""
id: int
name: str
enabled: bool
configs_count: int
bot_token: str
app_token: str
@classmethod
def from_model(cls, slack_bot_model: SlackAppModel) -> "SlackBot":
return cls(
id=slack_bot_model.id,
name=slack_bot_model.name,
enabled=slack_bot_model.enabled,
bot_token=slack_bot_model.bot_token,
app_token=slack_bot_model.app_token,
configs_count=len(slack_bot_model.slack_channel_configs),
)

View File

@ -4,53 +4,57 @@ from fastapi import HTTPException
from sqlalchemy.orm import Session
from danswer.auth.users import current_admin_user
from danswer.danswerbot.slack.config import validate_channel_names
from danswer.danswerbot.slack.tokens import fetch_tokens
from danswer.danswerbot.slack.tokens import save_tokens
from danswer.danswerbot.slack.config import validate_channel_name
from danswer.db.constants import SLACK_BOT_PERSONA_PREFIX
from danswer.db.engine import get_session
from danswer.db.models import ChannelConfig
from danswer.db.models import User
from danswer.db.persona import get_persona_by_id
from danswer.db.slack_bot_config import create_slack_bot_persona
from danswer.db.slack_bot_config import fetch_slack_bot_config
from danswer.db.slack_bot_config import fetch_slack_bot_configs
from danswer.db.slack_bot_config import insert_slack_bot_config
from danswer.db.slack_bot_config import remove_slack_bot_config
from danswer.db.slack_bot_config import update_slack_bot_config
from danswer.key_value_store.interface import KvKeyNotFoundError
from danswer.server.manage.models import SlackBotConfig
from danswer.server.manage.models import SlackBotConfigCreationRequest
from danswer.server.manage.models import SlackBotTokens
from danswer.db.slack_bot import fetch_slack_bot
from danswer.db.slack_bot import fetch_slack_bots
from danswer.db.slack_bot import insert_slack_bot
from danswer.db.slack_bot import remove_slack_bot
from danswer.db.slack_bot import update_slack_bot
from danswer.db.slack_channel_config import create_slack_channel_persona
from danswer.db.slack_channel_config import fetch_slack_channel_config
from danswer.db.slack_channel_config import fetch_slack_channel_configs
from danswer.db.slack_channel_config import insert_slack_channel_config
from danswer.db.slack_channel_config import remove_slack_channel_config
from danswer.db.slack_channel_config import update_slack_channel_config
from danswer.server.manage.models import SlackBot
from danswer.server.manage.models import SlackBotCreationRequest
from danswer.server.manage.models import SlackChannelConfig
from danswer.server.manage.models import SlackChannelConfigCreationRequest
router = APIRouter(prefix="/manage")
def _form_channel_config(
slack_bot_config_creation_request: SlackBotConfigCreationRequest,
current_slack_bot_config_id: int | None,
db_session: Session,
slack_channel_config_creation_request: SlackChannelConfigCreationRequest,
current_slack_channel_config_id: int | None,
) -> ChannelConfig:
raw_channel_names = slack_bot_config_creation_request.channel_names
respond_tag_only = slack_bot_config_creation_request.respond_tag_only
raw_channel_name = slack_channel_config_creation_request.channel_name
respond_tag_only = slack_channel_config_creation_request.respond_tag_only
respond_member_group_list = (
slack_bot_config_creation_request.respond_member_group_list
slack_channel_config_creation_request.respond_member_group_list
)
answer_filters = slack_bot_config_creation_request.answer_filters
follow_up_tags = slack_bot_config_creation_request.follow_up_tags
answer_filters = slack_channel_config_creation_request.answer_filters
follow_up_tags = slack_channel_config_creation_request.follow_up_tags
if not raw_channel_names:
if not raw_channel_name:
raise HTTPException(
status_code=400,
detail="Must provide at least one channel name",
)
try:
cleaned_channel_names = validate_channel_names(
channel_names=raw_channel_names,
current_slack_bot_config_id=current_slack_bot_config_id,
cleaned_channel_name = validate_channel_name(
db_session=db_session,
channel_name=raw_channel_name,
current_slack_channel_config_id=current_slack_channel_config_id,
current_slack_bot_id=slack_channel_config_creation_request.slack_bot_id,
)
except ValueError as e:
raise HTTPException(
@ -65,7 +69,7 @@ def _form_channel_config(
)
channel_config: ChannelConfig = {
"channel_names": cleaned_channel_names,
"channel_name": cleaned_channel_name,
}
if respond_tag_only is not None:
channel_config["respond_tag_only"] = respond_tag_only
@ -78,69 +82,73 @@ def _form_channel_config(
channel_config[
"respond_to_bots"
] = slack_bot_config_creation_request.respond_to_bots
] = slack_channel_config_creation_request.respond_to_bots
return channel_config
@router.post("/admin/slack-bot/config")
def create_slack_bot_config(
slack_bot_config_creation_request: SlackBotConfigCreationRequest,
@router.post("/admin/slack-app/channel")
def create_slack_channel_config(
slack_channel_config_creation_request: SlackChannelConfigCreationRequest,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> SlackBotConfig:
) -> SlackChannelConfig:
channel_config = _form_channel_config(
slack_bot_config_creation_request, None, db_session
db_session=db_session,
slack_channel_config_creation_request=slack_channel_config_creation_request,
current_slack_channel_config_id=None,
)
persona_id = None
if slack_bot_config_creation_request.persona_id is not None:
persona_id = slack_bot_config_creation_request.persona_id
elif slack_bot_config_creation_request.document_sets:
persona_id = create_slack_bot_persona(
if slack_channel_config_creation_request.persona_id is not None:
persona_id = slack_channel_config_creation_request.persona_id
elif slack_channel_config_creation_request.document_sets:
persona_id = create_slack_channel_persona(
db_session=db_session,
channel_names=channel_config["channel_names"],
document_set_ids=slack_bot_config_creation_request.document_sets,
channel_name=channel_config["channel_name"],
document_set_ids=slack_channel_config_creation_request.document_sets,
existing_persona_id=None,
).id
slack_bot_config_model = insert_slack_bot_config(
slack_channel_config_model = insert_slack_channel_config(
slack_bot_id=slack_channel_config_creation_request.slack_bot_id,
persona_id=persona_id,
channel_config=channel_config,
response_type=slack_bot_config_creation_request.response_type,
# XXX this is going away soon
standard_answer_category_ids=slack_bot_config_creation_request.standard_answer_categories,
response_type=slack_channel_config_creation_request.response_type,
standard_answer_category_ids=slack_channel_config_creation_request.standard_answer_categories,
db_session=db_session,
enable_auto_filters=slack_bot_config_creation_request.enable_auto_filters,
enable_auto_filters=slack_channel_config_creation_request.enable_auto_filters,
)
return SlackBotConfig.from_model(slack_bot_config_model)
return SlackChannelConfig.from_model(slack_channel_config_model)
@router.patch("/admin/slack-bot/config/{slack_bot_config_id}")
def patch_slack_bot_config(
slack_bot_config_id: int,
slack_bot_config_creation_request: SlackBotConfigCreationRequest,
@router.patch("/admin/slack-app/channel/{slack_channel_config_id}")
def patch_slack_channel_config(
slack_channel_config_id: int,
slack_channel_config_creation_request: SlackChannelConfigCreationRequest,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> SlackBotConfig:
) -> SlackChannelConfig:
channel_config = _form_channel_config(
slack_bot_config_creation_request, slack_bot_config_id, db_session
db_session=db_session,
slack_channel_config_creation_request=slack_channel_config_creation_request,
current_slack_channel_config_id=slack_channel_config_id,
)
persona_id = None
if slack_bot_config_creation_request.persona_id is not None:
persona_id = slack_bot_config_creation_request.persona_id
elif slack_bot_config_creation_request.document_sets:
existing_slack_bot_config = fetch_slack_bot_config(
db_session=db_session, slack_bot_config_id=slack_bot_config_id
if slack_channel_config_creation_request.persona_id is not None:
persona_id = slack_channel_config_creation_request.persona_id
elif slack_channel_config_creation_request.document_sets:
existing_slack_channel_config = fetch_slack_channel_config(
db_session=db_session, slack_channel_config_id=slack_channel_config_id
)
if existing_slack_bot_config is None:
if existing_slack_channel_config is None:
raise HTTPException(
status_code=404,
detail="Slack bot config not found",
detail="Slack channel config not found",
)
existing_persona_id = existing_slack_bot_config.persona_id
existing_persona_id = existing_slack_channel_config.persona_id
if existing_persona_id is not None:
persona = get_persona_by_id(
persona_id=existing_persona_id,
@ -155,62 +163,133 @@ def patch_slack_bot_config(
# for this DanswerBot config
existing_persona_id = None
else:
existing_persona_id = existing_slack_bot_config.persona_id
existing_persona_id = existing_slack_channel_config.persona_id
persona_id = create_slack_bot_persona(
persona_id = create_slack_channel_persona(
db_session=db_session,
channel_names=channel_config["channel_names"],
document_set_ids=slack_bot_config_creation_request.document_sets,
channel_name=channel_config["channel_name"],
document_set_ids=slack_channel_config_creation_request.document_sets,
existing_persona_id=existing_persona_id,
enable_auto_filters=slack_bot_config_creation_request.enable_auto_filters,
enable_auto_filters=slack_channel_config_creation_request.enable_auto_filters,
).id
slack_bot_config_model = update_slack_bot_config(
slack_bot_config_id=slack_bot_config_id,
slack_channel_config_model = update_slack_channel_config(
db_session=db_session,
slack_channel_config_id=slack_channel_config_id,
persona_id=persona_id,
channel_config=channel_config,
response_type=slack_bot_config_creation_request.response_type,
standard_answer_category_ids=slack_bot_config_creation_request.standard_answer_categories,
db_session=db_session,
enable_auto_filters=slack_bot_config_creation_request.enable_auto_filters,
response_type=slack_channel_config_creation_request.response_type,
standard_answer_category_ids=slack_channel_config_creation_request.standard_answer_categories,
enable_auto_filters=slack_channel_config_creation_request.enable_auto_filters,
)
return SlackBotConfig.from_model(slack_bot_config_model)
return SlackChannelConfig.from_model(slack_channel_config_model)
@router.delete("/admin/slack-bot/config/{slack_bot_config_id}")
def delete_slack_bot_config(
slack_bot_config_id: int,
@router.delete("/admin/slack-app/channel/{slack_channel_config_id}")
def delete_slack_channel_config(
slack_channel_config_id: int,
db_session: Session = Depends(get_session),
user: User | None = Depends(current_admin_user),
) -> None:
remove_slack_bot_config(
slack_bot_config_id=slack_bot_config_id, user=user, db_session=db_session
remove_slack_channel_config(
db_session=db_session,
slack_channel_config_id=slack_channel_config_id,
user=user,
)
@router.get("/admin/slack-bot/config")
def list_slack_bot_configs(
@router.get("/admin/slack-app/channel")
def list_slack_channel_configs(
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> list[SlackBotConfig]:
slack_bot_config_models = fetch_slack_bot_configs(db_session=db_session)
) -> list[SlackChannelConfig]:
slack_channel_config_models = fetch_slack_channel_configs(db_session=db_session)
return [
SlackBotConfig.from_model(slack_bot_config_model)
for slack_bot_config_model in slack_bot_config_models
SlackChannelConfig.from_model(slack_channel_config_model)
for slack_channel_config_model in slack_channel_config_models
]
@router.put("/admin/slack-bot/tokens")
def put_tokens(
tokens: SlackBotTokens,
@router.post("/admin/slack-app/bots")
def create_bot(
slack_bot_creation_request: SlackBotCreationRequest,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> SlackBot:
slack_bot_model = insert_slack_bot(
db_session=db_session,
name=slack_bot_creation_request.name,
enabled=slack_bot_creation_request.enabled,
bot_token=slack_bot_creation_request.bot_token,
app_token=slack_bot_creation_request.app_token,
)
return SlackBot.from_model(slack_bot_model)
@router.patch("/admin/slack-app/bots/{slack_bot_id}")
def patch_bot(
slack_bot_id: int,
slack_bot_creation_request: SlackBotCreationRequest,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> SlackBot:
slack_bot_model = update_slack_bot(
db_session=db_session,
slack_bot_id=slack_bot_id,
name=slack_bot_creation_request.name,
enabled=slack_bot_creation_request.enabled,
bot_token=slack_bot_creation_request.bot_token,
app_token=slack_bot_creation_request.app_token,
)
return SlackBot.from_model(slack_bot_model)
@router.delete("/admin/slack-app/bots/{slack_bot_id}")
def delete_bot(
slack_bot_id: int,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> None:
save_tokens(tokens=tokens)
remove_slack_bot(
db_session=db_session,
slack_bot_id=slack_bot_id,
)
@router.get("/admin/slack-bot/tokens")
def get_tokens(_: User | None = Depends(current_admin_user)) -> SlackBotTokens:
try:
return fetch_tokens()
except KvKeyNotFoundError:
raise HTTPException(status_code=404, detail="No tokens found")
@router.get("/admin/slack-app/bots/{slack_bot_id}")
def get_bot_by_id(
slack_bot_id: int,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> SlackBot:
slack_bot_model = fetch_slack_bot(
db_session=db_session,
slack_bot_id=slack_bot_id,
)
return SlackBot.from_model(slack_bot_model)
@router.get("/admin/slack-app/bots")
def list_bots(
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> list[SlackBot]:
slack_bot_models = fetch_slack_bots(db_session=db_session)
return [
SlackBot.from_model(slack_bot_model) for slack_bot_model in slack_bot_models
]
@router.get("/admin/slack-app/bots/{bot_id}/config")
def list_bot_configs(
bot_id: int,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_admin_user),
) -> list[SlackChannelConfig]:
slack_bot_config_models = fetch_slack_channel_configs(
db_session=db_session, slack_bot_id=bot_id
)
return [
SlackChannelConfig.from_model(slack_bot_config_model)
for slack_bot_config_model in slack_bot_config_models
]

View File

@ -19,7 +19,7 @@ from danswer.db.chat import get_chat_messages_by_sessions
from danswer.db.chat import get_chat_sessions_by_slack_thread_id
from danswer.db.chat import get_or_create_root_message
from danswer.db.models import Prompt
from danswer.db.models import SlackBotConfig
from danswer.db.models import SlackChannelConfig
from danswer.db.models import StandardAnswer as StandardAnswerModel
from danswer.utils.logger import DanswerLoggingAdapter
from danswer.utils.logger import setup_logger
@ -80,7 +80,7 @@ def oneoff_standard_answers(
def _handle_standard_answers(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
slack_bot_config: SlackBotConfig | None,
slack_channel_config: SlackChannelConfig | None,
prompt: Prompt | None,
logger: DanswerLoggingAdapter,
client: WebClient,
@ -95,12 +95,12 @@ def _handle_standard_answers(
we still need to respond to the users.
"""
# if no channel config, then no standard answers are configured
if not slack_bot_config:
if not slack_channel_config:
return False
slack_thread_id = message_info.thread_to_respond
configured_standard_answer_categories = (
slack_bot_config.standard_answer_categories if slack_bot_config else []
slack_channel_config.standard_answer_categories if slack_channel_config else []
)
configured_standard_answers = set(
[
@ -150,7 +150,9 @@ def _handle_standard_answers(
db_session=db_session,
description="",
user_id=None,
persona_id=slack_bot_config.persona.id if slack_bot_config.persona else 0,
persona_id=slack_channel_config.persona.id
if slack_channel_config.persona
else 0,
danswerbot_flow=True,
slack_thread_id=slack_thread_id,
one_shot=True,

View File

@ -188,8 +188,6 @@ services:
- CELERY_WORKER_LIGHT_PREFETCH_MULTIPLIER=${CELERY_WORKER_LIGHT_PREFETCH_MULTIPLIER:-}
# Danswer SlackBot Configs
- DANSWER_BOT_SLACK_APP_TOKEN=${DANSWER_BOT_SLACK_APP_TOKEN:-}
- DANSWER_BOT_SLACK_BOT_TOKEN=${DANSWER_BOT_SLACK_BOT_TOKEN:-}
- DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER=${DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER:-}
- DANSWER_BOT_FEEDBACK_VISIBILITY=${DANSWER_BOT_FEEDBACK_VISIBILITY:-}
- DANSWER_BOT_DISPLAY_ERROR_MSGS=${DANSWER_BOT_DISPLAY_ERROR_MSGS:-}

View File

@ -167,8 +167,6 @@ services:
- NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP=${NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP:-}
- GITHUB_CONNECTOR_BASE_URL=${GITHUB_CONNECTOR_BASE_URL:-}
# Danswer SlackBot Configs
- DANSWER_BOT_SLACK_APP_TOKEN=${DANSWER_BOT_SLACK_APP_TOKEN:-}
- DANSWER_BOT_SLACK_BOT_TOKEN=${DANSWER_BOT_SLACK_BOT_TOKEN:-}
- DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER=${DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER:-}
- DANSWER_BOT_FEEDBACK_VISIBILITY=${DANSWER_BOT_FEEDBACK_VISIBILITY:-}
- DANSWER_BOT_DISPLAY_ERROR_MSGS=${DANSWER_BOT_DISPLAY_ERROR_MSGS:-}

View File

@ -9,12 +9,6 @@ WEB_DOMAIN=http://localhost:3000
# NOTE: Generative AI configurations are done via the UI now
# If you want to setup a slack bot to answer questions automatically in Slack
# channels it is added to, you must specify the two below.
# More information in the guide here: https://docs.danswer.dev/slack_bot_setup
#DANSWER_BOT_SLACK_APP_TOKEN=
#DANSWER_BOT_SLACK_BOT_TOKEN=
# The following are for configuring User Authentication, supported flows are:
# disabled

View File

@ -387,8 +387,6 @@ auth:
oauth_client_id: ""
oauth_client_secret: ""
oauth_cookie_secret: ""
danswer_bot_slack_app_token: ""
danswer_bot_slack_bot_token: ""
redis_password: "redis_password"
# will be overridden by the existingSecret if set
secretName: "danswer-secrets"
@ -400,8 +398,6 @@ auth:
oauth_client_id: ""
oauth_client_secret: ""
oauth_cookie_secret: ""
danswer_bot_slack_app_token: ""
danswer_bot_slack_bot_token: ""
redis_password: "password"
configMap:
@ -451,8 +447,6 @@ configMap:
GONG_CONNECTOR_START_TIME: ""
NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP: ""
# DanswerBot SlackBot Configs
# DANSWER_BOT_SLACK_APP_TOKEN: ""
# DANSWER_BOT_SLACK_BOT_TOKEN: ""
DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER: ""
DANSWER_BOT_DISPLAY_ERROR_MSGS: ""
DANSWER_BOT_RESPOND_EVERY_CHANNEL: ""

View File

@ -62,8 +62,6 @@ data:
GONG_CONNECTOR_START_TIME: ""
NOTION_CONNECTOR_ENABLE_RECURSIVE_PAGE_LOOKUP: ""
# DanswerBot SlackBot Configs
DANSWER_BOT_SLACK_APP_TOKEN: ""
DANSWER_BOT_SLACK_BOT_TOKEN: ""
DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER: ""
DANSWER_BOT_DISPLAY_ERROR_MSGS: ""
DANSWER_BOT_RESPOND_EVERY_CHANNEL: ""

View File

@ -1,23 +0,0 @@
import { errorHandlingFetcher } from "@/lib/fetcher";
import { SlackBotConfig, SlackBotTokens } from "@/lib/types";
import useSWR, { mutate } from "swr";
export const useSlackBotConfigs = () => {
const url = "/api/manage/admin/slack-bot/config";
const swrResponse = useSWR<SlackBotConfig[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackBotConfigs: () => mutate(url),
};
};
export const useSlackBotTokens = () => {
const url = "/api/manage/admin/slack-bot/tokens";
const swrResponse = useSWR<SlackBotTokens>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackBotTokens: () => mutate(url),
};
};

View File

@ -1,65 +0,0 @@
import { AdminPageTitle } from "@/components/admin/Title";
import { CPUIcon } from "@/components/icons/icons";
import { SlackBotCreationForm } from "../SlackBotConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet } from "@/lib/types";
import { BackButton } from "@/components/BackButton";
import {
FetchAssistantsResponse,
fetchAssistantsSS,
} from "@/lib/assistants/fetchAssistantsSS";
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
async function Page() {
const tasks = [fetchSS("/manage/document-set"), fetchAssistantsSS()];
const [
documentSetsResponse,
[assistants, assistantsFetchError],
standardAnswerCategoriesResponse,
] = (await Promise.all(tasks)) as [
Response,
FetchAssistantsResponse,
Response,
];
const eeStandardAnswerCategoryResponse =
await getStandardAnswerCategoriesIfEE();
if (!documentSetsResponse.ok) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch document sets - ${await documentSetsResponse.text()}`}
/>
);
}
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
if (assistantsFetchError) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch assistants - ${assistantsFetchError}`}
/>
);
}
return (
<div className="container mx-auto">
<BackButton />
<AdminPageTitle
icon={<CPUIcon size={32} />}
title="New Slack Bot Config"
/>
<SlackBotCreationForm
documentSets={documentSets}
personas={assistants}
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
/>
</div>
);
}
export default Page;

View File

@ -1,304 +0,0 @@
"use client";
import { ThreeDotsLoader } from "@/components/Loading";
import { PageSelector } from "@/components/PageSelector";
import { EditIcon, SlackIcon, TrashIcon } from "@/components/icons/icons";
import { SlackBotConfig } from "@/lib/types";
import { useState } from "react";
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { deleteSlackBotConfig, isPersonaASlackBotPersona } from "./lib";
import { SlackBotTokensForm } from "./SlackBotTokensForm";
import { AdminPageTitle } from "@/components/admin/Title";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Text from "@/components/ui/text";
import Title from "@/components/ui/title";
import { FiArrowUpRight, FiChevronDown, FiChevronUp } from "react-icons/fi";
import Link from "next/link";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { ErrorCallout } from "@/components/ErrorCallout";
import { Button } from "@/components/ui/button";
const numToDisplay = 50;
const SlackBotConfigsTable = ({
slackBotConfigs,
refresh,
setPopup,
}: {
slackBotConfigs: SlackBotConfig[];
refresh: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}) => {
const [page, setPage] = useState(1);
// sort by name for consistent ordering
slackBotConfigs.sort((a, b) => {
if (a.id < b.id) {
return -1;
} else if (a.id > b.id) {
return 1;
} else {
return 0;
}
});
return (
<div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Channels</TableHead>
<TableHead>Assistant</TableHead>
<TableHead>Document Sets</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{slackBotConfigs
.slice(numToDisplay * (page - 1), numToDisplay * page)
.map((slackBotConfig) => {
return (
<TableRow key={slackBotConfig.id}>
<TableCell>
<div className="flex gap-x-2">
<Link
className="cursor-pointer my-auto"
href={`/admin/bot/${slackBotConfig.id}`}
>
<EditIcon />
</Link>
<div className="my-auto">
{slackBotConfig.channel_config.channel_names
.map((channel_name) => `#${channel_name}`)
.join(", ")}
</div>
</div>
</TableCell>
<TableCell>
{slackBotConfig.persona &&
!isPersonaASlackBotPersona(slackBotConfig.persona) ? (
<Link
href={`/admin/assistants/${slackBotConfig.persona.id}`}
className="text-blue-500 flex"
>
<FiArrowUpRight className="my-auto mr-1" />
{slackBotConfig.persona.name}
</Link>
) : (
"-"
)}
</TableCell>
<TableCell>
{" "}
<div>
{slackBotConfig.persona &&
slackBotConfig.persona.document_sets.length > 0
? slackBotConfig.persona.document_sets
.map((documentSet) => documentSet.name)
.join(", ")
: "-"}
</div>
</TableCell>
<TableCell>
{" "}
<div
className="cursor-pointer"
onClick={async () => {
const response = await deleteSlackBotConfig(
slackBotConfig.id
);
if (response.ok) {
setPopup({
message: `Slack bot config "${slackBotConfig.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>
);
})}
</TableBody>
</Table>
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(slackBotConfigs.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
</div>
</div>
);
};
const Main = () => {
const [slackBotTokensModalIsOpen, setSlackBotTokensModalIsOpen] =
useState(false);
const { popup, setPopup } = usePopup();
const {
data: slackBotConfigs,
isLoading: isSlackBotConfigsLoading,
error: slackBotConfigsError,
refreshSlackBotConfigs,
} = useSlackBotConfigs();
const { data: slackBotTokens, refreshSlackBotTokens } = useSlackBotTokens();
if (isSlackBotConfigsLoading) {
return <ThreeDotsLoader />;
}
if (slackBotConfigsError || !slackBotConfigs || !slackBotConfigs) {
return (
<ErrorCallout
errorTitle="Error loading slack bot configs"
errorMsg={
slackBotConfigsError.info?.message ||
slackBotConfigsError.info?.detail
}
/>
);
}
return (
<div className="mb-8">
{popup}
<Text className="mb-2">
Setup a Slack bot that connects to Danswer. Once setup, you will be able
to ask questions to Danswer directly from Slack. Additionally, you can:
</Text>
<Text className="mb-2">
<ul className="list-disc mt-2 ml-4">
<li>
Setup DanswerBot to automatically answer questions in certain
channels.
</li>
<li>
Choose which document sets DanswerBot should answer from, depending
on the channel the question is being asked.
</li>
<li>
Directly message DanswerBot to search just as you would in the web
UI.
</li>
</ul>
</Text>
<Text className="mb-6">
Follow the{" "}
<a
className="text-blue-500"
href="https://docs.danswer.dev/slack_bot_setup"
target="_blank"
rel="noreferrer"
>
guide{" "}
</a>
found in the Danswer documentation to get started!
</Text>
<Title>Step 1: Configure Slack Tokens</Title>
{!slackBotTokens ? (
<div className="mt-3">
<SlackBotTokensForm
onClose={() => refreshSlackBotTokens()}
setPopup={setPopup}
/>
</div>
) : (
<>
<Text className="italic mt-3">Tokens saved!</Text>
<Button
onClick={() => {
setSlackBotTokensModalIsOpen(!slackBotTokensModalIsOpen);
}}
variant="outline"
className="mt-2"
icon={slackBotTokensModalIsOpen ? FiChevronUp : FiChevronDown}
>
{slackBotTokensModalIsOpen ? "Hide" : "Edit Tokens"}
</Button>
{slackBotTokensModalIsOpen && (
<div className="mt-3">
<SlackBotTokensForm
onClose={() => {
refreshSlackBotTokens();
setSlackBotTokensModalIsOpen(false);
}}
setPopup={setPopup}
existingTokens={slackBotTokens}
/>
</div>
)}
</>
)}
{slackBotTokens && (
<>
<Title className="mb-2 mt-4">Step 2: Setup DanswerBot</Title>
<Text className="mb-3">
Configure Danswer to automatically answer questions in Slack
channels. By default, Danswer only responds in channels where a
configuration is setup unless it is explicitly tagged.
</Text>
<div className="mb-2"></div>
<Link className="flex mb-3 w-fit" href="/admin/bot/new">
<Button className="my-auto" variant="next">
New Slack Bot Configuration
</Button>
</Link>
{slackBotConfigs.length > 0 && (
<div className="mt-8">
<SlackBotConfigsTable
slackBotConfigs={slackBotConfigs}
refresh={refreshSlackBotConfigs}
setPopup={setPopup}
/>
</div>
)}
</>
)}
</div>
);
};
const Page = () => {
return (
<div className="container mx-auto">
<AdminPageTitle
icon={<SlackIcon size={32} />}
title="Slack Bot Configuration"
/>
<InstantSSRAutoRefresh />
<Main />
</div>
);
};
export default Page;

View File

@ -0,0 +1,31 @@
"use client";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { SlackTokensForm } from "./SlackTokensForm";
export const NewSlackBotForm = ({}: {}) => {
const [formValues] = useState({
name: "",
enabled: true,
bot_token: "",
app_token: "",
});
const { popup, setPopup } = usePopup();
const router = useRouter();
return (
<div>
{popup}
<div className="p-4">
<SlackTokensForm
isUpdate={false}
initialValues={formValues}
setPopup={setPopup}
router={router}
/>
</div>
</div>
);
};

View File

@ -0,0 +1,121 @@
"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 {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const NUM_IN_PAGE = 20;
function ClickableTableRow({
url,
children,
...props
}: {
url: string;
children: React.ReactNode;
[key: string]: any;
}) {
const router = useRouter();
useEffect(() => {
router.prefetch(url);
}, [router]);
const navigate = () => {
router.push(url);
};
return (
<TableRow {...props} onClick={navigate}>
{children}
</TableRow>
);
}
export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
const [page, setPage] = useState(1);
// sort by id for consistent ordering
slackBots.sort((a, b) => {
if (a.id < b.id) {
return -1;
} else if (a.id > b.id) {
return 1;
} else {
return 0;
}
});
const slackBotsForPage = slackBots.slice(
NUM_IN_PAGE * (page - 1),
NUM_IN_PAGE * page
);
return (
<div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Channel Count</TableHead>
<TableHead>Enabled</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{slackBotsForPage.map((slackBot) => {
return (
<ClickableTableRow
url={`/admin/bots/${slackBot.id}`}
key={slackBot.id}
className="hover:bg-muted cursor-pointer"
>
<TableCell>
<div className="flex items-center">
<FiEdit className="mr-4" />
{slackBot.name}
</div>
</TableCell>
<TableCell>{slackBot.configs_count}</TableCell>
<TableCell>
{slackBot.enabled ? (
<FiCheck className="text-emerald-600" size="18" />
) : (
<FiXCircle className="text-red-600" size="18" />
)}
</TableCell>
</ClickableTableRow>
);
})}
</TableBody>
</Table>
{slackBots.length > NUM_IN_PAGE && (
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(slackBots.length / NUM_IN_PAGE)}
currentPage={page}
onPageChange={(newPage) => {
setPage(newPage);
window.scrollTo({
top: 0,
left: 0,
behavior: "smooth",
});
}}
/>
</div>
</div>
)}
</div>
);
}

View File

@ -1,34 +1,52 @@
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { SlackBotTokens } from "@/lib/types";
import { SlackBot } from "@/lib/types";
import { TextFormField } from "@/components/admin/connectors/Field";
import { setSlackBotTokens } from "./lib";
import CardSection from "@/components/admin/CardSection";
import { Button } from "@/components/ui/button";
import { updateSlackBot, SlackBotCreationRequest } from "./new/lib";
interface SlackBotTokensFormProps {
onClose: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
existingTokens?: SlackBotTokens;
existingSlackApp?: SlackBot;
onTokensSet?: (tokens: { bot_token: string; app_token: string }) => void;
embedded?: boolean;
noForm?: boolean;
}
export const SlackBotTokensForm = ({
onClose,
setPopup,
existingTokens,
existingSlackApp,
onTokensSet,
embedded = true,
noForm = true,
}: SlackBotTokensFormProps) => {
const Wrapper = embedded ? "div" : CardSection;
const FormWrapper = noForm ? "div" : Form;
return (
<CardSection>
<Wrapper className="w-full">
<Formik
initialValues={existingTokens || { app_token: "", bot_token: "" }}
initialValues={existingSlackApp || { app_token: "", bot_token: "" }}
validationSchema={Yup.object().shape({
channel_names: Yup.array().of(Yup.string().required()),
document_sets: Yup.array().of(Yup.number()),
bot_token: Yup.string().required(),
app_token: Yup.string().required(),
})}
onSubmit={async (values, formikHelpers) => {
if (embedded && onTokensSet) {
onTokensSet(values);
return;
}
formikHelpers.setSubmitting(true);
const response = await setSlackBotTokens(values);
const response = await updateSlackBot(
existingSlackApp?.id || 0,
values as SlackBotCreationRequest
);
formikHelpers.setSubmitting(false);
if (response.ok) {
setPopup({
@ -46,25 +64,29 @@ export const SlackBotTokensForm = ({
}}
>
{({ isSubmitting }) => (
<Form>
<FormWrapper className="w-full">
<TextFormField
width="w-full"
name="bot_token"
label="Slack Bot Token"
type="password"
/>
<TextFormField
width="w-full"
name="app_token"
label="Slack App Token"
type="password"
/>
<div className="flex">
<Button type="submit" disabled={isSubmitting} variant="submit">
Set Tokens
</Button>
</div>
</Form>
{!embedded && (
<div className="flex w-full">
<Button type="submit" disabled={isSubmitting} variant="submit">
Set Tokens
</Button>
</div>
)}
</FormWrapper>
)}
</Formik>
</CardSection>
</Wrapper>
);
};

View File

@ -0,0 +1,170 @@
"use client";
import { usePopup } from "@/components/admin/connectors/Popup";
import { SlackBot } from "@/lib/types";
import { useRouter } from "next/navigation";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { updateSlackBotField } from "@/lib/updateSlackBotField";
import { Checkbox } from "@/app/admin/settings/SettingsForm";
import { SlackTokensForm } from "./SlackTokensForm";
import { SourceIcon } from "@/components/SourceIcon";
import { EditableStringFieldDisplay } from "@/components/EditableStringFieldDisplay";
import { deleteSlackBot } from "./new/lib";
import { GenericConfirmModal } from "@/components/modals/GenericConfirmModal";
import { FiTrash } from "react-icons/fi";
import { Button } from "@/components/ui/button";
export const ExistingSlackBotForm = ({
existingSlackBot,
refreshSlackBot,
}: {
existingSlackBot: SlackBot;
refreshSlackBot?: () => void;
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [formValues, setFormValues] = useState(existingSlackBot);
const { popup, setPopup } = usePopup();
const router = useRouter();
const dropdownRef = useRef<HTMLDivElement>(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const handleUpdateField = async (
field: keyof SlackBot,
value: string | boolean
) => {
try {
const response = await updateSlackBotField(
existingSlackBot,
field,
value
);
if (!response.ok) {
throw new Error(await response.text());
}
setPopup({
message: `Connector ${field} updated successfully`,
type: "success",
});
} catch (error) {
setPopup({
message: `Failed to update connector ${field}`,
type: "error",
});
}
setFormValues((prev) => ({ ...prev, [field]: value }));
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
isExpanded
) {
setIsExpanded(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isExpanded]);
return (
<div>
{popup}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="my-auto">
<SourceIcon iconSize={36} sourceType={"slack"} />
</div>
<EditableStringFieldDisplay
value={formValues.name}
isEditable={true}
onUpdate={(value) => handleUpdateField("name", value)}
scale={2.5}
/>
</div>
<div className="flex flex-col" ref={dropdownRef}>
<div className="flex items-center gap-4">
<div className="border rounded-lg border-gray-200">
<div
className="flex items-center gap-2 cursor-pointer hover:bg-gray-100 p-2"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<ChevronDown size={20} />
) : (
<ChevronRight size={20} />
)}
<span>Update Tokens</span>
</div>
</div>
<Button
variant="destructive"
onClick={() => setShowDeleteModal(true)}
icon={FiTrash}
tooltip="Click to delete"
>
Delete
</Button>
</div>
{isExpanded && (
<div className="bg-white border rounded-lg border-gray-200 shadow-lg absolute mt-12 right-0 z-10 w-full md:w-3/4 lg:w-1/2">
<div className="p-4">
<SlackTokensForm
isUpdate={true}
initialValues={formValues}
existingSlackBotId={existingSlackBot.id}
refreshSlackBot={refreshSlackBot}
setPopup={setPopup}
router={router}
/>
</div>
</div>
)}
</div>
</div>
<div className="mt-4">
<div className="inline-block border rounded-lg border-gray-200 px-2 py-2">
<Checkbox
label="Enabled"
checked={formValues.enabled}
onChange={(e) => handleUpdateField("enabled", e.target.checked)}
/>
</div>
{showDeleteModal && (
<GenericConfirmModal
title="Delete Slack Bot"
message="Are you sure you want to delete this Slack bot? This action cannot be undone."
confirmText="Delete"
onClose={() => setShowDeleteModal(false)}
onConfirm={async () => {
try {
const response = await deleteSlackBot(existingSlackBot.id);
if (!response.ok) {
throw new Error(await response.text());
}
setPopup({
message: "Slack bot deleted successfully",
type: "success",
});
router.push("/admin/bots");
} catch (error) {
setPopup({
message: "Failed to delete Slack bot",
type: "error",
});
}
setShowDeleteModal(false);
}}
/>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,108 @@
"use client";
import { TextFormField } from "@/components/admin/connectors/Field";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { createSlackBot, updateSlackBot } from "./new/lib";
import { Button } from "@/components/ui/button";
import { SourceIcon } from "@/components/SourceIcon";
export const SlackTokensForm = ({
isUpdate,
initialValues,
existingSlackBotId,
refreshSlackBot,
setPopup,
router,
}: {
isUpdate: boolean;
initialValues: any;
existingSlackBotId?: number;
refreshSlackBot?: () => void;
setPopup: (popup: { message: string; type: "error" | "success" }) => void;
router: any;
}) => (
<Formik
initialValues={initialValues}
validationSchema={Yup.object().shape({
bot_token: Yup.string().required(),
app_token: Yup.string().required(),
name: Yup.string().required(),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
let response;
if (isUpdate) {
response = await updateSlackBot(existingSlackBotId!, values);
} else {
response = await createSlackBot(values);
}
formikHelpers.setSubmitting(false);
if (response.ok) {
if (refreshSlackBot) {
refreshSlackBot();
}
const responseJson = await response.json();
const botId = isUpdate ? existingSlackBotId : responseJson.id;
setPopup({
message: isUpdate
? "Successfully updated Slack Bot!"
: "Successfully created Slack Bot!",
type: "success",
});
router.push(`/admin/bots/${botId}}`);
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: isUpdate
? `Error updating Slack Bot - ${errorMsg}`
: `Error creating Slack Bot - ${errorMsg}`,
type: "error",
});
}
}}
enableReinitialize={true}
>
{({ isSubmitting, setFieldValue, values }) => (
<Form className="w-full">
{!isUpdate && (
<div className="flex items-center gap-2 mb-4">
<div className="my-auto">
<SourceIcon iconSize={36} sourceType={"slack"} />
</div>
<TextFormField name="name" label="Slack Bot Name" type="text" />
</div>
)}
{!isUpdate && (
<div className="mb-4">
Please enter your Slack Bot Token and Slack App Token to give
Danswerbot access to your Slack!
</div>
)}
<TextFormField
name="bot_token"
label="Slack Bot Token"
type="password"
/>
<TextFormField
name="app_token"
label="Slack App Token"
type="password"
/>
<div className="flex justify-end w-full mt-4">
<Button
type="submit"
disabled={isSubmitting}
variant="submit"
size="default"
>
{isUpdate ? "Update!" : "Create!"}
</Button>
</div>
</Form>
)}
</Formik>
);

View File

@ -0,0 +1,157 @@
"use client";
import { PageSelector } from "@/components/PageSelector";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { EditIcon, TrashIcon } from "@/components/icons/icons";
import { SlackChannelConfig } from "@/lib/types";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Link from "next/link";
import { useState } from "react";
import { FiArrowUpRight } from "react-icons/fi";
import { deleteSlackChannelConfig, isPersonaASlackBotPersona } from "./lib";
const numToDisplay = 50;
export function SlackChannelConfigsTable({
slackBotId,
slackChannelConfigs,
refresh,
setPopup,
}: {
slackBotId: number;
slackChannelConfigs: SlackChannelConfig[];
refresh: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}) {
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;
}
});
return (
<div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Channel</TableHead>
<TableHead>Persona</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}>
<TableCell>
<div className="flex gap-x-2">
<Link
className="cursor-pointer my-auto"
href={`/admin/bots/${slackBotId}/channels/${slackChannelConfig.id}`}
>
<EditIcon />
</Link>
<div className="my-auto">
{"#" + slackChannelConfig.channel_config.channel_name}
</div>
</div>
</TableCell>
<TableCell>
{slackChannelConfig.persona &&
!isPersonaASlackBotPersona(slackChannelConfig.persona) ? (
<Link
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
className="text-blue-500 flex hover:underline"
>
<FiArrowUpRight className="my-auto mr-1" />
{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>
<div
className="cursor-pointer hover:text-destructive"
onClick={async () => {
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 Danswer!
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(slackChannelConfigs.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
</div>
</div>
);
}

View File

@ -3,47 +3,55 @@
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
import * as Yup from "yup";
import { usePopup } from "@/components/admin/connectors/Popup";
import { DocumentSet, SlackBotConfig } from "@/lib/types";
import { DocumentSet, SlackChannelConfig } from "@/lib/types";
import {
BooleanFormField,
Label,
SelectorFormField,
SubLabel,
TextArrayField,
TextFormField,
} from "@/components/admin/connectors/Field";
import {
createSlackBotConfig,
createSlackChannelConfig,
isPersonaASlackBotPersona,
updateSlackBotConfig,
} from "./lib";
import { Separator } from "@/components/ui/separator";
updateSlackChannelConfig,
} from "../lib";
import CardSection from "@/components/admin/CardSection";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { Persona } from "../assistants/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
import { useState } from "react";
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
import CollapsibleSection from "../assistants/CollapsibleSection";
import CollapsibleSection from "@/app/admin/assistants/CollapsibleSection";
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { StandardAnswerCategoryDropdownField } from "@/components/standardAnswers/StandardAnswerCategoryDropdown";
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@/components/ui/fully_wrapped_tabs";
export const SlackBotCreationForm = ({
export const SlackChannelConfigCreationForm = ({
slack_bot_id,
documentSets,
personas,
standardAnswerCategoryResponse,
existingSlackBotConfig,
existingSlackChannelConfig,
}: {
slack_bot_id: number;
documentSets: DocumentSet[];
personas: Persona[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
existingSlackBotConfig?: SlackBotConfig;
existingSlackChannelConfig?: SlackChannelConfig;
}) => {
const isUpdate = existingSlackBotConfig !== undefined;
const isUpdate = existingSlackChannelConfig !== undefined;
const { popup, setPopup } = usePopup();
const router = useRouter();
const existingSlackBotUsesPersona = existingSlackBotConfig?.persona
? !isPersonaASlackBotPersona(existingSlackBotConfig.persona)
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
: false;
const [usingPersonas, setUsingPersonas] = useState(
existingSlackBotUsesPersona
@ -58,48 +66,52 @@ export const SlackBotCreationForm = ({
{popup}
<Formik
initialValues={{
channel_names: existingSlackBotConfig
? existingSlackBotConfig.channel_config.channel_names
: ([""] as string[]),
slack_bot_id: slack_bot_id,
channel_name:
existingSlackChannelConfig?.channel_config.channel_name,
answer_validity_check_enabled: (
existingSlackBotConfig?.channel_config?.answer_filters || []
existingSlackChannelConfig?.channel_config?.answer_filters || []
).includes("well_answered_postfilter"),
questionmark_prefilter_enabled: (
existingSlackBotConfig?.channel_config?.answer_filters || []
existingSlackChannelConfig?.channel_config?.answer_filters || []
).includes("questionmark_prefilter"),
respond_tag_only:
existingSlackBotConfig?.channel_config?.respond_tag_only || false,
existingSlackChannelConfig?.channel_config?.respond_tag_only ||
false,
respond_to_bots:
existingSlackBotConfig?.channel_config?.respond_to_bots || false,
existingSlackChannelConfig?.channel_config?.respond_to_bots ||
false,
enable_auto_filters:
existingSlackBotConfig?.enable_auto_filters || false,
existingSlackChannelConfig?.enable_auto_filters || false,
respond_member_group_list:
existingSlackBotConfig?.channel_config
existingSlackChannelConfig?.channel_config
?.respond_member_group_list ?? [],
still_need_help_enabled:
existingSlackBotConfig?.channel_config?.follow_up_tags !==
existingSlackChannelConfig?.channel_config?.follow_up_tags !==
undefined,
follow_up_tags:
existingSlackBotConfig?.channel_config?.follow_up_tags,
existingSlackChannelConfig?.channel_config?.follow_up_tags,
document_sets:
existingSlackBotConfig && existingSlackBotConfig.persona
? existingSlackBotConfig.persona.document_sets.map(
existingSlackChannelConfig && existingSlackChannelConfig.persona
? existingSlackChannelConfig.persona.document_sets.map(
(documentSet) => documentSet.id
)
: ([] as number[]),
// prettier-ignore
persona_id:
existingSlackBotConfig?.persona &&
!isPersonaASlackBotPersona(existingSlackBotConfig.persona)
? existingSlackBotConfig.persona.id
existingSlackChannelConfig?.persona &&
!isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
? existingSlackChannelConfig.persona.id
: knowledgePersona?.id ?? null,
response_type: existingSlackBotConfig?.response_type || "citations",
standard_answer_categories: existingSlackBotConfig
? existingSlackBotConfig.standard_answer_categories
response_type:
existingSlackChannelConfig?.response_type || "citations",
standard_answer_categories: existingSlackChannelConfig
? existingSlackChannelConfig.standard_answer_categories
: [],
}}
validationSchema={Yup.object().shape({
channel_names: Yup.array().of(Yup.string()),
slack_bot_id: Yup.number().required(),
channel_name: Yup.string(),
response_type: Yup.string()
.oneOf(["quotes", "citations"])
.required(),
@ -118,12 +130,10 @@ export const SlackBotCreationForm = ({
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
// remove empty channel names
const cleanedValues = {
...values,
channel_names: values.channel_names.filter(
(channelName) => channelName !== ""
),
slack_bot_id: slack_bot_id,
channel_name: values.channel_name!,
respond_member_group_list: values.respond_member_group_list,
usePersona: usingPersonas,
standard_answer_categories: values.standard_answer_categories.map(
@ -139,16 +149,16 @@ export const SlackBotCreationForm = ({
}
let response;
if (isUpdate) {
response = await updateSlackBotConfig(
existingSlackBotConfig.id,
response = await updateSlackChannelConfig(
existingSlackChannelConfig.id,
cleanedValues
);
} else {
response = await createSlackBotConfig(cleanedValues);
response = await createSlackChannelConfig(cleanedValues);
}
formikHelpers.setSubmitting(false);
if (response.ok) {
router.push(`/admin/bot?u=${Date.now()}`);
router.push(`/admin/bots/${slack_bot_id}`);
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
@ -164,53 +174,38 @@ export const SlackBotCreationForm = ({
{({ isSubmitting, values, setFieldValue }) => (
<Form>
<div className="px-6 pb-6 pt-4 w-full">
<TextArrayField
name="channel_names"
label="Channel Names"
values={values}
subtext="The names of the Slack channels you want this configuration to apply to.
For example, #ask-danswer."
minFields={1}
placeholder="Enter channel name..."
<TextFormField
name="channel_name"
label="Slack Channel Name:"
/>
<div className="mt-6">
<Label>Knowledge Sources</Label>
<SubLabel>
Controls which information DanswerBot will pull from when
answering questions.
</SubLabel>
<div className="flex mt-4">
<button
type="button"
onClick={() => setUsingPersonas(false)}
className={`p-2 font-bold text-xs mr-3 ${
!usingPersonas
? "rounded bg-background-900 text-text-100 underline"
: "hover:underline bg-background-100"
}`}
>
Document Sets
</button>
<Tabs
defaultValue="document_sets"
className="w-full mt-4"
value={usingPersonas ? "assistants" : "document_sets"}
onValueChange={(value) =>
setUsingPersonas(value === "assistants")
}
>
<TabsList>
<TabsTrigger value="document_sets">
Document Sets
</TabsTrigger>
<TabsTrigger value="assistants">Assistants</TabsTrigger>
</TabsList>
<button
type="button"
onClick={() => setUsingPersonas(true)}
className={`p-2 font-bold text-xs ${
usingPersonas
? "rounded bg-background-900 text-text-100 underline"
: "hover:underline bg-background-100"
}`}
>
Assistants
</button>
</div>
<div className="mt-4">
{/* TODO: make this look nicer */}
{usingPersonas ? (
<TabsContent value="assistants">
<SubLabel>
Select the assistant DanswerBot will use while answering
questions in Slack.
</SubLabel>
<SelectorFormField
name="persona_id"
options={personas.map((persona) => {
@ -220,7 +215,17 @@ export const SlackBotCreationForm = ({
};
})}
/>
) : (
</TabsContent>
<TabsContent value="document_sets">
<SubLabel>
Select the document sets DanswerBot will use while
answering questions in Slack.
</SubLabel>
<SubLabel>
Note: If No Document Sets are selected, DanswerBot will
search through all connected documents.
</SubLabel>
<FieldArray
name="document_sets"
render={(arrayHelpers: ArrayHelpers) => (
@ -248,21 +253,14 @@ export const SlackBotCreationForm = ({
);
})}
</div>
<div>
<SubLabel>
Note: If left blank, DanswerBot will search
through all connected documents.
</SubLabel>
</div>
<div></div>
</div>
)}
/>
)}
</div>
</TabsContent>
</Tabs>
</div>
<Separator />
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}

View File

@ -1,10 +1,9 @@
import { AdminPageTitle } from "@/components/admin/Title";
import { CPUIcon } from "@/components/icons/icons";
import { SlackBotCreationForm } from "../SlackBotConfigCreationForm";
import { SourceIcon } from "@/components/SourceIcon";
import { SlackChannelConfigCreationForm } from "../SlackChannelConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet, SlackBotConfig } from "@/lib/types";
import Text from "@/components/ui/text";
import { DocumentSet, SlackChannelConfig } from "@/lib/types";
import { BackButton } from "@/components/BackButton";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import {
@ -13,16 +12,18 @@ import {
} from "@/lib/assistants/fetchAssistantsSS";
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
async function Page(props: { params: Promise<{ id: string }> }) {
async function EditslackChannelConfigPage(props: {
params: Promise<{ id: number }>;
}) {
const params = await props.params;
const tasks = [
fetchSS("/manage/admin/slack-bot/config"),
fetchSS("/manage/admin/slack-app/channel"),
fetchSS("/manage/document-set"),
fetchAssistantsSS(),
];
const [
slackBotsResponse,
slackChannelsResponse,
documentSetsResponse,
[assistants, assistantsFetchError],
] = (await Promise.all(tasks)) as [
@ -34,24 +35,26 @@ async function Page(props: { params: Promise<{ id: string }> }) {
const eeStandardAnswerCategoryResponse =
await getStandardAnswerCategoriesIfEE();
if (!slackBotsResponse.ok) {
if (!slackChannelsResponse.ok) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch slack bots - ${await slackBotsResponse.text()}`}
errorMsg={`Failed to fetch Slack Channels - ${await slackChannelsResponse.text()}`}
/>
);
}
const allSlackBotConfigs =
(await slackBotsResponse.json()) as SlackBotConfig[];
const slackBotConfig = allSlackBotConfigs.find(
(config) => config.id.toString() === params.id
const allslackChannelConfigs =
(await slackChannelsResponse.json()) as SlackChannelConfig[];
const slackChannelConfig = allslackChannelConfigs.find(
(config) => config.id === Number(params.id)
);
if (!slackBotConfig) {
if (!slackChannelConfig) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Did not find Slack Bot config with ID: ${params.id}`}
errorMsg={`Did not find Slack Channel config with ID: ${params.id}`}
/>
);
}
@ -81,23 +84,19 @@ async function Page(props: { params: Promise<{ id: string }> }) {
<BackButton />
<AdminPageTitle
icon={<CPUIcon size={32} />}
title="Edit Slack Bot Config"
icon={<SourceIcon sourceType={"slack"} iconSize={32} />}
title="Edit Slack Channel Config"
/>
<Text className="mb-8">
Edit the existing configuration below! This config will determine how
DanswerBot behaves in the specified channels.
</Text>
<SlackBotCreationForm
<SlackChannelConfigCreationForm
slack_bot_id={slackChannelConfig.slack_bot_id}
documentSets={documentSets}
personas={assistants}
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
existingSlackBotConfig={slackBotConfig}
existingSlackChannelConfig={slackChannelConfig}
/>
</div>
);
}
export default Page;
export default EditslackChannelConfigPage;

View File

@ -0,0 +1,76 @@
import { AdminPageTitle } from "@/components/admin/Title";
import { SlackChannelConfigCreationForm } from "../SlackChannelConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet } from "@/lib/types";
import { BackButton } from "@/components/BackButton";
import { fetchAssistantsSS } from "@/lib/assistants/fetchAssistantsSS";
import {
getStandardAnswerCategoriesIfEE,
StandardAnswerCategoryResponse,
} from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { redirect } from "next/navigation";
import { Persona } from "../../../../assistants/interfaces";
import { SourceIcon } from "@/components/SourceIcon";
async function NewChannelConfigPage(props: {
params: Promise<{ "bot-id": string }>;
}) {
const unwrappedParams = await props.params;
const slack_bot_id_raw = unwrappedParams?.["bot-id"] || null;
const slack_bot_id = slack_bot_id_raw
? parseInt(slack_bot_id_raw as string, 10)
: null;
if (!slack_bot_id || isNaN(slack_bot_id)) {
redirect("/admin/bots");
return null;
}
const [
documentSetsResponse,
assistantsResponse,
standardAnswerCategoryResponse,
] = await Promise.all([
fetchSS("/manage/document-set") as Promise<Response>,
fetchAssistantsSS() as Promise<[Persona[], string | null]>,
getStandardAnswerCategoriesIfEE() as Promise<StandardAnswerCategoryResponse>,
]);
if (!documentSetsResponse.ok) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch document sets - ${await documentSetsResponse.text()}`}
/>
);
}
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
if (assistantsResponse[1]) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch assistants - ${assistantsResponse[1]}`}
/>
);
}
return (
<div className="container mx-auto">
<BackButton />
<AdminPageTitle
icon={<SourceIcon iconSize={32} sourceType={"slack"} />}
title="Configure DanswerBot for Slack Channel"
/>
<SlackChannelConfigCreationForm
slack_bot_id={slack_bot_id}
documentSets={documentSets}
personas={assistantsResponse[0]}
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
/>
</div>
);
}
export default NewChannelConfigPage;

View File

@ -0,0 +1,43 @@
import { errorHandlingFetcher } from "@/lib/fetcher";
import { SlackBot, SlackChannelConfig } from "@/lib/types";
import useSWR, { mutate } from "swr";
export const useSlackChannelConfigs = () => {
const url = "/api/manage/admin/slack-app/channel";
const swrResponse = useSWR<SlackChannelConfig[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackChannelConfigs: () => mutate(url),
};
};
export const useSlackBots = () => {
const url = "/api/manage/admin/slack-app/bots";
const swrResponse = useSWR<SlackBot[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackBots: () => mutate(url),
};
};
export const useSlackBot = (botId: number) => {
const url = `/api/manage/admin/slack-app/bots/${botId}`;
const swrResponse = useSWR<SlackBot>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackBot: () => mutate(url),
};
};
export const useSlackChannelConfigsByBot = (botId: number) => {
const url = `/api/manage/admin/slack-app/bots/${botId}/config`;
const swrResponse = useSWR<SlackChannelConfig[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackChannelConfigs: () => mutate(url),
};
};

View File

@ -3,13 +3,14 @@ import {
SlackBotResponseType,
SlackBotTokens,
} from "@/lib/types";
import { Persona } from "../assistants/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
interface SlackBotConfigCreationRequest {
interface SlackChannelConfigCreationRequest {
slack_bot_id: number;
document_sets: number[];
persona_id: number | null;
enable_auto_filters: boolean;
channel_names: string[];
channel_name: string;
answer_validity_check_enabled: boolean;
questionmark_prefilter_enabled: boolean;
respond_tag_only: boolean;
@ -22,7 +23,7 @@ interface SlackBotConfigCreationRequest {
}
const buildFiltersFromCreationRequest = (
creationRequest: SlackBotConfigCreationRequest
creationRequest: SlackChannelConfigCreationRequest
): string[] => {
const answerFilters = [] as string[];
if (creationRequest.answer_validity_check_enabled) {
@ -35,10 +36,11 @@ const buildFiltersFromCreationRequest = (
};
const buildRequestBodyFromCreationRequest = (
creationRequest: SlackBotConfigCreationRequest
creationRequest: SlackChannelConfigCreationRequest
) => {
return JSON.stringify({
channel_names: creationRequest.channel_names,
slack_bot_id: creationRequest.slack_bot_id,
channel_name: creationRequest.channel_name,
respond_tag_only: creationRequest.respond_tag_only,
respond_to_bots: creationRequest.respond_to_bots,
enable_auto_filters: creationRequest.enable_auto_filters,
@ -53,10 +55,10 @@ const buildRequestBodyFromCreationRequest = (
});
};
export const createSlackBotConfig = async (
creationRequest: SlackBotConfigCreationRequest
export const createSlackChannelConfig = async (
creationRequest: SlackChannelConfigCreationRequest
) => {
return fetch("/api/manage/admin/slack-bot/config", {
return fetch("/api/manage/admin/slack-app/channel", {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -65,11 +67,11 @@ export const createSlackBotConfig = async (
});
};
export const updateSlackBotConfig = async (
export const updateSlackChannelConfig = async (
id: number,
creationRequest: SlackBotConfigCreationRequest
creationRequest: SlackChannelConfigCreationRequest
) => {
return fetch(`/api/manage/admin/slack-bot/config/${id}`, {
return fetch(`/api/manage/admin/slack-app/channel/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
@ -78,8 +80,8 @@ export const updateSlackBotConfig = async (
});
};
export const deleteSlackBotConfig = async (id: number) => {
return fetch(`/api/manage/admin/slack-bot/config/${id}`, {
export const deleteSlackChannelConfig = async (id: number) => {
return fetch(`/api/manage/admin/slack-app/channel/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@ -87,16 +89,6 @@ export const deleteSlackBotConfig = async (id: number) => {
});
};
export const setSlackBotTokens = async (slackBotTokens: SlackBotTokens) => {
return fetch(`/api/manage/admin/slack-bot/tokens`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(slackBotTokens),
});
};
export function isPersonaASlackBotPersona(persona: Persona) {
return persona.name.startsWith("__slack_bot_persona__");
}

View File

@ -0,0 +1,118 @@
"use client";
import { use } from "react";
import { BackButton } from "@/components/BackButton";
import { ErrorCallout } from "@/components/ErrorCallout";
import { ThreeDotsLoader } from "@/components/Loading";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { usePopup } from "@/components/admin/connectors/Popup";
import Link from "next/link";
import { SlackChannelConfigsTable } from "./SlackChannelConfigsTable";
import { useSlackBot, useSlackChannelConfigsByBot } from "./hooks";
import { ExistingSlackBotForm } from "../SlackBotUpdateForm";
import { FiPlusSquare } from "react-icons/fi";
import { Separator } from "@/components/ui/separator";
function SlackBotEditPage({
params,
}: {
params: Promise<{ "bot-id": string }>;
}) {
// Unwrap the params promise
const unwrappedParams = use(params);
const { popup, setPopup } = usePopup();
console.log("unwrappedParams", unwrappedParams);
const {
data: slackBot,
isLoading: isSlackBotLoading,
error: slackBotError,
refreshSlackBot,
} = useSlackBot(Number(unwrappedParams["bot-id"]));
const {
data: slackChannelConfigs,
isLoading: isSlackChannelConfigsLoading,
error: slackChannelConfigsError,
refreshSlackChannelConfigs,
} = useSlackChannelConfigsByBot(Number(unwrappedParams["bot-id"]));
if (isSlackBotLoading || isSlackChannelConfigsLoading) {
return <ThreeDotsLoader />;
}
if (slackBotError || !slackBot) {
const errorMsg =
slackBotError?.info?.message ||
slackBotError?.info?.detail ||
"An unknown error occurred";
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch Slack Bot ${unwrappedParams["bot-id"]}: ${errorMsg}`}
/>
);
}
if (slackChannelConfigsError || !slackChannelConfigs) {
const errorMsg =
slackChannelConfigsError?.info?.message ||
slackChannelConfigsError?.info?.detail ||
"An unknown error occurred";
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch Slack Bot ${unwrappedParams["bot-id"]}: ${errorMsg}`}
/>
);
}
return (
<div className="container mx-auto">
<InstantSSRAutoRefresh />
<BackButton routerOverride="/admin/bots" />
<ExistingSlackBotForm
existingSlackBot={slackBot}
refreshSlackBot={refreshSlackBot}
/>
<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/new?slack_bot_id=${unwrappedParams["bot-id"]}`}
>
<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}
slackChannelConfigs={slackChannelConfigs}
refresh={refreshSlackChannelConfigs}
setPopup={setPopup}
/>
</div>
</div>
);
}
export default SlackBotEditPage;

View File

@ -0,0 +1,52 @@
export interface SlackBotCreationRequest {
name: string;
enabled: boolean;
bot_token: string;
app_token: string;
}
const buildRequestBodyFromCreationRequest = (
creationRequest: SlackBotCreationRequest
) => {
return JSON.stringify({
name: creationRequest.name,
enabled: creationRequest.enabled,
bot_token: creationRequest.bot_token,
app_token: creationRequest.app_token,
});
};
export const createSlackBot = async (
creationRequest: SlackBotCreationRequest
) => {
return fetch("/api/manage/admin/slack-app/bots", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: buildRequestBodyFromCreationRequest(creationRequest),
});
};
export const updateSlackBot = async (
id: number,
creationRequest: SlackBotCreationRequest
) => {
return fetch(`/api/manage/admin/slack-app/bots/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: buildRequestBodyFromCreationRequest(creationRequest),
});
};
export const deleteSlackBot = async (id: number) => {
return fetch(`/api/manage/admin/slack-app/bots/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
};

View File

@ -0,0 +1,14 @@
import { BackButton } from "@/components/BackButton";
import { NewSlackBotForm } from "../SlackBotCreationForm";
async function NewSlackBotPage() {
return (
<div className="container mx-auto">
<BackButton routerOverride="/admin/bots" />
<NewSlackBotForm />
</div>
);
}
export default NewSlackBotPage;

View File

@ -0,0 +1,116 @@
"use client";
import { ErrorCallout } from "@/components/ErrorCallout";
import { FiPlusSquare } from "react-icons/fi";
import { ThreeDotsLoader } from "@/components/Loading";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { AdminPageTitle } from "@/components/admin/Title";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { SourceIcon } from "@/components/SourceIcon";
import { SlackBotTable } from "./SlackBotTable";
import { useSlackBots } from "./[bot-id]/hooks";
const Main = () => {
const {
data: slackBots,
isLoading: isSlackBotsLoading,
error: slackBotsError,
} = useSlackBots();
if (isSlackBotsLoading) {
return <ThreeDotsLoader />;
}
if (slackBotsError || !slackBots) {
const errorMsg =
slackBotsError?.info?.message ||
slackBotsError?.info?.detail ||
"An unknown error occurred";
return (
<ErrorCallout errorTitle="Error loading apps" errorMsg={`${errorMsg}`} />
);
}
return (
<div className="mb-8">
{/* {popup} */}
<p className="mb-2 text-sm text-muted-foreground">
Setup Slack bots that connect to Danswer. Once setup, you will be able
to ask questions to Danswer directly from Slack. Additionally, you can:
</p>
<div className="mb-2">
<ul className="list-disc mt-2 ml-4 text-sm text-muted-foreground">
<li>
Setup DanswerBot to automatically answer questions in certain
channels.
</li>
<li>
Choose which document sets DanswerBot should answer from, depending
on the channel the question is being asked.
</li>
<li>
Directly message DanswerBot to search just as you would in the web
UI.
</li>
</ul>
</div>
<p className="mb-6 text-sm text-muted-foreground">
Follow the{" "}
<a
className="text-blue-500 hover:underline"
href="https://docs.danswer.dev/slack_bot_setup"
target="_blank"
rel="noopener noreferrer"
>
guide{" "}
</a>
found in the Danswer documentation to get started!
</p>
<Link
className="
flex
py-2
px-4
mt-2
border
border-border
h-fit
cursor-pointer
hover:bg-hover
text-sm
w-40
"
href="/admin/bots/new"
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />
New Slack Bot
</div>
</Link>
<SlackBotTable slackBots={slackBots} />
</div>
);
};
const Page = () => {
return (
<div className="container mx-auto">
<AdminPageTitle
icon={<SourceIcon iconSize={36} sourceType={"slack"} />}
title="Slack Bots"
/>
<InstantSSRAutoRefresh />
<Main />
</div>
);
};
export default Page;

View File

@ -7,16 +7,14 @@ import { SourceIcon } from "@/components/SourceIcon";
import { CCPairStatus } from "@/components/Status";
import { usePopup } from "@/components/admin/connectors/Popup";
import CredentialSection from "@/components/credentials/CredentialSection";
import { CheckmarkIcon, EditIcon, XIcon } from "@/components/icons/icons";
import { updateConnectorCredentialPairName } from "@/lib/connector";
import { credentialTemplates } from "@/lib/connectors/credentials";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { ValidSources } from "@/lib/types";
import { Button } from "@/components/ui/button";
import Title from "@/components/ui/title";
import { Separator } from "@/components/ui/separator";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef, useState, use } from "react";
import { useCallback, useEffect, useState, use } from "react";
import useSWR, { mutate } from "swr";
import { AdvancedConfigDisplay, ConfigDisplay } from "./ConfigDisplay";
import { DeletionButton } from "./DeletionButton";
@ -26,6 +24,7 @@ import { ModifyStatusButtonCluster } from "./ModifyStatusButtonCluster";
import { ReIndexButton } from "./ReIndexButton";
import { buildCCPairInfoUrl } from "./lib";
import { CCPairFullInfo, ConnectorCredentialPairStatus } from "./types";
import { EditableStringFieldDisplay } from "@/components/EditableStringFieldDisplay";
// since the uploaded files are cleaned up after some period of time
// re-indexing will not work for the file connector. Also, it would not
@ -45,22 +44,12 @@ function Main({ ccPairId }: { ccPairId: number }) {
);
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
const [editableName, setEditableName] = useState(ccPair?.name || "");
const [isEditing, setIsEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const { popup, setPopup } = usePopup();
const finishConnectorDeletion = useCallback(() => {
router.push("/admin/indexing/status?message=connector-deleted");
}, [router]);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
useEffect(() => {
if (isLoading) {
return;
@ -78,21 +67,16 @@ function Main({ ccPairId }: { ccPairId: number }) {
}
}, [isLoading, ccPair, error, hasLoadedOnce, finishConnectorDeletion]);
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditableName(e.target.value);
};
const handleUpdateName = async () => {
const handleUpdateName = async (newName: string) => {
try {
const response = await updateConnectorCredentialPairName(
ccPair?.id!,
editableName
newName
);
if (!response.ok) {
throw new Error(await response.text());
}
mutate(buildCCPairInfoUrl(ccPairId));
setIsEditing(false);
setPopup({
message: "Connector name updated successfully",
type: "success",
@ -124,16 +108,6 @@ function Main({ ccPairId }: { ccPairId: number }) {
mutate(buildCCPairInfoUrl(ccPairId));
};
const startEditing = () => {
setEditableName(ccPair.name);
setIsEditing(true);
};
const resetEditing = () => {
setIsEditing(false);
setEditableName(ccPair.name);
};
const {
prune_freq: pruneFreq,
refresh_freq: refreshFreq,
@ -150,37 +124,11 @@ function Main({ ccPairId }: { ccPairId: number }) {
<SourceIcon iconSize={24} sourceType={ccPair.connector.source} />
</div>
{ccPair.is_editable_for_current_user && isEditing ? (
<div className="flex items-center">
<input
ref={inputRef}
type="text"
value={editableName}
onChange={handleNameChange}
className="text-3xl w-full ring ring-1 ring-neutral-800 text-emphasis font-bold"
/>
<Button onClick={handleUpdateName}>
<CheckmarkIcon className="text-neutral-200" />
</Button>
<Button onClick={() => resetEditing()}>
<XIcon className="text-neutral-200" />
</Button>
</div>
) : (
<h1
onClick={() =>
ccPair.is_editable_for_current_user && startEditing()
}
className={`group flex ${
ccPair.is_editable_for_current_user ? "cursor-pointer" : ""
} text-3xl text-emphasis gap-x-2 items-center font-bold`}
>
{ccPair.name}
{ccPair.is_editable_for_current_user && (
<EditIcon className="group-hover:visible invisible" />
)}
</h1>
)}
<EditableStringFieldDisplay
value={ccPair.name}
isEditable={ccPair.is_editable_for_current_user}
onUpdate={handleUpdateName}
/>
{ccPair.is_editable_for_current_user && (
<div className="ml-auto flex gap-x-2">

View File

@ -313,10 +313,8 @@ const Main = () => {
{popup}
<Text className="mb-3">
<b>Document Sets</b> allow you to group logically connected documents
into a single bundle. These can then be used as filter when performing
searches in the web UI or attached to slack bots to limit the amount of
information the bot searches over when answering in a specific channel
or with a certain command.
into a single bundle. These can then be used as a filter when performing
searches to control the scope of information Danswer searches over.
</Text>
<div className="mb-3"></div>

View File

@ -6,7 +6,6 @@ import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { useInputPrompt } from "../hooks";
import { EditPromptModalProps } from "../interfaces";
import { Input } from "@/components/ui/input";
const EditPromptSchema = Yup.object().shape({
prompt: Yup.string().required("Title is required"),
@ -75,7 +74,7 @@ const EditPromptModal = ({
Title
</label>
<Field
as={Input}
as={Textarea}
id="prompt"
name="prompt"
placeholder="Title (e.g. 'Draft email')"

View File

@ -11,19 +11,19 @@ import React, { useContext, useState, useEffect } from "react";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
function Checkbox({
export function Checkbox({
label,
sublabel,
checked,
onChange,
}: {
label: string;
sublabel: string;
sublabel?: string;
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
return (
<label className="flex text-sm mb-4">
<label className="flex text-sm">
<input
checked={checked}
onChange={onChange}
@ -32,7 +32,7 @@ function Checkbox({
/>
<div>
<Label>{label}</Label>
<SubLabel>{sublabel}</SubLabel>
{sublabel && <SubLabel>{sublabel}</SubLabel>}
</div>
</label>
);

View File

@ -65,7 +65,7 @@ export function DanswerBotChart({
return (
<CardSection className="mt-8">
<Title>Slack Bot</Title>
<Title>Slack Channel</Title>
<Text>Total Queries vs Auto Resolved</Text>
{chart}
</CardSection>

View File

@ -324,8 +324,8 @@ const StandardAnswersTable = ({
<div className="mt-4">
<Text>
Ensure that you have added the category to the relevant{" "}
<a className="text-link" href="/admin/bot">
Slack bot
<a className="text-link" href="/admin/bots">
Slack Bot
</a>
.
</Text>

View File

@ -6,8 +6,10 @@ import { FiChevronLeft } from "react-icons/fi";
export function BackButton({
behaviorOverride,
routerOverride,
}: {
behaviorOverride?: () => void;
routerOverride?: string;
}) {
const router = useRouter();
@ -27,6 +29,8 @@ export function BackButton({
onClick={() => {
if (behaviorOverride) {
behaviorOverride();
} else if (routerOverride) {
router.push(routerOverride);
} else {
router.back();
}

View File

@ -0,0 +1,130 @@
import { usePopup } from "@/components/admin/connectors/Popup";
import { CheckmarkIcon, EditIcon, XIcon } from "@/components/icons/icons";
import { Button } from "@/components/ui/button";
import { useEffect, useRef, useState } from "react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
interface EditableStringFieldDisplayProps {
value: string;
isEditable: boolean;
onUpdate: (newValue: string) => Promise<void>;
textClassName?: string;
scale?: number;
}
export function EditableStringFieldDisplay({
value,
isEditable,
onUpdate,
textClassName,
scale = 1,
}: EditableStringFieldDisplayProps) {
const [isEditing, setIsEditing] = useState(false);
const [editableValue, setEditableValue] = useState(value);
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
const { popup, setPopup } = usePopup();
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node) &&
isEditing
) {
resetEditing();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isEditing]);
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditableValue(e.target.value);
};
const handleUpdate = async () => {
await onUpdate(editableValue);
setIsEditing(false);
};
const resetEditing = () => {
setIsEditing(false);
setEditableValue(value);
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
if (e.key === "Enter") {
handleUpdate();
}
};
return (
<div ref={containerRef} className={"flex items-center"}>
{popup}
<Input
ref={inputRef as React.RefObject<HTMLInputElement>}
type="text"
value={editableValue}
onChange={handleValueChange}
onKeyDown={handleKeyDown}
className={cn(textClassName, isEditing ? "block" : "hidden")}
style={{ fontSize: `${scale}rem` }}
/>
{!isEditing && (
<span
onClick={() => isEditable && setIsEditing(true)}
className={cn(textClassName, "cursor-pointer")}
style={{ fontSize: `${scale}rem` }}
>
{value}
</span>
)}
{isEditing && isEditable ? (
<>
<div className={cn("flex", "flex-row")}>
<Button
onClick={handleUpdate}
variant="ghost"
size="sm"
className="p-0 hover:bg-transparent ml-2"
>
<CheckmarkIcon className={`text-600`} size={12 * scale} />
</Button>
<Button
onClick={resetEditing}
variant="ghost"
size="sm"
className="p-0 hover:bg-transparent ml-2"
>
<XIcon className={`text-600`} size={12 * scale} />
</Button>
</div>
</>
) : (
<h1
onClick={() => isEditable && setIsEditing(true)}
className={`group flex ${isEditable ? "cursor-pointer" : ""} ${""}`}
style={{ fontSize: `${scale}rem` }}
>
{isEditable && (
<EditIcon className={`visible ml-2`} size={8 * scale} />
)}
</h1>
)}
</div>
);
}

View File

@ -0,0 +1,111 @@
import { usePopup } from "@/components/admin/connectors/Popup";
import { CheckmarkIcon, XIcon } from "@/components/icons/icons";
import { Button } from "@/components/ui/button";
import { useEffect, useRef, useState } from "react";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
interface EditableTextAreaDisplayProps {
value: string;
isEditable: boolean;
onUpdate: (newValue: string) => Promise<void>;
textClassName?: string;
scale?: number;
}
export function EditableTextAreaDisplay({
value,
isEditable,
onUpdate,
textClassName,
scale = 1,
}: EditableTextAreaDisplayProps) {
const [isEditing, setIsEditing] = useState(false);
const [editableValue, setEditableValue] = useState(value);
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node) &&
isEditing
) {
resetEditing();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isEditing]);
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditableValue(e.target.value);
};
const handleUpdate = async () => {
await onUpdate(editableValue);
setIsEditing(false);
};
const resetEditing = () => {
setIsEditing(false);
setEditableValue(value);
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
if (e.key === "Enter") {
handleUpdate();
}
};
return (
<div ref={containerRef} className={"flex items-center"}>
<Textarea
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
value={editableValue}
onChange={(e) => setEditableValue(e.target.value)}
onKeyDown={handleKeyDown}
className={cn(
textClassName,
"bg-white",
isEditable && !isEditing && "cursor-pointer"
)}
style={{ fontSize: `${scale}rem` }}
readOnly={!isEditing}
onClick={() => isEditable && !isEditing && setIsEditing(true)}
/>
{isEditing && isEditable ? (
<div className={cn("flex", "flex-col gap-1")}>
<Button
onClick={handleUpdate}
variant="ghost"
size="sm"
className="p-0 hover:bg-transparent ml-2"
>
<CheckmarkIcon className={`text-600`} size={12 * scale} />
</Button>
<Button
onClick={resetEditing}
variant="ghost"
size="sm"
className="p-0 hover:bg-transparent ml-2"
>
<XIcon className={`text-600`} size={12 * scale} />
</Button>
</div>
) : null}
</div>
);
}

View File

@ -155,7 +155,7 @@ export function ClientLayout({
<div className="ml-1">Slack Bots</div>
</div>
),
link: "/admin/bot",
link: "/admin/bots",
},
{
name: (

View File

@ -2,7 +2,6 @@
import React from "react";
import Text from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { Modal } from "../../Modal";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";

View File

@ -3,17 +3,33 @@ import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {
isEditing?: boolean;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
({ className, type, isEditing = true, style, ...props }, ref) => {
const textClassName = "text-2xl text-strong dark:text-neutral-50";
if (!isEditing) {
return (
<span className={cn(textClassName, className)}>
{props.value || props.defaultValue}
</span>
);
}
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-neutral-950 placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:bg-neutral-950 dark:ring-offset-neutral-950 dark:file:text-neutral-50 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300",
textClassName,
"w-[1ch] min-w-[1ch] box-content pr-1",
className
)}
style={{
width: `${Math.max(1, String(props.value || props.defaultValue || "").length)}ch`,
...style,
}}
ref={ref}
{...props}
/>

View File

@ -10,7 +10,31 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:bg-neutral-950 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300",
[
"flex",
"min-h-[80px]",
"w-full",
"rounded-md",
"border",
"border-neutral-200",
"bg-white",
"px-3",
"py-2",
"text-sm",
"ring-offset-white",
"placeholder:text-neutral-500",
// "focus-visible:outline-none",
// "focus-visible:ring-2",
// "focus-visible:ring-neutral-950",
// "focus-visible:ring-offset-2",
"disabled:cursor-not-allowed",
"disabled:opacity-50",
"dark:border-neutral-800",
"dark:bg-neutral-950",
"dark:ring-offset-neutral-950",
"dark:placeholder:text-neutral-400",
"dark:focus-visible:ring-neutral-300",
].join(" "),
className
)}
ref={ref}

View File

@ -571,7 +571,7 @@ Hint: Use the singular form of the object name (e.g., 'Opportunity' instead of '
name: "channels",
description: `Specify 0 or more channels to index. For example, specifying the channel "support" will cause us to only index all content within the "#support" channel. If no channels are specified, all channels in your workspace will be indexed.`,
optional: true,
// Slack channels can only be lowercase
// Slack Channels can only be lowercase
transform: (values) => values.map((value) => value.toLowerCase()),
},
{

View File

@ -204,7 +204,7 @@ export type AnswerFilterOption =
| "questionmark_prefilter";
export interface ChannelConfig {
channel_names: string[];
channel_name: string;
respond_tag_only?: boolean;
respond_to_bots?: boolean;
respond_member_group_list?: string[];
@ -214,8 +214,9 @@ export interface ChannelConfig {
export type SlackBotResponseType = "quotes" | "citations";
export interface SlackBotConfig {
export interface SlackChannelConfig {
id: number;
slack_bot_id: number;
persona: Persona | null;
channel_config: ChannelConfig;
response_type: SlackBotResponseType;
@ -223,6 +224,17 @@ export interface SlackBotConfig {
enable_auto_filters: boolean;
}
export interface SlackBot {
id: number;
name: string;
enabled: boolean;
configs_count: number;
// tokens
bot_token: string;
app_token: string;
}
export interface SlackBotTokens {
bot_token: string;
app_token: string;

View File

@ -0,0 +1,18 @@
import { SlackBot } from "@/lib/types";
export async function updateSlackBotField(
slackBot: SlackBot,
field: keyof SlackBot,
value: any
): Promise<Response> {
return fetch(`/api/manage/admin/slack-app/bots/${slackBot.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...slackBot,
[field]: value,
}),
});
}