mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-26 11:58:28 +02:00
Migrate standard answers implementations to ee/ (#2378)
* Migrate standard answers implementations to ee/ * renaming * Clean up slackbot non-ee standard answers import * Move backend api/manage/standard_answer route to ee * Move standard answers web UI to ee * Hide standard answer controls in bot edit page * Kwargs for fetch_versioned_implementation * Add docstring explaining return types for handle_standard_answers * Consolidate blocks into ee/handle_standard_answers --------- Co-authored-by: Hyeong Joon Suh <hyeongjoonsuh@Hyeongs-MacBook-Pro.local> Co-authored-by: danswer-trial <danswer-trial@danswer-trials-MacBook-Pro.local>
This commit is contained in:
@@ -25,7 +25,6 @@ from danswer.danswerbot.slack.constants import DISLIKE_BLOCK_ACTION_ID
|
|||||||
from danswer.danswerbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID
|
from danswer.danswerbot.slack.constants import FEEDBACK_DOC_BUTTON_BLOCK_ACTION_ID
|
||||||
from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_ACTION_ID
|
from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_ACTION_ID
|
||||||
from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_RESOLVED_ACTION_ID
|
from danswer.danswerbot.slack.constants import FOLLOWUP_BUTTON_RESOLVED_ACTION_ID
|
||||||
from danswer.danswerbot.slack.constants import GENERATE_ANSWER_BUTTON_ACTION_ID
|
|
||||||
from danswer.danswerbot.slack.constants import IMMEDIATE_RESOLVED_BUTTON_ACTION_ID
|
from danswer.danswerbot.slack.constants import IMMEDIATE_RESOLVED_BUTTON_ACTION_ID
|
||||||
from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID
|
from danswer.danswerbot.slack.constants import LIKE_BLOCK_ACTION_ID
|
||||||
from danswer.danswerbot.slack.icons import source_to_github_img_link
|
from danswer.danswerbot.slack.icons import source_to_github_img_link
|
||||||
@@ -360,22 +359,6 @@ def build_quotes_block(
|
|||||||
return [SectionBlock(text="*Relevant Snippets*\n" + "\n".join(quote_lines))]
|
return [SectionBlock(text="*Relevant Snippets*\n" + "\n".join(quote_lines))]
|
||||||
|
|
||||||
|
|
||||||
def build_standard_answer_blocks(
|
|
||||||
answer_message: str,
|
|
||||||
) -> list[Block]:
|
|
||||||
generate_button_block = ButtonElement(
|
|
||||||
action_id=GENERATE_ANSWER_BUTTON_ACTION_ID,
|
|
||||||
text="Generate Full Answer",
|
|
||||||
)
|
|
||||||
answer_block = SectionBlock(text=answer_message)
|
|
||||||
return [
|
|
||||||
answer_block,
|
|
||||||
ActionsBlock(
|
|
||||||
elements=[generate_button_block],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def build_qa_response_blocks(
|
def build_qa_response_blocks(
|
||||||
message_id: int | None,
|
message_id: int | None,
|
||||||
answer: str | None,
|
answer: str | None,
|
||||||
|
@@ -1,62 +1,16 @@
|
|||||||
from slack_sdk import WebClient
|
from slack_sdk import WebClient
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from danswer.configs.constants import MessageType
|
|
||||||
from danswer.configs.danswerbot_configs import DANSWER_REACT_EMOJI
|
|
||||||
from danswer.danswerbot.slack.blocks import build_standard_answer_blocks
|
|
||||||
from danswer.danswerbot.slack.blocks import get_restate_blocks
|
|
||||||
from danswer.danswerbot.slack.handlers.utils import send_team_member_message
|
|
||||||
from danswer.danswerbot.slack.models import SlackMessageInfo
|
from danswer.danswerbot.slack.models import SlackMessageInfo
|
||||||
from danswer.danswerbot.slack.utils import respond_in_thread
|
|
||||||
from danswer.danswerbot.slack.utils import update_emote_react
|
|
||||||
from danswer.db.chat import create_chat_session
|
|
||||||
from danswer.db.chat import create_new_chat_message
|
|
||||||
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 Prompt
|
||||||
from danswer.db.models import SlackBotConfig
|
from danswer.db.models import SlackBotConfig
|
||||||
from danswer.db.models import StandardAnswer as StandardAnswerModel
|
|
||||||
from danswer.db.standard_answer import fetch_standard_answer_categories_by_names
|
|
||||||
from danswer.db.standard_answer import find_matching_standard_answers
|
|
||||||
from danswer.server.manage.models import StandardAnswer as PydanticStandardAnswer
|
|
||||||
from danswer.utils.logger import DanswerLoggingAdapter
|
from danswer.utils.logger import DanswerLoggingAdapter
|
||||||
from danswer.utils.logger import setup_logger
|
from danswer.utils.logger import setup_logger
|
||||||
|
from danswer.utils.variable_functionality import fetch_versioned_implementation
|
||||||
|
|
||||||
logger = setup_logger()
|
logger = setup_logger()
|
||||||
|
|
||||||
|
|
||||||
def oneoff_standard_answers(
|
|
||||||
message: str,
|
|
||||||
slack_bot_categories: list[str],
|
|
||||||
db_session: Session,
|
|
||||||
) -> list[PydanticStandardAnswer]:
|
|
||||||
"""
|
|
||||||
Respond to the user message if it matches any configured standard answers.
|
|
||||||
|
|
||||||
Returns a list of matching StandardAnswers if found, otherwise None.
|
|
||||||
"""
|
|
||||||
configured_standard_answers = {
|
|
||||||
standard_answer
|
|
||||||
for category in fetch_standard_answer_categories_by_names(
|
|
||||||
slack_bot_categories, db_session=db_session
|
|
||||||
)
|
|
||||||
for standard_answer in category.standard_answers
|
|
||||||
}
|
|
||||||
|
|
||||||
matching_standard_answers = find_matching_standard_answers(
|
|
||||||
query=message,
|
|
||||||
id_in=[answer.id for answer in configured_standard_answers],
|
|
||||||
db_session=db_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
server_standard_answers = [
|
|
||||||
PydanticStandardAnswer.from_model(standard_answer_model)
|
|
||||||
for (standard_answer_model, _) in matching_standard_answers
|
|
||||||
]
|
|
||||||
return server_standard_answers
|
|
||||||
|
|
||||||
|
|
||||||
def handle_standard_answers(
|
def handle_standard_answers(
|
||||||
message_info: SlackMessageInfo,
|
message_info: SlackMessageInfo,
|
||||||
receiver_ids: list[str] | None,
|
receiver_ids: list[str] | None,
|
||||||
@@ -66,153 +20,37 @@ def handle_standard_answers(
|
|||||||
client: WebClient,
|
client: WebClient,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""Returns whether one or more Standard Answer message blocks were
|
||||||
Potentially respond to the user message depending on whether the user's message matches
|
emitted by the Slack bot"""
|
||||||
any of the configured standard answers and also whether those answers have already been
|
versioned_handle_standard_answers = fetch_versioned_implementation(
|
||||||
provided in the current thread.
|
"danswer.danswerbot.slack.handlers.handle_standard_answers",
|
||||||
|
"_handle_standard_answers",
|
||||||
Returns True if standard answers are found to match the user's message and therefore,
|
|
||||||
we still need to respond to the users.
|
|
||||||
"""
|
|
||||||
# if no channel config, then no standard answers are configured
|
|
||||||
if not slack_bot_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 []
|
|
||||||
)
|
)
|
||||||
configured_standard_answers = set(
|
return versioned_handle_standard_answers(
|
||||||
[
|
message_info=message_info,
|
||||||
standard_answer
|
|
||||||
for standard_answer_category in configured_standard_answer_categories
|
|
||||||
for standard_answer in standard_answer_category.standard_answers
|
|
||||||
]
|
|
||||||
)
|
|
||||||
query_msg = message_info.thread_messages[-1]
|
|
||||||
|
|
||||||
if slack_thread_id is None:
|
|
||||||
used_standard_answer_ids = set([])
|
|
||||||
else:
|
|
||||||
chat_sessions = get_chat_sessions_by_slack_thread_id(
|
|
||||||
slack_thread_id=slack_thread_id,
|
|
||||||
user_id=None,
|
|
||||||
db_session=db_session,
|
|
||||||
)
|
|
||||||
chat_messages = get_chat_messages_by_sessions(
|
|
||||||
chat_session_ids=[chat_session.id for chat_session in chat_sessions],
|
|
||||||
user_id=None,
|
|
||||||
db_session=db_session,
|
|
||||||
skip_permission_check=True,
|
|
||||||
)
|
|
||||||
used_standard_answer_ids = set(
|
|
||||||
[
|
|
||||||
standard_answer.id
|
|
||||||
for chat_message in chat_messages
|
|
||||||
for standard_answer in chat_message.standard_answers
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
usable_standard_answers = configured_standard_answers.difference(
|
|
||||||
used_standard_answer_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
matching_standard_answers: list[tuple[StandardAnswerModel, str]] = []
|
|
||||||
if usable_standard_answers:
|
|
||||||
matching_standard_answers = find_matching_standard_answers(
|
|
||||||
query=query_msg.message,
|
|
||||||
id_in=[standard_answer.id for standard_answer in usable_standard_answers],
|
|
||||||
db_session=db_session,
|
|
||||||
)
|
|
||||||
|
|
||||||
if matching_standard_answers:
|
|
||||||
chat_session = create_chat_session(
|
|
||||||
db_session=db_session,
|
|
||||||
description="",
|
|
||||||
user_id=None,
|
|
||||||
persona_id=slack_bot_config.persona.id if slack_bot_config.persona else 0,
|
|
||||||
danswerbot_flow=True,
|
|
||||||
slack_thread_id=slack_thread_id,
|
|
||||||
one_shot=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
root_message = get_or_create_root_message(
|
|
||||||
chat_session_id=chat_session.id, db_session=db_session
|
|
||||||
)
|
|
||||||
|
|
||||||
new_user_message = create_new_chat_message(
|
|
||||||
chat_session_id=chat_session.id,
|
|
||||||
parent_message=root_message,
|
|
||||||
prompt_id=prompt.id if prompt else None,
|
|
||||||
message=query_msg.message,
|
|
||||||
token_count=0,
|
|
||||||
message_type=MessageType.USER,
|
|
||||||
db_session=db_session,
|
|
||||||
commit=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
formatted_answers = []
|
|
||||||
for standard_answer, match_str in matching_standard_answers:
|
|
||||||
since_you_mentioned_pretext = (
|
|
||||||
f'Since your question contained "_{match_str}_"'
|
|
||||||
)
|
|
||||||
block_quotified_answer = ">" + standard_answer.answer.replace("\n", "\n> ")
|
|
||||||
formatted_answer = f"{since_you_mentioned_pretext}, I thought this might be useful: \n\n{block_quotified_answer}"
|
|
||||||
formatted_answers.append(formatted_answer)
|
|
||||||
answer_message = "\n\n".join(formatted_answers)
|
|
||||||
|
|
||||||
_ = create_new_chat_message(
|
|
||||||
chat_session_id=chat_session.id,
|
|
||||||
parent_message=new_user_message,
|
|
||||||
prompt_id=prompt.id if prompt else None,
|
|
||||||
message=answer_message,
|
|
||||||
token_count=0,
|
|
||||||
message_type=MessageType.ASSISTANT,
|
|
||||||
error=None,
|
|
||||||
db_session=db_session,
|
|
||||||
commit=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
update_emote_react(
|
|
||||||
emoji=DANSWER_REACT_EMOJI,
|
|
||||||
channel=message_info.channel_to_respond,
|
|
||||||
message_ts=message_info.msg_to_respond,
|
|
||||||
remove=True,
|
|
||||||
client=client,
|
|
||||||
)
|
|
||||||
|
|
||||||
restate_question_blocks = get_restate_blocks(
|
|
||||||
msg=query_msg.message,
|
|
||||||
is_bot_msg=message_info.is_bot_msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
answer_blocks = build_standard_answer_blocks(
|
|
||||||
answer_message=answer_message,
|
|
||||||
)
|
|
||||||
|
|
||||||
all_blocks = restate_question_blocks + answer_blocks
|
|
||||||
|
|
||||||
try:
|
|
||||||
respond_in_thread(
|
|
||||||
client=client,
|
|
||||||
channel=message_info.channel_to_respond,
|
|
||||||
receiver_ids=receiver_ids,
|
receiver_ids=receiver_ids,
|
||||||
text="Hello! Danswer has some results for you!",
|
slack_bot_config=slack_bot_config,
|
||||||
blocks=all_blocks,
|
prompt=prompt,
|
||||||
thread_ts=message_info.msg_to_respond,
|
logger=logger,
|
||||||
unfurl=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if receiver_ids and slack_thread_id:
|
|
||||||
send_team_member_message(
|
|
||||||
client=client,
|
client=client,
|
||||||
channel=message_info.channel_to_respond,
|
db_session=db_session,
|
||||||
thread_ts=slack_thread_id,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
def _handle_standard_answers(
|
||||||
logger.exception(f"Unable to send standard answer message: {e}")
|
message_info: SlackMessageInfo,
|
||||||
return False
|
receiver_ids: list[str] | None,
|
||||||
else:
|
slack_bot_config: SlackBotConfig | None,
|
||||||
|
prompt: Prompt | None,
|
||||||
|
logger: DanswerLoggingAdapter,
|
||||||
|
client: WebClient,
|
||||||
|
db_session: Session,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Standard Answers are a paid Enterprise Edition feature. This is the fallback
|
||||||
|
function handling the case where EE features are not enabled.
|
||||||
|
|
||||||
|
Always returns false i.e. since EE features are not enabled, we NEVER create any
|
||||||
|
Slack message blocks.
|
||||||
|
"""
|
||||||
return False
|
return False
|
||||||
|
@@ -56,6 +56,7 @@ from danswer.one_shot_answer.models import ThreadMessage
|
|||||||
from danswer.search.retrieval.search_runner import download_nltk_data
|
from danswer.search.retrieval.search_runner import download_nltk_data
|
||||||
from danswer.server.manage.models import SlackBotTokens
|
from danswer.server.manage.models import SlackBotTokens
|
||||||
from danswer.utils.logger import setup_logger
|
from danswer.utils.logger import setup_logger
|
||||||
|
from danswer.utils.variable_functionality import set_is_ee_based_on_env_variable
|
||||||
from shared_configs.configs import MODEL_SERVER_HOST
|
from shared_configs.configs import MODEL_SERVER_HOST
|
||||||
from shared_configs.configs import MODEL_SERVER_PORT
|
from shared_configs.configs import MODEL_SERVER_PORT
|
||||||
from shared_configs.configs import SLACK_CHANNEL_ID
|
from shared_configs.configs import SLACK_CHANNEL_ID
|
||||||
@@ -481,6 +482,8 @@ if __name__ == "__main__":
|
|||||||
slack_bot_tokens: SlackBotTokens | None = None
|
slack_bot_tokens: SlackBotTokens | None = None
|
||||||
socket_client: SocketModeClient | None = None
|
socket_client: SocketModeClient | None = None
|
||||||
|
|
||||||
|
set_is_ee_based_on_env_variable()
|
||||||
|
|
||||||
logger.notice("Verifying query preprocessing (NLTK) data is downloaded")
|
logger.notice("Verifying query preprocessing (NLTK) data is downloaded")
|
||||||
download_nltk_data()
|
download_nltk_data()
|
||||||
|
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
import re
|
|
||||||
import string
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -145,17 +143,6 @@ def fetch_standard_answer_category(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def fetch_standard_answer_categories_by_names(
|
|
||||||
standard_answer_category_names: list[str],
|
|
||||||
db_session: Session,
|
|
||||||
) -> Sequence[StandardAnswerCategory]:
|
|
||||||
return db_session.scalars(
|
|
||||||
select(StandardAnswerCategory).where(
|
|
||||||
StandardAnswerCategory.name.in_(standard_answer_category_names)
|
|
||||||
)
|
|
||||||
).all()
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_standard_answer_categories_by_ids(
|
def fetch_standard_answer_categories_by_ids(
|
||||||
standard_answer_category_ids: list[int],
|
standard_answer_category_ids: list[int],
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
@@ -182,59 +169,6 @@ def fetch_standard_answer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def find_matching_standard_answers(
|
|
||||||
id_in: list[int],
|
|
||||||
query: str,
|
|
||||||
db_session: Session,
|
|
||||||
) -> list[tuple[StandardAnswer, str]]:
|
|
||||||
"""
|
|
||||||
Returns a list of tuples, where each tuple is a StandardAnswer definition matching
|
|
||||||
the query and a string representing the match (either the regex match group or the
|
|
||||||
set of keywords).
|
|
||||||
|
|
||||||
If `answer_instance.match_regex` is true, the definition is considered "matched"
|
|
||||||
if the query matches the `answer_instance.keyword` using `re.search`.
|
|
||||||
|
|
||||||
Otherwise, the definition is considered "matched" if each space-delimited token
|
|
||||||
in `keyword` exists in `query`.
|
|
||||||
"""
|
|
||||||
stmt = (
|
|
||||||
select(StandardAnswer)
|
|
||||||
.where(StandardAnswer.active.is_(True))
|
|
||||||
.where(StandardAnswer.id.in_(id_in))
|
|
||||||
)
|
|
||||||
possible_standard_answers: Sequence[StandardAnswer] = db_session.scalars(stmt).all()
|
|
||||||
|
|
||||||
matching_standard_answers: list[tuple[StandardAnswer, str]] = []
|
|
||||||
for standard_answer in possible_standard_answers:
|
|
||||||
if standard_answer.match_regex:
|
|
||||||
maybe_matches = re.search(standard_answer.keyword, query, re.IGNORECASE)
|
|
||||||
if maybe_matches is not None:
|
|
||||||
match_group = maybe_matches.group(0)
|
|
||||||
matching_standard_answers.append((standard_answer, match_group))
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Remove punctuation and split the keyword into individual words
|
|
||||||
keyword_words = "".join(
|
|
||||||
char
|
|
||||||
for char in standard_answer.keyword.lower()
|
|
||||||
if char not in string.punctuation
|
|
||||||
).split()
|
|
||||||
|
|
||||||
# Remove punctuation and split the query into individual words
|
|
||||||
query_words = "".join(
|
|
||||||
char for char in query.lower() if char not in string.punctuation
|
|
||||||
).split()
|
|
||||||
|
|
||||||
# Check if all of the keyword words are in the query words
|
|
||||||
if all(word in query_words for word in keyword_words):
|
|
||||||
matching_standard_answers.append(
|
|
||||||
(standard_answer, standard_answer.keyword)
|
|
||||||
)
|
|
||||||
|
|
||||||
return matching_standard_answers
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_standard_answers(db_session: Session) -> Sequence[StandardAnswer]:
|
def fetch_standard_answers(db_session: Session) -> Sequence[StandardAnswer]:
|
||||||
return db_session.scalars(
|
return db_session.scalars(
|
||||||
select(StandardAnswer).where(StandardAnswer.active.is_(True))
|
select(StandardAnswer).where(StandardAnswer.active.is_(True))
|
||||||
|
@@ -102,7 +102,6 @@ from danswer.server.manage.llm.api import basic_router as llm_router
|
|||||||
from danswer.server.manage.llm.models import LLMProviderUpsertRequest
|
from danswer.server.manage.llm.models import LLMProviderUpsertRequest
|
||||||
from danswer.server.manage.search_settings import router as search_settings_router
|
from danswer.server.manage.search_settings import router as search_settings_router
|
||||||
from danswer.server.manage.slack_bot import router as slack_bot_management_router
|
from danswer.server.manage.slack_bot import router as slack_bot_management_router
|
||||||
from danswer.server.manage.standard_answer import router as standard_answer_router
|
|
||||||
from danswer.server.manage.users import router as user_router
|
from danswer.server.manage.users import router as user_router
|
||||||
from danswer.server.middleware.latency_logging import add_latency_logging_middleware
|
from danswer.server.middleware.latency_logging import add_latency_logging_middleware
|
||||||
from danswer.server.query_and_chat.chat_backend import router as chat_router
|
from danswer.server.query_and_chat.chat_backend import router as chat_router
|
||||||
@@ -503,7 +502,6 @@ def get_application() -> FastAPI:
|
|||||||
include_router_with_global_prefix_prepended(
|
include_router_with_global_prefix_prepended(
|
||||||
application, slack_bot_management_router
|
application, slack_bot_management_router
|
||||||
)
|
)
|
||||||
include_router_with_global_prefix_prepended(application, standard_answer_router)
|
|
||||||
include_router_with_global_prefix_prepended(application, persona_router)
|
include_router_with_global_prefix_prepended(application, persona_router)
|
||||||
include_router_with_global_prefix_prepended(application, admin_persona_router)
|
include_router_with_global_prefix_prepended(application, admin_persona_router)
|
||||||
include_router_with_global_prefix_prepended(application, input_prompt_router)
|
include_router_with_global_prefix_prepended(application, input_prompt_router)
|
||||||
|
@@ -0,0 +1,238 @@
|
|||||||
|
from slack_sdk import WebClient
|
||||||
|
from slack_sdk.models.blocks import ActionsBlock
|
||||||
|
from slack_sdk.models.blocks import Block
|
||||||
|
from slack_sdk.models.blocks import ButtonElement
|
||||||
|
from slack_sdk.models.blocks import SectionBlock
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from danswer.configs.constants import MessageType
|
||||||
|
from danswer.configs.danswerbot_configs import DANSWER_REACT_EMOJI
|
||||||
|
from danswer.danswerbot.slack.blocks import get_restate_blocks
|
||||||
|
from danswer.danswerbot.slack.constants import GENERATE_ANSWER_BUTTON_ACTION_ID
|
||||||
|
from danswer.danswerbot.slack.handlers.utils import send_team_member_message
|
||||||
|
from danswer.danswerbot.slack.models import SlackMessageInfo
|
||||||
|
from danswer.danswerbot.slack.utils import respond_in_thread
|
||||||
|
from danswer.danswerbot.slack.utils import update_emote_react
|
||||||
|
from danswer.db.chat import create_chat_session
|
||||||
|
from danswer.db.chat import create_new_chat_message
|
||||||
|
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 StandardAnswer as StandardAnswerModel
|
||||||
|
from danswer.server.manage.models import StandardAnswer as PydanticStandardAnswer
|
||||||
|
from danswer.utils.logger import DanswerLoggingAdapter
|
||||||
|
from danswer.utils.logger import setup_logger
|
||||||
|
from ee.danswer.db.standard_answer import fetch_standard_answer_categories_by_names
|
||||||
|
from ee.danswer.db.standard_answer import find_matching_standard_answers
|
||||||
|
|
||||||
|
logger = setup_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def build_standard_answer_blocks(
|
||||||
|
answer_message: str,
|
||||||
|
) -> list[Block]:
|
||||||
|
generate_button_block = ButtonElement(
|
||||||
|
action_id=GENERATE_ANSWER_BUTTON_ACTION_ID,
|
||||||
|
text="Generate Full Answer",
|
||||||
|
)
|
||||||
|
answer_block = SectionBlock(text=answer_message)
|
||||||
|
return [
|
||||||
|
answer_block,
|
||||||
|
ActionsBlock(
|
||||||
|
elements=[generate_button_block],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def oneoff_standard_answers(
|
||||||
|
message: str,
|
||||||
|
slack_bot_categories: list[str],
|
||||||
|
db_session: Session,
|
||||||
|
) -> list[PydanticStandardAnswer]:
|
||||||
|
"""
|
||||||
|
Respond to the user message if it matches any configured standard answers.
|
||||||
|
|
||||||
|
Returns a list of matching StandardAnswers if found, otherwise None.
|
||||||
|
"""
|
||||||
|
configured_standard_answers = {
|
||||||
|
standard_answer
|
||||||
|
for category in fetch_standard_answer_categories_by_names(
|
||||||
|
slack_bot_categories, db_session=db_session
|
||||||
|
)
|
||||||
|
for standard_answer in category.standard_answers
|
||||||
|
}
|
||||||
|
|
||||||
|
matching_standard_answers = find_matching_standard_answers(
|
||||||
|
query=message,
|
||||||
|
id_in=[answer.id for answer in configured_standard_answers],
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
server_standard_answers = [
|
||||||
|
PydanticStandardAnswer.from_model(standard_answer_model)
|
||||||
|
for (standard_answer_model, _) in matching_standard_answers
|
||||||
|
]
|
||||||
|
return server_standard_answers
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_standard_answers(
|
||||||
|
message_info: SlackMessageInfo,
|
||||||
|
receiver_ids: list[str] | None,
|
||||||
|
slack_bot_config: SlackBotConfig | None,
|
||||||
|
prompt: Prompt | None,
|
||||||
|
logger: DanswerLoggingAdapter,
|
||||||
|
client: WebClient,
|
||||||
|
db_session: Session,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Potentially respond to the user message depending on whether the user's message matches
|
||||||
|
any of the configured standard answers and also whether those answers have already been
|
||||||
|
provided in the current thread.
|
||||||
|
|
||||||
|
Returns True if standard answers are found to match the user's message and therefore,
|
||||||
|
we still need to respond to the users.
|
||||||
|
"""
|
||||||
|
# if no channel config, then no standard answers are configured
|
||||||
|
if not slack_bot_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 []
|
||||||
|
)
|
||||||
|
configured_standard_answers = set(
|
||||||
|
[
|
||||||
|
standard_answer
|
||||||
|
for standard_answer_category in configured_standard_answer_categories
|
||||||
|
for standard_answer in standard_answer_category.standard_answers
|
||||||
|
]
|
||||||
|
)
|
||||||
|
query_msg = message_info.thread_messages[-1]
|
||||||
|
|
||||||
|
if slack_thread_id is None:
|
||||||
|
used_standard_answer_ids = set([])
|
||||||
|
else:
|
||||||
|
chat_sessions = get_chat_sessions_by_slack_thread_id(
|
||||||
|
slack_thread_id=slack_thread_id,
|
||||||
|
user_id=None,
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
chat_messages = get_chat_messages_by_sessions(
|
||||||
|
chat_session_ids=[chat_session.id for chat_session in chat_sessions],
|
||||||
|
user_id=None,
|
||||||
|
db_session=db_session,
|
||||||
|
skip_permission_check=True,
|
||||||
|
)
|
||||||
|
used_standard_answer_ids = set(
|
||||||
|
[
|
||||||
|
standard_answer.id
|
||||||
|
for chat_message in chat_messages
|
||||||
|
for standard_answer in chat_message.standard_answers
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
usable_standard_answers = configured_standard_answers.difference(
|
||||||
|
used_standard_answer_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
matching_standard_answers: list[tuple[StandardAnswerModel, str]] = []
|
||||||
|
if usable_standard_answers:
|
||||||
|
matching_standard_answers = find_matching_standard_answers(
|
||||||
|
query=query_msg.message,
|
||||||
|
id_in=[standard_answer.id for standard_answer in usable_standard_answers],
|
||||||
|
db_session=db_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
if matching_standard_answers:
|
||||||
|
chat_session = create_chat_session(
|
||||||
|
db_session=db_session,
|
||||||
|
description="",
|
||||||
|
user_id=None,
|
||||||
|
persona_id=slack_bot_config.persona.id if slack_bot_config.persona else 0,
|
||||||
|
danswerbot_flow=True,
|
||||||
|
slack_thread_id=slack_thread_id,
|
||||||
|
one_shot=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
root_message = get_or_create_root_message(
|
||||||
|
chat_session_id=chat_session.id, db_session=db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
new_user_message = create_new_chat_message(
|
||||||
|
chat_session_id=chat_session.id,
|
||||||
|
parent_message=root_message,
|
||||||
|
prompt_id=prompt.id if prompt else None,
|
||||||
|
message=query_msg.message,
|
||||||
|
token_count=0,
|
||||||
|
message_type=MessageType.USER,
|
||||||
|
db_session=db_session,
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
formatted_answers = []
|
||||||
|
for standard_answer, match_str in matching_standard_answers:
|
||||||
|
since_you_mentioned_pretext = (
|
||||||
|
f'Since your question contained "_{match_str}_"'
|
||||||
|
)
|
||||||
|
block_quotified_answer = ">" + standard_answer.answer.replace("\n", "\n> ")
|
||||||
|
formatted_answer = f"{since_you_mentioned_pretext}, I thought this might be useful: \n\n{block_quotified_answer}"
|
||||||
|
formatted_answers.append(formatted_answer)
|
||||||
|
answer_message = "\n\n".join(formatted_answers)
|
||||||
|
|
||||||
|
_ = create_new_chat_message(
|
||||||
|
chat_session_id=chat_session.id,
|
||||||
|
parent_message=new_user_message,
|
||||||
|
prompt_id=prompt.id if prompt else None,
|
||||||
|
message=answer_message,
|
||||||
|
token_count=0,
|
||||||
|
message_type=MessageType.ASSISTANT,
|
||||||
|
error=None,
|
||||||
|
db_session=db_session,
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_emote_react(
|
||||||
|
emoji=DANSWER_REACT_EMOJI,
|
||||||
|
channel=message_info.channel_to_respond,
|
||||||
|
message_ts=message_info.msg_to_respond,
|
||||||
|
remove=True,
|
||||||
|
client=client,
|
||||||
|
)
|
||||||
|
|
||||||
|
restate_question_blocks = get_restate_blocks(
|
||||||
|
msg=query_msg.message,
|
||||||
|
is_bot_msg=message_info.is_bot_msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer_blocks = build_standard_answer_blocks(
|
||||||
|
answer_message=answer_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
all_blocks = restate_question_blocks + answer_blocks
|
||||||
|
|
||||||
|
try:
|
||||||
|
respond_in_thread(
|
||||||
|
client=client,
|
||||||
|
channel=message_info.channel_to_respond,
|
||||||
|
receiver_ids=receiver_ids,
|
||||||
|
text="Hello! Danswer has some results for you!",
|
||||||
|
blocks=all_blocks,
|
||||||
|
thread_ts=message_info.msg_to_respond,
|
||||||
|
unfurl=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if receiver_ids and slack_thread_id:
|
||||||
|
send_team_member_message(
|
||||||
|
client=client,
|
||||||
|
channel=message_info.channel_to_respond,
|
||||||
|
thread_ts=slack_thread_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Unable to send standard answer message: {e}")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
76
backend/ee/danswer/db/standard_answer.py
Normal file
76
backend/ee/danswer/db/standard_answer.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import re
|
||||||
|
import string
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from danswer.db.models import StandardAnswer
|
||||||
|
from danswer.db.models import StandardAnswerCategory
|
||||||
|
from danswer.utils.logger import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_standard_answer_categories_by_names(
|
||||||
|
standard_answer_category_names: list[str],
|
||||||
|
db_session: Session,
|
||||||
|
) -> Sequence[StandardAnswerCategory]:
|
||||||
|
return db_session.scalars(
|
||||||
|
select(StandardAnswerCategory).where(
|
||||||
|
StandardAnswerCategory.name.in_(standard_answer_category_names)
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
|
||||||
|
def find_matching_standard_answers(
|
||||||
|
id_in: list[int],
|
||||||
|
query: str,
|
||||||
|
db_session: Session,
|
||||||
|
) -> list[tuple[StandardAnswer, str]]:
|
||||||
|
"""
|
||||||
|
Returns a list of tuples, where each tuple is a StandardAnswer definition matching
|
||||||
|
the query and a string representing the match (either the regex match group or the
|
||||||
|
set of keywords).
|
||||||
|
|
||||||
|
If `answer_instance.match_regex` is true, the definition is considered "matched"
|
||||||
|
if the query matches the `answer_instance.keyword` using `re.search`.
|
||||||
|
|
||||||
|
Otherwise, the definition is considered "matched" if each space-delimited token
|
||||||
|
in `keyword` exists in `query`.
|
||||||
|
"""
|
||||||
|
stmt = (
|
||||||
|
select(StandardAnswer)
|
||||||
|
.where(StandardAnswer.active.is_(True))
|
||||||
|
.where(StandardAnswer.id.in_(id_in))
|
||||||
|
)
|
||||||
|
possible_standard_answers: Sequence[StandardAnswer] = db_session.scalars(stmt).all()
|
||||||
|
|
||||||
|
matching_standard_answers: list[tuple[StandardAnswer, str]] = []
|
||||||
|
for standard_answer in possible_standard_answers:
|
||||||
|
if standard_answer.match_regex:
|
||||||
|
maybe_matches = re.search(standard_answer.keyword, query, re.IGNORECASE)
|
||||||
|
if maybe_matches is not None:
|
||||||
|
match_group = maybe_matches.group(0)
|
||||||
|
matching_standard_answers.append((standard_answer, match_group))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Remove punctuation and split the keyword into individual words
|
||||||
|
keyword_words = "".join(
|
||||||
|
char
|
||||||
|
for char in standard_answer.keyword.lower()
|
||||||
|
if char not in string.punctuation
|
||||||
|
).split()
|
||||||
|
|
||||||
|
# Remove punctuation and split the query into individual words
|
||||||
|
query_words = "".join(
|
||||||
|
char for char in query.lower() if char not in string.punctuation
|
||||||
|
).split()
|
||||||
|
|
||||||
|
# Check if all of the keyword words are in the query words
|
||||||
|
if all(word in query_words for word in keyword_words):
|
||||||
|
matching_standard_answers.append(
|
||||||
|
(standard_answer, standard_answer.keyword)
|
||||||
|
)
|
||||||
|
|
||||||
|
return matching_standard_answers
|
@@ -23,6 +23,7 @@ from ee.danswer.server.enterprise_settings.api import (
|
|||||||
from ee.danswer.server.enterprise_settings.api import (
|
from ee.danswer.server.enterprise_settings.api import (
|
||||||
basic_router as enterprise_settings_router,
|
basic_router as enterprise_settings_router,
|
||||||
)
|
)
|
||||||
|
from ee.danswer.server.manage.standard_answer import router as standard_answer_router
|
||||||
from ee.danswer.server.query_and_chat.chat_backend import (
|
from ee.danswer.server.query_and_chat.chat_backend import (
|
||||||
router as chat_router,
|
router as chat_router,
|
||||||
)
|
)
|
||||||
@@ -86,6 +87,7 @@ def get_application() -> FastAPI:
|
|||||||
# EE only backend APIs
|
# EE only backend APIs
|
||||||
include_router_with_global_prefix_prepended(application, query_router)
|
include_router_with_global_prefix_prepended(application, query_router)
|
||||||
include_router_with_global_prefix_prepended(application, chat_router)
|
include_router_with_global_prefix_prepended(application, chat_router)
|
||||||
|
include_router_with_global_prefix_prepended(application, standard_answer_router)
|
||||||
# Enterprise-only global settings
|
# Enterprise-only global settings
|
||||||
include_router_with_global_prefix_prepended(
|
include_router_with_global_prefix_prepended(
|
||||||
application, enterprise_settings_admin_router
|
application, enterprise_settings_admin_router
|
||||||
|
@@ -6,9 +6,6 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from danswer.auth.users import current_user
|
from danswer.auth.users import current_user
|
||||||
from danswer.configs.danswerbot_configs import DANSWER_BOT_TARGET_CHUNK_PERCENTAGE
|
from danswer.configs.danswerbot_configs import DANSWER_BOT_TARGET_CHUNK_PERCENTAGE
|
||||||
from danswer.danswerbot.slack.handlers.handle_standard_answers import (
|
|
||||||
oneoff_standard_answers,
|
|
||||||
)
|
|
||||||
from danswer.db.engine import get_session
|
from danswer.db.engine import get_session
|
||||||
from danswer.db.models import User
|
from danswer.db.models import User
|
||||||
from danswer.db.persona import get_persona_by_id
|
from danswer.db.persona import get_persona_by_id
|
||||||
@@ -29,6 +26,9 @@ from danswer.search.utils import dedupe_documents
|
|||||||
from danswer.search.utils import drop_llm_indices
|
from danswer.search.utils import drop_llm_indices
|
||||||
from danswer.search.utils import relevant_sections_to_indices
|
from danswer.search.utils import relevant_sections_to_indices
|
||||||
from danswer.utils.logger import setup_logger
|
from danswer.utils.logger import setup_logger
|
||||||
|
from ee.danswer.danswerbot.slack.handlers.handle_standard_answers import (
|
||||||
|
oneoff_standard_answers,
|
||||||
|
)
|
||||||
from ee.danswer.server.query_and_chat.models import DocumentSearchRequest
|
from ee.danswer.server.query_and_chat.models import DocumentSearchRequest
|
||||||
from ee.danswer.server.query_and_chat.models import StandardAnswerRequest
|
from ee.danswer.server.query_and_chat.models import StandardAnswerRequest
|
||||||
from ee.danswer.server.query_and_chat.models import StandardAnswerResponse
|
from ee.danswer.server.query_and_chat.models import StandardAnswerResponse
|
||||||
|
@@ -3,11 +3,7 @@
|
|||||||
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
|
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||||
import {
|
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||||
DocumentSet,
|
|
||||||
SlackBotConfig,
|
|
||||||
StandardAnswerCategory,
|
|
||||||
} from "@/lib/types";
|
|
||||||
import {
|
import {
|
||||||
BooleanFormField,
|
BooleanFormField,
|
||||||
Label,
|
Label,
|
||||||
@@ -28,16 +24,18 @@ import MultiSelectDropdown from "@/components/MultiSelectDropdown";
|
|||||||
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
|
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
|
||||||
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
|
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
|
||||||
import CollapsibleSection from "../assistants/CollapsibleSection";
|
import CollapsibleSection from "../assistants/CollapsibleSection";
|
||||||
|
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
|
||||||
|
import { StandardAnswerCategoryDropdownField } from "@/components/standardAnswers/StandardAnswerCategoryDropdown";
|
||||||
|
|
||||||
export const SlackBotCreationForm = ({
|
export const SlackBotCreationForm = ({
|
||||||
documentSets,
|
documentSets,
|
||||||
personas,
|
personas,
|
||||||
standardAnswerCategories,
|
standardAnswerCategoryResponse,
|
||||||
existingSlackBotConfig,
|
existingSlackBotConfig,
|
||||||
}: {
|
}: {
|
||||||
documentSets: DocumentSet[];
|
documentSets: DocumentSet[];
|
||||||
personas: Persona[];
|
personas: Persona[];
|
||||||
standardAnswerCategories: StandardAnswerCategory[];
|
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
|
||||||
existingSlackBotConfig?: SlackBotConfig;
|
existingSlackBotConfig?: SlackBotConfig;
|
||||||
}) => {
|
}) => {
|
||||||
const isUpdate = existingSlackBotConfig !== undefined;
|
const isUpdate = existingSlackBotConfig !== undefined;
|
||||||
@@ -356,39 +354,16 @@ export const SlackBotCreationForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Label>Standard Answer Categories</Label>
|
<StandardAnswerCategoryDropdownField
|
||||||
<div className="w-4/12">
|
standardAnswerCategoryResponse={
|
||||||
<MultiSelectDropdown
|
standardAnswerCategoryResponse
|
||||||
name="standard_answer_categories"
|
}
|
||||||
label=""
|
categories={values.standard_answer_categories}
|
||||||
onChange={(selected_options) => {
|
setCategories={(categories) =>
|
||||||
const selected_categories = selected_options.map(
|
setFieldValue("standard_answer_categories", categories)
|
||||||
(option) => {
|
|
||||||
return {
|
|
||||||
id: Number(option.value),
|
|
||||||
name: option.label,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
setFieldValue(
|
|
||||||
"standard_answer_categories",
|
|
||||||
selected_categories
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
creatable={false}
|
|
||||||
options={standardAnswerCategories.map((category) => ({
|
|
||||||
label: category.name,
|
|
||||||
value: category.id.toString(),
|
|
||||||
}))}
|
|
||||||
initialSelectedOptions={values.standard_answer_categories.map(
|
|
||||||
(category) => ({
|
|
||||||
label: category.name,
|
|
||||||
value: category.id.toString(),
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
@@ -3,11 +3,7 @@ import { CPUIcon } from "@/components/icons/icons";
|
|||||||
import { SlackBotCreationForm } from "../SlackBotConfigCreationForm";
|
import { SlackBotCreationForm } from "../SlackBotConfigCreationForm";
|
||||||
import { fetchSS } from "@/lib/utilsSS";
|
import { fetchSS } from "@/lib/utilsSS";
|
||||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
import {
|
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||||
DocumentSet,
|
|
||||||
SlackBotConfig,
|
|
||||||
StandardAnswerCategory,
|
|
||||||
} from "@/lib/types";
|
|
||||||
import { Text } from "@tremor/react";
|
import { Text } from "@tremor/react";
|
||||||
import { BackButton } from "@/components/BackButton";
|
import { BackButton } from "@/components/BackButton";
|
||||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||||
@@ -15,27 +11,28 @@ import {
|
|||||||
FetchAssistantsResponse,
|
FetchAssistantsResponse,
|
||||||
fetchAssistantsSS,
|
fetchAssistantsSS,
|
||||||
} from "@/lib/assistants/fetchAssistantsSS";
|
} from "@/lib/assistants/fetchAssistantsSS";
|
||||||
|
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
|
||||||
|
|
||||||
async function Page({ params }: { params: { id: string } }) {
|
async function Page({ params }: { params: { id: string } }) {
|
||||||
const tasks = [
|
const tasks = [
|
||||||
fetchSS("/manage/admin/slack-bot/config"),
|
fetchSS("/manage/admin/slack-bot/config"),
|
||||||
fetchSS("/manage/document-set"),
|
fetchSS("/manage/document-set"),
|
||||||
fetchAssistantsSS(),
|
fetchAssistantsSS(),
|
||||||
fetchSS("/manage/admin/standard-answer/category"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const [
|
const [
|
||||||
slackBotsResponse,
|
slackBotsResponse,
|
||||||
documentSetsResponse,
|
documentSetsResponse,
|
||||||
[assistants, assistantsFetchError],
|
[assistants, assistantsFetchError],
|
||||||
standardAnswerCategoriesResponse,
|
|
||||||
] = (await Promise.all(tasks)) as [
|
] = (await Promise.all(tasks)) as [
|
||||||
Response,
|
Response,
|
||||||
Response,
|
Response,
|
||||||
FetchAssistantsResponse,
|
FetchAssistantsResponse,
|
||||||
Response,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const eeStandardAnswerCategoryResponse =
|
||||||
|
await getStandardAnswerCategoriesIfEE();
|
||||||
|
|
||||||
if (!slackBotsResponse.ok) {
|
if (!slackBotsResponse.ok) {
|
||||||
return (
|
return (
|
||||||
<ErrorCallout
|
<ErrorCallout
|
||||||
@@ -77,18 +74,6 @@ async function Page({ params }: { params: { id: string } }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!standardAnswerCategoriesResponse.ok) {
|
|
||||||
return (
|
|
||||||
<ErrorCallout
|
|
||||||
errorTitle="Something went wrong :("
|
|
||||||
errorMsg={`Failed to fetch standard answer categories - ${await standardAnswerCategoriesResponse.text()}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const standardAnswerCategories =
|
|
||||||
(await standardAnswerCategoriesResponse.json()) as StandardAnswerCategory[];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<InstantSSRAutoRefresh />
|
<InstantSSRAutoRefresh />
|
||||||
@@ -107,7 +92,7 @@ async function Page({ params }: { params: { id: string } }) {
|
|||||||
<SlackBotCreationForm
|
<SlackBotCreationForm
|
||||||
documentSets={documentSets}
|
documentSets={documentSets}
|
||||||
personas={assistants}
|
personas={assistants}
|
||||||
standardAnswerCategories={standardAnswerCategories}
|
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
|
||||||
existingSlackBotConfig={slackBotConfig}
|
existingSlackBotConfig={slackBotConfig}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -10,13 +10,10 @@ import {
|
|||||||
FetchAssistantsResponse,
|
FetchAssistantsResponse,
|
||||||
fetchAssistantsSS,
|
fetchAssistantsSS,
|
||||||
} from "@/lib/assistants/fetchAssistantsSS";
|
} from "@/lib/assistants/fetchAssistantsSS";
|
||||||
|
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
|
||||||
|
|
||||||
async function Page() {
|
async function Page() {
|
||||||
const tasks = [
|
const tasks = [fetchSS("/manage/document-set"), fetchAssistantsSS()];
|
||||||
fetchSS("/manage/document-set"),
|
|
||||||
fetchAssistantsSS(),
|
|
||||||
fetchSS("/manage/admin/standard-answer/category"),
|
|
||||||
];
|
|
||||||
const [
|
const [
|
||||||
documentSetsResponse,
|
documentSetsResponse,
|
||||||
[assistants, assistantsFetchError],
|
[assistants, assistantsFetchError],
|
||||||
@@ -27,6 +24,9 @@ async function Page() {
|
|||||||
Response,
|
Response,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const eeStandardAnswerCategoryResponse =
|
||||||
|
await getStandardAnswerCategoriesIfEE();
|
||||||
|
|
||||||
if (!documentSetsResponse.ok) {
|
if (!documentSetsResponse.ok) {
|
||||||
return (
|
return (
|
||||||
<ErrorCallout
|
<ErrorCallout
|
||||||
@@ -46,18 +46,6 @@ async function Page() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!standardAnswerCategoriesResponse.ok) {
|
|
||||||
return (
|
|
||||||
<ErrorCallout
|
|
||||||
errorTitle="Something went wrong :("
|
|
||||||
errorMsg={`Failed to fetch standard answer categories - ${await standardAnswerCategoriesResponse.text()}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const standardAnswerCategories =
|
|
||||||
(await standardAnswerCategoriesResponse.json()) as StandardAnswerCategory[];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<BackButton />
|
<BackButton />
|
||||||
@@ -69,7 +57,7 @@ async function Page() {
|
|||||||
<SlackBotCreationForm
|
<SlackBotCreationForm
|
||||||
documentSets={documentSets}
|
documentSets={documentSets}
|
||||||
personas={assistants}
|
personas={assistants}
|
||||||
standardAnswerCategories={standardAnswerCategories}
|
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { AdminPageTitle } from "@/components/admin/Title";
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
import { StandardAnswerCreationForm } from "@/app/admin/standard-answer/StandardAnswerCreationForm";
|
import { StandardAnswerCreationForm } from "@/app/ee/admin/standard-answer/StandardAnswerCreationForm";
|
||||||
import { fetchSS } from "@/lib/utilsSS";
|
import { fetchSS } from "@/lib/utilsSS";
|
||||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
import { BackButton } from "@/components/BackButton";
|
import { BackButton } from "@/components/BackButton";
|
@@ -1,5 +1,5 @@
|
|||||||
import { AdminPageTitle } from "@/components/admin/Title";
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
import { StandardAnswerCreationForm } from "@/app/admin/standard-answer/StandardAnswerCreationForm";
|
import { StandardAnswerCreationForm } from "@/app/ee/admin/standard-answer/StandardAnswerCreationForm";
|
||||||
import { fetchSS } from "@/lib/utilsSS";
|
import { fetchSS } from "@/lib/utilsSS";
|
||||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
import { BackButton } from "@/components/BackButton";
|
import { BackButton } from "@/components/BackButton";
|
@@ -145,15 +145,6 @@ export function ClientLayout({
|
|||||||
),
|
),
|
||||||
link: "/admin/tools",
|
link: "/admin/tools",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: (
|
|
||||||
<div className="flex">
|
|
||||||
<ClipboardIcon size={18} />
|
|
||||||
<div className="ml-1">Standard Answers</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
link: "/admin/standard-answer",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: (
|
name: (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -165,6 +156,19 @@ export function ClientLayout({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(enableEnterprise
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<div className="flex">
|
||||||
|
<ClipboardIcon size={18} />
|
||||||
|
<div className="ml-1">Standard Answers</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
link: "/admin/standard-answer",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
...(isCurator
|
...(isCurator
|
||||||
|
@@ -0,0 +1,71 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { StandardAnswerCategoryResponse } from "./getStandardAnswerCategoriesIfEE";
|
||||||
|
import { Label } from "../admin/connectors/Field";
|
||||||
|
import MultiSelectDropdown from "../MultiSelectDropdown";
|
||||||
|
import { StandardAnswerCategory } from "@/lib/types";
|
||||||
|
import { ErrorCallout } from "../ErrorCallout";
|
||||||
|
import { LoadingAnimation } from "../Loading";
|
||||||
|
import { Divider } from "@tremor/react";
|
||||||
|
|
||||||
|
interface StandardAnswerCategoryDropdownFieldProps {
|
||||||
|
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
|
||||||
|
categories: StandardAnswerCategory[];
|
||||||
|
setCategories: (categories: StandardAnswerCategory[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StandardAnswerCategoryDropdownField: FC<
|
||||||
|
StandardAnswerCategoryDropdownFieldProps
|
||||||
|
> = ({ standardAnswerCategoryResponse, categories, setCategories }) => {
|
||||||
|
if (!standardAnswerCategoryResponse.paidEnterpriseFeaturesEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (standardAnswerCategoryResponse.error != null) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Something went wrong :("
|
||||||
|
errorMsg={`Failed to fetch standard answer categories - ${standardAnswerCategoryResponse.error.message}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (standardAnswerCategoryResponse.categories == null) {
|
||||||
|
return <LoadingAnimation />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label>Standard Answer Categories</Label>
|
||||||
|
<div className="w-4/12">
|
||||||
|
<MultiSelectDropdown
|
||||||
|
name="standard_answer_categories"
|
||||||
|
label=""
|
||||||
|
onChange={(selectedOptions) => {
|
||||||
|
const selectedCategories = selectedOptions.map((option) => {
|
||||||
|
return {
|
||||||
|
id: Number(option.value),
|
||||||
|
name: option.label,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setCategories(selectedCategories);
|
||||||
|
}}
|
||||||
|
creatable={false}
|
||||||
|
options={standardAnswerCategoryResponse.categories.map(
|
||||||
|
(category) => ({
|
||||||
|
label: category.name,
|
||||||
|
value: category.id.toString(),
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
initialSelectedOptions={categories.map((category) => ({
|
||||||
|
label: category.name,
|
||||||
|
value: category.id.toString(),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,47 @@
|
|||||||
|
import { SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED } from "@/lib/constants";
|
||||||
|
import { StandardAnswerCategory } from "@/lib/types";
|
||||||
|
import { fetchSS } from "@/lib/utilsSS";
|
||||||
|
|
||||||
|
export type StandardAnswerCategoryResponse =
|
||||||
|
| EEStandardAnswerCategoryResponse
|
||||||
|
| NoEEAvailable;
|
||||||
|
|
||||||
|
interface NoEEAvailable {
|
||||||
|
paidEnterpriseFeaturesEnabled: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EEStandardAnswerCategoryResponse {
|
||||||
|
paidEnterpriseFeaturesEnabled: true;
|
||||||
|
error?: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
categories?: StandardAnswerCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStandardAnswerCategoriesIfEE(): Promise<StandardAnswerCategoryResponse> {
|
||||||
|
if (!SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED) {
|
||||||
|
return {
|
||||||
|
paidEnterpriseFeaturesEnabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const standardAnswerCategoriesResponse = await fetchSS(
|
||||||
|
"/manage/admin/standard-answer/category"
|
||||||
|
);
|
||||||
|
if (!standardAnswerCategoriesResponse.ok) {
|
||||||
|
return {
|
||||||
|
paidEnterpriseFeaturesEnabled: true,
|
||||||
|
error: {
|
||||||
|
message: await standardAnswerCategoriesResponse.text(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories =
|
||||||
|
(await standardAnswerCategoriesResponse.json()) as StandardAnswerCategory[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
paidEnterpriseFeaturesEnabled: true,
|
||||||
|
categories,
|
||||||
|
};
|
||||||
|
}
|
@@ -9,6 +9,7 @@ const eePaths = [
|
|||||||
"/admin/performance/query-history",
|
"/admin/performance/query-history",
|
||||||
"/admin/whitelabeling",
|
"/admin/whitelabeling",
|
||||||
"/admin/performance/custom-analytics",
|
"/admin/performance/custom-analytics",
|
||||||
|
"/admin/standard-answer",
|
||||||
];
|
];
|
||||||
|
|
||||||
const eePathsForMatcher = eePaths.map((path) => `${path}/:path*`);
|
const eePathsForMatcher = eePaths.map((path) => `${path}/:path*`);
|
||||||
|
Reference in New Issue
Block a user