Allows for Slackbots that do not have search enabled

Allow no search
This commit is contained in:
pablonyx 2025-02-05 19:20:20 -08:00 committed by GitHub
commit 396f096dda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 617 additions and 156 deletions

View File

@ -0,0 +1,53 @@
"""delete non-search assistants
Revision ID: f5437cc136c5
Revises: eaa3b5593925
Create Date: 2025-02-04 16:17:15.677256
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "f5437cc136c5"
down_revision = "eaa3b5593925"
branch_labels = None
depends_on = None
def upgrade() -> None:
pass
def downgrade() -> None:
# Fix: split the statements into multiple op.execute() calls
op.execute(
"""
WITH personas_without_search AS (
SELECT p.id
FROM persona p
LEFT JOIN persona__tool pt ON p.id = pt.persona_id
LEFT JOIN tool t ON pt.tool_id = t.id
GROUP BY p.id
HAVING COUNT(CASE WHEN t.in_code_tool_id = 'run_search' THEN 1 END) = 0
)
UPDATE slack_channel_config
SET persona_id = NULL
WHERE is_default = TRUE AND persona_id IN (SELECT id FROM personas_without_search)
"""
)
op.execute(
"""
WITH personas_without_search AS (
SELECT p.id
FROM persona p
LEFT JOIN persona__tool pt ON p.id = pt.persona_id
LEFT JOIN tool t ON pt.tool_id = t.id
GROUP BY p.id
HAVING COUNT(CASE WHEN t.in_code_tool_id = 'run_search' THEN 1 END) = 0
)
DELETE FROM slack_channel_config
WHERE is_default = FALSE AND persona_id IN (SELECT id FROM personas_without_search)
"""
)

View File

@ -11,6 +11,7 @@ from sqlalchemy import Select
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy import update from sqlalchemy import update
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@ -708,3 +709,15 @@ def update_persona_label(
def delete_persona_label(label_id: int, db_session: Session) -> None: def delete_persona_label(label_id: int, db_session: Session) -> None:
db_session.query(PersonaLabel).filter(PersonaLabel.id == label_id).delete() db_session.query(PersonaLabel).filter(PersonaLabel.id == label_id).delete()
db_session.commit() 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)

View File

@ -256,7 +256,7 @@ def fetch_slack_channel_config_for_channel_or_default(
db_session: Session, slack_bot_id: int, channel_name: str | None db_session: Session, slack_bot_id: int, channel_name: str | None
) -> SlackChannelConfig | None: ) -> SlackChannelConfig | None:
# attempt to find channel-specific config first # attempt to find channel-specific config first
if channel_name: if channel_name is not None:
sc_config = db_session.scalar( sc_config = db_session.scalar(
select(SlackChannelConfig).where( select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id, SlackChannelConfig.slack_bot_id == slack_bot_id,

View File

@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import cast
import pytz import pytz
import timeago # type: ignore import timeago # type: ignore
@ -338,6 +339,23 @@ def _build_citations_blocks(
return citations_block return citations_block
def _build_answer_blocks(
answer: ChatOnyxBotResponse, fallback_answer: str
) -> list[SectionBlock]:
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( def _build_qa_response_blocks(
answer: ChatOnyxBotResponse, answer: ChatOnyxBotResponse,
) -> list[Block]: ) -> list[Block]:
@ -376,21 +394,10 @@ def _build_qa_response_blocks(
filter_block = SectionBlock(text=f"_{filter_text}_") filter_block = SectionBlock(text=f"_{filter_text}_")
if not answer.answer: answer_blocks = _build_answer_blocks(
answer_blocks = [ answer=answer,
SectionBlock( fallback_answer="Sorry, I was unable to find an answer, but I did find some potentially relevant docs 🤓",
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)
]
response_blocks: list[Block] = [] response_blocks: list[Block] = []
@ -481,6 +488,7 @@ def build_slack_response_blocks(
use_citations: bool, use_citations: bool,
feedback_reminder_id: str | None, feedback_reminder_id: str | None,
skip_ai_feedback: bool = False, skip_ai_feedback: bool = False,
expecting_search_result: bool = False,
) -> list[Block]: ) -> list[Block]:
""" """
This function is a top level function that builds all the blocks for the Slack response. This function is a top level function that builds all the blocks for the Slack response.
@ -491,9 +499,19 @@ def build_slack_response_blocks(
message_info.thread_messages[-1].message, message_info.is_bot_msg message_info.thread_messages[-1].message, message_info.is_bot_msg
) )
answer_blocks = _build_qa_response_blocks( if expecting_search_result:
answer=answer, answer_blocks = _build_qa_response_blocks(
) answer=answer,
)
else:
answer_blocks = cast(
list[Block],
_build_answer_blocks(
answer=answer,
fallback_answer="Sorry, I was unable to generate an answer.",
),
)
web_follow_up_block = [] web_follow_up_block = []
if channel_conf and channel_conf.get("show_continue_in_web_ui"): if channel_conf and channel_conf.get("show_continue_in_web_ui"):

View File

@ -27,6 +27,7 @@ from onyx.db.engine import get_session_with_tenant
from onyx.db.models import SlackChannelConfig from onyx.db.models import SlackChannelConfig
from onyx.db.models import User from onyx.db.models import User
from onyx.db.persona import get_persona_by_id 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.db.users import get_user_by_email
from onyx.onyxbot.slack.blocks import build_slack_response_blocks from onyx.onyxbot.slack.blocks import build_slack_response_blocks
from onyx.onyxbot.slack.handlers.utils import send_team_member_message from onyx.onyxbot.slack.handlers.utils import send_team_member_message
@ -106,7 +107,8 @@ def handle_regular_answer(
] ]
prompt = persona.prompts[0] if persona.prompts else None prompt = persona.prompts[0] if persona.prompts else None
should_respond_even_with_no_docs = persona.num_chunks == 0 if persona else False 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 # TODO: Add in support for Slack to truncate messages based on max LLM context
# llm, _ = get_llms_for_persona(persona) # llm, _ = get_llms_for_persona(persona)
@ -303,12 +305,12 @@ def handle_regular_answer(
return True return True
retrieval_info = answer.docs retrieval_info = answer.docs
if not retrieval_info: if not retrieval_info and expecting_search_result:
# This should not happen, even with no docs retrieved, there is still info returned # This should not happen, even with no docs retrieved, there is still info returned
raise RuntimeError("Failed to retrieve docs, cannot answer question.") raise RuntimeError("Failed to retrieve docs, cannot answer question.")
top_docs = retrieval_info.top_documents top_docs = retrieval_info.top_documents if retrieval_info else []
if not top_docs and not should_respond_even_with_no_docs: if not top_docs and expecting_search_result:
logger.error( logger.error(
f"Unable to answer question: '{user_message}' - no documents found" f"Unable to answer question: '{user_message}' - no documents found"
) )
@ -337,7 +339,8 @@ def handle_regular_answer(
) )
if ( if (
only_respond_if_citations expecting_search_result
and only_respond_if_citations
and not answer.citations and not answer.citations
and not message_info.bypass_filters and not message_info.bypass_filters
): ):
@ -363,6 +366,7 @@ def handle_regular_answer(
channel_conf=channel_conf, channel_conf=channel_conf,
use_citations=True, # No longer supporting quotes use_citations=True, # No longer supporting quotes
feedback_reminder_id=feedback_reminder_id, feedback_reminder_id=feedback_reminder_id,
expecting_search_result=expecting_search_result,
) )
try: try:

View File

@ -801,18 +801,6 @@ def process_message(
channel_name=channel_name, channel_name=channel_name,
) )
# Be careful about this default, don't want to accidentally spam every channel
# Users should be able to DM slack bot in their private channels though
if (
not respond_every_channel
# Can't have configs for DMs so don't toss them out
and not is_dm
# If /OnyxBot (is_bot_msg) or @OnyxBot (bypass_filters)
# always respond with the default configs
and not (details.is_bot_msg or details.bypass_filters)
):
return
follow_up = bool( follow_up = bool(
slack_channel_config.channel_config slack_channel_config.channel_config
and slack_channel_config.channel_config.get("follow_up_tags") and slack_channel_config.channel_config.get("follow_up_tags")

135
web/package-lock.json generated
View File

@ -15,6 +15,7 @@
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.1", "@headlessui/tailwindcss": "^0.2.1",
"@phosphor-icons/react": "^2.0.8", "@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2",
@ -3442,6 +3443,140 @@
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@radix-ui/react-accordion": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz",
"integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-collection": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-id": "1.1.0",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
"integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": { "node_modules/@radix-ui/react-arrow": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",

View File

@ -18,6 +18,7 @@
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.1", "@headlessui/tailwindcss": "^0.2.1",
"@phosphor-icons/react": "^2.0.8", "@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2",

View File

@ -49,7 +49,7 @@ export function SlackChannelConfigsTable({
}} }}
> >
<FiSettings /> <FiSettings />
Edit Default Config Edit Default Configuration
</Button> </Button>
<Link href={`/admin/bots/${slackBotId}/channels/new`}> <Link href={`/admin/bots/${slackBotId}/channels/new`}>
<Button variant="outline"> <Button variant="outline">

View File

@ -45,13 +45,26 @@ export const SlackChannelConfigCreationForm = ({
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona) ? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
: false; : false;
const existingPersonaHasSearchTool = existingSlackChannelConfig?.persona
? existingSlackChannelConfig.persona.tools.some(
(tool) => tool.in_code_tool_id === SEARCH_TOOL_ID
)
: false;
const searchEnabledAssistants = useMemo(() => { const [searchEnabledAssistants, nonSearchAssistants] = useMemo(() => {
return personas.filter((persona) => { return personas.reduce(
return persona.tools.some( (acc, persona) => {
(tool) => tool.in_code_tool_id == SEARCH_TOOL_ID if (
); persona.tools.some((tool) => tool.in_code_tool_id === SEARCH_TOOL_ID)
}); ) {
acc[0].push(persona);
} else {
acc[1].push(persona);
}
return acc;
},
[[], []] as [Persona[], Persona[]]
);
}, [personas]); }, [personas]);
return ( return (
@ -105,7 +118,9 @@ export const SlackChannelConfigCreationForm = ({
standard_answer_categories: standard_answer_categories:
existingSlackChannelConfig?.standard_answer_categories || [], existingSlackChannelConfig?.standard_answer_categories || [],
knowledge_source: existingSlackBotUsesPersona knowledge_source: existingSlackBotUsesPersona
? "assistant" ? existingPersonaHasSearchTool
? "assistant"
: "non_search_assistant"
: existingSlackChannelConfig?.persona : existingSlackChannelConfig?.persona
? "document_sets" ? "document_sets"
: "all_public", : "all_public",
@ -148,7 +163,12 @@ export const SlackChannelConfigCreationForm = ({
}), }),
standard_answer_categories: Yup.array(), standard_answer_categories: Yup.array(),
knowledge_source: Yup.string() knowledge_source: Yup.string()
.oneOf(["all_public", "document_sets", "assistant"]) .oneOf([
"all_public",
"document_sets",
"assistant",
"non_search_assistant",
])
.required(), .required(),
})} })}
onSubmit={async (values, formikHelpers) => { onSubmit={async (values, formikHelpers) => {
@ -159,13 +179,16 @@ export const SlackChannelConfigCreationForm = ({
slack_bot_id, slack_bot_id,
channel_name: values.channel_name, channel_name: values.channel_name,
respond_member_group_list: values.respond_member_group_list, respond_member_group_list: values.respond_member_group_list,
usePersona: values.knowledge_source === "assistant", usePersona:
values.knowledge_source === "assistant" ||
values.knowledge_source === "non_search_assistant",
document_sets: document_sets:
values.knowledge_source === "document_sets" values.knowledge_source === "document_sets"
? values.document_sets ? values.document_sets
: [], : [],
persona_id: persona_id:
values.knowledge_source === "assistant" values.knowledge_source === "assistant" ||
values.knowledge_source === "non_search_assistant"
? values.persona_id ? values.persona_id
: null, : null,
standard_answer_categories: values.standard_answer_categories.map( standard_answer_categories: values.standard_answer_categories.map(
@ -204,7 +227,7 @@ export const SlackChannelConfigCreationForm = ({
} }
}} }}
> >
{({ isSubmitting, values, setFieldValue }) => ( {({ isSubmitting, values, setFieldValue, ...formikProps }) => (
<Form> <Form>
<div className="pb-6 w-full"> <div className="pb-6 w-full">
<SlackChannelConfigFormFields <SlackChannelConfigFormFields
@ -213,9 +236,11 @@ export const SlackChannelConfigCreationForm = ({
isDefault={isDefault} isDefault={isDefault}
documentSets={documentSets} documentSets={documentSets}
searchEnabledAssistants={searchEnabledAssistants} searchEnabledAssistants={searchEnabledAssistants}
nonSearchAssistants={nonSearchAssistants}
standardAnswerCategoryResponse={standardAnswerCategoryResponse} standardAnswerCategoryResponse={standardAnswerCategoryResponse}
setPopup={setPopup} setPopup={setPopup}
slack_bot_id={slack_bot_id} slack_bot_id={slack_bot_id}
formikProps={formikProps}
/> />
</div> </div>
</Form> </Form>

View File

@ -10,7 +10,6 @@ import {
} from "formik"; } from "formik";
import { CCPairDescriptor, DocumentSet } from "@/lib/types"; import { CCPairDescriptor, DocumentSet } from "@/lib/types";
import { import {
BooleanFormField,
Label, Label,
SelectorFormField, SelectorFormField,
SubLabel, SubLabel,
@ -42,18 +41,29 @@ import { fetchSlackChannels } from "../lib";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import useSWR from "swr"; import useSWR from "swr";
import { ThreeDotsLoader } from "@/components/Loading"; import { ThreeDotsLoader } from "@/components/Loading";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Separator } from "@/components/ui/separator";
import { CheckFormField } from "@/components/ui/CheckField";
export interface SlackChannelConfigFormFieldsProps { export interface SlackChannelConfigFormFieldsProps {
isUpdate: boolean; isUpdate: boolean;
isDefault: boolean; isDefault: boolean;
documentSets: DocumentSet[]; documentSets: DocumentSet[];
searchEnabledAssistants: Persona[]; searchEnabledAssistants: Persona[];
nonSearchAssistants: Persona[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse; standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
setPopup: (popup: { setPopup: (popup: {
message: string; message: string;
type: "error" | "success" | "warning"; type: "error" | "success" | "warning";
}) => void; }) => void;
slack_bot_id: number; slack_bot_id: number;
formikProps: any;
} }
export function SlackChannelConfigFormFields({ export function SlackChannelConfigFormFields({
@ -61,15 +71,15 @@ export function SlackChannelConfigFormFields({
isDefault, isDefault,
documentSets, documentSets,
searchEnabledAssistants, searchEnabledAssistants,
nonSearchAssistants,
standardAnswerCategoryResponse, standardAnswerCategoryResponse,
setPopup, setPopup,
slack_bot_id, slack_bot_id,
formikProps,
}: SlackChannelConfigFormFieldsProps) { }: SlackChannelConfigFormFieldsProps) {
const router = useRouter(); const router = useRouter();
const { values, setFieldValue } = useFormikContext<any>(); const { values, setFieldValue } = useFormikContext<any>();
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [viewUnselectableSets, setViewUnselectableSets] = useState(false); const [viewUnselectableSets, setViewUnselectableSets] = useState(false);
const [currentSearchTerm, setCurrentSearchTerm] = useState("");
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] = const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
useState(false); useState(false);
@ -178,6 +188,7 @@ export function SlackChannelConfigFormFields({
})); }));
} }
); );
if (isLoading) { if (isLoading) {
return <ThreeDotsLoader />; return <ThreeDotsLoader />;
} }
@ -194,7 +205,7 @@ export function SlackChannelConfigFormFields({
<> <>
<label <label
htmlFor="channel_name" htmlFor="channel_name"
className="block font-medium text-base mb-2" className="block text-text font-medium text-base mb-2"
> >
Select A Slack Channel: Select A Slack Channel:
</label>{" "} </label>{" "}
@ -204,11 +215,9 @@ export function SlackChannelConfigFormFields({
options={channelOptions || []} options={channelOptions || []}
onSelect={(selected) => { onSelect={(selected) => {
form.setFieldValue("channel_name", selected.name); form.setFieldValue("channel_name", selected.name);
setCurrentSearchTerm(selected.name);
}} }}
initialSearchTerm={field.value} initialSearchTerm={field.value}
onSearchTermChange={(term) => { onSearchTermChange={(term) => {
setCurrentSearchTerm(term);
form.setFieldValue("channel_name", term); form.setFieldValue("channel_name", term);
}} }}
/> />
@ -242,9 +251,15 @@ export function SlackChannelConfigFormFields({
<RadioGroupItemField <RadioGroupItemField
value="assistant" value="assistant"
id="assistant" id="assistant"
label="Specific Assistant" label="Search Assistant"
sublabel="Control both the documents and the prompt to use for answering questions" sublabel="Control both the documents and the prompt to use for answering questions"
/> />
<RadioGroupItemField
value="non_search_assistant"
id="non_search_assistant"
label="Non-Search Assistant"
sublabel="Chat with an assistant that does not use documents"
/>
</RadioGroup> </RadioGroup>
</div> </div>
{values.knowledge_source === "document_sets" && {values.knowledge_source === "document_sets" &&
@ -408,118 +423,165 @@ export function SlackChannelConfigFormFields({
)} )}
</div> </div>
)} )}
</div> {values.knowledge_source === "non_search_assistant" && (
<div className="mt-4">
<SubLabel>
<>
Select the non-search assistant OnyxBot will use while answering
questions in Slack.
{syncEnabledAssistants.length > 0 && (
<>
<br />
<span className="text-sm text-text-dark/80">
Note: Some of your assistants have auto-synced connectors
in their document sets. You cannot select these assistants
as they will not be able to answer questions in Slack.{" "}
<button
type="button"
onClick={() =>
setViewSyncEnabledAssistants(
(viewSyncEnabledAssistants) =>
!viewSyncEnabledAssistants
)
}
className="text-sm text-link"
>
{viewSyncEnabledAssistants
? "Hide un-selectable "
: "View all "}
assistants
</button>
</span>
</>
)}
</>
</SubLabel>
<div className="mt-6">
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}
/>
</div>
{showAdvancedOptions && (
<div className="mt-2 space-y-4">
<div className="w-64">
<SelectorFormField <SelectorFormField
name="response_type" name="persona_id"
label="Answer Type" options={nonSearchAssistants.map((persona) => ({
tooltip="Controls the format of OnyxBot's responses." name: persona.name,
options={[ value: persona.id,
{ name: "Standard", value: "citations" }, }))}
{ name: "Detailed", value: "quotes" },
]}
/> />
</div> </div>
)}
</div>
<Separator className="my-4" />
<Accordion type="multiple" className=" gap-y-2 w-full">
{values.knowledge_source !== "non_search_assistant" && (
<AccordionItem value="search-options">
<AccordionTrigger className="text-text">
Search Configuration
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<div className="w-64">
<SelectorFormField
name="response_type"
label="Answer Type"
tooltip="Controls the format of OnyxBot's responses."
options={[
{ name: "Standard", value: "citations" },
{ name: "Detailed", value: "quotes" },
]}
/>
</div>
<CheckFormField
name="enable_auto_filters"
label="Enable LLM Autofiltering"
tooltip="If set, the LLM will generate source and time filters based on the user's query"
/>
<BooleanFormField <CheckFormField
name="show_continue_in_web_ui" name="answer_validity_check_enabled"
removeIndent label="Only respond if citations found"
label="Show Continue in Web UI button" tooltip="If set, will only answer questions where the model successfully produces citations"
tooltip="If set, will show a button at the bottom of the response that allows the user to continue the conversation in the Onyx Web UI" />
/> </div>
</AccordionContent>
</AccordionItem>
)}
<AccordionItem className="mt-4" value="general-options">
<AccordionTrigger>General Configuration</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<CheckFormField
name="show_continue_in_web_ui"
label="Show Continue in Web UI button"
tooltip="If set, will show a button at the bottom of the response that allows the user to continue the conversation in the Onyx Web UI"
/>
<CheckFormField
name="still_need_help_enabled"
onChange={(checked: boolean) => {
setFieldValue("still_need_help_enabled", checked);
if (!checked) {
setFieldValue("follow_up_tags", []);
}
}}
label={'Give a "Still need help?" button'}
tooltip={`OnyxBot's response will include a button at the bottom
of the response that asks the user if they still need help.`}
/>
{values.still_need_help_enabled && (
<CollapsibleSection prompt="Configure Still Need Help Button">
<TextArrayField
name="follow_up_tags"
label="(Optional) Users / Groups to Tag"
values={values}
subtext={
<div>
The Slack users / groups we should tag if the user
clicks the &quot;Still need help?&quot; button. If no
emails are provided, we will not tag anyone and will
just react with a 🆘 emoji to the original message.
</div>
}
placeholder="User email or user group name..."
/>
</CollapsibleSection>
)}
<CheckFormField
name="questionmark_prefilter_enabled"
label="Only respond to questions"
tooltip="If set, OnyxBot will only respond to messages that contain a question mark"
/>
<CheckFormField
name="respond_tag_only"
label="Respond to @OnyxBot Only"
tooltip="If set, OnyxBot will only respond when directly tagged"
/>
<CheckFormField
name="respond_to_bots"
label="Respond to Bot messages"
tooltip="If not set, OnyxBot will always ignore messages from Bots"
/>
<BooleanFormField
name="still_need_help_enabled"
removeIndent
onChange={(checked: boolean) => {
setFieldValue("still_need_help_enabled", checked);
if (!checked) {
setFieldValue("follow_up_tags", []);
}
}}
label={'Give a "Still need help?" button'}
tooltip={`OnyxBot's response will include a button at the bottom
of the response that asks the user if they still need help.`}
/>
{values.still_need_help_enabled && (
<CollapsibleSection prompt="Configure Still Need Help Button">
<TextArrayField <TextArrayField
name="follow_up_tags" name="respond_member_group_list"
label="(Optional) Users / Groups to Tag" label="(Optional) Respond to Certain Users / Groups"
values={values}
subtext={ subtext={
<div> "If specified, OnyxBot responses will only " +
The Slack users / groups we should tag if the user clicks "be visible to the members or groups in this list."
the &quot;Still need help?&quot; button. If no emails are
provided, we will not tag anyone and will just react with a
🆘 emoji to the original message.
</div>
} }
values={values}
placeholder="User email or user group name..." placeholder="User email or user group name..."
/> />
</CollapsibleSection>
)}
<BooleanFormField <StandardAnswerCategoryDropdownField
name="answer_validity_check_enabled" standardAnswerCategoryResponse={standardAnswerCategoryResponse}
removeIndent categories={values.standard_answer_categories}
label="Only respond if citations found" setCategories={(categories: any) =>
tooltip="If set, will only answer questions where the model successfully produces citations" setFieldValue("standard_answer_categories", categories)
/> }
<BooleanFormField />
name="questionmark_prefilter_enabled" </div>
removeIndent </AccordionContent>
label="Only respond to questions" </AccordionItem>
tooltip="If set, OnyxBot will only respond to messages that contain a question mark" </Accordion>
/>
<BooleanFormField
name="respond_tag_only"
removeIndent
label="Respond to @OnyxBot Only"
tooltip="If set, OnyxBot will only respond when directly tagged"
/>
<BooleanFormField
name="respond_to_bots"
removeIndent
label="Respond to Bot messages"
tooltip="If not set, OnyxBot will always ignore messages from Bots"
/>
<BooleanFormField
name="enable_auto_filters"
removeIndent
label="Enable LLM Autofiltering"
tooltip="If set, the LLM will generate source and time filters based on the user's query"
/>
<TextArrayField
name="respond_member_group_list"
label="(Optional) Respond to Certain Users / Groups"
subtext={
"If specified, OnyxBot responses will only " +
"be visible to the members or groups in this list."
}
values={values}
placeholder="User email or user group name..."
/>
<StandardAnswerCategoryDropdownField
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
categories={values.standard_answer_categories}
setCategories={(categories: any) =>
setFieldValue("standard_answer_categories", categories)
}
/>
</div>
)}
<div className="flex mt-8 gap-x-2 w-full justify-end"> <div className="flex mt-8 gap-x-2 w-full justify-end">
{shouldShowPrivacyAlert && ( {shouldShowPrivacyAlert && (

View File

@ -51,7 +51,7 @@ export function Label({
}) { }) {
return ( return (
<div <div
className={`block font-medium base ${className} ${ className={`block text-text-darker font-medium base ${className} ${
small ? "text-xs" : "text-sm" small ? "text-xs" : "text-sm"
}`} }`}
> >

View File

@ -0,0 +1,102 @@
"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;
onChange?: (checked: boolean) => void;
}
export const CheckFormField: React.FC<CheckFieldProps> = ({
name,
label,
onChange,
sublabel,
size = "md",
tooltip,
...props
}) => {
const [field, , helpers] = useField<boolean>({ 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<HTMLLabelElement>) => {
e.preventDefault();
helpers.setValue(!field.value);
onChange?.(field.value);
};
const checkboxContent = (
<div className="flex w-fit items-start space-x-2">
<CheckboxPrimitive.Root
id={name}
checked={field.value}
onCheckedChange={(checked) => {
helpers.setValue(Boolean(checked));
onChange?.(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}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
<Check className={sizeClasses[size]} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
<div className="flex flex-col">
<label
htmlFor={name}
className="flex flex-col cursor-pointer"
onClick={handleClick}
>
<span className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{label}
</span>
{sublabel && (
<span className="text-sm text-muted-foreground mt-1">
{sublabel}
</span>
)}
</label>
</div>
</div>
);
return tooltip ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>{checkboxContent}</TooltipTrigger>
<TooltipContent className="mb-4" side="top" align="center">
<p>{tooltip}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
checkboxContent
);
};

View File

@ -0,0 +1,60 @@
"use client";
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 text-base items-center text-text justify-between pb-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = "AccordionTrigger";
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className
)}
{...props}
>
<div className="pb-4 pt-0">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = "AccordionContent";
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };