From 405c047c0ccc2e082f562799263556dfcf70f7fb Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Mon, 7 Oct 2024 00:21:55 -0700 Subject: [PATCH] Include agent personality through subtasks and support custom agents (#916) Currently, the personality of the agent is only included in the final response that it returns to the user. Historically, this was because models were quite bad at navigating the additional context of personality, and there was a bias towards having more control over certain operations (e.g., tool selection, question extraction). Going forward, it should be more approachable to have prompts included in the sub tasks that Khoj runs in order to response to a given query. Make this possible in this PR. This also sets us up for agent creation becoming available soon. Create custom agents in #928 Agents are useful insofar as you can personalize them to fulfill specific subtasks you need to accomplish. In this PR, we add support for using custom agents that can be configured with a custom system prompt (aka persona) and knowledge base (from your own indexed documents). Once created, private agents can be accessible only to the creator, and protected agents can be accessible via a direct link. Custom tool selection for agents in #930 Expose the functionality to select which tools a given agent has access to. By default, they have all. Can limit both information sources and output modes. Add new tools to the agent modification form --- src/interface/web/app/agents/page.tsx | 1514 +++++++++++++++-- src/interface/web/app/common/auth.ts | 11 +- src/interface/web/app/common/colorUtils.ts | 2 +- src/interface/web/app/common/iconUtils.tsx | 112 +- .../components/chatHistory/chatHistory.tsx | 4 +- .../chatInputArea/chatInputArea.tsx | 47 +- .../components/profileCard/profileCard.tsx | 7 +- .../app/components/shareLink/shareLink.tsx | 14 +- .../sidePanel/chatHistorySidePanel.tsx | 3 - src/interface/web/app/page.tsx | 13 +- src/interface/web/app/settings/page.tsx | 9 +- src/khoj/database/adapters/__init__.py | 138 +- ...ent_avatar_remove_agent_public_and_more.py | 49 + ..._agent_tools_agent_input_tools_and_more.py | 69 + .../migrations/0067_alter_agent_style_icon.py | 50 + src/khoj/database/models/__init__.py | 78 +- .../conversation/anthropic/anthropic_chat.py | 2 + .../conversation/google/gemini_chat.py | 2 + .../conversation/offline/chat_model.py | 4 +- src/khoj/processor/conversation/openai/gpt.py | 2 + src/khoj/processor/conversation/prompts.py | 61 +- src/khoj/processor/image/generate.py | 4 +- src/khoj/processor/tools/online_search.py | 16 +- src/khoj/routers/api.py | 25 +- src/khoj/routers/api_agents.py | 228 ++- src/khoj/routers/api_chat.py | 35 +- src/khoj/routers/helpers.py | 113 +- src/khoj/search_type/text_search.py | 5 +- src/khoj/utils/helpers.py | 17 +- 29 files changed, 2350 insertions(+), 284 deletions(-) create mode 100644 src/khoj/database/migrations/0065_remove_agent_avatar_remove_agent_public_and_more.py create mode 100644 src/khoj/database/migrations/0066_remove_agent_tools_agent_input_tools_and_more.py create mode 100644 src/khoj/database/migrations/0067_alter_agent_style_icon.py diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx index b5c1524f..fef3e932 100644 --- a/src/interface/web/app/agents/page.tsx +++ b/src/interface/web/app/agents/page.tsx @@ -2,18 +2,43 @@ import styles from "./agents.module.css"; -import Image from "next/image"; import useSWR from "swr"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; -import { useAuthenticatedData, UserProfile } from "../common/auth"; +import { + useAuthenticatedData, + UserProfile, + ModelOptions, + useUserConfig, + UserConfig, + SubscriptionStates, +} from "../common/auth"; import { Button } from "@/components/ui/button"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { PaperPlaneTilt, Lightning, Plus } from "@phosphor-icons/react"; - -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + PaperPlaneTilt, + Lightning, + Plus, + Circle, + Info, + Check, + ShieldWarning, + Lock, + Book, + Brain, + Waveform, + CaretUpDown, + Globe, + LockOpen, + FloppyDisk, + DotsThreeCircleVertical, + DotsThreeVertical, + Pencil, + Trash, +} from "@phosphor-icons/react"; +import { set, z } from "zod"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, @@ -34,18 +59,73 @@ import { import LoginPrompt from "../components/loginPrompt/loginPrompt"; import { InlineLoading } from "../components/loading/loading"; import SidePanel from "../components/sidePanel/chatHistorySidePanel"; -import { getIconFromIconName } from "../common/iconUtils"; -import { convertColorToTextClass } from "../common/colorUtils"; +import { + getAvailableIcons, + getIconForSlashCommand, + getIconFromIconName, +} from "../common/iconUtils"; +import { convertColorToTextClass, tailwindColors } from "../common/colorUtils"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { useIsMobileWidth } from "../common/utils"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { useForm, UseFormReturn } from "react-hook-form"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Textarea } from "@/components/ui/textarea"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { DialogTitle } from "@radix-ui/react-dialog"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import { cn } from "@/lib/utils"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { uploadDataForIndexing } from "../common/chatFunctions"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Progress } from "@/components/ui/progress"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import ShareLink from "../components/shareLink/shareLink"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; + export interface AgentData { slug: string; - avatar: string; name: string; persona: string; color: string; icon: string; + privacy_level: string; + files?: string[]; + creator?: string; + managed_by_admin: boolean; + chat_model: string; + input_tools: string[]; + output_modes: string[]; } async function openChat(slug: string, userData: UserProfile | null) { @@ -66,26 +146,86 @@ async function openChat(slug: string, userData: UserProfile | null) { } } +function Badge(props: { icon: JSX.Element; text?: string; hoverText?: string }) { + // Always convert text to proper case (e.g., "public" -> "Public") + const displayBadgeText = props.text?.replace(/^\w/, (c) => c.toUpperCase()) || ""; + + return ( + + + +
{props.hoverText || displayBadgeText}
+
+ +
+
{props.icon}
+ {displayBadgeText && displayBadgeText.length > 0 && ( +
{displayBadgeText}
+ )} +
+
+
+
+ ); +} + const agentsFetcher = () => window .fetch("/api/agents") .then((res) => res.json()) .catch((err) => console.log(err)); +// A generic fetcher function that uses the fetch API to make a request to a given URL and returns the response as JSON. +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + interface AgentCardProps { data: AgentData; userProfile: UserProfile | null; isMobileWidth: boolean; + editCard: boolean; + filesOptions: string[]; + modelOptions: ModelOptions[]; + selectedChatModelOption: string; + isSubscribed: boolean; + setAgentChangeTriggered: (value: boolean) => void; + agentSlug: string; + inputToolOptions: { [key: string]: string }; + outputModeOptions: { [key: string]: string }; } function AgentCard(props: AgentCardProps) { - const searchParams = new URLSearchParams(window.location.search); - const agentSlug = searchParams.get("agent"); - const [showModal, setShowModal] = useState(agentSlug === props.data.slug); + const [showModal, setShowModal] = useState(props.agentSlug === props.data.slug); const [showLoginPrompt, setShowLoginPrompt] = useState(false); + const [errors, setErrors] = useState(null); + + let lockIcon = ; + let privacyHoverText = "Private agents are only visible to you."; + + if (props.data.privacy_level === "public") { + lockIcon = ; + privacyHoverText = "Public agents are visible to everyone."; + } else if (props.data.privacy_level === "protected") { + lockIcon = ; + privacyHoverText = "Protected agents are visible to anyone with a direct link."; + } const userData = props.userProfile; + const form = useForm>({ + resolver: zodResolver(EditAgentSchema), + defaultValues: { + name: props.data.name, + persona: props.data.persona, + color: props.data.color, + icon: props.data.icon, + privacy_level: props.data.privacy_level, + chat_model: props.data.chat_model, + files: props.data.files, + input_tools: props.data.input_tools, + output_modes: props.data.output_modes, + }, + }); + if (showModal) { window.history.pushState( {}, @@ -94,8 +234,85 @@ function AgentCard(props: AgentCardProps) { ); } + const onSubmit = (values: z.infer) => { + let agentsApiUrl = `/api/agents`; + let method = props.editCard ? "PATCH" : "POST"; + + fetch(agentsApiUrl, { + method: method, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(values), + }) + .then((response) => { + console.log(response); + if (response.status === 200) { + form.reset(); + setShowModal(false); + setErrors(null); + props.setAgentChangeTriggered(true); + } else { + response.json().then((data) => { + console.error(data); + form.clearErrors(); + if (data.error) { + setErrors(data.error); + } + }); + } + }) + .catch((error) => { + console.error("Error:", error); + setErrors(error); + form.clearErrors(); + }); + }; + const stylingString = convertColorToTextClass(props.data.color); + function makeBadgeFooter() { + return ( +
+ {props.editCard && ( + + )} + {props.data.files && props.data.files.length > 0 && ( + } + text={`knowledge`} + hoverText={ + "The agent has a custom knowledge base it can use to give you answers." + } + /> + )} + } + text={props.data.chat_model} + hoverText={`The agent uses the ${props.data.chat_model} model to chat with you.`} + /> + {props.data.output_modes.map((outputMode) => ( + + ))} + {props.data.input_tools.map((inputTool) => ( + + ))} +
+ ); + } + return (
- {getIconFromIconName(props.data.icon, props.data.color) || ( - {props.data.name} - )} + {getIconFromIconName(props.data.icon, props.data.color)} {props.data.name}
-
- {props.userProfile ? ( - - ) : ( - - )} -
- - -
- {getIconFromIconName(props.data.icon, props.data.color) || ( - {props.data.name} - )} -

{props.data.name}

+
+ {props.editCard && ( +
+ + + + + + + {props.editCard && + props.data.privacy_level !== "private" && ( + + )} + {props.data.creator === userData?.username && ( + + )} + +
- -
- {props.data.persona} + )} +
+ {props.userProfile ? ( + + ) : ( + + )}
- - - - +
+ {props.editCard ? ( + + + Edit {props.data.name} + + + + ) : ( + + +
+ {getIconFromIconName(props.data.icon, props.data.color)} +

{props.data.name}

+
+
+
+ {props.data.persona} +
+
+ {makeBadgeFooter()} +
+ + + +
+ )} ) : (
- {getIconFromIconName(props.data.icon, props.data.color) || ( - {props.data.name} - )} + {getIconFromIconName(props.data.icon, props.data.color)} {props.data.name}
-
- {props.userProfile ? ( - - ) : ( - +
+ {props.editCard && ( +
+ + + + + + + {props.editCard && + props.data.privacy_level !== "private" && ( + + )} + {props.data.creator === userData?.username && ( + + )} + + +
)} +
+ {props.userProfile ? ( + + ) : ( + + )} +
- - - {props.data.name} - Full Prompt - - {props.data.persona} - - Done - - + {props.editCard ? ( + + + + ) : ( + + + {props.data.name} + Persona + + {props.data.persona} +
+ {makeBadgeFooter()} +
+ + Done + +
+ )} )} @@ -249,18 +610,847 @@ function AgentCard(props: AgentCardProps) {
+ +
{makeBadgeFooter()}
+
); } +const EditAgentSchema = z.object({ + name: z.string({ required_error: "Name is required" }).min(1, "Name is required"), + persona: z + .string({ required_error: "Personality is required" }) + .min(1, "Personality is required"), + color: z.string({ required_error: "Color is required" }).min(1, "Color is required"), + icon: z.string({ required_error: "Icon is required" }).min(1, "Icon is required"), + privacy_level: z + .string({ required_error: "Privacy level is required" }) + .min(1, "Privacy level is required"), + chat_model: z + .string({ required_error: "Chat model is required" }) + .min(1, "Chat model is required"), + files: z.array(z.string()).default([]).optional(), + input_tools: z.array(z.string()).default([]).optional(), + output_modes: z.array(z.string()).default([]).optional(), +}); + +interface AgentModificationFormProps { + form: UseFormReturn>; + onSubmit: (values: z.infer) => void; + create?: boolean; + errors?: string | null; + modelOptions: ModelOptions[]; + filesOptions: string[]; + inputToolOptions: { [key: string]: string }; + outputModeOptions: { [key: string]: string }; + slug?: string; +} + +function AgentModificationForm(props: AgentModificationFormProps) { + const [isSaving, setIsSaving] = useState(false); + + const iconOptions = getAvailableIcons(); + const colorOptions = tailwindColors; + const colorOptionClassName = convertColorToTextClass(props.form.getValues("color")); + + const [isDragAndDropping, setIsDragAndDropping] = useState(false); + const [warning, setWarning] = useState(null); + const [error, setError] = useState(null); + const [uploading, setUploading] = useState(false); + const [progressValue, setProgressValue] = useState(0); + const [uploadedFiles, setUploadedFiles] = useState([]); + const fileInputRef = useRef(null); + + useEffect(() => { + if (!uploading) { + setProgressValue(0); + } + + if (uploading) { + const interval = setInterval(() => { + setProgressValue((prev) => { + const increment = Math.floor(Math.random() * 5) + 1; // Generates a random number between 1 and 5 + const nextValue = prev + increment; + return nextValue < 100 ? nextValue : 100; // Ensures progress does not exceed 100 + }); + }, 800); + return () => clearInterval(interval); + } + }, [uploading]); + + useEffect(() => { + if (uploadedFiles.length > 0) { + handleAgentFileChange(uploadedFiles); + } + }, [uploadedFiles]); + + useEffect(() => { + if (props.errors) { + setIsSaving(false); + } + }, [props.errors]); + + function handleDragOver(event: React.DragEvent) { + event.preventDefault(); + setIsDragAndDropping(true); + } + + function handleDragLeave(event: React.DragEvent) { + event.preventDefault(); + setIsDragAndDropping(false); + } + + function handleDragAndDropFiles(event: React.DragEvent) { + event.preventDefault(); + setIsDragAndDropping(false); + + if (!event.dataTransfer.files) return; + + uploadFiles(event.dataTransfer.files); + } + + function uploadFiles(files: FileList) { + uploadDataForIndexing(files, setWarning, setUploading, setError, setUploadedFiles); + } + + function openFileInput() { + if (fileInputRef && fileInputRef.current) { + fileInputRef.current.click(); + } + } + + function handleFileChange(event: React.ChangeEvent) { + if (!event.target.files) return; + + uploadFiles(event.target.files); + } + + const handleAgentFileChange = (files: string[]) => { + for (const file of files) { + const currentFiles = props.form.getValues("files") || []; + const newFiles = currentFiles.includes(file) + ? currentFiles.filter((item) => item !== file) + : [...currentFiles, file]; + props.form.setValue("files", newFiles); + } + }; + + const privacyOptions = ["public", "private", "protected"]; + + return ( +
+ { + props.onSubmit(values); + setIsSaving(true); + })} + className="space-y-6" + > + ( + + Name + + What should this agent be called? Pick something descriptive & + memorable. + + + + + + + )} + /> + + ( + + Personality + + What is the personality, thought process, or tuning of this agent? + Get creative; this is how you can influence the agent constitution. + + +