Merge branch 'master' into features/advanced-reasoning

This commit is contained in:
Debanjum Singh Solanky 2024-10-15 01:27:36 -07:00
commit feb6d65ef8
9 changed files with 693 additions and 515 deletions

View file

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

View file

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

View file

@ -114,6 +114,7 @@ class CrossEncoderModel:
payload = {"inputs": {"query": query, "passages": [hit.additional[key] for hit in hits]}}
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
response = requests.post(target_url, json=payload, headers=headers)
response.raise_for_status()
return response.json()["scores"]
cross_inp = [[query, hit.additional[key]] for hit in hits]

View file

@ -143,7 +143,6 @@ async def read_webpages(
conversation_history: dict,
location: LocationData,
user: KhojUser,
subscribed: bool = False,
send_status_func: Optional[Callable] = None,
uploaded_image_url: str = None,
agent: Agent = None,

View file

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

View file

@ -213,7 +213,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 = {
@ -853,6 +853,8 @@ async def chat(
return
# # Gather Context
# # Extract Document References
# try:
# async for result in extract_references_and_questions(
# request,
# meta_log,
@ -872,8 +874,15 @@ async def chat(
# compiled_references.extend(result[0])
# inferred_queries.extend(result[1])
# defiltered_query = result[2]
# if not is_none_or_empty(compiled_references):
# except Exception as e:
# error_message = f"Error searching knowledge base: {e}. Attempting to respond without document references."
# logger.warning(error_message)
# async for result in send_event(
# ChatEvent.STATUS, "Document search failed. I'll try respond without document references"
# ):
# yield result
#
# # if not is_none_or_empty(compiled_references):
# try:
# headings = "\n- " + "\n- ".join(set([c.get("compiled", c).split("\n")[0] for c in compiled_references]))
# # Strip only leading # from headings
@ -910,12 +919,13 @@ async def chat(
yield result[ChatEvent.STATUS]
else:
online_results = result
except ValueError as e:
except Exception as e:
error_message = f"Error searching online: {e}. Attempting to respond without online results"
logger.warning(error_message)
async for result in send_llm_response(error_message):
async for result in send_event(
ChatEvent.STATUS, "Online search failed. I'll try respond without online references"
):
yield result
return
## Gather Webpage References
if ConversationCommand.Webpage in conversation_commands and pending_research:
@ -925,7 +935,6 @@ async def chat(
meta_log,
location,
user,
subscribed,
partial(send_event, ChatEvent.STATUS),
uploaded_image_url=uploaded_image_url,
agent=agent,
@ -945,11 +954,15 @@ async def chat(
webpages.append(webpage["link"])
async for result in send_event(ChatEvent.STATUS, f"**Read web pages**: {webpages}"):
yield result
except ValueError as e:
except Exception as e:
logger.warning(
f"Error directly reading webpages: {e}. Attempting to respond without online results",
f"Error reading webpages: {e}. Attempting to respond without webpage results",
exc_info=True,
)
async for result in send_event(
ChatEvent.STATUS, "Webpage read failed. I'll try respond without webpage references"
):
yield result
## Gather Code Results
if ConversationCommand.Code in conversation_commands and pending_research:

View file

@ -345,13 +345,13 @@ async def aget_relevant_information_sources(
final_response = [ConversationCommand.Default]
else:
final_response = [ConversationCommand.General]
return final_response
except Exception as e:
except Exception:
logger.error(f"Invalid response for determining relevant tools: {response}")
if len(agent_tools) == 0:
final_response = [ConversationCommand.Default]
else:
final_response = agent_tools
return final_response
async def aget_relevant_output_modes(

View file

@ -227,7 +227,6 @@ async def execute_information_collection(
conversation_history,
location,
user,
subscribed,
send_status_func,
uploaded_image_url=uploaded_image_url,
agent=agent,

View file

@ -3,6 +3,7 @@ import math
from pathlib import Path
from typing import List, Optional, Tuple, Type, Union
import requests
import torch
from asgiref.sync import sync_to_async
from sentence_transformers import util
@ -231,8 +232,12 @@ def setup(
def cross_encoder_score(query: str, hits: List[SearchResponse], search_model_name: str) -> List[SearchResponse]:
"""Score all retrieved entries using the cross-encoder"""
try:
with timer("Cross-Encoder Predict Time", logger, state.device):
cross_scores = state.cross_encoder_model[search_model_name].predict(query, hits)
except requests.exceptions.HTTPError as e:
logger.error(f"Failed to rerank documents using the inference endpoint. Error: {e}.", exc_info=True)
cross_scores = [0.0] * len(hits)
# Convert cross-encoder scores to distances and pass in hits for reranking
for idx in range(len(cross_scores)):