mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-08 05:39:13 +00:00
Improve Automation Flexibility and Automation Email Format (#1016)
- Format AI response to send in automation email - Let Khoj infer chat query based on user automation query - Decide if automation emails should be sent based on response - Fix the `to_notify_or_not` decider AI - Ask reason before decision to improve to_notify decider AI - Show error message on web app when fail to create/update automation
This commit is contained in:
@@ -167,68 +167,68 @@ const timestamp = Date.now();
|
|||||||
const suggestedAutomationsMetadata: AutomationsData[] = [
|
const suggestedAutomationsMetadata: AutomationsData[] = [
|
||||||
{
|
{
|
||||||
subject: "Weekly Newsletter",
|
subject: "Weekly Newsletter",
|
||||||
query_to_run:
|
scheduling_request:
|
||||||
"/research Compile a message including: 1. A recap of news from last week 2. An at-home workout I can do before work 3. A quote to inspire me for the week ahead",
|
"/research Compile a message including: 1. A recap of news from last week 2. An at-home workout I can do before work 3. A quote to inspire me for the week ahead",
|
||||||
schedule: "9AM every Monday",
|
schedule: "9AM every Monday",
|
||||||
next: "Next run at 9AM on Monday",
|
next: "Next run at 9AM on Monday",
|
||||||
crontime: "0 9 * * 1",
|
crontime: "0 9 * * 1",
|
||||||
id: timestamp,
|
id: timestamp,
|
||||||
scheduling_request: "",
|
query_to_run: "",
|
||||||
},
|
|
||||||
{
|
|
||||||
subject: "Daily Bedtime Story",
|
|
||||||
query_to_run:
|
|
||||||
"Compose a bedtime story that a five-year-old might enjoy. It should not exceed five paragraphs. Appeal to the imagination, but weave in learnings.",
|
|
||||||
schedule: "9PM every night",
|
|
||||||
next: "Next run at 9PM today",
|
|
||||||
crontime: "0 21 * * *",
|
|
||||||
id: timestamp + 1,
|
|
||||||
scheduling_request: "",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subject: "Front Page of Hacker News",
|
subject: "Front Page of Hacker News",
|
||||||
query_to_run:
|
scheduling_request:
|
||||||
"/research Summarize the top 5 posts from https://news.ycombinator.com/best and share them with me, including links",
|
"/research Summarize the top 5 posts from https://news.ycombinator.com/best and share them with me, including links",
|
||||||
schedule: "9PM on every Wednesday",
|
schedule: "9PM on every Wednesday",
|
||||||
next: "Next run at 9PM on Wednesday",
|
next: "Next run at 9PM on Wednesday",
|
||||||
crontime: "0 21 * * 3",
|
crontime: "0 21 * * 3",
|
||||||
id: timestamp + 2,
|
id: timestamp + 2,
|
||||||
scheduling_request: "",
|
query_to_run: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subject: "Market Summary",
|
subject: "Market Summary",
|
||||||
query_to_run:
|
scheduling_request:
|
||||||
"/research Get the market summary for today and share it with me. Focus on tech stocks and the S&P 500.",
|
"/research Get the market summary for today and share it with me. Focus on tech stocks and the S&P 500.",
|
||||||
schedule: "9AM on every weekday",
|
schedule: "9AM on every weekday",
|
||||||
next: "Next run at 9AM on Monday",
|
next: "Next run at 9AM on Monday",
|
||||||
crontime: "0 9 * * *",
|
crontime: "0 9 * * *",
|
||||||
id: timestamp + 3,
|
id: timestamp + 3,
|
||||||
scheduling_request: "",
|
query_to_run: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subject: "Market Crash Notification",
|
subject: "Market Crash Notification",
|
||||||
query_to_run: "Notify me if the stock market fell by more than 5% today.",
|
scheduling_request: "Notify me if the stock market fell by more than 5% today.",
|
||||||
schedule: "5PM every evening",
|
schedule: "5PM every evening",
|
||||||
next: "Next run at 5PM today",
|
next: "Next run at 5PM today",
|
||||||
crontime: "0 17 * * *",
|
crontime: "0 17 * * *",
|
||||||
id: timestamp + 5,
|
id: timestamp + 5,
|
||||||
scheduling_request: "",
|
query_to_run: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subject: "Round-up of research papers about AI in healthcare",
|
subject: "Round-up of research papers about AI in healthcare",
|
||||||
query_to_run:
|
scheduling_request:
|
||||||
"/research Summarize the top 3 research papers about AI in healthcare that were published in the last week. Include links to the full papers.",
|
"/research Summarize the top 3 research papers about AI in healthcare that were published in the last week. Include links to the full papers.",
|
||||||
schedule: "9AM every Friday",
|
schedule: "9AM every Friday",
|
||||||
next: "Next run at 9AM on Friday",
|
next: "Next run at 9AM on Friday",
|
||||||
crontime: "0 9 * * 5",
|
crontime: "0 9 * * 5",
|
||||||
id: timestamp + 4,
|
id: timestamp + 4,
|
||||||
scheduling_request: "",
|
query_to_run: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: "Daily Bedtime Story",
|
||||||
|
scheduling_request:
|
||||||
|
"Compose a bedtime story that a five-year-old might enjoy. It should not exceed five paragraphs. Appeal to the imagination, but weave in learnings.",
|
||||||
|
schedule: "9PM every night",
|
||||||
|
next: "Next run at 9PM today",
|
||||||
|
crontime: "0 21 * * *",
|
||||||
|
id: timestamp + 1,
|
||||||
|
query_to_run: "",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function createShareLink(automation: AutomationsData) {
|
function createShareLink(automation: AutomationsData) {
|
||||||
const encodedSubject = encodeURIComponent(automation.subject);
|
const encodedSubject = encodeURIComponent(automation.subject);
|
||||||
const encodedQuery = encodeURIComponent(automation.query_to_run);
|
const encodedQuery = encodeURIComponent(automation.scheduling_request);
|
||||||
const encodedCrontime = encodeURIComponent(automation.crontime);
|
const encodedCrontime = encodeURIComponent(automation.crontime);
|
||||||
|
|
||||||
const shareLink = `${window.location.origin}/automations?subject=${encodedSubject}&query=${encodedQuery}&crontime=${encodedCrontime}`;
|
const shareLink = `${window.location.origin}/automations?subject=${encodedSubject}&query=${encodedQuery}&crontime=${encodedCrontime}`;
|
||||||
@@ -269,6 +269,7 @@ interface AutomationsCardProps {
|
|||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
|
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
|
||||||
authenticatedData: UserProfile | null;
|
authenticatedData: UserProfile | null;
|
||||||
|
setToastMessage: (toastMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AutomationsCard(props: AutomationsCardProps) {
|
function AutomationsCard(props: AutomationsCardProps) {
|
||||||
@@ -277,8 +278,6 @@ function AutomationsCard(props: AutomationsCardProps) {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [isDeleted, setIsDeleted] = useState(false);
|
const [isDeleted, setIsDeleted] = useState(false);
|
||||||
const [toastMessage, setToastMessage] = useState("");
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const automation = props.automation;
|
const automation = props.automation;
|
||||||
|
|
||||||
@@ -306,18 +305,6 @@ function AutomationsCard(props: AutomationsCardProps) {
|
|||||||
}
|
}
|
||||||
}, [updatedAutomationData, automation]);
|
}, [updatedAutomationData, automation]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const toastTitle = `Automation: ${updatedAutomationData?.subject || automation.subject}`;
|
|
||||||
if (toastMessage) {
|
|
||||||
toast({
|
|
||||||
title: toastTitle,
|
|
||||||
description: toastMessage,
|
|
||||||
action: <ToastAction altText="Dismiss">Ok</ToastAction>,
|
|
||||||
});
|
|
||||||
setToastMessage("");
|
|
||||||
}
|
|
||||||
}, [toastMessage, updatedAutomationData, automation, toast]);
|
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -346,6 +333,7 @@ function AutomationsCard(props: AutomationsCardProps) {
|
|||||||
isCreating={isEditing}
|
isCreating={isEditing}
|
||||||
automation={updatedAutomationData || automation}
|
automation={updatedAutomationData || automation}
|
||||||
ipLocationData={props.locationData}
|
ipLocationData={props.locationData}
|
||||||
|
setToastMessage={props.setToastMessage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ShareLink
|
<ShareLink
|
||||||
@@ -365,7 +353,10 @@ function AutomationsCard(props: AutomationsCardProps) {
|
|||||||
variant={"outline"}
|
variant={"outline"}
|
||||||
className="justify-start"
|
className="justify-start"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sendAPreview(automation.id.toString(), setToastMessage);
|
sendAPreview(
|
||||||
|
automation.id.toString(),
|
||||||
|
props.setToastMessage,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Play className="h-4 w-4 mr-2" />
|
<Play className="h-4 w-4 mr-2" />
|
||||||
@@ -391,7 +382,7 @@ function AutomationsCard(props: AutomationsCardProps) {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-secondary-foreground break-all">
|
<CardContent className="text-secondary-foreground break-all">
|
||||||
{updatedAutomationData?.query_to_run || automation.query_to_run}
|
{updatedAutomationData?.scheduling_request || automation.scheduling_request}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex flex-col items-start md:flex-row md:justify-between md:items-center gap-2">
|
<CardFooter className="flex flex-col items-start md:flex-row md:justify-between md:items-center gap-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -420,6 +411,7 @@ function AutomationsCard(props: AutomationsCardProps) {
|
|||||||
isCreating={isEditing}
|
isCreating={isEditing}
|
||||||
automation={automation}
|
automation={automation}
|
||||||
ipLocationData={props.locationData}
|
ipLocationData={props.locationData}
|
||||||
|
setToastMessage={props.setToastMessage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -434,6 +426,7 @@ interface SharedAutomationCardProps {
|
|||||||
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
|
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
|
||||||
authenticatedData: UserProfile | null;
|
authenticatedData: UserProfile | null;
|
||||||
isMobileWidth: boolean;
|
isMobileWidth: boolean;
|
||||||
|
setToastMessage: (toastMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SharedAutomationCard(props: SharedAutomationCardProps) {
|
function SharedAutomationCard(props: SharedAutomationCardProps) {
|
||||||
@@ -451,8 +444,8 @@ function SharedAutomationCard(props: SharedAutomationCardProps) {
|
|||||||
const automation: AutomationsData = {
|
const automation: AutomationsData = {
|
||||||
id: 0,
|
id: 0,
|
||||||
subject: decodeURIComponent(subject),
|
subject: decodeURIComponent(subject),
|
||||||
query_to_run: decodeURIComponent(query),
|
scheduling_request: decodeURIComponent(query),
|
||||||
scheduling_request: "",
|
query_to_run: "",
|
||||||
schedule: cronToHumanReadableString(decodeURIComponent(crontime)),
|
schedule: cronToHumanReadableString(decodeURIComponent(crontime)),
|
||||||
crontime: decodeURIComponent(crontime),
|
crontime: decodeURIComponent(crontime),
|
||||||
next: "",
|
next: "",
|
||||||
@@ -470,6 +463,7 @@ function SharedAutomationCard(props: SharedAutomationCardProps) {
|
|||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
automation={automation}
|
automation={automation}
|
||||||
ipLocationData={props.locationData}
|
ipLocationData={props.locationData}
|
||||||
|
setToastMessage={props.setToastMessage}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
@@ -480,7 +474,7 @@ const EditAutomationSchema = z.object({
|
|||||||
dayOfWeek: z.optional(z.number()),
|
dayOfWeek: z.optional(z.number()),
|
||||||
dayOfMonth: z.optional(z.string()),
|
dayOfMonth: z.optional(z.string()),
|
||||||
timeRecurrence: z.string({ required_error: "Time Recurrence is required" }),
|
timeRecurrence: z.string({ required_error: "Time Recurrence is required" }),
|
||||||
queryToRun: z.string({ required_error: "Query to Run is required" }),
|
schedulingRequest: z.string({ required_error: "Query to Run is required" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
interface EditCardProps {
|
interface EditCardProps {
|
||||||
@@ -492,6 +486,7 @@ interface EditCardProps {
|
|||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
|
setShowLoginPrompt: (showLoginPrompt: boolean) => void;
|
||||||
authenticatedData: UserProfile | null;
|
authenticatedData: UserProfile | null;
|
||||||
|
setToastMessage: (toastMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditCard(props: EditCardProps) {
|
function EditCard(props: EditCardProps) {
|
||||||
@@ -507,7 +502,7 @@ function EditCard(props: EditCardProps) {
|
|||||||
? getTimeRecurrenceFromCron(automation.crontime)
|
? getTimeRecurrenceFromCron(automation.crontime)
|
||||||
: "12:00 PM",
|
: "12:00 PM",
|
||||||
dayOfMonth: automation?.crontime ? getDayOfMonthFromCron(automation.crontime) : "1",
|
dayOfMonth: automation?.crontime ? getDayOfMonthFromCron(automation.crontime) : "1",
|
||||||
queryToRun: automation?.query_to_run,
|
schedulingRequest: automation?.scheduling_request,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -520,7 +515,7 @@ function EditCard(props: EditCardProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let updateQueryUrl = `/api/automation?`;
|
let updateQueryUrl = `/api/automation?`;
|
||||||
updateQueryUrl += `q=${encodeURIComponent(values.queryToRun)}`;
|
updateQueryUrl += `q=${encodeURIComponent(values.schedulingRequest)}`;
|
||||||
if (automation?.id && !props.createNew) {
|
if (automation?.id && !props.createNew) {
|
||||||
updateQueryUrl += `&automation_id=${encodeURIComponent(automation.id)}`;
|
updateQueryUrl += `&automation_id=${encodeURIComponent(automation.id)}`;
|
||||||
}
|
}
|
||||||
@@ -552,6 +547,15 @@ function EditCard(props: EditCardProps) {
|
|||||||
crontime: data.crontime,
|
crontime: data.crontime,
|
||||||
next: data.next,
|
next: data.next,
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error saving automation:", error);
|
||||||
|
// Reset saving state
|
||||||
|
props.setIsEditing(false);
|
||||||
|
// Show error message
|
||||||
|
props.setToastMessage(
|
||||||
|
"Sorry, something went wrong. Try again or contact team@khoj.dev.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -829,7 +833,7 @@ function AutomationModificationForm(props: AutomationModificationFormProps) {
|
|||||||
)}
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={props.form.control}
|
control={props.form.control}
|
||||||
name="queryToRun"
|
name="schedulingRequest"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="space-y-1">
|
<FormItem className="space-y-1">
|
||||||
<FormLabel>Instructions</FormLabel>
|
<FormLabel>Instructions</FormLabel>
|
||||||
@@ -850,7 +854,7 @@ function AutomationModificationForm(props: AutomationModificationFormProps) {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
{errors.subject && (
|
{errors.subject && (
|
||||||
<FormMessage>{errors.queryToRun?.message}</FormMessage>
|
<FormMessage>{errors.schedulingRequest?.message}</FormMessage>
|
||||||
)}
|
)}
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -919,6 +923,7 @@ interface AutomationComponentWrapperProps {
|
|||||||
isCreating: boolean;
|
isCreating: boolean;
|
||||||
ipLocationData: LocationData | null | undefined;
|
ipLocationData: LocationData | null | undefined;
|
||||||
automation?: AutomationsData;
|
automation?: AutomationsData;
|
||||||
|
setToastMessage: (toastMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AutomationComponentWrapper(props: AutomationComponentWrapperProps) {
|
function AutomationComponentWrapper(props: AutomationComponentWrapperProps) {
|
||||||
@@ -946,6 +951,7 @@ function AutomationComponentWrapper(props: AutomationComponentWrapperProps) {
|
|||||||
setShowLoginPrompt={props.setShowLoginPrompt}
|
setShowLoginPrompt={props.setShowLoginPrompt}
|
||||||
setUpdatedAutomationData={props.setNewAutomationData}
|
setUpdatedAutomationData={props.setNewAutomationData}
|
||||||
locationData={props.ipLocationData}
|
locationData={props.ipLocationData}
|
||||||
|
setToastMessage={props.setToastMessage}
|
||||||
/>
|
/>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
@@ -973,6 +979,7 @@ function AutomationComponentWrapper(props: AutomationComponentWrapperProps) {
|
|||||||
setShowLoginPrompt={props.setShowLoginPrompt}
|
setShowLoginPrompt={props.setShowLoginPrompt}
|
||||||
setUpdatedAutomationData={props.setNewAutomationData}
|
setUpdatedAutomationData={props.setNewAutomationData}
|
||||||
locationData={props.ipLocationData}
|
locationData={props.ipLocationData}
|
||||||
|
setToastMessage={props.setToastMessage}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -1000,6 +1007,8 @@ export default function Automations() {
|
|||||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||||
const isMobileWidth = useIsMobileWidth();
|
const isMobileWidth = useIsMobileWidth();
|
||||||
const { locationData, locationDataError, locationDataLoading } = useIPLocationData();
|
const { locationData, locationDataError, locationDataLoading } = useIPLocationData();
|
||||||
|
const [toastMessage, setToastMessage] = useState("");
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (newAutomationData) {
|
if (newAutomationData) {
|
||||||
@@ -1026,6 +1035,19 @@ export default function Automations() {
|
|||||||
}
|
}
|
||||||
}, [personalAutomations, allNewAutomations]);
|
}, [personalAutomations, allNewAutomations]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const toastTitle = `Automation`;
|
||||||
|
if (toastMessage) {
|
||||||
|
toast({
|
||||||
|
title: toastTitle,
|
||||||
|
description: toastMessage,
|
||||||
|
action: <ToastAction altText="Dismiss">Ok</ToastAction>,
|
||||||
|
variant: toastMessage.includes("Sorry") ? "destructive" : "default",
|
||||||
|
});
|
||||||
|
setToastMessage("");
|
||||||
|
}
|
||||||
|
}, [toastMessage]);
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return <InlineLoading message="Oops, something went wrong. Please refresh the page." />;
|
return <InlineLoading message="Oops, something went wrong. Please refresh the page." />;
|
||||||
|
|
||||||
@@ -1100,6 +1122,7 @@ export default function Automations() {
|
|||||||
authenticatedData={authenticatedData}
|
authenticatedData={authenticatedData}
|
||||||
isCreating={isCreating}
|
isCreating={isCreating}
|
||||||
ipLocationData={locationData}
|
ipLocationData={locationData}
|
||||||
|
setToastMessage={setToastMessage}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -1120,6 +1143,7 @@ export default function Automations() {
|
|||||||
isLoggedIn={authenticatedData ? true : false}
|
isLoggedIn={authenticatedData ? true : false}
|
||||||
setShowLoginPrompt={setShowLoginPrompt}
|
setShowLoginPrompt={setShowLoginPrompt}
|
||||||
setNewAutomationData={setNewAutomationData}
|
setNewAutomationData={setNewAutomationData}
|
||||||
|
setToastMessage={setToastMessage}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
{isLoading && <InlineLoading message="booting up your automations" />}
|
{isLoading && <InlineLoading message="booting up your automations" />}
|
||||||
@@ -1135,6 +1159,7 @@ export default function Automations() {
|
|||||||
locationData={locationData}
|
locationData={locationData}
|
||||||
isLoggedIn={authenticatedData ? true : false}
|
isLoggedIn={authenticatedData ? true : false}
|
||||||
setShowLoginPrompt={setShowLoginPrompt}
|
setShowLoginPrompt={setShowLoginPrompt}
|
||||||
|
setToastMessage={setToastMessage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{authenticatedData &&
|
{authenticatedData &&
|
||||||
@@ -1147,6 +1172,7 @@ export default function Automations() {
|
|||||||
locationData={locationData}
|
locationData={locationData}
|
||||||
isLoggedIn={authenticatedData ? true : false}
|
isLoggedIn={authenticatedData ? true : false}
|
||||||
setShowLoginPrompt={setShowLoginPrompt}
|
setShowLoginPrompt={setShowLoginPrompt}
|
||||||
|
setToastMessage={setToastMessage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1163,6 +1189,7 @@ export default function Automations() {
|
|||||||
isLoggedIn={authenticatedData ? true : false}
|
isLoggedIn={authenticatedData ? true : false}
|
||||||
setShowLoginPrompt={setShowLoginPrompt}
|
setShowLoginPrompt={setShowLoginPrompt}
|
||||||
suggestedCard={true}
|
suggestedCard={true}
|
||||||
|
setToastMessage={setToastMessage}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1741,7 +1741,7 @@ class AutomationAdapters:
|
|||||||
return {
|
return {
|
||||||
"id": automation.id,
|
"id": automation.id,
|
||||||
"subject": automation_metadata["subject"],
|
"subject": automation_metadata["subject"],
|
||||||
"query_to_run": re.sub(r"^/automated_task\s*", "", automation_metadata["query_to_run"]),
|
"query_to_run": automation_metadata["query_to_run"],
|
||||||
"scheduling_request": automation_metadata["scheduling_request"],
|
"scheduling_request": automation_metadata["scheduling_request"],
|
||||||
"schedule": schedule,
|
"schedule": schedule,
|
||||||
"crontime": crontime,
|
"crontime": crontime,
|
||||||
@@ -1783,6 +1783,19 @@ class AutomationAdapters:
|
|||||||
|
|
||||||
return automation
|
return automation
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def aget_automation(user: KhojUser, automation_id: str) -> Job:
|
||||||
|
# Perform validation checks
|
||||||
|
# Check if user is allowed to delete this automation id
|
||||||
|
if not automation_id.startswith(f"automation_{user.uuid}_"):
|
||||||
|
raise ValueError("Invalid automation id")
|
||||||
|
# Check if automation with this id exist
|
||||||
|
automation: Job = await sync_to_async(state.scheduler.get_job)(job_id=automation_id)
|
||||||
|
if not automation:
|
||||||
|
raise ValueError("Invalid automation id")
|
||||||
|
|
||||||
|
return automation
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_automation(user: KhojUser, automation_id: str):
|
def delete_automation(user: KhojUser, automation_id: str):
|
||||||
# Get valid, user-owned automation
|
# Get valid, user-owned automation
|
||||||
|
|||||||
@@ -935,7 +935,7 @@ AI: Here is one I found: "It's not denial. I'm just selective about the reality
|
|||||||
User: Hahah, nice! Show a new one every morning.
|
User: Hahah, nice! Show a new one every morning.
|
||||||
Khoj: {{
|
Khoj: {{
|
||||||
"crontime": "0 9 * * *",
|
"crontime": "0 9 * * *",
|
||||||
"query": "/automated_task Share a funny Calvin and Hobbes or Bill Watterson quote from my notes",
|
"query": "Share a funny Calvin and Hobbes or Bill Watterson quote from my notes",
|
||||||
"subject": "Your Calvin and Hobbes Quote for the Day"
|
"subject": "Your Calvin and Hobbes Quote for the Day"
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -955,7 +955,7 @@ AI: The latest released Khoj python package version is 1.5.0.
|
|||||||
User: Notify me when version 2.0.0 is released
|
User: Notify me when version 2.0.0 is released
|
||||||
Khoj: {{
|
Khoj: {{
|
||||||
"crontime": "0 10 * * *",
|
"crontime": "0 10 * * *",
|
||||||
"query": "/automated_task What is the latest released version of the Khoj python package?",
|
"query": "/automated_task /research What is the latest released version of the Khoj python package?",
|
||||||
"subject": "Khoj Python Package Version 2.0.0 Release"
|
"subject": "Khoj Python Package Version 2.0.0 Release"
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@@ -1101,33 +1101,33 @@ to_notify_or_not = PromptTemplate.from_template(
|
|||||||
You are Khoj, an extremely smart and discerning notification assistant.
|
You are Khoj, an extremely smart and discerning notification assistant.
|
||||||
- Decide whether the user should be notified of the AI's response using the Original User Query, Executed User Query and AI Response triplet.
|
- Decide whether the user should be notified of the AI's response using the Original User Query, Executed User Query and AI Response triplet.
|
||||||
- Notify the user only if the AI's response satisfies the user specified requirements.
|
- Notify the user only if the AI's response satisfies the user specified requirements.
|
||||||
- You should only respond with a "Yes" or "No". Do not say anything else.
|
- You should return a response with your reason and "Yes" or "No" decision in JSON format. Do not say anything else.
|
||||||
|
|
||||||
# Examples:
|
# Examples:
|
||||||
Original User Query: Hahah, nice! Show a new one every morning at 9am. My Current Location: Shanghai, China
|
Original User Query: Hahah, nice! Show a new one every morning at 9am. My Current Location: Shanghai, China
|
||||||
Executed User Query: Could you share a funny Calvin and Hobbes quote from my notes?
|
Executed User Query: Could you share a funny Calvin and Hobbes quote from my notes?
|
||||||
AI Reponse: Here is one I found: "It's not denial. I'm just selective about the reality I accept."
|
AI Reponse: Here is one I found: "It's not denial. I'm just selective about the reality I accept."
|
||||||
Khoj: Yes
|
Khoj: {{ "reason": "The AI has shared a funny Calvin and Hobbes quote." , "decision": "Yes" }}
|
||||||
|
|
||||||
Original User Query: Every evening check if it's going to rain tomorrow. Notify me only if I'll need an umbrella. My Current Location: Nairobi, Kenya
|
Original User Query: Every evening check if it's going to rain tomorrow. Notify me only if I'll need an umbrella. My Current Location: Nairobi, Kenya
|
||||||
Executed User Query: Is it going to rain tomorrow in Nairobi, Kenya
|
Executed User Query: Is it going to rain tomorrow in Nairobi, Kenya
|
||||||
AI Response: Tomorrow's forecast is sunny with a high of 28°C and a low of 18°C
|
AI Response: Tomorrow's forecast is sunny with a high of 28°C and a low of 18°C
|
||||||
Khoj: No
|
Khoj: {{ "reason": "It is not expected to rain tomorrow.", "decision": "No" }}
|
||||||
|
|
||||||
Original User Query: Tell me when version 2.0.0 is released. My Current Location: Mexico City, Mexico
|
Original User Query: Paint a sunset for me every evening. My Current Location: Shanghai, China
|
||||||
Executed User Query: Check if version 2.0.0 of the Khoj python package is released
|
Executed User Query: Paint a sunset in Shanghai, China
|
||||||
AI Response: The latest released Khoj python package version is 1.5.0.
|
|
||||||
Khoj: No
|
|
||||||
|
|
||||||
Original User Query: Paint me a sunset every evening. My Current Location: Shanghai, China
|
|
||||||
Executed User Query: Paint me a sunset in Shanghai, China
|
|
||||||
AI Response: https://khoj-generated-images.khoj.dev/user110/image78124.webp
|
AI Response: https://khoj-generated-images.khoj.dev/user110/image78124.webp
|
||||||
Khoj: Yes
|
Khoj: {{ "reason": "The AI has created an image.", "decision": "Yes" }}
|
||||||
|
|
||||||
Original User Query: Share a summary of the tasks I've completed at the end of the day. My Current Location: Oslo, Norway
|
Original User Query: Notify me when Khoj version 2.0.0 is released
|
||||||
Executed User Query: Share a summary of the tasks I've completed today.
|
Executed User Query: What is the latest released version of the Khoj python package
|
||||||
AI Response: I'm sorry, I couldn't find any relevant notes to respond to your message.
|
AI Response: The latest released Khoj python package version is 1.5.0.
|
||||||
Khoj: No
|
Khoj: {{ "reason": "Version 2.0.0 of Khoj has not been released yet." , "decision": "No" }}
|
||||||
|
|
||||||
|
Original User Query: Share a summary of the tasks I've completed at the end of the day.
|
||||||
|
Executed User Query: Generate a summary of the tasks I've completed today.
|
||||||
|
AI Response: You have completed the following tasks today: 1. Meeting with the team 2. Submit travel expense report
|
||||||
|
Khoj: {{ "reason": "The AI has provided a summary of completed tasks.", "decision": "Yes" }}
|
||||||
|
|
||||||
Original User Query: {original_query}
|
Original User Query: {original_query}
|
||||||
Executed User Query: {executed_query}
|
Executed User Query: {executed_query}
|
||||||
@@ -1137,6 +1137,26 @@ Khoj:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
automation_format_prompt = PromptTemplate.from_template(
|
||||||
|
"""
|
||||||
|
You are Khoj, a smart and creative researcher and writer with a knack for creating engaging content.
|
||||||
|
- You *CAN REMEMBER ALL NOTES and PERSONAL INFORMATION FOREVER* that the user ever shares with you.
|
||||||
|
- You *CAN* generate look-up real-time information from the internet, send notifications and answer questions based on the user's notes.
|
||||||
|
|
||||||
|
Convert the AI response into a clear, structured markdown report with section headings to improve readability.
|
||||||
|
Your response will be sent in the body of an email to the user.
|
||||||
|
Do not add an email subject. Never add disclaimers in your final response.
|
||||||
|
|
||||||
|
You are provided the following details for context.
|
||||||
|
|
||||||
|
{username}
|
||||||
|
Original User Query: {original_query}
|
||||||
|
Executed Chat Request: {executed_query}
|
||||||
|
AI Response: {response}
|
||||||
|
Khoj:
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
|
||||||
# System messages to user
|
# System messages to user
|
||||||
# --
|
# --
|
||||||
help_message = PromptTemplate.from_template(
|
help_message = PromptTemplate.from_template(
|
||||||
|
|||||||
@@ -38,15 +38,15 @@ from khoj.processor.conversation.offline.chat_model import extract_questions_off
|
|||||||
from khoj.processor.conversation.offline.whisper import transcribe_audio_offline
|
from khoj.processor.conversation.offline.whisper import transcribe_audio_offline
|
||||||
from khoj.processor.conversation.openai.gpt import extract_questions
|
from khoj.processor.conversation.openai.gpt import extract_questions
|
||||||
from khoj.processor.conversation.openai.whisper import transcribe_audio
|
from khoj.processor.conversation.openai.whisper import transcribe_audio
|
||||||
from khoj.processor.conversation.utils import defilter_query
|
from khoj.processor.conversation.utils import clean_json, defilter_query
|
||||||
from khoj.routers.helpers import (
|
from khoj.routers.helpers import (
|
||||||
ApiUserRateLimiter,
|
ApiUserRateLimiter,
|
||||||
ChatEvent,
|
ChatEvent,
|
||||||
CommonQueryParams,
|
CommonQueryParams,
|
||||||
ConversationCommandRateLimiter,
|
ConversationCommandRateLimiter,
|
||||||
acreate_title_from_query,
|
|
||||||
get_user_config,
|
get_user_config,
|
||||||
schedule_automation,
|
schedule_automation,
|
||||||
|
schedule_query,
|
||||||
update_telemetry_state,
|
update_telemetry_state,
|
||||||
)
|
)
|
||||||
from khoj.search_filter.date_filter import DateFilter
|
from khoj.search_filter.date_filter import DateFilter
|
||||||
@@ -566,7 +566,7 @@ def delete_automation(request: Request, automation_id: str) -> Response:
|
|||||||
|
|
||||||
@api.post("/automation", response_class=Response)
|
@api.post("/automation", response_class=Response)
|
||||||
@requires(["authenticated"])
|
@requires(["authenticated"])
|
||||||
async def post_automation(
|
def post_automation(
|
||||||
request: Request,
|
request: Request,
|
||||||
q: str,
|
q: str,
|
||||||
crontime: str,
|
crontime: str,
|
||||||
@@ -584,11 +584,15 @@ async def post_automation(
|
|||||||
if not cron_descriptor.get_description(crontime):
|
if not cron_descriptor.get_description(crontime):
|
||||||
return Response(content="Invalid crontime", status_code=400)
|
return Response(content="Invalid crontime", status_code=400)
|
||||||
|
|
||||||
|
# Infer subject, query to run
|
||||||
|
_, query_to_run, generated_subject = schedule_query(q, conversation_history={}, user=user)
|
||||||
|
subject = subject or generated_subject
|
||||||
|
|
||||||
# Normalize query parameters
|
# Normalize query parameters
|
||||||
# Add /automated_task prefix to query if not present
|
# Add /automated_task prefix to query if not present
|
||||||
q = q.strip()
|
query_to_run = query_to_run.strip()
|
||||||
if not q.startswith("/automated_task"):
|
if not query_to_run.startswith("/automated_task"):
|
||||||
query_to_run = f"/automated_task {q}"
|
query_to_run = f"/automated_task {query_to_run}"
|
||||||
|
|
||||||
# Normalize crontime for AP Scheduler CronTrigger
|
# Normalize crontime for AP Scheduler CronTrigger
|
||||||
crontime = crontime.strip()
|
crontime = crontime.strip()
|
||||||
@@ -603,24 +607,19 @@ async def post_automation(
|
|||||||
minute_value = crontime.split(" ")[0]
|
minute_value = crontime.split(" ")[0]
|
||||||
if not minute_value.isdigit():
|
if not minute_value.isdigit():
|
||||||
return Response(
|
return Response(
|
||||||
content="Recurrence of every X minutes is unsupported. Please create a less frequent schedule.",
|
content="Minute level recurrence is unsupported. Please create a less frequent schedule.",
|
||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not subject:
|
|
||||||
subject = await acreate_title_from_query(q, user)
|
|
||||||
|
|
||||||
title = f"Automation: {subject}"
|
|
||||||
|
|
||||||
# Create new Conversation Session associated with this new task
|
# Create new Conversation Session associated with this new task
|
||||||
conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app, title=title)
|
title = f"Automation: {subject}"
|
||||||
|
conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
|
||||||
calling_url = request.url.replace(query=f"{request.url.query}")
|
|
||||||
|
|
||||||
# Schedule automation with query_to_run, timezone, subject directly provided by user
|
# Schedule automation with query_to_run, timezone, subject directly provided by user
|
||||||
try:
|
try:
|
||||||
# Use the query to run as the scheduling request if the scheduling request is unset
|
# Use the query to run as the scheduling request if the scheduling request is unset
|
||||||
automation = await schedule_automation(
|
calling_url = request.url.replace(query=f"{request.url.query}")
|
||||||
|
automation = schedule_automation(
|
||||||
query_to_run, subject, crontime, timezone, q, user, calling_url, str(conversation.id)
|
query_to_run, subject, crontime, timezone, q, user, calling_url, str(conversation.id)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -691,11 +690,15 @@ def edit_job(
|
|||||||
logger.error(f"Error editing automation {automation_id} for {user.email}: {e}", exc_info=True)
|
logger.error(f"Error editing automation {automation_id} for {user.email}: {e}", exc_info=True)
|
||||||
return Response(content="Invalid automation", status_code=403)
|
return Response(content="Invalid automation", status_code=403)
|
||||||
|
|
||||||
|
# Infer subject, query to run
|
||||||
|
_, query_to_run, _ = schedule_query(q, conversation_history={}, user=user)
|
||||||
|
subject = subject
|
||||||
|
|
||||||
# Normalize query parameters
|
# Normalize query parameters
|
||||||
# Add /automated_task prefix to query if not present
|
# Add /automated_task prefix to query if not present
|
||||||
q = q.strip()
|
query_to_run = query_to_run.strip()
|
||||||
if not q.startswith("/automated_task"):
|
if not query_to_run.startswith("/automated_task"):
|
||||||
query_to_run = f"/automated_task {q}"
|
query_to_run = f"/automated_task {query_to_run}"
|
||||||
# Normalize crontime for AP Scheduler CronTrigger
|
# Normalize crontime for AP Scheduler CronTrigger
|
||||||
crontime = crontime.strip()
|
crontime = crontime.strip()
|
||||||
if len(crontime.split(" ")) > 5:
|
if len(crontime.split(" ")) > 5:
|
||||||
@@ -713,7 +716,7 @@ def edit_job(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Construct updated automation metadata
|
# Construct updated automation metadata
|
||||||
automation_metadata = json.loads(automation.name)
|
automation_metadata: dict[str, str] = json.loads(clean_json(automation.name))
|
||||||
automation_metadata["scheduling_request"] = q
|
automation_metadata["scheduling_request"] = q
|
||||||
automation_metadata["query_to_run"] = query_to_run
|
automation_metadata["query_to_run"] = query_to_run
|
||||||
automation_metadata["subject"] = subject.strip()
|
automation_metadata["subject"] = subject.strip()
|
||||||
|
|||||||
@@ -551,9 +551,37 @@ async def generate_online_subqueries(
|
|||||||
return {q}
|
return {q}
|
||||||
|
|
||||||
|
|
||||||
async def schedule_query(
|
def schedule_query(
|
||||||
q: str, conversation_history: dict, user: KhojUser, query_images: List[str] = None, tracer: dict = {}
|
q: str, conversation_history: dict, user: KhojUser, query_images: List[str] = None, tracer: dict = {}
|
||||||
) -> Tuple[str, ...]:
|
) -> Tuple[str, str, str]:
|
||||||
|
"""
|
||||||
|
Schedule the date, time to run the query. Assume the server timezone is UTC.
|
||||||
|
"""
|
||||||
|
chat_history = construct_chat_history(conversation_history)
|
||||||
|
|
||||||
|
crontime_prompt = prompts.crontime_prompt.format(
|
||||||
|
query=q,
|
||||||
|
chat_history=chat_history,
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_response = send_message_to_model_wrapper_sync(
|
||||||
|
crontime_prompt, query_images=query_images, response_type="json_object", user=user, tracer=tracer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate that the response is a non-empty, JSON-serializable list
|
||||||
|
try:
|
||||||
|
raw_response = raw_response.strip()
|
||||||
|
response: Dict[str, str] = json.loads(clean_json(raw_response))
|
||||||
|
if not response or not isinstance(response, Dict) or len(response) != 3:
|
||||||
|
raise AssertionError(f"Invalid response for scheduling query : {response}")
|
||||||
|
return response.get("crontime"), response.get("query"), response.get("subject")
|
||||||
|
except Exception:
|
||||||
|
raise AssertionError(f"Invalid response for scheduling query: {raw_response}")
|
||||||
|
|
||||||
|
|
||||||
|
async def aschedule_query(
|
||||||
|
q: str, conversation_history: dict, user: KhojUser, query_images: List[str] = None, tracer: dict = {}
|
||||||
|
) -> Tuple[str, str, str]:
|
||||||
"""
|
"""
|
||||||
Schedule the date, time to run the query. Assume the server timezone is UTC.
|
Schedule the date, time to run the query. Assume the server timezone is UTC.
|
||||||
"""
|
"""
|
||||||
@@ -571,7 +599,7 @@ async def schedule_query(
|
|||||||
# Validate that the response is a non-empty, JSON-serializable list
|
# Validate that the response is a non-empty, JSON-serializable list
|
||||||
try:
|
try:
|
||||||
raw_response = raw_response.strip()
|
raw_response = raw_response.strip()
|
||||||
response: Dict[str, str] = json.loads(raw_response)
|
response: Dict[str, str] = json.loads(clean_json(raw_response))
|
||||||
if not response or not isinstance(response, Dict) or len(response) != 3:
|
if not response or not isinstance(response, Dict) or len(response) != 3:
|
||||||
raise AssertionError(f"Invalid response for scheduling query : {response}")
|
raise AssertionError(f"Invalid response for scheduling query : {response}")
|
||||||
return response.get("crontime"), response.get("query"), response.get("subject")
|
return response.get("crontime"), response.get("query"), response.get("subject")
|
||||||
@@ -1065,6 +1093,7 @@ def send_message_to_model_wrapper_sync(
|
|||||||
system_message: str = "",
|
system_message: str = "",
|
||||||
response_type: str = "text",
|
response_type: str = "text",
|
||||||
user: KhojUser = None,
|
user: KhojUser = None,
|
||||||
|
query_images: List[str] = None,
|
||||||
query_files: str = "",
|
query_files: str = "",
|
||||||
tracer: dict = {},
|
tracer: dict = {},
|
||||||
):
|
):
|
||||||
@@ -1090,6 +1119,7 @@ def send_message_to_model_wrapper_sync(
|
|||||||
max_prompt_size=max_tokens,
|
max_prompt_size=max_tokens,
|
||||||
vision_enabled=vision_available,
|
vision_enabled=vision_available,
|
||||||
model_type=chat_model.model_type,
|
model_type=chat_model.model_type,
|
||||||
|
query_images=query_images,
|
||||||
query_files=query_files,
|
query_files=query_files,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1112,6 +1142,7 @@ def send_message_to_model_wrapper_sync(
|
|||||||
max_prompt_size=max_tokens,
|
max_prompt_size=max_tokens,
|
||||||
vision_enabled=vision_available,
|
vision_enabled=vision_available,
|
||||||
model_type=chat_model.model_type,
|
model_type=chat_model.model_type,
|
||||||
|
query_images=query_images,
|
||||||
query_files=query_files,
|
query_files=query_files,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1134,6 +1165,7 @@ def send_message_to_model_wrapper_sync(
|
|||||||
max_prompt_size=max_tokens,
|
max_prompt_size=max_tokens,
|
||||||
vision_enabled=vision_available,
|
vision_enabled=vision_available,
|
||||||
model_type=chat_model.model_type,
|
model_type=chat_model.model_type,
|
||||||
|
query_images=query_images,
|
||||||
query_files=query_files,
|
query_files=query_files,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1154,6 +1186,7 @@ def send_message_to_model_wrapper_sync(
|
|||||||
max_prompt_size=max_tokens,
|
max_prompt_size=max_tokens,
|
||||||
vision_enabled=vision_available,
|
vision_enabled=vision_available,
|
||||||
model_type=chat_model.model_type,
|
model_type=chat_model.model_type,
|
||||||
|
query_images=query_images,
|
||||||
query_files=query_files,
|
query_files=query_files,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1634,6 +1667,25 @@ class CommonQueryParamsClass:
|
|||||||
CommonQueryParams = Annotated[CommonQueryParamsClass, Depends()]
|
CommonQueryParams = Annotated[CommonQueryParamsClass, Depends()]
|
||||||
|
|
||||||
|
|
||||||
|
def format_automation_response(scheduling_request: str, executed_query: str, ai_response: str, user: KhojUser) -> bool:
|
||||||
|
"""
|
||||||
|
Format the AI response to send in automation email to user.
|
||||||
|
"""
|
||||||
|
name = get_user_name(user)
|
||||||
|
if name:
|
||||||
|
username = prompts.user_name.format(name=name)
|
||||||
|
|
||||||
|
automation_format_prompt = prompts.automation_format_prompt.format(
|
||||||
|
original_query=scheduling_request,
|
||||||
|
executed_query=executed_query,
|
||||||
|
response=ai_response,
|
||||||
|
username=username,
|
||||||
|
)
|
||||||
|
|
||||||
|
with timer("Chat actor: Format automation response", logger):
|
||||||
|
return send_message_to_model_wrapper_sync(automation_format_prompt, user=user)
|
||||||
|
|
||||||
|
|
||||||
def should_notify(original_query: str, executed_query: str, ai_response: str, user: KhojUser) -> bool:
|
def should_notify(original_query: str, executed_query: str, ai_response: str, user: KhojUser) -> bool:
|
||||||
"""
|
"""
|
||||||
Decide whether to notify the user of the AI response.
|
Decide whether to notify the user of the AI response.
|
||||||
@@ -1651,12 +1703,19 @@ def should_notify(original_query: str, executed_query: str, ai_response: str, us
|
|||||||
with timer("Chat actor: Decide to notify user of automation response", logger):
|
with timer("Chat actor: Decide to notify user of automation response", logger):
|
||||||
try:
|
try:
|
||||||
# TODO Replace with async call so we don't have to maintain a sync version
|
# TODO Replace with async call so we don't have to maintain a sync version
|
||||||
response = send_message_to_model_wrapper_sync(to_notify_or_not, user)
|
raw_response = send_message_to_model_wrapper_sync(to_notify_or_not, user=user, response_type="json_object")
|
||||||
should_notify_result = "no" not in response.lower()
|
response = json.loads(raw_response)
|
||||||
logger.info(f'Decided to {"not " if not should_notify_result else ""}notify user of automation response.')
|
should_notify_result = response["decision"] == "Yes"
|
||||||
|
reason = response.get("reason", "unknown")
|
||||||
|
logger.info(
|
||||||
|
f'Decided to {"not " if not should_notify_result else ""}notify user of automation response because of reason: {reason}.'
|
||||||
|
)
|
||||||
return should_notify_result
|
return should_notify_result
|
||||||
except:
|
except Exception as e:
|
||||||
logger.warning(f"Fallback to notify user of automation response as failed to infer should notify or not.")
|
logger.warning(
|
||||||
|
f"Fallback to notify user of automation response as failed to infer should notify or not. {e}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -1751,10 +1810,12 @@ def scheduled_chat(
|
|||||||
if should_notify(
|
if should_notify(
|
||||||
original_query=scheduling_request, executed_query=cleaned_query, ai_response=ai_response, user=user
|
original_query=scheduling_request, executed_query=cleaned_query, ai_response=ai_response, user=user
|
||||||
):
|
):
|
||||||
|
formatted_response = format_automation_response(scheduling_request, cleaned_query, ai_response, user)
|
||||||
|
|
||||||
if is_resend_enabled():
|
if is_resend_enabled():
|
||||||
send_task_email(user.get_short_name(), user.email, cleaned_query, ai_response, subject, is_image)
|
send_task_email(user.get_short_name(), user.email, cleaned_query, formatted_response, subject, is_image)
|
||||||
else:
|
else:
|
||||||
return raw_response
|
return formatted_response
|
||||||
|
|
||||||
|
|
||||||
async def create_automation(
|
async def create_automation(
|
||||||
@@ -1766,12 +1827,66 @@ async def create_automation(
|
|||||||
conversation_id: str = None,
|
conversation_id: str = None,
|
||||||
tracer: dict = {},
|
tracer: dict = {},
|
||||||
):
|
):
|
||||||
crontime, query_to_run, subject = await schedule_query(q, meta_log, user, tracer=tracer)
|
crontime, query_to_run, subject = await aschedule_query(q, meta_log, user, tracer=tracer)
|
||||||
job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url, conversation_id)
|
job = await aschedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url, conversation_id)
|
||||||
return job, crontime, query_to_run, subject
|
return job, crontime, query_to_run, subject
|
||||||
|
|
||||||
|
|
||||||
async def schedule_automation(
|
def schedule_automation(
|
||||||
|
query_to_run: str,
|
||||||
|
subject: str,
|
||||||
|
crontime: str,
|
||||||
|
timezone: str,
|
||||||
|
scheduling_request: str,
|
||||||
|
user: KhojUser,
|
||||||
|
calling_url: URL,
|
||||||
|
conversation_id: str,
|
||||||
|
):
|
||||||
|
# Disable minute level automation recurrence
|
||||||
|
minute_value = crontime.split(" ")[0]
|
||||||
|
if not minute_value.isdigit():
|
||||||
|
# Run automation at some random minute (to distribute request load) instead of running every X minutes
|
||||||
|
crontime = " ".join([str(math.floor(random() * 60))] + crontime.split(" ")[1:])
|
||||||
|
|
||||||
|
user_timezone = pytz.timezone(timezone)
|
||||||
|
trigger = CronTrigger.from_crontab(crontime, user_timezone)
|
||||||
|
trigger.jitter = 60
|
||||||
|
# Generate id and metadata used by task scheduler and process locks for the task runs
|
||||||
|
job_metadata = json.dumps(
|
||||||
|
{
|
||||||
|
"query_to_run": query_to_run,
|
||||||
|
"scheduling_request": scheduling_request,
|
||||||
|
"subject": subject,
|
||||||
|
"crontime": crontime,
|
||||||
|
"conversation_id": str(conversation_id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
query_id = hashlib.md5(f"{query_to_run}_{crontime}".encode("utf-8")).hexdigest()
|
||||||
|
job_id = f"automation_{user.uuid}_{query_id}"
|
||||||
|
job = state.scheduler.add_job(
|
||||||
|
run_with_process_lock,
|
||||||
|
trigger=trigger,
|
||||||
|
args=(
|
||||||
|
scheduled_chat,
|
||||||
|
f"{ProcessLock.Operation.SCHEDULED_JOB}_{user.uuid}_{query_id}",
|
||||||
|
),
|
||||||
|
kwargs={
|
||||||
|
"query_to_run": query_to_run,
|
||||||
|
"scheduling_request": scheduling_request,
|
||||||
|
"subject": subject,
|
||||||
|
"user": user,
|
||||||
|
"calling_url": calling_url,
|
||||||
|
"job_id": job_id,
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
},
|
||||||
|
id=job_id,
|
||||||
|
name=job_metadata,
|
||||||
|
max_instances=2, # Allow second instance to kill any previous instance with stale lock
|
||||||
|
)
|
||||||
|
return job
|
||||||
|
|
||||||
|
|
||||||
|
async def aschedule_automation(
|
||||||
query_to_run: str,
|
query_to_run: str,
|
||||||
subject: str,
|
subject: str,
|
||||||
crontime: str,
|
crontime: str,
|
||||||
|
|||||||
@@ -636,11 +636,11 @@ async def test_infer_webpage_urls_actor_extracts_correct_links(chat_client, defa
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_infer_task_scheduling_request(
|
def test_infer_task_scheduling_request(
|
||||||
chat_client, user_query, expected_crontime, expected_qs, unexpected_qs, default_user2
|
chat_client, user_query, expected_crontime, expected_qs, unexpected_qs, default_user2
|
||||||
):
|
):
|
||||||
# Act
|
# Act
|
||||||
crontime, inferred_query, _ = await schedule_query(user_query, {}, default_user2)
|
crontime, inferred_query, _ = schedule_query(user_query, {}, default_user2)
|
||||||
inferred_query = inferred_query.lower()
|
inferred_query = inferred_query.lower()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
|||||||
Reference in New Issue
Block a user