diff --git a/src/interface/web/app/automations/page.tsx b/src/interface/web/app/automations/page.tsx index a4becd1b..39448ed2 100644 --- a/src/interface/web/app/automations/page.tsx +++ b/src/interface/web/app/automations/page.tsx @@ -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}`; @@ -391,7 +391,7 @@ function AutomationsCard(props: AutomationsCardProps) { - {updatedAutomationData?.query_to_run || automation.query_to_run} + {updatedAutomationData?.scheduling_request || automation.scheduling_request}
@@ -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: "", @@ -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 { @@ -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, }, }); @@ -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)}`; } @@ -829,7 +829,7 @@ function AutomationModificationForm(props: AutomationModificationFormProps) { )} ( Instructions @@ -850,7 +850,7 @@ function AutomationModificationForm(props: AutomationModificationFormProps) { {errors.subject && ( - {errors.queryToRun?.message} + {errors.schedulingRequest?.message} )} )} diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index c707a08f..9342b913 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -1783,6 +1783,19 @@ class AutomationAdapters: 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 diff --git a/src/khoj/processor/conversation/prompts.py b/src/khoj/processor/conversation/prompts.py index 0a021135..357f406e 100644 --- a/src/khoj/processor/conversation/prompts.py +++ b/src/khoj/processor/conversation/prompts.py @@ -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. 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" }} @@ -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 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" }} diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 3f58ca1a..b936488f 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -47,6 +47,7 @@ from khoj.routers.helpers import ( acreate_title_from_query, get_user_config, schedule_automation, + schedule_query, update_telemetry_state, ) from khoj.search_filter.date_filter import DateFilter @@ -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() @@ -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) ) @@ -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], @@ -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: @@ -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, @@ -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)