mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-29 03:01:59 +01:00
* Make it fail fast + succeed validation if rate limiting is happening * Add logging + reduce spam
443 lines
16 KiB
Python
443 lines
16 KiB
Python
from fastapi import APIRouter
|
|
from fastapi import Depends
|
|
from fastapi import HTTPException
|
|
from slack_sdk import WebClient
|
|
from slack_sdk.errors import SlackApiError
|
|
from sqlalchemy.orm import Session
|
|
|
|
from onyx.auth.users import current_admin_user
|
|
from onyx.configs.constants import MilestoneRecordType
|
|
from onyx.db.constants import SLACK_BOT_PERSONA_PREFIX
|
|
from onyx.db.engine import get_session
|
|
from onyx.db.models import ChannelConfig
|
|
from onyx.db.models import User
|
|
from onyx.db.persona import get_persona_by_id
|
|
from onyx.db.slack_bot import fetch_slack_bot
|
|
from onyx.db.slack_bot import fetch_slack_bot_tokens
|
|
from onyx.db.slack_bot import fetch_slack_bots
|
|
from onyx.db.slack_bot import insert_slack_bot
|
|
from onyx.db.slack_bot import remove_slack_bot
|
|
from onyx.db.slack_bot import update_slack_bot
|
|
from onyx.db.slack_channel_config import create_slack_channel_persona
|
|
from onyx.db.slack_channel_config import fetch_slack_channel_config
|
|
from onyx.db.slack_channel_config import fetch_slack_channel_configs
|
|
from onyx.db.slack_channel_config import insert_slack_channel_config
|
|
from onyx.db.slack_channel_config import remove_slack_channel_config
|
|
from onyx.db.slack_channel_config import update_slack_channel_config
|
|
from onyx.onyxbot.slack.config import validate_channel_name
|
|
from onyx.server.manage.models import SlackBot
|
|
from onyx.server.manage.models import SlackBotCreationRequest
|
|
from onyx.server.manage.models import SlackChannel
|
|
from onyx.server.manage.models import SlackChannelConfig
|
|
from onyx.server.manage.models import SlackChannelConfigCreationRequest
|
|
from onyx.server.manage.validate_tokens import validate_app_token
|
|
from onyx.server.manage.validate_tokens import validate_bot_token
|
|
from onyx.utils.logger import setup_logger
|
|
from onyx.utils.telemetry import create_milestone_and_report
|
|
from shared_configs.contextvars import get_current_tenant_id
|
|
|
|
|
|
logger = setup_logger()
|
|
|
|
|
|
router = APIRouter(prefix="/manage")
|
|
|
|
|
|
def _form_channel_config(
|
|
db_session: Session,
|
|
slack_channel_config_creation_request: SlackChannelConfigCreationRequest,
|
|
current_slack_channel_config_id: int | None,
|
|
) -> ChannelConfig:
|
|
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_channel_config_creation_request.respond_member_group_list
|
|
)
|
|
answer_filters = slack_channel_config_creation_request.answer_filters
|
|
follow_up_tags = slack_channel_config_creation_request.follow_up_tags
|
|
|
|
try:
|
|
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(
|
|
status_code=400,
|
|
detail=str(e),
|
|
)
|
|
|
|
if respond_tag_only and respond_member_group_list:
|
|
raise ValueError(
|
|
"Cannot set OnyxBot to only respond to tags only and "
|
|
"also respond to a predetermined set of users."
|
|
)
|
|
|
|
if (
|
|
slack_channel_config_creation_request.is_ephemeral
|
|
and slack_channel_config_creation_request.respond_member_group_list
|
|
):
|
|
raise ValueError(
|
|
"Cannot set OnyxBot to respond to users in a private (ephemeral) message "
|
|
"and also respond to a selected list of users."
|
|
)
|
|
|
|
channel_config: ChannelConfig = {
|
|
"channel_name": cleaned_channel_name,
|
|
}
|
|
if respond_tag_only is not None:
|
|
channel_config["respond_tag_only"] = respond_tag_only
|
|
if respond_member_group_list:
|
|
channel_config["respond_member_group_list"] = respond_member_group_list
|
|
if answer_filters:
|
|
channel_config["answer_filters"] = answer_filters
|
|
if follow_up_tags is not None:
|
|
channel_config["follow_up_tags"] = follow_up_tags
|
|
|
|
channel_config[
|
|
"show_continue_in_web_ui"
|
|
] = slack_channel_config_creation_request.show_continue_in_web_ui
|
|
|
|
channel_config[
|
|
"respond_to_bots"
|
|
] = slack_channel_config_creation_request.respond_to_bots
|
|
|
|
channel_config["is_ephemeral"] = slack_channel_config_creation_request.is_ephemeral
|
|
|
|
channel_config["disabled"] = slack_channel_config_creation_request.disabled
|
|
|
|
return channel_config
|
|
|
|
|
|
@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),
|
|
) -> SlackChannelConfig:
|
|
channel_config = _form_channel_config(
|
|
db_session=db_session,
|
|
slack_channel_config_creation_request=slack_channel_config_creation_request,
|
|
current_slack_channel_config_id=None,
|
|
)
|
|
|
|
if channel_config["channel_name"] is None:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Channel name is required",
|
|
)
|
|
|
|
persona_id = None
|
|
if slack_channel_config_creation_request.persona_id is not None:
|
|
persona_id = slack_channel_config_creation_request.persona_id
|
|
elif slack_channel_config_creation_request.document_sets:
|
|
persona_id = create_slack_channel_persona(
|
|
db_session=db_session,
|
|
channel_name=channel_config["channel_name"],
|
|
document_set_ids=slack_channel_config_creation_request.document_sets,
|
|
existing_persona_id=None,
|
|
).id
|
|
|
|
slack_channel_config_model = insert_slack_channel_config(
|
|
db_session=db_session,
|
|
slack_bot_id=slack_channel_config_creation_request.slack_bot_id,
|
|
persona_id=persona_id,
|
|
channel_config=channel_config,
|
|
standard_answer_category_ids=slack_channel_config_creation_request.standard_answer_categories,
|
|
enable_auto_filters=slack_channel_config_creation_request.enable_auto_filters,
|
|
)
|
|
return SlackChannelConfig.from_model(slack_channel_config_model)
|
|
|
|
|
|
@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),
|
|
) -> SlackChannelConfig:
|
|
channel_config = _form_channel_config(
|
|
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_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_channel_config is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Slack channel config not found",
|
|
)
|
|
|
|
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,
|
|
user=None,
|
|
db_session=db_session,
|
|
is_for_edit=False,
|
|
)
|
|
|
|
if not persona.name.startswith(SLACK_BOT_PERSONA_PREFIX):
|
|
# Don't update actual non-slackbot specific personas
|
|
# Since this one specified document sets, we have to create a new persona
|
|
# for this OnyxBot config
|
|
existing_persona_id = None
|
|
else:
|
|
existing_persona_id = existing_slack_channel_config.persona_id
|
|
|
|
persona_id = create_slack_channel_persona(
|
|
db_session=db_session,
|
|
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_channel_config_creation_request.enable_auto_filters,
|
|
).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,
|
|
standard_answer_category_ids=slack_channel_config_creation_request.standard_answer_categories,
|
|
enable_auto_filters=slack_channel_config_creation_request.enable_auto_filters,
|
|
disabled=slack_channel_config_creation_request.disabled,
|
|
)
|
|
return SlackChannelConfig.from_model(slack_channel_config_model)
|
|
|
|
|
|
@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_channel_config(
|
|
db_session=db_session,
|
|
slack_channel_config_id=slack_channel_config_id,
|
|
user=user,
|
|
)
|
|
|
|
|
|
@router.get("/admin/slack-app/channel")
|
|
def list_slack_channel_configs(
|
|
db_session: Session = Depends(get_session),
|
|
_: User | None = Depends(current_admin_user),
|
|
) -> list[SlackChannelConfig]:
|
|
slack_channel_config_models = fetch_slack_channel_configs(db_session=db_session)
|
|
return [
|
|
SlackChannelConfig.from_model(slack_channel_config_model)
|
|
for slack_channel_config_model in slack_channel_config_models
|
|
]
|
|
|
|
|
|
@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:
|
|
tenant_id = get_current_tenant_id()
|
|
|
|
validate_app_token(slack_bot_creation_request.app_token)
|
|
validate_bot_token(slack_bot_creation_request.bot_token)
|
|
|
|
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,
|
|
)
|
|
|
|
# Create a default Slack channel config
|
|
default_channel_config = ChannelConfig(
|
|
channel_name=None,
|
|
respond_member_group_list=[],
|
|
answer_filters=[],
|
|
follow_up_tags=[],
|
|
respond_tag_only=True,
|
|
)
|
|
insert_slack_channel_config(
|
|
db_session=db_session,
|
|
slack_bot_id=slack_bot_model.id,
|
|
persona_id=None,
|
|
channel_config=default_channel_config,
|
|
standard_answer_category_ids=[],
|
|
enable_auto_filters=False,
|
|
is_default=True,
|
|
)
|
|
|
|
create_milestone_and_report(
|
|
user=None,
|
|
distinct_id=tenant_id or "N/A",
|
|
event_type=MilestoneRecordType.CREATED_ONYX_BOT,
|
|
properties=None,
|
|
db_session=db_session,
|
|
)
|
|
|
|
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:
|
|
validate_bot_token(slack_bot_creation_request.bot_token)
|
|
validate_app_token(slack_bot_creation_request.app_token)
|
|
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:
|
|
remove_slack_bot(
|
|
db_session=db_session,
|
|
slack_bot_id=slack_bot_id,
|
|
)
|
|
|
|
|
|
@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
|
|
]
|
|
|
|
|
|
MAX_SLACK_PAGES = 5
|
|
SLACK_API_CHANNELS_PER_PAGE = 100
|
|
|
|
|
|
@router.get(
|
|
"/admin/slack-app/bots/{bot_id}/channels",
|
|
)
|
|
def get_all_channels_from_slack_api(
|
|
bot_id: int,
|
|
db_session: Session = Depends(get_session),
|
|
_: User | None = Depends(current_admin_user),
|
|
) -> list[SlackChannel]:
|
|
"""
|
|
Fetches channels the bot is a member of from the Slack API.
|
|
Handles pagination with a limit to avoid excessive API calls.
|
|
"""
|
|
tokens = fetch_slack_bot_tokens(db_session, bot_id)
|
|
if not tokens or "bot_token" not in tokens:
|
|
raise HTTPException(
|
|
status_code=404, detail="Bot token not found for the given bot ID"
|
|
)
|
|
|
|
client = WebClient(token=tokens["bot_token"], timeout=1)
|
|
all_channels = []
|
|
next_cursor = None
|
|
current_page = 0
|
|
|
|
try:
|
|
# Use users_conversations with limited pagination
|
|
while current_page < MAX_SLACK_PAGES:
|
|
current_page += 1
|
|
|
|
# Make API call with cursor if we have one
|
|
if next_cursor:
|
|
response = client.users_conversations(
|
|
types="public_channel,private_channel",
|
|
exclude_archived=True,
|
|
cursor=next_cursor,
|
|
limit=SLACK_API_CHANNELS_PER_PAGE,
|
|
)
|
|
else:
|
|
response = client.users_conversations(
|
|
types="public_channel,private_channel",
|
|
exclude_archived=True,
|
|
limit=SLACK_API_CHANNELS_PER_PAGE,
|
|
)
|
|
|
|
# Add channels to our list
|
|
if "channels" in response and response["channels"]:
|
|
all_channels.extend(response["channels"])
|
|
|
|
# Check if we need to paginate
|
|
if (
|
|
"response_metadata" in response
|
|
and "next_cursor" in response["response_metadata"]
|
|
):
|
|
next_cursor = response["response_metadata"]["next_cursor"]
|
|
if next_cursor:
|
|
if current_page == MAX_SLACK_PAGES:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Workspace has too many channels to paginate over in this call.",
|
|
)
|
|
continue
|
|
|
|
# If we get here, no more pages
|
|
break
|
|
|
|
channels = [
|
|
SlackChannel(id=channel["id"], name=channel["name"])
|
|
for channel in all_channels
|
|
]
|
|
|
|
return channels
|
|
|
|
except SlackApiError as e:
|
|
# Handle rate limiting or other API errors
|
|
logger.exception("Error fetching channels from Slack API")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Error fetching channels from Slack API: {str(e)}",
|
|
)
|