mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-05 12:39:33 +02:00
Slack bot management dashboard (#483)
This commit is contained in:
parent
0c58c8d6cb
commit
d41d844116
@ -0,0 +1,38 @@
|
|||||||
|
"""Add SlackBotConfig table
|
||||||
|
|
||||||
|
Revision ID: 7da543f5672f
|
||||||
|
Revises: febe9eaa0644
|
||||||
|
Create Date: 2023-09-24 16:34:17.526128
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "7da543f5672f"
|
||||||
|
down_revision = "febe9eaa0644"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
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(astext_type=sa.Text()),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["persona_id"],
|
||||||
|
["persona.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("slack_bot_config")
|
@ -0,0 +1,37 @@
|
|||||||
|
"""Add document_set / persona relationship table
|
||||||
|
|
||||||
|
Revision ID: febe9eaa0644
|
||||||
|
Revises: 57b53544726e
|
||||||
|
Create Date: 2023-09-24 13:06:24.018610
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "febe9eaa0644"
|
||||||
|
down_revision = "57b53544726e"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"persona__document_set",
|
||||||
|
sa.Column("persona_id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("document_set_id", sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["document_set_id"],
|
||||||
|
["document_set.id"],
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(
|
||||||
|
["persona_id"],
|
||||||
|
["persona.id"],
|
||||||
|
),
|
||||||
|
sa.PrimaryKeyConstraint("persona_id", "document_set_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("persona__document_set")
|
40
backend/danswer/bots/slack/config.py
Normal file
40
backend/danswer/bots/slack/config.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from danswer.db.models import SlackBotConfig
|
||||||
|
from danswer.db.slack_bot_config import fetch_slack_bot_configs
|
||||||
|
|
||||||
|
|
||||||
|
def get_slack_bot_config_for_channel(
|
||||||
|
channel_name: str, db_session: Session
|
||||||
|
) -> SlackBotConfig | None:
|
||||||
|
slack_bot_configs = fetch_slack_bot_configs(db_session=db_session)
|
||||||
|
for config in slack_bot_configs:
|
||||||
|
if channel_name in config.channel_config["channel_names"]:
|
||||||
|
return config
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_channel_names(
|
||||||
|
channel_names: list[str],
|
||||||
|
current_slack_bot_config_id: int | None,
|
||||||
|
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:
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
return cleaned_channel_names
|
@ -6,12 +6,16 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from danswer.bots.slack.blocks import build_documents_blocks
|
from danswer.bots.slack.blocks import build_documents_blocks
|
||||||
from danswer.bots.slack.blocks import build_qa_response_blocks
|
from danswer.bots.slack.blocks import build_qa_response_blocks
|
||||||
|
from danswer.bots.slack.config import get_slack_bot_config_for_channel
|
||||||
|
from danswer.bots.slack.utils import get_channel_name_from_id
|
||||||
from danswer.bots.slack.utils import respond_in_thread
|
from danswer.bots.slack.utils import respond_in_thread
|
||||||
from danswer.configs.app_configs import DANSWER_BOT_ANSWER_GENERATION_TIMEOUT
|
from danswer.configs.app_configs import DANSWER_BOT_ANSWER_GENERATION_TIMEOUT
|
||||||
from danswer.configs.app_configs import DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER
|
from danswer.configs.app_configs import DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER
|
||||||
from danswer.configs.app_configs import DANSWER_BOT_DISPLAY_ERROR_MSGS
|
from danswer.configs.app_configs import DANSWER_BOT_DISPLAY_ERROR_MSGS
|
||||||
from danswer.configs.app_configs import DANSWER_BOT_NUM_RETRIES
|
from danswer.configs.app_configs import DANSWER_BOT_NUM_RETRIES
|
||||||
from danswer.configs.app_configs import DOCUMENT_INDEX_NAME
|
from danswer.configs.app_configs import DOCUMENT_INDEX_NAME
|
||||||
|
from danswer.configs.app_configs import ENABLE_DANSWERBOT_REFLEXION
|
||||||
|
from danswer.configs.constants import DOCUMENT_SETS
|
||||||
from danswer.db.engine import get_sqlalchemy_engine
|
from danswer.db.engine import get_sqlalchemy_engine
|
||||||
from danswer.direct_qa.answer_question import answer_qa_query
|
from danswer.direct_qa.answer_question import answer_qa_query
|
||||||
from danswer.server.models import QAResponse
|
from danswer.server.models import QAResponse
|
||||||
@ -29,6 +33,27 @@ def handle_message(
|
|||||||
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
|
should_respond_with_error_msgs: bool = DANSWER_BOT_DISPLAY_ERROR_MSGS,
|
||||||
disable_docs_only_answer: bool = DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER,
|
disable_docs_only_answer: bool = DANSWER_BOT_DISABLE_DOCS_ONLY_ANSWER,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
engine = get_sqlalchemy_engine()
|
||||||
|
with Session(engine) as db_session:
|
||||||
|
channel_name = get_channel_name_from_id(client=client, channel_id=channel)
|
||||||
|
slack_bot_config = get_slack_bot_config_for_channel(
|
||||||
|
channel_name=channel_name, db_session=db_session
|
||||||
|
)
|
||||||
|
document_set_names: list[str] | None = None
|
||||||
|
validity_check_enabled = ENABLE_DANSWERBOT_REFLEXION
|
||||||
|
if slack_bot_config and slack_bot_config.persona:
|
||||||
|
document_set_names = [
|
||||||
|
document_set.name
|
||||||
|
for document_set in slack_bot_config.persona.document_sets
|
||||||
|
]
|
||||||
|
validity_check_enabled = slack_bot_config.channel_config.get(
|
||||||
|
"answer_validity_check_enabled", validity_check_enabled
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Found slack bot config for channel. Restricting bot to use document "
|
||||||
|
f"sets: {document_set_names}, validity check enabled: {validity_check_enabled}"
|
||||||
|
)
|
||||||
|
|
||||||
@retry(
|
@retry(
|
||||||
tries=num_retries,
|
tries=num_retries,
|
||||||
delay=0.25,
|
delay=0.25,
|
||||||
@ -45,6 +70,7 @@ def handle_message(
|
|||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
answer_generation_timeout=answer_generation_timeout,
|
answer_generation_timeout=answer_generation_timeout,
|
||||||
real_time_flow=False,
|
real_time_flow=False,
|
||||||
|
enable_reflexion=validity_check_enabled,
|
||||||
)
|
)
|
||||||
if not answer.error_msg:
|
if not answer.error_msg:
|
||||||
return answer
|
return answer
|
||||||
@ -57,7 +83,9 @@ def handle_message(
|
|||||||
query=msg,
|
query=msg,
|
||||||
collection=DOCUMENT_INDEX_NAME,
|
collection=DOCUMENT_INDEX_NAME,
|
||||||
use_keyword=False, # always use semantic search when handling Slack messages
|
use_keyword=False, # always use semantic search when handling Slack messages
|
||||||
filters=None,
|
filters=[{DOCUMENT_SETS: document_set_names}]
|
||||||
|
if document_set_names
|
||||||
|
else None,
|
||||||
offset=None,
|
offset=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from collections.abc import MutableMapping
|
from collections.abc import MutableMapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import cast
|
from typing import cast
|
||||||
@ -12,6 +11,8 @@ from slack_sdk.socket_mode.response import SocketModeResponse
|
|||||||
from danswer.bots.slack.handlers.handle_feedback import handle_slack_feedback
|
from danswer.bots.slack.handlers.handle_feedback import handle_slack_feedback
|
||||||
from danswer.bots.slack.handlers.handle_message import handle_message
|
from danswer.bots.slack.handlers.handle_message import handle_message
|
||||||
from danswer.bots.slack.utils import decompose_block_id
|
from danswer.bots.slack.utils import decompose_block_id
|
||||||
|
from danswer.dynamic_configs.interface import ConfigNotFoundError
|
||||||
|
from danswer.server.slack_bot_management import get_tokens
|
||||||
from danswer.utils.logger import setup_logger
|
from danswer.utils.logger import setup_logger
|
||||||
|
|
||||||
logger = setup_logger()
|
logger = setup_logger()
|
||||||
@ -37,16 +38,14 @@ class _ChannelIdAdapter(logging.LoggerAdapter):
|
|||||||
def _get_socket_client() -> SocketModeClient:
|
def _get_socket_client() -> SocketModeClient:
|
||||||
# For more info on how to set this up, checkout the docs:
|
# For more info on how to set this up, checkout the docs:
|
||||||
# https://docs.danswer.dev/slack_bot_setup
|
# https://docs.danswer.dev/slack_bot_setup
|
||||||
app_token = os.environ.get("DANSWER_BOT_SLACK_APP_TOKEN")
|
try:
|
||||||
if not app_token:
|
slack_bot_tokens = get_tokens()
|
||||||
raise RuntimeError("DANSWER_BOT_SLACK_APP_TOKEN is not set")
|
except ConfigNotFoundError:
|
||||||
bot_token = os.environ.get("DANSWER_BOT_SLACK_BOT_TOKEN")
|
raise RuntimeError("Slack tokens not found")
|
||||||
if not bot_token:
|
|
||||||
raise RuntimeError("DANSWER_BOT_SLACK_BOT_TOKEN is not set")
|
|
||||||
return SocketModeClient(
|
return SocketModeClient(
|
||||||
# This app-level token will be used only for establishing a connection
|
# This app-level token will be used only for establishing a connection
|
||||||
app_token=app_token,
|
app_token=slack_bot_tokens.app_token,
|
||||||
web_client=WebClient(token=bot_token),
|
web_client=WebClient(token=slack_bot_tokens.bot_token),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
35
backend/danswer/bots/slack/tokens.py
Normal file
35
backend/danswer/bots/slack/tokens.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import os
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from danswer.dynamic_configs import get_dynamic_config_store
|
||||||
|
from danswer.dynamic_configs.interface import ConfigNotFoundError
|
||||||
|
from danswer.server.models import SlackBotTokens
|
||||||
|
|
||||||
|
|
||||||
|
_SLACK_BOT_TOKENS_CONFIG_KEY = "slack_bot_tokens_config_key"
|
||||||
|
|
||||||
|
|
||||||
|
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_dynamic_config_store()
|
||||||
|
try:
|
||||||
|
return SlackBotTokens(
|
||||||
|
**cast(dict, dynamic_config_store.load(key=_SLACK_BOT_TOKENS_CONFIG_KEY))
|
||||||
|
)
|
||||||
|
except ConfigNotFoundError as e:
|
||||||
|
raise ValueError from e
|
||||||
|
|
||||||
|
|
||||||
|
def save_tokens(
|
||||||
|
tokens: SlackBotTokens,
|
||||||
|
) -> None:
|
||||||
|
dynamic_config_store = get_dynamic_config_store()
|
||||||
|
dynamic_config_store.store(
|
||||||
|
key=_SLACK_BOT_TOKENS_CONFIG_KEY,
|
||||||
|
val=dict(tokens),
|
||||||
|
)
|
@ -2,6 +2,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
from typing import Any
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from retry import retry
|
from retry import retry
|
||||||
@ -9,6 +10,7 @@ from slack_sdk import WebClient
|
|||||||
from slack_sdk.models.blocks import Block
|
from slack_sdk.models.blocks import Block
|
||||||
from slack_sdk.models.metadata import Metadata
|
from slack_sdk.models.metadata import Metadata
|
||||||
|
|
||||||
|
from danswer.bots.slack.tokens import fetch_tokens
|
||||||
from danswer.configs.app_configs import DANSWER_BOT_NUM_RETRIES
|
from danswer.configs.app_configs import DANSWER_BOT_NUM_RETRIES
|
||||||
from danswer.configs.constants import ID_SEPARATOR
|
from danswer.configs.constants import ID_SEPARATOR
|
||||||
from danswer.connectors.slack.utils import make_slack_api_rate_limited
|
from danswer.connectors.slack.utils import make_slack_api_rate_limited
|
||||||
@ -19,6 +21,11 @@ from danswer.utils.text_processing import replace_whitespaces_w_space
|
|||||||
logger = setup_logger()
|
logger = setup_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def get_web_client() -> WebClient:
|
||||||
|
slack_tokens = fetch_tokens()
|
||||||
|
return WebClient(token=slack_tokens.bot_token)
|
||||||
|
|
||||||
|
|
||||||
@retry(
|
@retry(
|
||||||
tries=DANSWER_BOT_NUM_RETRIES,
|
tries=DANSWER_BOT_NUM_RETRIES,
|
||||||
delay=0.25,
|
delay=0.25,
|
||||||
@ -119,3 +126,13 @@ def remove_slack_text_interactions(slack_str: str) -> str:
|
|||||||
slack_str = UserIdReplacer.replace_links(slack_str)
|
slack_str = UserIdReplacer.replace_links(slack_str)
|
||||||
slack_str = UserIdReplacer.add_zero_width_whitespace_after_tag(slack_str)
|
slack_str = UserIdReplacer.add_zero_width_whitespace_after_tag(slack_str)
|
||||||
return slack_str
|
return slack_str
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_from_id(client: WebClient, channel_id: str) -> dict[str, Any]:
|
||||||
|
response = client.conversations_info(channel=channel_id)
|
||||||
|
response.validate()
|
||||||
|
return response["channel"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_name_from_id(client: WebClient, channel_id: str) -> str:
|
||||||
|
return get_channel_from_id(client, channel_id)["name"]
|
||||||
|
@ -270,6 +270,7 @@ def create_persona(
|
|||||||
hint_text: str | None,
|
hint_text: str | None,
|
||||||
default_persona: bool,
|
default_persona: bool,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
|
commit: bool = True,
|
||||||
) -> Persona:
|
) -> Persona:
|
||||||
persona = db_session.query(Persona).filter_by(id=persona_id).first()
|
persona = db_session.query(Persona).filter_by(id=persona_id).first()
|
||||||
|
|
||||||
@ -292,6 +293,10 @@ def create_persona(
|
|||||||
)
|
)
|
||||||
db_session.add(persona)
|
db_session.add(persona)
|
||||||
|
|
||||||
|
if commit:
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
else:
|
||||||
|
# flush the session so that the persona has an ID
|
||||||
|
db_session.flush()
|
||||||
|
|
||||||
return persona
|
return persona
|
||||||
|
@ -2,6 +2,8 @@ import datetime
|
|||||||
from enum import Enum as PyEnum
|
from enum import Enum as PyEnum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import NotRequired
|
||||||
|
from typing import TypedDict
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi_users.db import SQLAlchemyBaseOAuthAccountTableUUID
|
from fastapi_users.db import SQLAlchemyBaseOAuthAccountTableUUID
|
||||||
@ -79,6 +81,43 @@ class AccessToken(SQLAlchemyBaseAccessTokenTableUUID, Base):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Association tables
|
||||||
|
NOTE: must be at the top since they are referenced by other tables
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Persona__DocumentSet(Base):
|
||||||
|
__tablename__ = "persona__document_set"
|
||||||
|
|
||||||
|
persona_id: Mapped[int] = mapped_column(ForeignKey("persona.id"), primary_key=True)
|
||||||
|
document_set_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("document_set.id"), primary_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentSet__ConnectorCredentialPair(Base):
|
||||||
|
__tablename__ = "document_set__connector_credential_pair"
|
||||||
|
|
||||||
|
document_set_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("document_set.id"), primary_key=True
|
||||||
|
)
|
||||||
|
connector_credential_pair_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("connector_credential_pair.id"), primary_key=True
|
||||||
|
)
|
||||||
|
# if `True`, then is part of the current state of the document set
|
||||||
|
# if `False`, then is a part of the prior state of the document set
|
||||||
|
# rows with `is_current=False` should be deleted when the document
|
||||||
|
# set is updated and should not exist for a given document set if
|
||||||
|
# `DocumentSet.is_up_to_date == True`
|
||||||
|
is_current: Mapped[bool] = mapped_column(
|
||||||
|
Boolean,
|
||||||
|
nullable=False,
|
||||||
|
default=True,
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConnectorCredentialPair(Base):
|
class ConnectorCredentialPair(Base):
|
||||||
"""Connectors and Credentials can have a many-to-many relationship
|
"""Connectors and Credentials can have a many-to-many relationship
|
||||||
I.e. A Confluence Connector may have multiple admin users who can run it with their own credentials
|
I.e. A Confluence Connector may have multiple admin users who can run it with their own credentials
|
||||||
@ -118,6 +157,11 @@ class ConnectorCredentialPair(Base):
|
|||||||
credential: Mapped["Credential"] = relationship(
|
credential: Mapped["Credential"] = relationship(
|
||||||
"Credential", back_populates="connectors"
|
"Credential", back_populates="connectors"
|
||||||
)
|
)
|
||||||
|
document_sets: Mapped[List["DocumentSet"]] = relationship(
|
||||||
|
"DocumentSet",
|
||||||
|
secondary=DocumentSet__ConnectorCredentialPair.__table__,
|
||||||
|
back_populates="connector_credential_pairs",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Connector(Base):
|
class Connector(Base):
|
||||||
@ -349,36 +393,15 @@ class DocumentSet(Base):
|
|||||||
# whether or not changes to the document set have been propogated
|
# whether or not changes to the document set have been propogated
|
||||||
is_up_to_date: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
is_up_to_date: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
connector_credential_pair_relationships: Mapped[
|
connector_credential_pairs: Mapped[list[ConnectorCredentialPair]] = relationship(
|
||||||
list["DocumentSet__ConnectorCredentialPair"]
|
"ConnectorCredentialPair",
|
||||||
] = relationship(
|
secondary=DocumentSet__ConnectorCredentialPair.__table__,
|
||||||
"DocumentSet__ConnectorCredentialPair", back_populates="document_set"
|
back_populates="document_sets",
|
||||||
)
|
)
|
||||||
|
personas: Mapped[list["Persona"]] = relationship(
|
||||||
|
"Persona",
|
||||||
class DocumentSet__ConnectorCredentialPair(Base):
|
secondary=Persona__DocumentSet.__table__,
|
||||||
__tablename__ = "document_set__connector_credential_pair"
|
back_populates="document_sets",
|
||||||
|
|
||||||
document_set_id: Mapped[int] = mapped_column(
|
|
||||||
ForeignKey("document_set.id"), primary_key=True
|
|
||||||
)
|
|
||||||
connector_credential_pair_id: Mapped[int] = mapped_column(
|
|
||||||
ForeignKey("connector_credential_pair.id"), primary_key=True
|
|
||||||
)
|
|
||||||
# if `True`, then is part of the current state of the document set
|
|
||||||
# if `False`, then is a part of the prior state of the document set
|
|
||||||
# rows with `is_current=False` should be deleted when the document
|
|
||||||
# set is updated and should not exist for a given document set if
|
|
||||||
# `DocumentSet.is_up_to_date == True`
|
|
||||||
is_current: Mapped[bool] = mapped_column(
|
|
||||||
Boolean,
|
|
||||||
nullable=False,
|
|
||||||
default=True,
|
|
||||||
primary_key=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
document_set: Mapped[DocumentSet] = relationship(
|
|
||||||
"DocumentSet", back_populates="connector_credential_pair_relationships"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -421,6 +444,12 @@ class Persona(Base):
|
|||||||
# If it's updated and no longer latest (should no longer be shown), it is also considered deleted
|
# If it's updated and no longer latest (should no longer be shown), it is also considered deleted
|
||||||
deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
deleted: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
|
|
||||||
|
document_sets: Mapped[list[DocumentSet]] = relationship(
|
||||||
|
"DocumentSet",
|
||||||
|
secondary=Persona__DocumentSet.__table__,
|
||||||
|
back_populates="personas",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChatMessage(Base):
|
class ChatMessage(Base):
|
||||||
__tablename__ = "chat_message"
|
__tablename__ = "chat_message"
|
||||||
@ -446,3 +475,27 @@ class ChatMessage(Base):
|
|||||||
|
|
||||||
chat_session: Mapped[ChatSession] = relationship("ChatSession")
|
chat_session: Mapped[ChatSession] = relationship("ChatSession")
|
||||||
persona: Mapped[Persona | None] = relationship("Persona")
|
persona: Mapped[Persona | None] = relationship("Persona")
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelConfig(TypedDict):
|
||||||
|
"""NOTE: is a `TypedDict` so it can be used a type hint for a JSONB column
|
||||||
|
in Postgres"""
|
||||||
|
|
||||||
|
channel_names: list[str]
|
||||||
|
answer_validity_check_enabled: NotRequired[bool] # not specified => False
|
||||||
|
team_members: NotRequired[list[str]]
|
||||||
|
|
||||||
|
|
||||||
|
class SlackBotConfig(Base):
|
||||||
|
__tablename__ = "slack_bot_config"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
persona_id: Mapped[int | None] = mapped_column(
|
||||||
|
ForeignKey("persona.id"), nullable=True
|
||||||
|
)
|
||||||
|
# JSON for flexibility. Contains things like: channel name, team members, etc.
|
||||||
|
channel_config: Mapped[ChannelConfig] = mapped_column(
|
||||||
|
postgresql.JSONB(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
persona: Mapped[Persona | None] = relationship("Persona")
|
||||||
|
149
backend/danswer/db/slack_bot_config.py
Normal file
149
backend/danswer/db/slack_bot_config.py
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from danswer.db.chat import create_persona
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def _build_persona_name(channel_names: list[str]) -> str:
|
||||||
|
return f"__slack_bot_persona__{'-'.join(channel_names)}"
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
|
||||||
|
"""NOTE: does not commit changes"""
|
||||||
|
# delete existing persona-document_set relationships
|
||||||
|
existing_relationships = db_session.scalars(
|
||||||
|
select(Persona__DocumentSet).where(
|
||||||
|
Persona__DocumentSet.persona_id == persona_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for rel in existing_relationships:
|
||||||
|
db_session.delete(rel)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_slack_bot_persona(
|
||||||
|
db_session: Session,
|
||||||
|
channel_names: list[str],
|
||||||
|
document_sets: list[int],
|
||||||
|
existing_persona_id: int | None = None,
|
||||||
|
) -> Persona:
|
||||||
|
"""NOTE: does not commit changes"""
|
||||||
|
# create/update persona associated with the slack bot
|
||||||
|
persona_name = _build_persona_name(channel_names)
|
||||||
|
persona = create_persona(
|
||||||
|
persona_id=existing_persona_id,
|
||||||
|
name=persona_name,
|
||||||
|
retrieval_enabled=True,
|
||||||
|
system_text=None,
|
||||||
|
tools_text=None,
|
||||||
|
hint_text=None,
|
||||||
|
default_persona=False,
|
||||||
|
db_session=db_session,
|
||||||
|
commit=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_persona_id:
|
||||||
|
_cleanup_relationships(db_session=db_session, persona_id=existing_persona_id)
|
||||||
|
|
||||||
|
# create relationship between the new persona and the desired document_sets
|
||||||
|
for document_set_id in document_sets:
|
||||||
|
db_session.add(
|
||||||
|
Persona__DocumentSet(persona_id=persona.id, document_set_id=document_set_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return persona
|
||||||
|
|
||||||
|
|
||||||
|
def insert_slack_bot_config(
|
||||||
|
document_sets: list[int],
|
||||||
|
channel_config: ChannelConfig,
|
||||||
|
db_session: Session,
|
||||||
|
) -> SlackBotConfig:
|
||||||
|
persona = None
|
||||||
|
if document_sets:
|
||||||
|
persona = _create_slack_bot_persona(
|
||||||
|
db_session=db_session,
|
||||||
|
channel_names=channel_config["channel_names"],
|
||||||
|
document_sets=document_sets,
|
||||||
|
)
|
||||||
|
|
||||||
|
slack_bot_config = SlackBotConfig(
|
||||||
|
persona_id=persona.id if persona else None,
|
||||||
|
channel_config=channel_config,
|
||||||
|
)
|
||||||
|
db_session.add(slack_bot_config)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
return slack_bot_config
|
||||||
|
|
||||||
|
|
||||||
|
def update_slack_bot_config(
|
||||||
|
slack_bot_config_id: int,
|
||||||
|
document_sets: list[int],
|
||||||
|
channel_config: ChannelConfig,
|
||||||
|
db_session: Session,
|
||||||
|
) -> SlackBotConfig:
|
||||||
|
slack_bot_config = db_session.scalar(
|
||||||
|
select(SlackBotConfig).where(SlackBotConfig.id == slack_bot_config_id)
|
||||||
|
)
|
||||||
|
if slack_bot_config is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unable to find slack bot config with ID {slack_bot_config_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_persona_id = slack_bot_config.persona_id
|
||||||
|
|
||||||
|
persona = None
|
||||||
|
if document_sets:
|
||||||
|
persona = _create_slack_bot_persona(
|
||||||
|
db_session=db_session,
|
||||||
|
channel_names=channel_config["channel_names"],
|
||||||
|
document_sets=document_sets,
|
||||||
|
existing_persona_id=slack_bot_config.persona_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# if no document sets and an existing persona exists, then
|
||||||
|
# remove persona + persona -> document set relationships
|
||||||
|
if existing_persona_id:
|
||||||
|
_cleanup_relationships(
|
||||||
|
db_session=db_session, persona_id=existing_persona_id
|
||||||
|
)
|
||||||
|
existing_persona = db_session.scalar(
|
||||||
|
select(Persona).where(Persona.id == existing_persona_id)
|
||||||
|
)
|
||||||
|
db_session.delete(existing_persona)
|
||||||
|
|
||||||
|
slack_bot_config.persona_id = persona.id if persona else None
|
||||||
|
slack_bot_config.channel_config = channel_config
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
return slack_bot_config
|
||||||
|
|
||||||
|
|
||||||
|
def remove_slack_bot_config(
|
||||||
|
slack_bot_config_id: int,
|
||||||
|
db_session: Session,
|
||||||
|
) -> None:
|
||||||
|
slack_bot_config = db_session.scalar(
|
||||||
|
select(SlackBotConfig).where(SlackBotConfig.id == slack_bot_config_id)
|
||||||
|
)
|
||||||
|
if slack_bot_config is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unable to find slack bot config with ID {slack_bot_config_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_persona_id = slack_bot_config.persona_id
|
||||||
|
if existing_persona_id:
|
||||||
|
_cleanup_relationships(db_session=db_session, persona_id=existing_persona_id)
|
||||||
|
|
||||||
|
db_session.delete(slack_bot_config)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_slack_bot_configs(db_session: Session) -> Sequence[SlackBotConfig]:
|
||||||
|
return db_session.scalars(select(SlackBotConfig)).all()
|
@ -42,6 +42,7 @@ from danswer.server.event_loading import router as event_processing_router
|
|||||||
from danswer.server.health import router as health_router
|
from danswer.server.health import router as health_router
|
||||||
from danswer.server.manage import router as admin_router
|
from danswer.server.manage import router as admin_router
|
||||||
from danswer.server.search_backend import router as backend_router
|
from danswer.server.search_backend import router as backend_router
|
||||||
|
from danswer.server.slack_bot_management import router as slack_bot_management_router
|
||||||
from danswer.server.users import router as user_router
|
from danswer.server.users import router as user_router
|
||||||
from danswer.utils.logger import setup_logger
|
from danswer.utils.logger import setup_logger
|
||||||
|
|
||||||
@ -79,6 +80,7 @@ def get_application() -> FastAPI:
|
|||||||
application.include_router(user_router)
|
application.include_router(user_router)
|
||||||
application.include_router(credential_router)
|
application.include_router(credential_router)
|
||||||
application.include_router(document_set_router)
|
application.include_router(document_set_router)
|
||||||
|
application.include_router(slack_bot_management_router)
|
||||||
application.include_router(health_router)
|
application.include_router(health_router)
|
||||||
|
|
||||||
application.include_router(
|
application.include_router(
|
||||||
|
@ -16,9 +16,11 @@ from danswer.configs.constants import QAFeedbackType
|
|||||||
from danswer.configs.constants import SearchFeedbackType
|
from danswer.configs.constants import SearchFeedbackType
|
||||||
from danswer.connectors.models import InputType
|
from danswer.connectors.models import InputType
|
||||||
from danswer.datastores.interfaces import IndexFilter
|
from danswer.datastores.interfaces import IndexFilter
|
||||||
|
from danswer.db.models import ChannelConfig
|
||||||
from danswer.db.models import Connector
|
from danswer.db.models import Connector
|
||||||
from danswer.db.models import Credential
|
from danswer.db.models import Credential
|
||||||
from danswer.db.models import DeletionStatus
|
from danswer.db.models import DeletionStatus
|
||||||
|
from danswer.db.models import DocumentSet as DocumentSetDBModel
|
||||||
from danswer.db.models import IndexAttempt
|
from danswer.db.models import IndexAttempt
|
||||||
from danswer.db.models import IndexingStatus
|
from danswer.db.models import IndexingStatus
|
||||||
from danswer.direct_qa.interfaces import DanswerQuote
|
from danswer.direct_qa.interfaces import DanswerQuote
|
||||||
@ -386,3 +388,50 @@ class DocumentSet(BaseModel):
|
|||||||
description: str
|
description: str
|
||||||
cc_pair_descriptors: list[ConnectorCredentialPairDescriptor]
|
cc_pair_descriptors: list[ConnectorCredentialPairDescriptor]
|
||||||
is_up_to_date: bool
|
is_up_to_date: bool
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_model(cls, document_set_model: DocumentSetDBModel) -> "DocumentSet":
|
||||||
|
return cls(
|
||||||
|
id=document_set_model.id,
|
||||||
|
name=document_set_model.name,
|
||||||
|
description=document_set_model.description,
|
||||||
|
cc_pair_descriptors=[
|
||||||
|
ConnectorCredentialPairDescriptor(
|
||||||
|
id=cc_pair.id,
|
||||||
|
name=cc_pair.name,
|
||||||
|
connector=ConnectorSnapshot.from_connector_db_model(
|
||||||
|
cc_pair.connector
|
||||||
|
),
|
||||||
|
credential=CredentialSnapshot.from_credential_db_model(
|
||||||
|
cc_pair.credential
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for cc_pair in document_set_model.connector_credential_pairs
|
||||||
|
],
|
||||||
|
is_up_to_date=document_set_model.is_up_to_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SlackBotTokens(BaseModel):
|
||||||
|
bot_token: str
|
||||||
|
app_token: str
|
||||||
|
|
||||||
|
|
||||||
|
class SlackBotConfigCreationRequest(BaseModel):
|
||||||
|
# currently, a persona is created for each slack bot 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]
|
||||||
|
channel_names: list[str]
|
||||||
|
answer_validity_check_enabled: bool
|
||||||
|
|
||||||
|
|
||||||
|
class SlackBotConfig(BaseModel):
|
||||||
|
id: int
|
||||||
|
# currently, a persona is created for each slack bot 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[DocumentSet]
|
||||||
|
channel_config: ChannelConfig
|
||||||
|
159
backend/danswer/server/slack_bot_management.py
Normal file
159
backend/danswer/server/slack_bot_management.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from danswer.auth.users import current_admin_user
|
||||||
|
from danswer.bots.slack.config import validate_channel_names
|
||||||
|
from danswer.bots.slack.tokens import fetch_tokens
|
||||||
|
from danswer.bots.slack.tokens import save_tokens
|
||||||
|
from danswer.db.engine import get_session
|
||||||
|
from danswer.db.models import ChannelConfig
|
||||||
|
from danswer.db.models import User
|
||||||
|
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.server.models import DocumentSet
|
||||||
|
from danswer.server.models import SlackBotConfig
|
||||||
|
from danswer.server.models import SlackBotConfigCreationRequest
|
||||||
|
from danswer.server.models import SlackBotTokens
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/manage")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/admin/slack-bot/config")
|
||||||
|
def create_slack_bot_config(
|
||||||
|
slack_bot_config_creation_request: SlackBotConfigCreationRequest,
|
||||||
|
db_session: Session = Depends(get_session),
|
||||||
|
_: User | None = Depends(current_admin_user),
|
||||||
|
) -> SlackBotConfig:
|
||||||
|
if not slack_bot_config_creation_request.channel_names:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Must provide at least one channel name",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cleaned_channel_names = validate_channel_names(
|
||||||
|
channel_names=slack_bot_config_creation_request.channel_names,
|
||||||
|
current_slack_bot_config_id=None,
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_config: ChannelConfig = {
|
||||||
|
"channel_names": cleaned_channel_names,
|
||||||
|
"answer_validity_check_enabled": slack_bot_config_creation_request.answer_validity_check_enabled,
|
||||||
|
}
|
||||||
|
slack_bot_config_model = insert_slack_bot_config(
|
||||||
|
document_sets=slack_bot_config_creation_request.document_sets,
|
||||||
|
channel_config=channel_config,
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
return SlackBotConfig(
|
||||||
|
id=slack_bot_config_model.id,
|
||||||
|
document_sets=[
|
||||||
|
DocumentSet.from_model(document_set)
|
||||||
|
for document_set in slack_bot_config_model.persona.document_sets
|
||||||
|
]
|
||||||
|
if slack_bot_config_model.persona
|
||||||
|
else [],
|
||||||
|
channel_config=slack_bot_config_model.channel_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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,
|
||||||
|
db_session: Session = Depends(get_session),
|
||||||
|
_: User | None = Depends(current_admin_user),
|
||||||
|
) -> SlackBotConfig:
|
||||||
|
if not slack_bot_config_creation_request.channel_names:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Must provide at least one channel name",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cleaned_channel_names = validate_channel_names(
|
||||||
|
channel_names=slack_bot_config_creation_request.channel_names,
|
||||||
|
current_slack_bot_config_id=slack_bot_config_id,
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=str(e),
|
||||||
|
)
|
||||||
|
|
||||||
|
slack_bot_config_model = update_slack_bot_config(
|
||||||
|
slack_bot_config_id=slack_bot_config_id,
|
||||||
|
document_sets=slack_bot_config_creation_request.document_sets,
|
||||||
|
channel_config={
|
||||||
|
"channel_names": cleaned_channel_names,
|
||||||
|
"answer_validity_check_enabled": slack_bot_config_creation_request.answer_validity_check_enabled,
|
||||||
|
},
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
return SlackBotConfig(
|
||||||
|
id=slack_bot_config_model.id,
|
||||||
|
document_sets=[
|
||||||
|
DocumentSet.from_model(document_set)
|
||||||
|
for document_set in slack_bot_config_model.persona.document_sets
|
||||||
|
]
|
||||||
|
if slack_bot_config_model.persona
|
||||||
|
else [],
|
||||||
|
channel_config=slack_bot_config_model.channel_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/admin/slack-bot/config/{slack_bot_config_id}")
|
||||||
|
def delete_slack_bot_config(
|
||||||
|
slack_bot_config_id: int,
|
||||||
|
db_session: Session = Depends(get_session),
|
||||||
|
_: User | None = Depends(current_admin_user),
|
||||||
|
) -> None:
|
||||||
|
remove_slack_bot_config(
|
||||||
|
slack_bot_config_id=slack_bot_config_id, db_session=db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/slack-bot/config")
|
||||||
|
def list_slack_bot_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)
|
||||||
|
return [
|
||||||
|
SlackBotConfig(
|
||||||
|
id=slack_bot_config_model.id,
|
||||||
|
document_sets=[
|
||||||
|
DocumentSet.from_model(document_set)
|
||||||
|
for document_set in slack_bot_config_model.persona.document_sets
|
||||||
|
]
|
||||||
|
if slack_bot_config_model.persona
|
||||||
|
else [],
|
||||||
|
channel_config=slack_bot_config_model.channel_config,
|
||||||
|
)
|
||||||
|
for slack_bot_config_model in slack_bot_config_models
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/admin/slack-bot/tokens")
|
||||||
|
def put_tokens(tokens: SlackBotTokens) -> None:
|
||||||
|
save_tokens(tokens=tokens)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/admin/slack-bot/tokens")
|
||||||
|
def get_tokens() -> SlackBotTokens:
|
||||||
|
try:
|
||||||
|
return fetch_tokens()
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=404, detail="No tokens found")
|
188
web/src/app/admin/bot/SlackBotConfigCreationForm.tsx
Normal file
188
web/src/app/admin/bot/SlackBotConfigCreationForm.tsx
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||||
|
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||||
|
import {
|
||||||
|
BooleanFormField,
|
||||||
|
TextArrayField,
|
||||||
|
} from "@/components/admin/connectors/Field";
|
||||||
|
import { createSlackBotConfig, updateSlackBotConfig } from "./lib";
|
||||||
|
import { channel } from "diagnostics_channel";
|
||||||
|
|
||||||
|
interface SetCreationPopupProps {
|
||||||
|
onClose: () => void;
|
||||||
|
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||||
|
documentSets: DocumentSet<any, any>[];
|
||||||
|
existingSlackBotConfig?: SlackBotConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SlackBotCreationForm = ({
|
||||||
|
onClose,
|
||||||
|
setPopup,
|
||||||
|
documentSets,
|
||||||
|
existingSlackBotConfig,
|
||||||
|
}: SetCreationPopupProps) => {
|
||||||
|
const isUpdate = existingSlackBotConfig !== undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
channel_names: existingSlackBotConfig
|
||||||
|
? existingSlackBotConfig.channel_config.channel_names
|
||||||
|
: ([] as string[]),
|
||||||
|
answer_validity_check_enabled:
|
||||||
|
existingSlackBotConfig?.channel_config
|
||||||
|
?.answer_validity_check_enabled || false,
|
||||||
|
document_sets: existingSlackBotConfig
|
||||||
|
? existingSlackBotConfig.document_sets.map(
|
||||||
|
(documentSet) => documentSet.id
|
||||||
|
)
|
||||||
|
: ([] as number[]),
|
||||||
|
}}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
channel_names: Yup.array().of(Yup.string()),
|
||||||
|
answer_validity_check_enabled: Yup.boolean().required(),
|
||||||
|
document_sets: Yup.array().of(Yup.number()),
|
||||||
|
})}
|
||||||
|
onSubmit={async (values, formikHelpers) => {
|
||||||
|
formikHelpers.setSubmitting(true);
|
||||||
|
|
||||||
|
// remove empty channel names
|
||||||
|
const cleanedValues = {
|
||||||
|
...values,
|
||||||
|
channel_names: values.channel_names.filter(
|
||||||
|
(channelName) => channelName !== ""
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response;
|
||||||
|
if (isUpdate) {
|
||||||
|
response = await updateSlackBotConfig(
|
||||||
|
existingSlackBotConfig.id,
|
||||||
|
cleanedValues
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
response = await createSlackBotConfig(cleanedValues);
|
||||||
|
}
|
||||||
|
formikHelpers.setSubmitting(false);
|
||||||
|
if (response.ok) {
|
||||||
|
setPopup({
|
||||||
|
message: isUpdate
|
||||||
|
? "Successfully updated DanswerBot config!"
|
||||||
|
: "Successfully created DanswerBot config!",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
const errorMsg = (await response.json()).detail;
|
||||||
|
setPopup({
|
||||||
|
message: isUpdate
|
||||||
|
? `Error updating DanswerBot config - ${errorMsg}`
|
||||||
|
: `Error creating DanswerBot config - ${errorMsg}`,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, values }) => (
|
||||||
|
<Form>
|
||||||
|
<h2 className="text-lg font-bold mb-3">
|
||||||
|
{isUpdate
|
||||||
|
? "Update a DanswerBot Config"
|
||||||
|
: "Create a new DanswerBot Config"}
|
||||||
|
</h2>
|
||||||
|
<TextArrayField
|
||||||
|
name="channel_names"
|
||||||
|
label="Channel Names:"
|
||||||
|
values={values}
|
||||||
|
subtext="The names of the Slack channels you want DanswerBot to assist in. For example, '#ask-danswer'."
|
||||||
|
/>
|
||||||
|
<div className="border-t border-gray-700 py-2" />
|
||||||
|
<BooleanFormField
|
||||||
|
name="answer_validity_check_enabled"
|
||||||
|
label="Hide Non-Answers"
|
||||||
|
subtext="If set, will only answer questions that the model determines it can answer"
|
||||||
|
/>
|
||||||
|
<div className="border-t border-gray-700 py-2" />
|
||||||
|
<FieldArray
|
||||||
|
name="document_sets"
|
||||||
|
render={(arrayHelpers: ArrayHelpers) => (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Document Sets:
|
||||||
|
<br />
|
||||||
|
<div className="text-xs">
|
||||||
|
The document sets that DanswerBot should search
|
||||||
|
through. If left blank, DanswerBot will search through
|
||||||
|
all documents.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 mt-2 flex gap-2 flex-wrap">
|
||||||
|
{documentSets.map((documentSet) => {
|
||||||
|
const ind = values.document_sets.indexOf(
|
||||||
|
documentSet.id
|
||||||
|
);
|
||||||
|
let isSelected = ind !== -1;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={documentSet.id}
|
||||||
|
className={
|
||||||
|
`
|
||||||
|
px-3
|
||||||
|
py-1
|
||||||
|
rounded-lg
|
||||||
|
border
|
||||||
|
border-gray-700
|
||||||
|
w-fit
|
||||||
|
flex
|
||||||
|
cursor-pointer ` +
|
||||||
|
(isSelected
|
||||||
|
? " bg-gray-600"
|
||||||
|
: " bg-gray-900 hover:bg-gray-700")
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (isSelected) {
|
||||||
|
arrayHelpers.remove(ind);
|
||||||
|
} else {
|
||||||
|
arrayHelpers.push(documentSet.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="my-auto">{documentSet.name}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={
|
||||||
|
"bg-slate-500 hover:bg-slate-700 text-white " +
|
||||||
|
"font-bold py-2 px-4 rounded focus:outline-none " +
|
||||||
|
"focus:shadow-outline w-full max-w-sm mx-auto"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update!" : "Create!"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
94
web/src/app/admin/bot/SlackBotTokensForm.tsx
Normal file
94
web/src/app/admin/bot/SlackBotTokensForm.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { Form, Formik } from "formik";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||||
|
import { SlackBotTokens } from "@/lib/types";
|
||||||
|
import {
|
||||||
|
TextArrayField,
|
||||||
|
TextFormField,
|
||||||
|
} from "@/components/admin/connectors/Field";
|
||||||
|
import {
|
||||||
|
createSlackBotConfig,
|
||||||
|
setSlackBotTokens,
|
||||||
|
updateSlackBotConfig,
|
||||||
|
} from "./lib";
|
||||||
|
|
||||||
|
interface SlackBotTokensFormProps {
|
||||||
|
onClose: () => void;
|
||||||
|
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||||
|
existingTokens?: SlackBotTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SlackBotTokensForm = ({
|
||||||
|
onClose,
|
||||||
|
setPopup,
|
||||||
|
existingTokens,
|
||||||
|
}: SlackBotTokensFormProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Formik
|
||||||
|
initialValues={existingTokens || { app_token: "", bot_token: "" }}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
channel_names: Yup.array().of(Yup.string().required()),
|
||||||
|
document_sets: Yup.array().of(Yup.number()),
|
||||||
|
})}
|
||||||
|
onSubmit={async (values, formikHelpers) => {
|
||||||
|
formikHelpers.setSubmitting(true);
|
||||||
|
const response = await setSlackBotTokens(values);
|
||||||
|
formikHelpers.setSubmitting(false);
|
||||||
|
if (response.ok) {
|
||||||
|
setPopup({
|
||||||
|
message: "Successfully set Slack tokens!",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
const errorMsg = await response.text();
|
||||||
|
setPopup({
|
||||||
|
message: `Error setting Slack tokens - ${errorMsg}`,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isSubmitting }) => (
|
||||||
|
<Form>
|
||||||
|
<h2 className="text-lg font-bold mb-3">Set Slack Bot Tokens</h2>
|
||||||
|
<TextFormField
|
||||||
|
name="bot_token"
|
||||||
|
label="Slack Bot Token"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<TextFormField
|
||||||
|
name="app_token"
|
||||||
|
label="Slack App Token"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={
|
||||||
|
"bg-slate-500 hover:bg-slate-700 text-white " +
|
||||||
|
"font-bold py-2 px-4 rounded focus:outline-none " +
|
||||||
|
"focus:shadow-outline w-full max-w-sm mx-auto"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Set Tokens
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
23
web/src/app/admin/bot/hooks.ts
Normal file
23
web/src/app/admin/bot/hooks.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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),
|
||||||
|
};
|
||||||
|
};
|
51
web/src/app/admin/bot/lib.ts
Normal file
51
web/src/app/admin/bot/lib.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { ChannelConfig, SlackBotTokens } from "@/lib/types";
|
||||||
|
|
||||||
|
interface SlackBotConfigCreationRequest {
|
||||||
|
document_sets: number[];
|
||||||
|
channel_names: string[];
|
||||||
|
answer_validity_check_enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSlackBotConfig = async (
|
||||||
|
creationRequest: SlackBotConfigCreationRequest
|
||||||
|
) => {
|
||||||
|
return fetch("/api/manage/admin/slack-bot/config", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(creationRequest),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSlackBotConfig = async (
|
||||||
|
id: number,
|
||||||
|
creationRequest: SlackBotConfigCreationRequest
|
||||||
|
) => {
|
||||||
|
return fetch(`/api/manage/admin/slack-bot/config/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(creationRequest),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSlackBotConfig = async (id: number) => {
|
||||||
|
return fetch(`/api/manage/admin/slack-bot/config/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
};
|
300
web/src/app/admin/bot/page.tsx
Normal file
300
web/src/app/admin/bot/page.tsx
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { ThreeDotsLoader } from "@/components/Loading";
|
||||||
|
import { PageSelector } from "@/components/PageSelector";
|
||||||
|
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||||
|
import {
|
||||||
|
BookmarkIcon,
|
||||||
|
CPUIcon,
|
||||||
|
EditIcon,
|
||||||
|
TrashIcon,
|
||||||
|
} from "@/components/icons/icons";
|
||||||
|
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
|
||||||
|
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||||
|
import { SlackBotCreationForm } from "./SlackBotConfigCreationForm";
|
||||||
|
import { deleteSlackBotConfig } from "./lib";
|
||||||
|
import { SlackBotTokensForm } from "./SlackBotTokensForm";
|
||||||
|
import { useDocumentSets } from "../documents/sets/hooks";
|
||||||
|
|
||||||
|
const numToDisplay = 50;
|
||||||
|
|
||||||
|
const EditRow = ({
|
||||||
|
existingSlackBotConfig,
|
||||||
|
setPopup,
|
||||||
|
documentSets,
|
||||||
|
refreshSlackBotConfigs,
|
||||||
|
}: {
|
||||||
|
existingSlackBotConfig: SlackBotConfig;
|
||||||
|
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||||
|
documentSets: DocumentSet<any, any>[];
|
||||||
|
refreshSlackBotConfigs: () => void;
|
||||||
|
}) => {
|
||||||
|
const [isEditPopupOpen, setEditPopupOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isEditPopupOpen && (
|
||||||
|
<SlackBotCreationForm
|
||||||
|
onClose={() => {
|
||||||
|
setEditPopupOpen(false);
|
||||||
|
refreshSlackBotConfigs();
|
||||||
|
}}
|
||||||
|
setPopup={setPopup}
|
||||||
|
documentSets={documentSets}
|
||||||
|
existingSlackBotConfig={existingSlackBotConfig}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="cursor-pointer my-auto"
|
||||||
|
onClick={() => setEditPopupOpen(true)}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DocumentFeedbackTableProps {
|
||||||
|
slackBotConfigs: SlackBotConfig[];
|
||||||
|
documentSets: DocumentSet<any, any>[];
|
||||||
|
refresh: () => void;
|
||||||
|
setPopup: (popupSpec: PopupSpec | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SlackBotConfigsTable = ({
|
||||||
|
slackBotConfigs,
|
||||||
|
documentSets,
|
||||||
|
refresh,
|
||||||
|
setPopup,
|
||||||
|
}: DocumentFeedbackTableProps) => {
|
||||||
|
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>
|
||||||
|
<BasicTable
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: "Channels",
|
||||||
|
key: "channels",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Document Sets",
|
||||||
|
key: "document_sets",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Hide Non-Answers",
|
||||||
|
key: "answer_validity_check_enabled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Delete",
|
||||||
|
key: "delete",
|
||||||
|
width: "50px",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={slackBotConfigs
|
||||||
|
.slice((page - 1) * numToDisplay, page * numToDisplay)
|
||||||
|
.map((slackBotConfig) => {
|
||||||
|
return {
|
||||||
|
channels: (
|
||||||
|
<div className="flex gap-x-2">
|
||||||
|
<EditRow
|
||||||
|
existingSlackBotConfig={slackBotConfig}
|
||||||
|
setPopup={setPopup}
|
||||||
|
refreshSlackBotConfigs={refresh}
|
||||||
|
documentSets={documentSets}
|
||||||
|
/>
|
||||||
|
<div className="my-auto">
|
||||||
|
{slackBotConfig.channel_config.channel_names
|
||||||
|
.map((channel_name) => `#${channel_name}`)
|
||||||
|
.join(", ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
document_sets: (
|
||||||
|
<div>
|
||||||
|
{slackBotConfig.document_sets
|
||||||
|
.map((documentSet) => documentSet.name)
|
||||||
|
.join(", ")}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
answer_validity_check_enabled: slackBotConfig.channel_config
|
||||||
|
.answer_validity_check_enabled ? (
|
||||||
|
<div className="text-gray-300">Yes</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-300">No</div>
|
||||||
|
),
|
||||||
|
delete: (
|
||||||
|
<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>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<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 [slackBotConfigModalIsOpen, setSlackBotConfigModalIsOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [slackBotTokensModalIsOpen, setSlackBotTokensModalIsOpen] =
|
||||||
|
useState(false);
|
||||||
|
const { popup, setPopup } = usePopup();
|
||||||
|
const {
|
||||||
|
data: slackBotConfigs,
|
||||||
|
isLoading: isSlackBotConfigsLoading,
|
||||||
|
error: slackBotConfigsError,
|
||||||
|
refreshSlackBotConfigs,
|
||||||
|
} = useSlackBotConfigs();
|
||||||
|
const {
|
||||||
|
data: documentSets,
|
||||||
|
isLoading: isDocumentSetsLoading,
|
||||||
|
error: documentSetsError,
|
||||||
|
} = useDocumentSets();
|
||||||
|
|
||||||
|
const { data: slackBotTokens, refreshSlackBotTokens } = useSlackBotTokens();
|
||||||
|
|
||||||
|
if (isSlackBotConfigsLoading || isDocumentSetsLoading) {
|
||||||
|
return <ThreeDotsLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slackBotConfigsError || !slackBotConfigs) {
|
||||||
|
return <div>Error: {slackBotConfigsError}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (documentSetsError || !documentSets) {
|
||||||
|
return <div>Error: {documentSetsError}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8">
|
||||||
|
{popup}
|
||||||
|
|
||||||
|
<h2 className="text-lg font-bold mb-2">Step 1: Configure Slack Tokens</h2>
|
||||||
|
{!slackBotTokens ? (
|
||||||
|
<SlackBotTokensForm
|
||||||
|
onClose={() => refreshSlackBotTokens()}
|
||||||
|
setPopup={setPopup}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-sm italic">Tokens saved!</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => setSlackBotTokensModalIsOpen(true)}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
Edit Tokens
|
||||||
|
</Button>
|
||||||
|
{slackBotTokensModalIsOpen && (
|
||||||
|
<SlackBotTokensForm
|
||||||
|
onClose={() => {
|
||||||
|
refreshSlackBotTokens();
|
||||||
|
setSlackBotTokensModalIsOpen(false);
|
||||||
|
}}
|
||||||
|
setPopup={setPopup}
|
||||||
|
existingTokens={slackBotTokens}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{slackBotTokens && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-lg font-bold mb-2 mt-4">
|
||||||
|
Step 2: Setup DanswerBot
|
||||||
|
</h2>
|
||||||
|
<div className="text-sm mb-3">
|
||||||
|
Configure Danswer to automatically answer questions in Slack
|
||||||
|
channels.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2"></div>
|
||||||
|
|
||||||
|
<div className="flex mb-3">
|
||||||
|
<Button
|
||||||
|
className="ml-2 my-auto"
|
||||||
|
onClick={() => setSlackBotConfigModalIsOpen(true)}
|
||||||
|
>
|
||||||
|
New Slack Bot
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SlackBotConfigsTable
|
||||||
|
slackBotConfigs={slackBotConfigs}
|
||||||
|
documentSets={documentSets}
|
||||||
|
refresh={refreshSlackBotConfigs}
|
||||||
|
setPopup={setPopup}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{slackBotConfigModalIsOpen && (
|
||||||
|
<SlackBotCreationForm
|
||||||
|
documentSets={documentSets}
|
||||||
|
onClose={() => {
|
||||||
|
refreshSlackBotConfigs();
|
||||||
|
setSlackBotConfigModalIsOpen(false);
|
||||||
|
}}
|
||||||
|
setPopup={setPopup}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
|
||||||
|
<CPUIcon size={32} />
|
||||||
|
<h1 className="text-3xl font-bold pl-2">Slack Bot Configuration</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Main />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
@ -1,10 +1,13 @@
|
|||||||
import { fetcher } from "@/lib/fetcher";
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
import { DocumentSet } from "@/lib/types";
|
import { DocumentSet } from "@/lib/types";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
|
|
||||||
export const useDocumentSets = () => {
|
export const useDocumentSets = () => {
|
||||||
const url = "/api/manage/document-set";
|
const url = "/api/manage/document-set";
|
||||||
const swrResponse = useSWR<DocumentSet<any, any>[]>(url, fetcher);
|
const swrResponse = useSWR<DocumentSet<any, any>[]>(
|
||||||
|
url,
|
||||||
|
errorHandlingFetcher
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...swrResponse,
|
...swrResponse,
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
UsersIcon,
|
UsersIcon,
|
||||||
ThumbsUpIcon,
|
ThumbsUpIcon,
|
||||||
BookmarkIcon,
|
BookmarkIcon,
|
||||||
|
CPUIcon,
|
||||||
} from "@/components/icons/icons";
|
} from "@/components/icons/icons";
|
||||||
import { DISABLE_AUTH } from "@/lib/constants";
|
import { DISABLE_AUTH } from "@/lib/constants";
|
||||||
import { getCurrentUserSS } from "@/lib/userSS";
|
import { getCurrentUserSS } from "@/lib/userSS";
|
||||||
@ -44,7 +45,7 @@ export default async function AdminLayout({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header user={user} />
|
<Header user={user} />
|
||||||
<div className="bg-gray-900 pt-8 flex">
|
<div className="bg-gray-900 pt-8 pb-8 flex">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
title="Connector"
|
title="Connector"
|
||||||
collections={[
|
collections={[
|
||||||
@ -244,6 +245,20 @@ export default async function AdminLayout({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Bots",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<div className="flex">
|
||||||
|
<CPUIcon size={18} />
|
||||||
|
<div className="ml-1">Slack Bot</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
link: "/admin/bot",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<div className="px-12 min-h-screen bg-gray-900 text-gray-100 w-full">
|
<div className="px-12 min-h-screen bg-gray-900 text-gray-100 w-full">
|
||||||
|
@ -87,20 +87,22 @@ export const BooleanFormField = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TextArrayFieldProps {
|
interface TextArrayFieldProps<T extends Yup.AnyObject> {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
values: T;
|
||||||
subtext?: string;
|
subtext?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TextArrayFieldBuilder<T extends Yup.AnyObject>({
|
export function TextArrayField<T extends Yup.AnyObject>({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
|
values,
|
||||||
subtext,
|
subtext,
|
||||||
type = "text",
|
type,
|
||||||
}: TextArrayFieldProps): FormBodyBuilder<T> {
|
}: TextArrayFieldProps<T>) {
|
||||||
const TextArrayField: FormBodyBuilder<T> = (values) => (
|
return (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor={name} className="block">
|
<label htmlFor={name} className="block">
|
||||||
{label}
|
{label}
|
||||||
@ -153,5 +155,20 @@ export function TextArrayFieldBuilder<T extends Yup.AnyObject>({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
return TextArrayField;
|
}
|
||||||
|
|
||||||
|
interface TextArrayFieldBuilderProps<T extends Yup.AnyObject> {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
subtext?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextArrayFieldBuilder<T extends Yup.AnyObject>(
|
||||||
|
props: TextArrayFieldBuilderProps<T>
|
||||||
|
): FormBodyBuilder<T> {
|
||||||
|
const _TextArrayField: FormBodyBuilder<T> = (values) => (
|
||||||
|
<TextArrayField {...props} values={values} />
|
||||||
|
);
|
||||||
|
return _TextArrayField;
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
FiZoomIn,
|
FiZoomIn,
|
||||||
FiCopy,
|
FiCopy,
|
||||||
FiBookmark,
|
FiBookmark,
|
||||||
|
FiCpu,
|
||||||
} from "react-icons/fi";
|
} from "react-icons/fi";
|
||||||
import { SiBookstack } from "react-icons/si";
|
import { SiBookstack } from "react-icons/si";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@ -251,6 +252,13 @@ export const BookmarkIcon = ({
|
|||||||
return <FiBookmark size={size} className={className} />;
|
return <FiBookmark size={size} className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CPUIcon = ({
|
||||||
|
size = 16,
|
||||||
|
className = defaultTailwindCSS,
|
||||||
|
}: IconProps) => {
|
||||||
|
return <FiCpu size={size} className={className} />;
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// COMPANY LOGOS
|
// COMPANY LOGOS
|
||||||
//
|
//
|
||||||
|
@ -1 +1,25 @@
|
|||||||
export const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
export const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
|
|
||||||
|
class FetchError extends Error {
|
||||||
|
status: number;
|
||||||
|
info: any;
|
||||||
|
|
||||||
|
constructor(message: string, status: number, info: any) {
|
||||||
|
super(message);
|
||||||
|
this.status = status;
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const errorHandlingFetcher = async (url: string) => {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = new FetchError(
|
||||||
|
"An error occurred while fetching the data.",
|
||||||
|
res.status,
|
||||||
|
await res.json()
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
};
|
||||||
|
@ -228,3 +228,21 @@ export interface DocumentSet<ConnectorType, CredentialType> {
|
|||||||
cc_pair_descriptors: CCPairDescriptor<ConnectorType, CredentialType>[];
|
cc_pair_descriptors: CCPairDescriptor<ConnectorType, CredentialType>[];
|
||||||
is_up_to_date: boolean;
|
is_up_to_date: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SLACK BOT CONFIGS
|
||||||
|
export interface ChannelConfig {
|
||||||
|
channel_names: string[];
|
||||||
|
answer_validity_check_enabled?: boolean;
|
||||||
|
team_members?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackBotConfig {
|
||||||
|
id: number;
|
||||||
|
document_sets: DocumentSet<any, any>[];
|
||||||
|
channel_config: ChannelConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackBotTokens {
|
||||||
|
bot_token: string;
|
||||||
|
app_token: string;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user