Skip to content

Commit

Permalink
Switch to let Khoj infer chat query based on user automation query
Browse files Browse the repository at this point in the history
This tries to decouple the automation query from the chat query. So
the chat model doesn't have to know it is running in an automation
context or figure how to notify user or send automation response. It
just has to respond to the AI generated `query_to_run' corresponding
to the `scheduling_request` automation by the user.

For example, a `scheduling_request' of `notify me when X happens'
results in the automation calling the chat api with a `query_to_run`
like `tell me about X` and deciding if to notify based on information
gathered about X from the scheduled run. If these two are not
decoupled, the chat model may respond with how it can notify about X
instead of just asking about it.

Swap query_to_run with scheduling_request on the automation web page
  • Loading branch information
debanjum committed Dec 27, 2024
1 parent 3600a9a commit 6d219dc
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 52 deletions.
58 changes: 29 additions & 29 deletions src/interface/web/app/automations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,68 +167,68 @@ const timestamp = Date.now();
const suggestedAutomationsMetadata: AutomationsData[] = [
{
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",
schedule: "9AM every Monday",
next: "Next run at 9AM on Monday",
crontime: "0 9 * * 1",
id: timestamp,
scheduling_request: "",
},
{
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: "",
query_to_run: "",
},
{
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",
schedule: "9PM on every Wednesday",
next: "Next run at 9PM on Wednesday",
crontime: "0 21 * * 3",
id: timestamp + 2,
scheduling_request: "",
query_to_run: "",
},
{
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.",
schedule: "9AM on every weekday",
next: "Next run at 9AM on Monday",
crontime: "0 9 * * *",
id: timestamp + 3,
scheduling_request: "",
query_to_run: "",
},
{
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",
next: "Next run at 5PM today",
crontime: "0 17 * * *",
id: timestamp + 5,
scheduling_request: "",
query_to_run: "",
},
{
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.",
schedule: "9AM every Friday",
next: "Next run at 9AM on Friday",
crontime: "0 9 * * 5",
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) {
const encodedSubject = encodeURIComponent(automation.subject);
const encodedQuery = encodeURIComponent(automation.query_to_run);
const encodedQuery = encodeURIComponent(automation.scheduling_request);
const encodedCrontime = encodeURIComponent(automation.crontime);

const shareLink = `${window.location.origin}/automations?subject=${encodedSubject}&query=${encodedQuery}&crontime=${encodedCrontime}`;
Expand Down Expand Up @@ -391,7 +391,7 @@ function AutomationsCard(props: AutomationsCardProps) {
</CardTitle>
</CardHeader>
<CardContent className="text-secondary-foreground break-all">
{updatedAutomationData?.query_to_run || automation.query_to_run}
{updatedAutomationData?.scheduling_request || automation.scheduling_request}
</CardContent>
<CardFooter className="flex flex-col items-start md:flex-row md:justify-between md:items-center gap-2">
<div className="flex gap-2">
Expand Down Expand Up @@ -451,8 +451,8 @@ function SharedAutomationCard(props: SharedAutomationCardProps) {
const automation: AutomationsData = {
id: 0,
subject: decodeURIComponent(subject),
query_to_run: decodeURIComponent(query),
scheduling_request: "",
scheduling_request: decodeURIComponent(query),
query_to_run: "",
schedule: cronToHumanReadableString(decodeURIComponent(crontime)),
crontime: decodeURIComponent(crontime),
next: "",
Expand Down Expand Up @@ -480,7 +480,7 @@ const EditAutomationSchema = z.object({
dayOfWeek: z.optional(z.number()),
dayOfMonth: z.optional(z.string()),
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 {
Expand All @@ -507,7 +507,7 @@ function EditCard(props: EditCardProps) {
? getTimeRecurrenceFromCron(automation.crontime)
: "12:00 PM",
dayOfMonth: automation?.crontime ? getDayOfMonthFromCron(automation.crontime) : "1",
queryToRun: automation?.query_to_run,
schedulingRequest: automation?.scheduling_request,
},
});

Expand All @@ -520,7 +520,7 @@ function EditCard(props: EditCardProps) {
);

let updateQueryUrl = `/api/automation?`;
updateQueryUrl += `q=${encodeURIComponent(values.queryToRun)}`;
updateQueryUrl += `q=${encodeURIComponent(values.schedulingRequest)}`;
if (automation?.id && !props.createNew) {
updateQueryUrl += `&automation_id=${encodeURIComponent(automation.id)}`;
}
Expand Down Expand Up @@ -829,7 +829,7 @@ function AutomationModificationForm(props: AutomationModificationFormProps) {
)}
<FormField
control={props.form.control}
name="queryToRun"
name="schedulingRequest"
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel>Instructions</FormLabel>
Expand All @@ -850,7 +850,7 @@ function AutomationModificationForm(props: AutomationModificationFormProps) {
</FormControl>
<FormMessage />
{errors.subject && (
<FormMessage>{errors.queryToRun?.message}</FormMessage>
<FormMessage>{errors.schedulingRequest?.message}</FormMessage>
)}
</FormItem>
)}
Expand Down
13 changes: 13 additions & 0 deletions src/khoj/database/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,19 @@ def get_automation(user: KhojUser, automation_id: str) -> Job:

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
def delete_automation(user: KhojUser, automation_id: str):
# Get valid, user-owned automation
Expand Down
4 changes: 2 additions & 2 deletions src/khoj/processor/conversation/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,7 @@
User: Hahah, nice! Show a new one every morning.
Khoj: {{
"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"
}}
Expand All @@ -955,7 +955,7 @@
User: Notify me when version 2.0.0 is released
Khoj: {{
"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"
}}
Expand Down
48 changes: 27 additions & 21 deletions src/khoj/routers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
acreate_title_from_query,
get_user_config,
schedule_automation,
schedule_query,
update_telemetry_state,
)
from khoj.search_filter.date_filter import DateFilter
Expand Down Expand Up @@ -584,11 +585,15 @@ async def post_automation(
if not cron_descriptor.get_description(crontime):
return Response(content="Invalid crontime", status_code=400)

# Infer subject, query to run
_, query_to_run, generated_subject = await schedule_query(q, conversation_history={}, user=user)
subject = subject or generated_subject

# Normalize query parameters
# Add /automated_task prefix to query if not present
q = q.strip()
if not q.startswith("/automated_task"):
query_to_run = f"/automated_task {q}"
query_to_run = query_to_run.strip()
if not query_to_run.startswith("/automated_task"):
query_to_run = f"/automated_task {query_to_run}"

# Normalize crontime for AP Scheduler CronTrigger
crontime = crontime.strip()
Expand All @@ -603,23 +608,18 @@ async def post_automation(
minute_value = crontime.split(" ")[0]
if not minute_value.isdigit():
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,
)

if not subject:
subject = await acreate_title_from_query(q, user)

title = f"Automation: {subject}"

# Create new Conversation Session associated with this new task
title = f"Automation: {subject}"
conversation = await ConversationAdapters.acreate_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
try:
# Use the query to run as the scheduling request if the scheduling request is unset
calling_url = request.url.replace(query=f"{request.url.query}")
automation = await schedule_automation(
query_to_run, subject, crontime, timezone, q, user, calling_url, str(conversation.id)
)
Expand Down Expand Up @@ -665,7 +665,7 @@ def trigger_manual_job(

@api.put("/automation", response_class=Response)
@requires(["authenticated"])
def edit_job(
async def edit_job(
request: Request,
automation_id: str,
q: Optional[str],
Expand All @@ -686,16 +686,20 @@ def edit_job(

# Check, get automation to edit
try:
automation: Job = AutomationAdapters.get_automation(user, automation_id)
automation: Job = await AutomationAdapters.aget_automation(user, automation_id)
except ValueError as e:
logger.error(f"Error editing automation {automation_id} for {user.email}: {e}", exc_info=True)
return Response(content="Invalid automation", status_code=403)

# Infer subject, query to run
_, query_to_run, _ = await schedule_query(q, conversation_history={}, user=user)
subject = subject

# Normalize query parameters
# Add /automated_task prefix to query if not present
q = q.strip()
if not q.startswith("/automated_task"):
query_to_run = f"/automated_task {q}"
query_to_run = query_to_run.strip()
if not query_to_run.startswith("/automated_task"):
query_to_run = f"/automated_task {query_to_run}"
# Normalize crontime for AP Scheduler CronTrigger
crontime = crontime.strip()
if len(crontime.split(" ")) > 5:
Expand Down Expand Up @@ -724,13 +728,15 @@ def edit_job(
title = f"Automation: {subject}"

# Create new Conversation Session associated with this new task
conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
conversation = await ConversationAdapters.acreate_conversation_session(
user, request.user.client_app, title=title
)

conversation_id = str(conversation.id)
automation_metadata["conversation_id"] = conversation_id

# Modify automation with updated query, subject
automation.modify(
await sync_to_async(automation.modify)(
name=json.dumps(automation_metadata),
kwargs={
"query_to_run": query_to_run,
Expand All @@ -746,11 +752,11 @@ def edit_job(
user_timezone = pytz.timezone(timezone)
trigger = CronTrigger.from_crontab(crontime, user_timezone)
if automation.trigger != trigger:
automation.reschedule(trigger=trigger)
await sync_to_async(automation.reschedule)(trigger=trigger)

# Collate info about the updated user automation
automation = AutomationAdapters.get_automation(user, automation.id)
automation_info = AutomationAdapters.get_automation_metadata(user, automation)
automation = await AutomationAdapters.aget_automation(user, automation.id)
automation_info = await sync_to_async(AutomationAdapters.get_automation_metadata)(user, automation)

# Return modified automation information as a JSON response
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)

0 comments on commit 6d219dc

Please sign in to comment.