mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-21 14:12:42 +02:00
Persona enhancements
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { DocumentSet } from "@/lib/types";
|
||||
import { Button, Divider, Text } from "@tremor/react";
|
||||
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
|
||||
import { ArrayHelpers, ErrorMessage, FieldArray, Form, Formik } from "formik";
|
||||
|
||||
import * as Yup from "yup";
|
||||
import { buildFinalPrompt, createPersona, updatePersona } from "./lib";
|
||||
@@ -13,6 +13,7 @@ import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
BooleanFormField,
|
||||
ManualErrorMessage,
|
||||
SelectorFormField,
|
||||
TextFormField,
|
||||
} from "@/components/admin/connectors/Field";
|
||||
@@ -46,12 +47,18 @@ export function PersonaEditor({
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const [finalPrompt, setFinalPrompt] = useState<string | null>("");
|
||||
const [finalPromptError, setFinalPromptError] = useState<string>("");
|
||||
|
||||
const triggerFinalPromptUpdate = async (
|
||||
systemPrompt: string,
|
||||
taskPrompt: string
|
||||
taskPrompt: string,
|
||||
retrievalDisabled: boolean
|
||||
) => {
|
||||
const response = await buildFinalPrompt(systemPrompt, taskPrompt);
|
||||
const response = await buildFinalPrompt(
|
||||
systemPrompt,
|
||||
taskPrompt,
|
||||
retrievalDisabled
|
||||
);
|
||||
if (response.ok) {
|
||||
setFinalPrompt((await response.json()).final_prompt_template);
|
||||
}
|
||||
@@ -63,7 +70,8 @@ export function PersonaEditor({
|
||||
if (isUpdate) {
|
||||
triggerFinalPromptUpdate(
|
||||
existingPersona.system_prompt,
|
||||
existingPersona.task_prompt
|
||||
existingPersona.task_prompt,
|
||||
existingPersona.num_chunks === 0
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
@@ -78,6 +86,7 @@ export function PersonaEditor({
|
||||
description: existingPersona?.description ?? "",
|
||||
system_prompt: existingPersona?.system_prompt ?? "",
|
||||
task_prompt: existingPersona?.task_prompt ?? "",
|
||||
disable_retrieval: (existingPersona?.num_chunks ?? 5) === 0,
|
||||
document_set_ids:
|
||||
existingPersona?.document_sets?.map(
|
||||
(documentSet) => documentSet.id
|
||||
@@ -88,36 +97,68 @@ export function PersonaEditor({
|
||||
llm_model_version_override:
|
||||
existingPersona?.llm_model_version_override ?? null,
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
name: Yup.string().required("Must give the Persona a name!"),
|
||||
description: Yup.string().required(
|
||||
"Must give the Persona a description!"
|
||||
),
|
||||
system_prompt: Yup.string().required(
|
||||
"Must give the Persona a system prompt!"
|
||||
),
|
||||
task_prompt: Yup.string().required(
|
||||
"Must give the Persona a task prompt!"
|
||||
),
|
||||
document_set_ids: Yup.array().of(Yup.number()),
|
||||
num_chunks: Yup.number().max(20).nullable(),
|
||||
apply_llm_relevance_filter: Yup.boolean().required(),
|
||||
llm_model_version_override: Yup.string().nullable(),
|
||||
})}
|
||||
validationSchema={Yup.object()
|
||||
.shape({
|
||||
name: Yup.string().required("Must give the Persona a name!"),
|
||||
description: Yup.string().required(
|
||||
"Must give the Persona a description!"
|
||||
),
|
||||
system_prompt: Yup.string(),
|
||||
task_prompt: Yup.string(),
|
||||
disable_retrieval: Yup.boolean().required(),
|
||||
document_set_ids: Yup.array().of(Yup.number()),
|
||||
num_chunks: Yup.number().max(20).nullable(),
|
||||
apply_llm_relevance_filter: Yup.boolean().required(),
|
||||
llm_model_version_override: Yup.string().nullable(),
|
||||
})
|
||||
.test(
|
||||
"system-prompt-or-task-prompt",
|
||||
"Must provide at least one of System Prompt or Task Prompt",
|
||||
(values) => {
|
||||
const systemPromptSpecified = values.system_prompt
|
||||
? values.system_prompt.length > 0
|
||||
: false;
|
||||
const taskPromptSpecified = values.task_prompt
|
||||
? values.task_prompt.length > 0
|
||||
: false;
|
||||
if (systemPromptSpecified || taskPromptSpecified) {
|
||||
setFinalPromptError("");
|
||||
return true;
|
||||
} // Return true if at least one field has a value
|
||||
|
||||
setFinalPromptError(
|
||||
"Must provide at least one of System Prompt or Task Prompt"
|
||||
);
|
||||
}
|
||||
)}
|
||||
onSubmit={async (values, formikHelpers) => {
|
||||
if (finalPromptError) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: "Cannot submit while there are errors in the form!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
formikHelpers.setSubmitting(true);
|
||||
|
||||
// if disable_retrieval is set, set num_chunks to 0
|
||||
// to tell the backend to not fetch any documents
|
||||
const numChunks = values.disable_retrieval
|
||||
? 0
|
||||
: values.num_chunks || 5;
|
||||
|
||||
let response;
|
||||
if (isUpdate) {
|
||||
response = await updatePersona({
|
||||
id: existingPersona.id,
|
||||
...values,
|
||||
num_chunks: values.num_chunks || null,
|
||||
num_chunks: numChunks,
|
||||
});
|
||||
} else {
|
||||
response = await createPersona({
|
||||
...values,
|
||||
num_chunks: values.num_chunks || null,
|
||||
num_chunks: numChunks,
|
||||
});
|
||||
}
|
||||
if (response.ok) {
|
||||
@@ -163,8 +204,13 @@ export function PersonaEditor({
|
||||
}
|
||||
onChange={(e) => {
|
||||
setFieldValue("system_prompt", e.target.value);
|
||||
triggerFinalPromptUpdate(e.target.value, values.task_prompt);
|
||||
triggerFinalPromptUpdate(
|
||||
e.target.value,
|
||||
values.task_prompt,
|
||||
values.disable_retrieval
|
||||
);
|
||||
}}
|
||||
error={finalPromptError}
|
||||
/>
|
||||
|
||||
<TextFormField
|
||||
@@ -178,7 +224,26 @@ export function PersonaEditor({
|
||||
setFieldValue("task_prompt", e.target.value);
|
||||
triggerFinalPromptUpdate(
|
||||
values.system_prompt,
|
||||
e.target.value
|
||||
e.target.value,
|
||||
values.disable_retrieval
|
||||
);
|
||||
}}
|
||||
error={finalPromptError}
|
||||
/>
|
||||
|
||||
<BooleanFormField
|
||||
name="disable_retrieval"
|
||||
label="Disable Retrieval"
|
||||
subtext={`
|
||||
If set, the Persona will not fetch any context documents to aid in the response.
|
||||
Instead, it will only use the supplied system and task prompts plus the user
|
||||
query in order to generate a response`}
|
||||
onChange={(e) => {
|
||||
setFieldValue("disable_retrieval", e.target.checked);
|
||||
triggerFinalPromptUpdate(
|
||||
values.system_prompt,
|
||||
values.task_prompt,
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -195,41 +260,45 @@ export function PersonaEditor({
|
||||
|
||||
<Divider />
|
||||
|
||||
<SectionHeader>What data should I have access to?</SectionHeader>
|
||||
{!values.disable_retrieval && (
|
||||
<>
|
||||
<SectionHeader>
|
||||
What data should I have access to?
|
||||
</SectionHeader>
|
||||
|
||||
<FieldArray
|
||||
name="document_set_ids"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
<div>
|
||||
<div>
|
||||
<SubLabel>
|
||||
<>
|
||||
Select which{" "}
|
||||
<Link
|
||||
href="/admin/documents/sets"
|
||||
className="text-blue-500"
|
||||
target="_blank"
|
||||
>
|
||||
Document Sets
|
||||
</Link>{" "}
|
||||
that this Persona should search through. If none are
|
||||
specified, the Persona will search through all
|
||||
available documents in order to try and response to
|
||||
queries.
|
||||
</>
|
||||
</SubLabel>
|
||||
</div>
|
||||
<div className="mb-3 mt-2 flex gap-2 flex-wrap text-sm">
|
||||
{documentSets.map((documentSet) => {
|
||||
const ind = values.document_set_ids.indexOf(
|
||||
documentSet.id
|
||||
);
|
||||
let isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={documentSet.id}
|
||||
className={
|
||||
`
|
||||
<FieldArray
|
||||
name="document_set_ids"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
<div>
|
||||
<div>
|
||||
<SubLabel>
|
||||
<>
|
||||
Select which{" "}
|
||||
<Link
|
||||
href="/admin/documents/sets"
|
||||
className="text-blue-500"
|
||||
target="_blank"
|
||||
>
|
||||
Document Sets
|
||||
</Link>{" "}
|
||||
that this Persona should search through. If none
|
||||
are specified, the Persona will search through all
|
||||
available documents in order to try and response
|
||||
to queries.
|
||||
</>
|
||||
</SubLabel>
|
||||
</div>
|
||||
<div className="mb-3 mt-2 flex gap-2 flex-wrap text-sm">
|
||||
{documentSets.map((documentSet) => {
|
||||
const ind = values.document_set_ids.indexOf(
|
||||
documentSet.id
|
||||
);
|
||||
let isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={documentSet.id}
|
||||
className={
|
||||
`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
@@ -238,28 +307,32 @@ export function PersonaEditor({
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-gray-600"
|
||||
: " bg-gray-900 hover:bg-gray-700")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(documentSet.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">{documentSet.name}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
(isSelected
|
||||
? " bg-gray-600"
|
||||
: " bg-gray-900 hover:bg-gray-700")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(documentSet.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
{documentSet.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{llmOverrideOptions.length > 0 && defaultLLM && (
|
||||
<>
|
||||
@@ -296,41 +369,48 @@ export function PersonaEditor({
|
||||
|
||||
<Divider />
|
||||
|
||||
<SectionHeader>[Advanced] Retrieval Customization</SectionHeader>
|
||||
{!values.disable_retrieval && (
|
||||
<>
|
||||
<SectionHeader>
|
||||
[Advanced] Retrieval Customization
|
||||
</SectionHeader>
|
||||
|
||||
<TextFormField
|
||||
name="num_chunks"
|
||||
label="Number of Chunks"
|
||||
subtext={
|
||||
<div>
|
||||
How many chunks should we feed into the LLM when generating
|
||||
the final response? Each chunk is ~400 words long. If you
|
||||
are using gpt-3.5-turbo or other similar models, setting
|
||||
this to a value greater than 5 will result in errors at
|
||||
query time due to the model's input length limit.
|
||||
<br />
|
||||
<br />
|
||||
If unspecified, will use 5 chunks.
|
||||
</div>
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Allow only integer values
|
||||
if (value === "" || /^[0-9]+$/.test(value)) {
|
||||
setFieldValue("num_chunks", value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TextFormField
|
||||
name="num_chunks"
|
||||
label="Number of Chunks"
|
||||
subtext={
|
||||
<div>
|
||||
How many chunks should we feed into the LLM when
|
||||
generating the final response? Each chunk is ~400 words
|
||||
long. If you are using gpt-3.5-turbo or other similar
|
||||
models, setting this to a value greater than 5 will
|
||||
result in errors at query time due to the model's
|
||||
input length limit.
|
||||
<br />
|
||||
<br />
|
||||
If unspecified, will use 5 chunks.
|
||||
</div>
|
||||
}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Allow only integer values
|
||||
if (value === "" || /^[0-9]+$/.test(value)) {
|
||||
setFieldValue("num_chunks", value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<BooleanFormField
|
||||
name="apply_llm_relevance_filter"
|
||||
label="Apply LLM Relevance Filter"
|
||||
subtext={
|
||||
"If enabled, the LLM will filter out chunks that are not relevant to the user query."
|
||||
}
|
||||
/>
|
||||
<BooleanFormField
|
||||
name="apply_llm_relevance_filter"
|
||||
label="Apply LLM Relevance Filter"
|
||||
subtext={
|
||||
"If enabled, the LLM will filter out chunks that are not relevant to the user query."
|
||||
}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
<Button
|
||||
|
@@ -46,10 +46,15 @@ export function deletePersona(personaId: number) {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildFinalPrompt(systemPrompt: string, taskPrompt: string) {
|
||||
export function buildFinalPrompt(
|
||||
systemPrompt: string,
|
||||
taskPrompt: string,
|
||||
retrievalDisabled: boolean
|
||||
) {
|
||||
let queryString = Object.entries({
|
||||
system_prompt: systemPrompt,
|
||||
task_prompt: taskPrompt,
|
||||
retrieval_disabled: retrievalDisabled,
|
||||
})
|
||||
.map(
|
||||
([key, value]) =>
|
||||
|
@@ -30,6 +30,10 @@ export function SubLabel({ children }: { children: string | JSX.Element }) {
|
||||
return <div className="text-sm text-gray-300 mb-2">{children}</div>;
|
||||
}
|
||||
|
||||
export function ManualErrorMessage({ children }: { children: string }) {
|
||||
return <div className="text-red-500 text-sm mt-1">{children}</div>;
|
||||
}
|
||||
|
||||
export function TextFormField({
|
||||
name,
|
||||
label,
|
||||
@@ -40,6 +44,7 @@ export function TextFormField({
|
||||
isTextArea = false,
|
||||
disabled = false,
|
||||
autoCompleteDisabled = true,
|
||||
error,
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
@@ -50,6 +55,7 @@ export function TextFormField({
|
||||
isTextArea?: boolean;
|
||||
disabled?: boolean;
|
||||
autoCompleteDisabled?: boolean;
|
||||
error?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
@@ -78,11 +84,15 @@ export function TextFormField({
|
||||
autoComplete={autoCompleteDisabled ? "off" : undefined}
|
||||
{...(onChange ? { onChange } : {})}
|
||||
/>
|
||||
<ErrorMessage
|
||||
name={name}
|
||||
component="div"
|
||||
className="text-red-500 text-sm mt-1"
|
||||
/>
|
||||
{error ? (
|
||||
<ManualErrorMessage>{error}</ManualErrorMessage>
|
||||
) : (
|
||||
<ErrorMessage
|
||||
name={name}
|
||||
component="div"
|
||||
className="text-red-500 text-sm mt-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -91,12 +101,14 @@ interface BooleanFormFieldProps {
|
||||
name: string;
|
||||
label: string;
|
||||
subtext?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export const BooleanFormField = ({
|
||||
name,
|
||||
label,
|
||||
subtext,
|
||||
onChange,
|
||||
}: BooleanFormFieldProps) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
@@ -105,6 +117,7 @@ export const BooleanFormField = ({
|
||||
name={name}
|
||||
type="checkbox"
|
||||
className="mx-3 px-5 w-3.5 h-3.5 my-auto"
|
||||
{...(onChange ? { onChange } : {})}
|
||||
/>
|
||||
<div>
|
||||
<Label>{label}</Label>
|
||||
|
@@ -86,7 +86,8 @@ export const SearchResultsDisplay = ({
|
||||
if (
|
||||
answer === null &&
|
||||
(documents === null || documents.length === 0) &&
|
||||
quotes === null
|
||||
quotes === null &&
|
||||
!isFetching
|
||||
) {
|
||||
return (
|
||||
<div className="mt-4">
|
||||
|
@@ -43,7 +43,6 @@ export const questionValidationStreamed = async <T>({
|
||||
let previousPartialChunk: string | null = null;
|
||||
while (true) {
|
||||
const rawChunk = await reader?.read();
|
||||
console.log(rawChunk);
|
||||
if (!rawChunk) {
|
||||
throw new Error("Unable to process chunk");
|
||||
}
|
||||
|
Reference in New Issue
Block a user