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:
hj-danswer
2024-09-13 18:57:03 -07:00
committed by GitHub
parent a63cb9da43
commit 974f85da66
24 changed files with 514 additions and 371 deletions

View File

@@ -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 FOLLOWUP_BUTTON_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 LIKE_BLOCK_ACTION_ID
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))]
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(
message_id: int | None,
answer: str | None,

View File

@@ -1,62 +1,16 @@
from slack_sdk import WebClient
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.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.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 setup_logger
from danswer.utils.variable_functionality import fetch_versioned_implementation
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(
message_info: SlackMessageInfo,
receiver_ids: list[str] | None,
@@ -66,153 +20,37 @@ def handle_standard_answers(
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 []
"""Returns whether one or more Standard Answer message blocks were
emitted by the Slack bot"""
versioned_handle_standard_answers = fetch_versioned_implementation(
"danswer.danswerbot.slack.handlers.handle_standard_answers",
"_handle_standard_answers",
)
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,
return versioned_handle_standard_answers(
message_info=message_info,
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(
slack_bot_config=slack_bot_config,
prompt=prompt,
logger=logger,
client=client,
channel=message_info.channel_to_respond,
thread_ts=slack_thread_id,
db_session=db_session,
)
return True
except Exception as e:
logger.exception(f"Unable to send standard answer message: {e}")
return False
else:
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:
"""
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

View File

@@ -56,6 +56,7 @@ from danswer.one_shot_answer.models import ThreadMessage
from danswer.search.retrieval.search_runner import download_nltk_data
from danswer.server.manage.models import SlackBotTokens
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_PORT
from shared_configs.configs import SLACK_CHANNEL_ID
@@ -481,6 +482,8 @@ if __name__ == "__main__":
slack_bot_tokens: SlackBotTokens | None = None
socket_client: SocketModeClient | None = None
set_is_ee_based_on_env_variable()
logger.notice("Verifying query preprocessing (NLTK) data is downloaded")
download_nltk_data()

View File

@@ -1,5 +1,3 @@
import re
import string
from collections.abc import Sequence
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(
standard_answer_category_ids: list[int],
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]:
return db_session.scalars(
select(StandardAnswer).where(StandardAnswer.active.is_(True))

View File

@@ -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.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.standard_answer import router as standard_answer_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.query_and_chat.chat_backend import router as chat_router
@@ -503,7 +502,6 @@ def get_application() -> FastAPI:
include_router_with_global_prefix_prepended(
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, admin_persona_router)
include_router_with_global_prefix_prepended(application, input_prompt_router)

View File

@@ -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

View 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

View File

@@ -23,6 +23,7 @@ from ee.danswer.server.enterprise_settings.api import (
from ee.danswer.server.enterprise_settings.api import (
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 (
router as chat_router,
)
@@ -86,6 +87,7 @@ def get_application() -> FastAPI:
# EE only backend APIs
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, standard_answer_router)
# Enterprise-only global settings
include_router_with_global_prefix_prepended(
application, enterprise_settings_admin_router

View File

@@ -6,9 +6,6 @@ from sqlalchemy.orm import Session
from danswer.auth.users import current_user
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.models import User
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 relevant_sections_to_indices
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 StandardAnswerRequest
from ee.danswer.server.query_and_chat.models import StandardAnswerResponse

View File

@@ -3,11 +3,7 @@
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
import * as Yup from "yup";
import { usePopup } from "@/components/admin/connectors/Popup";
import {
DocumentSet,
SlackBotConfig,
StandardAnswerCategory,
} from "@/lib/types";
import { DocumentSet, SlackBotConfig } from "@/lib/types";
import {
BooleanFormField,
Label,
@@ -28,16 +24,18 @@ import MultiSelectDropdown from "@/components/MultiSelectDropdown";
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
import CollapsibleSection from "../assistants/CollapsibleSection";
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { StandardAnswerCategoryDropdownField } from "@/components/standardAnswers/StandardAnswerCategoryDropdown";
export const SlackBotCreationForm = ({
documentSets,
personas,
standardAnswerCategories,
standardAnswerCategoryResponse,
existingSlackBotConfig,
}: {
documentSets: DocumentSet[];
personas: Persona[];
standardAnswerCategories: StandardAnswerCategory[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
existingSlackBotConfig?: SlackBotConfig;
}) => {
const isUpdate = existingSlackBotConfig !== undefined;
@@ -356,39 +354,16 @@ export const SlackBotCreationForm = ({
</div>
</div>
<Label>Standard Answer Categories</Label>
<div className="w-4/12">
<MultiSelectDropdown
name="standard_answer_categories"
label=""
onChange={(selected_options) => {
const selected_categories = selected_options.map(
(option) => {
return {
id: Number(option.value),
name: option.label,
};
<StandardAnswerCategoryDropdownField
standardAnswerCategoryResponse={
standardAnswerCategoryResponse
}
categories={values.standard_answer_categories}
setCategories={(categories) =>
setFieldValue("standard_answer_categories", categories)
}
);
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 className="flex">

View File

@@ -3,11 +3,7 @@ import { CPUIcon } from "@/components/icons/icons";
import { SlackBotCreationForm } from "../SlackBotConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import {
DocumentSet,
SlackBotConfig,
StandardAnswerCategory,
} from "@/lib/types";
import { DocumentSet, SlackBotConfig } from "@/lib/types";
import { Text } from "@tremor/react";
import { BackButton } from "@/components/BackButton";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
@@ -15,27 +11,28 @@ import {
FetchAssistantsResponse,
fetchAssistantsSS,
} from "@/lib/assistants/fetchAssistantsSS";
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
async function Page({ params }: { params: { id: string } }) {
const tasks = [
fetchSS("/manage/admin/slack-bot/config"),
fetchSS("/manage/document-set"),
fetchAssistantsSS(),
fetchSS("/manage/admin/standard-answer/category"),
];
const [
slackBotsResponse,
documentSetsResponse,
[assistants, assistantsFetchError],
standardAnswerCategoriesResponse,
] = (await Promise.all(tasks)) as [
Response,
Response,
FetchAssistantsResponse,
Response,
];
const eeStandardAnswerCategoryResponse =
await getStandardAnswerCategoriesIfEE();
if (!slackBotsResponse.ok) {
return (
<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 (
<div className="container mx-auto">
<InstantSSRAutoRefresh />
@@ -107,7 +92,7 @@ async function Page({ params }: { params: { id: string } }) {
<SlackBotCreationForm
documentSets={documentSets}
personas={assistants}
standardAnswerCategories={standardAnswerCategories}
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
existingSlackBotConfig={slackBotConfig}
/>
</div>

View File

@@ -10,13 +10,10 @@ import {
FetchAssistantsResponse,
fetchAssistantsSS,
} from "@/lib/assistants/fetchAssistantsSS";
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
async function Page() {
const tasks = [
fetchSS("/manage/document-set"),
fetchAssistantsSS(),
fetchSS("/manage/admin/standard-answer/category"),
];
const tasks = [fetchSS("/manage/document-set"), fetchAssistantsSS()];
const [
documentSetsResponse,
[assistants, assistantsFetchError],
@@ -27,6 +24,9 @@ async function Page() {
Response,
];
const eeStandardAnswerCategoryResponse =
await getStandardAnswerCategoriesIfEE();
if (!documentSetsResponse.ok) {
return (
<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 (
<div className="container mx-auto">
<BackButton />
@@ -69,7 +57,7 @@ async function Page() {
<SlackBotCreationForm
documentSets={documentSets}
personas={assistants}
standardAnswerCategories={standardAnswerCategories}
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
/>
</div>
);

View File

@@ -1,5 +1,5 @@
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 { ErrorCallout } from "@/components/ErrorCallout";
import { BackButton } from "@/components/BackButton";

View File

@@ -1,5 +1,5 @@
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 { ErrorCallout } from "@/components/ErrorCallout";
import { BackButton } from "@/components/BackButton";

View File

@@ -145,15 +145,6 @@ export function ClientLayout({
),
link: "/admin/tools",
},
{
name: (
<div className="flex">
<ClipboardIcon size={18} />
<div className="ml-1">Standard Answers</div>
</div>
),
link: "/admin/standard-answer",
},
{
name: (
<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

View File

@@ -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 />
</>
);
};

View File

@@ -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,
};
}

View File

@@ -9,6 +9,7 @@ const eePaths = [
"/admin/performance/query-history",
"/admin/whitelabeling",
"/admin/performance/custom-analytics",
"/admin/standard-answer",
];
const eePathsForMatcher = eePaths.map((path) => `${path}/:path*`);