mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-29 11:12:02 +01:00
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:
parent
b712877701
commit
9209fc804b
@ -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>
|
||||
|
||||
|
@ -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")
|
@ -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 = (
|
||||
|
@ -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
|
||||
|
||||
"""
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
)
|
@ -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
|
||||
|
@ -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",
|
||||
)
|
||||
|
||||
|
76
backend/danswer/db/slack_bot.py
Normal file
76
backend/danswer/db/slack_bot.py
Normal 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()
|
@ -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()
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
]
|
||||
|
@ -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,
|
||||
|
@ -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:-}
|
||||
|
@ -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:-}
|
||||
|
@ -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
|
||||
|
@ -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: ""
|
||||
|
@ -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: ""
|
||||
|
@ -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),
|
||||
};
|
||||
};
|
@ -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;
|
@ -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;
|
31
web/src/app/admin/bots/SlackBotCreationForm.tsx
Normal file
31
web/src/app/admin/bots/SlackBotCreationForm.tsx
Normal 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>
|
||||
);
|
||||
};
|
121
web/src/app/admin/bots/SlackBotTable.tsx
Normal file
121
web/src/app/admin/bots/SlackBotTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
170
web/src/app/admin/bots/SlackBotUpdateForm.tsx
Normal file
170
web/src/app/admin/bots/SlackBotUpdateForm.tsx
Normal 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>
|
||||
);
|
||||
};
|
108
web/src/app/admin/bots/SlackTokensForm.tsx
Normal file
108
web/src/app/admin/bots/SlackTokensForm.tsx
Normal 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>
|
||||
);
|
157
web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx
Normal file
157
web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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}
|
@ -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;
|
76
web/src/app/admin/bots/[bot-id]/channels/new/page.tsx
Normal file
76
web/src/app/admin/bots/[bot-id]/channels/new/page.tsx
Normal 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;
|
43
web/src/app/admin/bots/[bot-id]/hooks.ts
Normal file
43
web/src/app/admin/bots/[bot-id]/hooks.ts
Normal 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),
|
||||
};
|
||||
};
|
@ -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__");
|
||||
}
|
118
web/src/app/admin/bots/[bot-id]/page.tsx
Normal file
118
web/src/app/admin/bots/[bot-id]/page.tsx
Normal 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;
|
52
web/src/app/admin/bots/new/lib.ts
Normal file
52
web/src/app/admin/bots/new/lib.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
};
|
14
web/src/app/admin/bots/new/page.tsx
Normal file
14
web/src/app/admin/bots/new/page.tsx
Normal 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;
|
116
web/src/app/admin/bots/page.tsx
Normal file
116
web/src/app/admin/bots/page.tsx
Normal 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;
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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')"
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
|
130
web/src/components/EditableStringFieldDisplay.tsx
Normal file
130
web/src/components/EditableStringFieldDisplay.tsx
Normal 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>
|
||||
);
|
||||
}
|
111
web/src/components/EditableTextAreaDisplay.tsx
Normal file
111
web/src/components/EditableTextAreaDisplay.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -155,7 +155,7 @@ export function ClientLayout({
|
||||
<div className="ml-1">Slack Bots</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/bot",
|
||||
link: "/admin/bots",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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()),
|
||||
},
|
||||
{
|
||||
|
@ -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;
|
||||
|
18
web/src/lib/updateSlackBotField.ts
Normal file
18
web/src/lib/updateSlackBotField.ts
Normal 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,
|
||||
}),
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user