Allows for Slackbots that do not have search enabled

This commit is contained in:
pablodanswer 2025-02-05 19:19:50 -08:00
parent d1e9760b92
commit e04b2d6ff3
15 changed files with 618 additions and 157 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 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)

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
) -> SlackChannelConfig | None:
# attempt to find channel-specific config first
if channel_name:
if channel_name is not None:
sc_config = db_session.scalar(
select(SlackChannelConfig).where(
SlackChannelConfig.slack_bot_id == slack_bot_id,

View File

@ -1,4 +1,5 @@
from datetime import datetime
from typing import cast
import pytz
import timeago # type: ignore
@ -338,6 +339,23 @@ def _build_citations_blocks(
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(
answer: ChatOnyxBotResponse,
) -> list[Block]:
@ -376,21 +394,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 +488,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 +499,19 @@ 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 = cast(
list[Block],
_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"):

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 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,7 +107,8 @@ def handle_regular_answer(
]
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
# llm, _ = get_llms_for_persona(persona)
@ -303,12 +305,12 @@ def handle_regular_answer(
return True
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
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:
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"
)
@ -337,7 +339,8 @@ def handle_regular_answer(
)
if (
only_respond_if_citations
expecting_search_result
and only_respond_if_citations
and not answer.citations
and not message_info.bypass_filters
):
@ -363,6 +366,7 @@ def handle_regular_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:

View File

@ -787,18 +787,6 @@ def process_message(
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(
slack_channel_config.channel_config
and slack_channel_config.channel_config.get("follow_up_tags")

View File

@ -743,7 +743,7 @@ def upload_files_for_chat(
# to re-extract it every time we send a message
if file_type == ChatFileType.DOC:
extracted_text = extract_file_text(
file=file_content_io, # use the bytes we already read
file=file_content_io, # use the bytes we already read
file_name=file.filename or "",
)
text_file_id = str(uuid.uuid4())

135
web/package-lock.json generated
View File

@ -15,6 +15,7 @@
"@headlessui/react": "^2.2.0",
"@headlessui/tailwindcss": "^0.2.1",
"@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
@ -3442,6 +3443,140 @@
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
"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": {
"version": "1.1.0",
"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/tailwindcss": "^0.2.1",
"@phosphor-icons/react": "^2.0.8",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",

View File

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

View File

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

View File

@ -10,7 +10,6 @@ import {
} from "formik";
import { CCPairDescriptor, DocumentSet } from "@/lib/types";
import {
BooleanFormField,
Label,
SelectorFormField,
SubLabel,
@ -42,18 +41,29 @@ import { fetchSlackChannels } from "../lib";
import { Badge } from "@/components/ui/badge";
import useSWR from "swr";
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 {
isUpdate: boolean;
isDefault: boolean;
documentSets: DocumentSet[];
searchEnabledAssistants: Persona[];
nonSearchAssistants: Persona[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
setPopup: (popup: {
message: string;
type: "error" | "success" | "warning";
}) => void;
slack_bot_id: number;
formikProps: any;
}
export function SlackChannelConfigFormFields({
@ -61,15 +71,15 @@ export function SlackChannelConfigFormFields({
isDefault,
documentSets,
searchEnabledAssistants,
nonSearchAssistants,
standardAnswerCategoryResponse,
setPopup,
slack_bot_id,
formikProps,
}: SlackChannelConfigFormFieldsProps) {
const router = useRouter();
const { values, setFieldValue } = useFormikContext<any>();
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [viewUnselectableSets, setViewUnselectableSets] = useState(false);
const [currentSearchTerm, setCurrentSearchTerm] = useState("");
const [viewSyncEnabledAssistants, setViewSyncEnabledAssistants] =
useState(false);
@ -178,6 +188,7 @@ export function SlackChannelConfigFormFields({
}));
}
);
if (isLoading) {
return <ThreeDotsLoader />;
}
@ -194,7 +205,7 @@ export function SlackChannelConfigFormFields({
<>
<label
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:
</label>{" "}
@ -204,11 +215,9 @@ export function SlackChannelConfigFormFields({
options={channelOptions || []}
onSelect={(selected) => {
form.setFieldValue("channel_name", selected.name);
setCurrentSearchTerm(selected.name);
}}
initialSearchTerm={field.value}
onSearchTermChange={(term) => {
setCurrentSearchTerm(term);
form.setFieldValue("channel_name", term);
}}
/>
@ -242,9 +251,15 @@ export function SlackChannelConfigFormFields({
<RadioGroupItemField
value="assistant"
id="assistant"
label="Specific Assistant"
label="Search Assistant"
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>
</div>
{values.knowledge_source === "document_sets" &&
@ -408,118 +423,165 @@ export function SlackChannelConfigFormFields({
)}
</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
name="response_type"
label="Answer Type"
tooltip="Controls the format of OnyxBot's responses."
options={[
{ name: "Standard", value: "citations" },
{ name: "Detailed", value: "quotes" },
]}
name="persona_id"
options={nonSearchAssistants.map((persona) => ({
name: persona.name,
value: persona.id,
}))}
/>
</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
name="show_continue_in_web_ui"
removeIndent
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="answer_validity_check_enabled"
label="Only respond if citations found"
tooltip="If set, will only answer questions where the model successfully produces citations"
/>
</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
name="follow_up_tags"
label="(Optional) Users / Groups to Tag"
values={values}
name="respond_member_group_list"
label="(Optional) Respond to Certain Users / Groups"
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>
"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..."
/>
</CollapsibleSection>
)}
<BooleanFormField
name="answer_validity_check_enabled"
removeIndent
label="Only respond if citations found"
tooltip="If set, will only answer questions where the model successfully produces citations"
/>
<BooleanFormField
name="questionmark_prefilter_enabled"
removeIndent
label="Only respond to questions"
tooltip="If set, OnyxBot will only respond to messages that contain a question mark"
/>
<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>
)}
<StandardAnswerCategoryDropdownField
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
categories={values.standard_answer_categories}
setCategories={(categories: any) =>
setFieldValue("standard_answer_categories", categories)
}
/>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex mt-8 gap-x-2 w-full justify-end">
{shouldShowPrivacyAlert && (

View File

@ -51,7 +51,7 @@ export function Label({
}) {
return (
<div
className={`block font-medium base ${className} ${
className={`block text-text-darker font-medium base ${className} ${
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 };