mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 17:35:07 +01:00
Update some edge cases and usability of create agent flow
- Use the slug to determine which agent to PATCH - Make the agent creation form multi-step to streamline the process
This commit is contained in:
parent
8ff13e4cf6
commit
81aa1b5589
4 changed files with 643 additions and 482 deletions
|
@ -35,8 +35,10 @@ import {
|
|||
DotsThreeVertical,
|
||||
Pencil,
|
||||
Trash,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
} from "@phosphor-icons/react";
|
||||
import { set, z } from "zod";
|
||||
import { set, z, ZodError } from "zod";
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
|
@ -245,12 +247,18 @@ function AgentCard(props: AgentCardProps) {
|
|||
let agentsApiUrl = `/api/agents`;
|
||||
let method = props.editCard ? "PATCH" : "POST";
|
||||
|
||||
let valuesToSend: any = values;
|
||||
|
||||
if (props.editCard) {
|
||||
valuesToSend = { ...values, slug: props.data.slug };
|
||||
}
|
||||
|
||||
fetch(agentsApiUrl, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(values),
|
||||
body: JSON.stringify(valuesToSend),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
|
@ -537,9 +545,36 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
const [progressValue, setProgressValue] = useState(0);
|
||||
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
||||
const [allFileOptions, setAllFileOptions] = useState<string[]>([]);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
|
||||
const [showSubscribeDialog, setShowSubscribeDialog] = useState(true);
|
||||
|
||||
const privacyOptions = ["public", "private", "protected"];
|
||||
|
||||
const basicFields = [
|
||||
{ name: "name", label: "Name" },
|
||||
{ name: "persona", label: "Personality" },
|
||||
];
|
||||
|
||||
const advancedFields = [
|
||||
{ name: "files", label: "Knowledge Base" },
|
||||
{ name: "input_tools", label: "Input Tools" },
|
||||
{ name: "output_modes", label: "Output Modes" },
|
||||
];
|
||||
|
||||
const customizationFields = [
|
||||
{ name: "color", label: "Color" },
|
||||
{ name: "icon", label: "Icon" },
|
||||
{ name: "chat_model", label: "Chat Model" },
|
||||
{ name: "privacy_level", label: "Privacy Level" },
|
||||
];
|
||||
|
||||
const formGroups = [
|
||||
{ fields: basicFields, label: "Basic Settings" },
|
||||
{ fields: customizationFields, label: "Customization & Access" },
|
||||
{ fields: advancedFields, label: "Advanced Settings" },
|
||||
];
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -563,7 +598,9 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
const currentFiles = props.form.getValues("files") || [];
|
||||
const fileOptions = props.filesOptions || [];
|
||||
const concatenatedFiles = [...currentFiles, ...fileOptions];
|
||||
setAllFileOptions((prev) => [...prev, ...concatenatedFiles]);
|
||||
const fullAllFileOptions = [...allFileOptions, ...concatenatedFiles];
|
||||
const dedupedAllFileOptions = Array.from(new Set(fullAllFileOptions));
|
||||
setAllFileOptions(dedupedAllFileOptions);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -614,6 +651,25 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
uploadFiles(event.target.files);
|
||||
}
|
||||
|
||||
const handleNext = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
if (currentStep < formGroups.length - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (values: any) => {
|
||||
props.onSubmit(values);
|
||||
setIsSaving(true);
|
||||
};
|
||||
|
||||
const handleAgentFileChange = (files: string[]) => {
|
||||
for (const file of files) {
|
||||
const currentFiles = props.form.getValues("files") || [];
|
||||
|
@ -624,7 +680,30 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const privacyOptions = ["public", "private", "protected"];
|
||||
const areRequiredFieldsCompletedForCurrentStep = (formGroup: {
|
||||
fields: { name: string }[];
|
||||
}) => {
|
||||
try {
|
||||
EditAgentSchema.parse(props.form.getValues());
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errors: { [key: string]: string } = (error as ZodError).errors.reduce(
|
||||
(acc: any, curr: any) => {
|
||||
acc[curr.path[0]] = curr.message;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
for (const field of formGroup.fields) {
|
||||
if (errors[field.name]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
if (!props.isSubscribed && showSubscribeDialog) {
|
||||
return (
|
||||
|
@ -658,16 +737,12 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const renderFormField = (fieldName: string) => {
|
||||
switch (fieldName) {
|
||||
case "name":
|
||||
return (
|
||||
<Form {...props.form}>
|
||||
<form
|
||||
onSubmit={props.form.handleSubmit((values) => {
|
||||
props.onSubmit(values);
|
||||
setIsSaving(true);
|
||||
})}
|
||||
className="space-y-6"
|
||||
>
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
|
@ -684,29 +759,11 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={props.form.control}
|
||||
name="persona"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-1 grid gap-2">
|
||||
<FormLabel>Personality</FormLabel>
|
||||
<FormDescription>
|
||||
What is the personality, thought process, or tuning of this agent?
|
||||
Get creative; this is how you can influence the agent constitution.
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="You are an excellent biologist, at the top of your field in marine biology."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
);
|
||||
case "chat_model":
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="chat_model"
|
||||
render={({ field }) => (
|
||||
|
@ -723,7 +780,10 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormControl>
|
||||
<SelectContent className="items-start space-y-1 inline-flex flex-col">
|
||||
{props.modelOptions.map((modelOption) => (
|
||||
<SelectItem key={modelOption.id} value={modelOption.name}>
|
||||
<SelectItem
|
||||
key={modelOption.id}
|
||||
value={modelOption.name}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
{modelOption.name}
|
||||
</div>
|
||||
|
@ -735,7 +795,11 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
case "privacy_level":
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="privacy_level"
|
||||
render={({ field }) => (
|
||||
|
@ -746,7 +810,10 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
<FormDescription>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant={"ghost" as const} className="p-0 h-fit">
|
||||
<Button
|
||||
variant={"ghost" as const}
|
||||
className="p-0 h-fit"
|
||||
>
|
||||
<span className="items-center flex gap-1 text-sm">
|
||||
<Info className="inline" />
|
||||
<p className="text-sm">Learn more</p>
|
||||
|
@ -785,18 +852,18 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid">
|
||||
<FormLabel className="mb-2">Look & Feel</FormLabel>
|
||||
<div className="flex gap-1 justify-left flex-col md:flex-row">
|
||||
);
|
||||
case "color":
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="color"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormLabel>Color</FormLabel>
|
||||
<FormDescription>Choose a color for your agent.</FormDescription>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Color" />
|
||||
|
@ -820,16 +887,18 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
);
|
||||
case "icon":
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="icon"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormLabel>Icon</FormLabel>
|
||||
<FormDescription>Choose an icon for your agent.</FormDescription>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Icon" />
|
||||
|
@ -855,16 +924,36 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel className="text-md">Advanced Settings</FormLabel>
|
||||
<FormDescription>
|
||||
These are optional settings that you can use to customize your agent.
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
|
||||
);
|
||||
case "persona":
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="persona"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-1 grid gap-2">
|
||||
<FormLabel>Personality</FormLabel>
|
||||
<FormDescription>
|
||||
What is the personality, thought process, or tuning of this
|
||||
agent? Get creative; this is how you can influence the agent
|
||||
constitution.
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="You are an excellent biologist, at the top of your field in marine biology."
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
case "files":
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="files"
|
||||
render={({ field }) => (
|
||||
|
@ -938,7 +1027,9 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
) : (
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<Plus className="h-6 w-6 mr-2" />
|
||||
<span>Drag and drop files here</span>
|
||||
<span>
|
||||
Drag and drop files here
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -954,15 +1045,19 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
key={file}
|
||||
onSelect={() => {
|
||||
const currentFiles =
|
||||
props.form.getValues("files") || [];
|
||||
const newFiles = currentFiles.includes(
|
||||
file,
|
||||
)
|
||||
props.form.getValues("files") ||
|
||||
[];
|
||||
const newFiles =
|
||||
currentFiles.includes(file)
|
||||
? currentFiles.filter(
|
||||
(item) => item !== file,
|
||||
(item) =>
|
||||
item !== file,
|
||||
)
|
||||
: [...currentFiles, file];
|
||||
props.form.setValue("files", newFiles);
|
||||
props.form.setValue(
|
||||
"files",
|
||||
newFiles,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
|
@ -985,7 +1080,11 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
case "input_tools":
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="input_tools"
|
||||
render={({ field }) => (
|
||||
|
@ -1018,7 +1117,9 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
"input_tools",
|
||||
) || [];
|
||||
const newInputTools =
|
||||
currentInputTools.includes(key)
|
||||
currentInputTools.includes(
|
||||
key,
|
||||
)
|
||||
? currentInputTools.filter(
|
||||
(item) =>
|
||||
item !== key,
|
||||
|
@ -1037,12 +1138,23 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value &&
|
||||
field.value.includes(key)
|
||||
field.value.includes(
|
||||
key,
|
||||
)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<b>{key}</b>: {value}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center space-x-2",
|
||||
)}
|
||||
>
|
||||
<p>
|
||||
<b>{key}</b>
|
||||
</p>
|
||||
<p>{value}</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
),
|
||||
)}
|
||||
|
@ -1054,7 +1166,11 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
case "output_modes":
|
||||
return (
|
||||
<FormField
|
||||
key={fieldName}
|
||||
control={props.form.control}
|
||||
name="output_modes"
|
||||
render={({ field }) => (
|
||||
|
@ -1087,7 +1203,9 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
"output_modes",
|
||||
) || [];
|
||||
const newOutputModes =
|
||||
currentOutputModes.includes(key)
|
||||
currentOutputModes.includes(
|
||||
key,
|
||||
)
|
||||
? currentOutputModes.filter(
|
||||
(item) =>
|
||||
item !== key,
|
||||
|
@ -1106,12 +1224,23 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value &&
|
||||
field.value.includes(key)
|
||||
field.value.includes(
|
||||
key,
|
||||
)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<b>{key}</b>: {value}
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center space-x-2",
|
||||
)}
|
||||
>
|
||||
<p>
|
||||
<b>{key}</b>
|
||||
</p>
|
||||
<p>{value}</p>
|
||||
</div>
|
||||
</CommandItem>
|
||||
),
|
||||
)}
|
||||
|
@ -1123,6 +1252,55 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...props.form}>
|
||||
<form onSubmit={props.form.handleSubmit(handleSubmit)} className="space-y-6">
|
||||
<div className="space-y-6">{formGroups[currentStep].label}</div>
|
||||
{currentStep < formGroups.length &&
|
||||
formGroups[currentStep].fields.map((field) => renderFormField(field.name))}
|
||||
<div className="flex justify-between mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant={"outline"}
|
||||
onClick={handlePrevious}
|
||||
disabled={currentStep === 0}
|
||||
className={`items-center ${isSaving ? "bg-stone-100 dark:bg-neutral-900" : ""} text-white ${colorOptionClassName}`}
|
||||
>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Previous
|
||||
</Button>
|
||||
{currentStep < formGroups.length - 1 ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant={"outline"}
|
||||
onClick={handleNext}
|
||||
disabled={
|
||||
!areRequiredFieldsCompletedForCurrentStep(formGroups[currentStep])
|
||||
}
|
||||
className={`items-center ${isSaving ? "bg-stone-100 dark:bg-neutral-900" : ""} text-white ${colorOptionClassName}`}
|
||||
>
|
||||
Next
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"outline"}
|
||||
disabled={isSaving || !props.isSubscribed}
|
||||
className={`items-center ${isSaving ? "bg-stone-100 dark:bg-neutral-900" : ""} text-white ${colorOptionClassName}`}
|
||||
>
|
||||
<FloppyDisk className="h-4 w-4 mr-2" />
|
||||
{isSaving ? "Booting..." : "Save"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.errors && (
|
||||
<Alert className="bg-secondary border-none my-4">
|
||||
<AlertDescription className="flex items-center gap-1">
|
||||
|
@ -1134,28 +1312,6 @@ function AgentModificationForm(props: AgentModificationFormProps) {
|
|||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<fieldset>
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"ghost"}
|
||||
disabled={isSaving || !props.isSubscribed}
|
||||
className={`items-center ${isSaving ? "bg-stone-100 dark:bg-neutral-900" : ""} text-white ${colorOptionClassName}`}
|
||||
>
|
||||
<FloppyDisk className="h-4 w-4 mr-2" />
|
||||
{isSaving ? "Booting..." : "Save"}
|
||||
</Button>
|
||||
{!!!props.create && props.form.getValues("privacy_level") !== "private" && (
|
||||
<ShareLink
|
||||
buttonTitle="Share"
|
||||
title="Share Agent"
|
||||
description="Share a link to this agent with others. They'll be able to chat with it, and ask questions to all of its knowledge base."
|
||||
buttonVariant={"ghost" as const}
|
||||
buttonClassName={`${colorOptionClassName}`}
|
||||
includeIcon={true}
|
||||
url={`${window.location.origin}/agents?agent=${props.slug}`}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -696,10 +696,12 @@ class AgentAdapters:
|
|||
files: List[str],
|
||||
input_tools: List[str],
|
||||
output_modes: List[str],
|
||||
slug: Optional[str] = None,
|
||||
):
|
||||
chat_model_option = await ChatModelOptions.objects.filter(chat_model=chat_model).afirst()
|
||||
|
||||
agent, created = await Agent.objects.filter(name=name, creator=user).aupdate_or_create(
|
||||
# Slug will be None for new agents, which will trigger a new agent creation with a generated, immutable slug
|
||||
agent, created = await Agent.objects.filter(slug=slug, creator=user).aupdate_or_create(
|
||||
defaults={
|
||||
"name": name,
|
||||
"creator": user,
|
||||
|
|
|
@ -35,6 +35,7 @@ class ModifyAgentBody(BaseModel):
|
|||
files: Optional[List[str]] = []
|
||||
input_tools: Optional[List[str]] = []
|
||||
output_modes: Optional[List[str]] = []
|
||||
slug: Optional[str] = None
|
||||
|
||||
|
||||
@api_agents.get("", response_class=Response)
|
||||
|
@ -192,6 +193,7 @@ async def create_agent(
|
|||
body.files,
|
||||
body.input_tools,
|
||||
body.output_modes,
|
||||
body.slug,
|
||||
)
|
||||
|
||||
agents_packet = {
|
||||
|
@ -233,7 +235,7 @@ async def update_agent(
|
|||
status_code=400,
|
||||
)
|
||||
|
||||
selected_agent = await AgentAdapters.aget_agent_by_name(body.name, user)
|
||||
selected_agent = await AgentAdapters.aget_agent_by_slug(body.slug, user)
|
||||
|
||||
if not selected_agent:
|
||||
return Response(
|
||||
|
@ -253,6 +255,7 @@ async def update_agent(
|
|||
body.files,
|
||||
body.input_tools,
|
||||
body.output_modes,
|
||||
body.slug,
|
||||
)
|
||||
|
||||
agents_packet = {
|
||||
|
|
|
@ -209,7 +209,7 @@ def chat_history(
|
|||
|
||||
agent_metadata = None
|
||||
if conversation.agent:
|
||||
if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE:
|
||||
if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE and conversation.agent.creator != user:
|
||||
conversation.agent = None
|
||||
else:
|
||||
agent_metadata = {
|
||||
|
|
Loading…
Reference in a new issue