Persona enhancements

This commit is contained in:
Weves
2023-12-07 10:45:59 -08:00
committed by Chris Weaver
parent ddf3f99da4
commit d5658ce477
12 changed files with 304 additions and 149 deletions

View File

@@ -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&apos;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&apos;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

View File

@@ -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]) =>

View File

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

View File

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

View File

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