diff --git a/backend/onyx/db/persona.py b/backend/onyx/db/persona.py index 94a60e7b5cd..638ee74a6ef 100644 --- a/backend/onyx/db/persona.py +++ b/backend/onyx/db/persona.py @@ -11,6 +11,7 @@ from sqlalchemy import select from sqlalchemy import update from sqlalchemy.orm import aliased +from sqlalchemy.orm import joinedload from sqlalchemy.orm import selectinload from sqlalchemy.orm import Session @@ -708,3 +709,15 @@ def update_persona_label( def delete_persona_label(label_id: int, db_session: Session) -> None: db_session.query(PersonaLabel).filter(PersonaLabel.id == label_id).delete() db_session.commit() + + +def persona_has_search_tool(persona_id: int, db_session: Session) -> bool: + persona = ( + db_session.query(Persona) + .options(joinedload(Persona.tools)) + .filter(Persona.id == persona_id) + .one_or_none() + ) + if persona is None: + raise ValueError(f"Persona with ID {persona_id} does not exist") + return any(tool.in_code_tool_id == "run_search" for tool in persona.tools) diff --git a/backend/onyx/onyxbot/slack/blocks.py b/backend/onyx/onyxbot/slack/blocks.py index 73b8a098d6f..96c2aa1a440 100644 --- a/backend/onyx/onyxbot/slack/blocks.py +++ b/backend/onyx/onyxbot/slack/blocks.py @@ -338,13 +338,30 @@ def _build_citations_blocks( return citations_block +def _build_answer_blocks( + answer: ChatOnyxBotResponse, fallback_answer: str +) -> list[Block]: + if not answer.answer: + answer_blocks = [SectionBlock(text=fallback_answer)] + else: + # replaces markdown links with slack format links + formatted_answer = format_slack_message(answer.answer) + answer_processed = decode_escapes( + remove_slack_text_interactions(formatted_answer) + ) + answer_blocks = [ + SectionBlock(text=text) for text in _split_text(answer_processed) + ] + return answer_blocks + + def _build_qa_response_blocks( answer: ChatOnyxBotResponse, ) -> list[Block]: retrieval_info = answer.docs - # if not retrieval_info: - # This should not happen, even with no docs retrieved, there is still info returned - # raise RuntimeError("Failed to retrieve docs, cannot answer question.") + if not retrieval_info: + # This should not happen, even with no docs retrieved, there is still info returned + raise RuntimeError("Failed to retrieve docs, cannot answer question.") if DISABLE_GENERATIVE_AI: return [] @@ -376,21 +393,10 @@ def _build_qa_response_blocks( filter_block = SectionBlock(text=f"_{filter_text}_") - if not answer.answer: - answer_blocks = [ - SectionBlock( - text="Sorry, I was unable to find an answer, but I did find some potentially relevant docs 🤓" - ) - ] - else: - # replaces markdown links with slack format links - formatted_answer = format_slack_message(answer.answer) - answer_processed = decode_escapes( - remove_slack_text_interactions(formatted_answer) - ) - answer_blocks = [ - SectionBlock(text=text) for text in _split_text(answer_processed) - ] + answer_blocks = _build_answer_blocks( + answer=answer, + fallback_answer="Sorry, I was unable to find an answer, but I did find some potentially relevant docs 🤓", + ) response_blocks: list[Block] = [] @@ -481,6 +487,7 @@ def build_slack_response_blocks( use_citations: bool, feedback_reminder_id: str | None, skip_ai_feedback: bool = False, + expecting_search_result: bool = False, ) -> list[Block]: """ This function is a top level function that builds all the blocks for the Slack response. @@ -491,9 +498,15 @@ def build_slack_response_blocks( message_info.thread_messages[-1].message, message_info.is_bot_msg ) - answer_blocks = _build_qa_response_blocks( - answer=answer, - ) + if expecting_search_result: + answer_blocks = _build_qa_response_blocks( + answer=answer, + ) + + else: + answer_blocks = _build_answer_blocks( + answer=answer, fallback_answer="Sorry, I was unable to generate an answer." + ) web_follow_up_block = [] if channel_conf and channel_conf.get("show_continue_in_web_ui"): diff --git a/backend/onyx/onyxbot/slack/handlers/handle_regular_answer.py b/backend/onyx/onyxbot/slack/handlers/handle_regular_answer.py index c359b3fe0e9..4479bbcd536 100644 --- a/backend/onyx/onyxbot/slack/handlers/handle_regular_answer.py +++ b/backend/onyx/onyxbot/slack/handlers/handle_regular_answer.py @@ -27,6 +27,7 @@ from onyx.db.models import SlackChannelConfig from onyx.db.models import User from onyx.db.persona import get_persona_by_id +from onyx.db.persona import persona_has_search_tool from onyx.db.users import get_user_by_email from onyx.onyxbot.slack.blocks import build_slack_response_blocks from onyx.onyxbot.slack.handlers.utils import send_team_member_message @@ -106,6 +107,9 @@ def handle_regular_answer( ] prompt = persona.prompts[0] if persona.prompts else None + with get_session_with_tenant(tenant_id) as db_session: + expecting_search_result = persona_has_search_tool(persona.id, db_session) + # TODO: Add in support for Slack to truncate messages based on max LLM context # llm, _ = get_llms_for_persona(persona) @@ -300,27 +304,27 @@ def _get_slack_answer( logger.debug(answer.answer) return True - answer.docs - # if not retrieval_info: - # This should not happen, even with no docs retrieved, there is still info returned - # raise RuntimeError("Failed to retrieve docs, cannot answer question.") - - # top_docs = retrieval_info.top_documents - # if not top_docs and not should_respond_even_with_no_docs: - # logger.error( - # f"Unable to answer question: '{user_message}' - no documents found" - # ) - # # Optionally, respond in thread with the error message - # # Used primarily for debugging purposes - # if should_respond_with_error_msgs: - # respond_in_thread( - # client=client, - # channel=channel, - # receiver_ids=None, - # text="Found no documents when trying to answer. Did you index any documents?", - # thread_ts=message_ts_to_respond_to, - # ) - # return True + retrieval_info = answer.docs + if not retrieval_info and expecting_search_result: + # This should not happen, even with no docs retrieved, there is still info returned + raise RuntimeError("Failed to retrieve docs, cannot answer question.") + + top_docs = retrieval_info.top_documents if retrieval_info else [] + if not top_docs and expecting_search_result: + logger.error( + f"Unable to answer question: '{user_message}' - no documents found" + ) + # Optionally, respond in thread with the error message + # Used primarily for debugging purposes + if should_respond_with_error_msgs: + respond_in_thread( + client=client, + channel=channel, + receiver_ids=None, + text="Found no documents when trying to answer. Did you index any documents?", + thread_ts=message_ts_to_respond_to, + ) + return True if not answer.answer and disable_docs_only_answer: logger.notice( @@ -335,7 +339,8 @@ def _get_slack_answer( ) if ( - only_respond_if_citations + expecting_search_result + and only_respond_if_citations and not answer.citations and not message_info.bypass_filters ): @@ -361,6 +366,7 @@ def _get_slack_answer( channel_conf=channel_conf, use_citations=True, # No longer supporting quotes feedback_reminder_id=feedback_reminder_id, + expecting_search_result=expecting_search_result, ) try: diff --git a/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx b/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx index e01d13e3fb4..4415d9e4cea 100644 --- a/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx +++ b/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigCreationForm.tsx @@ -120,7 +120,7 @@ export const SlackChannelConfigCreationForm = ({ knowledge_source: existingSlackBotUsesPersona ? existingPersonaHasSearchTool ? "assistant" - : "nonsearch_assistant" + : "non_search_assistant" : existingSlackChannelConfig?.persona ? "document_sets" : "all_public", @@ -167,7 +167,7 @@ export const SlackChannelConfigCreationForm = ({ "all_public", "document_sets", "assistant", - "nonsearch_assistant", + "non_search_assistant", ]) .required(), })} @@ -181,14 +181,14 @@ export const SlackChannelConfigCreationForm = ({ respond_member_group_list: values.respond_member_group_list, usePersona: values.knowledge_source === "assistant" || - values.knowledge_source === "nonsearch_assistant", + values.knowledge_source === "non_search_assistant", document_sets: values.knowledge_source === "document_sets" ? values.document_sets : [], persona_id: values.knowledge_source === "assistant" || - values.knowledge_source === "nonsearch_assistant" + values.knowledge_source === "non_search_assistant" ? values.persona_id : null, standard_answer_categories: values.standard_answer_categories.map( diff --git a/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigFormFields.tsx b/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigFormFields.tsx index a883ee022e5..aa79e73d6fe 100644 --- a/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigFormFields.tsx +++ b/web/src/app/admin/bots/[bot-id]/channels/SlackChannelConfigFormFields.tsx @@ -10,7 +10,6 @@ import { } from "formik"; import { CCPairDescriptor, DocumentSet } from "@/lib/types"; import { - BooleanFormField, Label, SelectorFormField, SubLabel, @@ -50,6 +49,8 @@ import { } from "@/components/ui/accordion"; import { Separator } from "@/components/ui/separator"; +import { CheckFormField } from "@/components/ui/CheckField"; + export interface SlackChannelConfigFormFieldsProps { isUpdate: boolean; isDefault: boolean; @@ -254,8 +255,8 @@ export function SlackChannelConfigFormFields({ sublabel="Control both the documents and the prompt to use for answering questions" /> @@ -422,7 +423,7 @@ export function SlackChannelConfigFormFields({ )} )} - {values.knowledge_source === "nonsearch_assistant" && ( + {values.knowledge_source === "non_search_assistant" && (
<> @@ -468,7 +469,7 @@ export function SlackChannelConfigFormFields({
- {values.knowledge_source !== "nonsearch_assistant" && ( + {values.knowledge_source !== "non_search_assistant" && ( Search Configuration @@ -486,15 +487,14 @@ export function SlackChannelConfigFormFields({ ]} /> - - @@ -507,16 +507,14 @@ export function SlackChannelConfigFormFields({ General Configuration
- - { setFieldValue("still_need_help_enabled", checked); if (!checked) { @@ -546,21 +544,18 @@ export function SlackChannelConfigFormFields({ )} - - - diff --git a/web/src/components/ui/CheckField.tsx b/web/src/components/ui/CheckField.tsx new file mode 100644 index 00000000000..3ccb0f8c7fd --- /dev/null +++ b/web/src/components/ui/CheckField.tsx @@ -0,0 +1,98 @@ +"use client"; + +import React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; +import { useField } from "formik"; +import { cn } from "@/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +interface CheckFieldProps { + name: string; + label: string; + sublabel?: string; + size?: "sm" | "md" | "lg"; + tooltip?: string; +} + +export const CheckFormField: React.FC = ({ + name, + label, + sublabel, + size = "md", + tooltip, + ...props +}) => { + const [field, , helpers] = useField({ name, type: "checkbox" }); + + const sizeClasses = { + sm: "h-2 w-2", + md: "h-3 w-3", + lg: "h-4 w-4", + }; + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + helpers.setValue(!field.value); + }; + + const checkboxContent = ( +
+ { + helpers.setValue(Boolean(checked)); + }} + className={cn( + "peer shrink-0 rounded-sm border border-neutral-200 bg-white ring-offset-white " + + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 " + + "focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 " + + "data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 " + + "dark:border-neutral-800 dark:ring-offset-neutral-950 dark:focus-visible:ring-neutral-300 " + + "dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900", + sizeClasses[size] + )} + {...props} + > + + + + +
+ +
+
+ ); + + return tooltip ? ( + + + {checkboxContent} + +

{tooltip}

+
+
+
+ ) : ( + checkboxContent + ); +};