diff --git a/src/khoj/interface/email/task.html b/src/khoj/interface/email/task.html index 86a801ac..1e78ce34 100644 --- a/src/khoj/interface/email/task.html +++ b/src/khoj/interface/email/task.html @@ -1,7 +1,7 @@ - Khoj AI - Task + Khoj AI - Automation @@ -13,7 +13,7 @@

Your Open, Personal AI

Hey {{name}}!

-

I've shared your scheduled task results below:

+

I've shared your automation results below:

@@ -23,8 +23,8 @@

{{result}}

-

The scheduled query I ran on your behalf: {query}

-

You can view, delete and manage your scheduled tasks via the settings page

+

The automation query I ran on your behalf: {{query}}

+

You can view, delete your automations via the settings page

- Khoj

diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index a17cd0f9..e9c24d06 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -272,17 +272,17 @@ {% endif %} {% endif %} -
-

Scheduled Tasks

-
+
+

Automations

+
- Scheduled Tasks -

Tasks

+ Automations +

Automations

-

Manage your scheduled tasks

+

Manage your automations

- +
@@ -292,10 +292,10 @@ - +
NameActions
-
@@ -661,46 +661,42 @@ // List user's API keys on page load listApiKeys(); - function deleteTask(taskId) { - const scheduledTaskList = document.getElementById("scheduled-tasks-list"); - fetch(`/api/task?task_id=${taskId}`, { + function deleteAutomation(automationId) { + const AutomationList = document.getElementById("automations-list"); + fetch(`/api/automation?automation_id=${automationId}`, { method: 'DELETE', }) .then(response => { if (response.status == 200) { - const scheduledTaskItem = document.getElementById(`scheduled-task-item-${taskId}`); - scheduledTaskList.removeChild(scheduledTaskItem); + const AutomationItem = document.getElementById(`automation-item-${automationId}`); + AutomationList.removeChild(AutomationItem); } }); } - function generateTaskRow(taskObj) { - let taskId = taskObj.id; - let taskSchedulingRequest = taskObj.scheduling_request; - let taskQuery = taskObj.query_to_run; - let taskSubject = taskObj.subject; - let taskNextRun = `Next run at ${taskObj.next}`; - let taskSchedule = taskObj.schedule; + function generateAutomationRow(automationObj) { + let automationId = automationObj.id; + let automationNextRun = `Next run at ${automationObj.next}`; return ` - - ${taskSubject} - ${taskSchedulingRequest} - ${taskQuery} - ${taskSchedule} + + ${automationObj.subject} + ${automationObj.scheduling_request} + ${automationObj.query_to_run} + ${automationObj.schedule} - Delete Task + Delete Automation `; } - function listScheduledTasks() { - const scheduledTasksList = document.getElementById("scheduled-tasks-list"); - fetch('/api/tasks') + function listAutomations() { + const AutomationsList = document.getElementById("automations-list"); + fetch('/api/automations') .then(response => response.json()) - .then(tasks => { - if (!tasks?.length > 0) return; - scheduledTasksList.innerHTML = tasks.map(generateTaskRow).join(""); + .then(automations => { + if (!automations?.length > 0) return; + AutomationsList.innerHTML = automations.map(generateAutomationRow).join(""); }); } @@ -713,8 +709,8 @@ }); } - // List user's scheduled tasks on page load - listScheduledTasks(); + // List user's automations on page load + listAutomations(); function removeFile(path) { fetch('/api/config/data/file?filename=' + path, { diff --git a/src/khoj/processor/conversation/prompts.py b/src/khoj/processor/conversation/prompts.py index dd22ecd3..a95bbe73 100644 --- a/src/khoj/processor/conversation/prompts.py +++ b/src/khoj/processor/conversation/prompts.py @@ -507,7 +507,7 @@ Khoj: """.strip() ) -# Schedule task +# Automations # -- crontime_prompt = PromptTemplate.from_template( """ @@ -525,7 +525,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": "/task Share a funny Calvin and Hobbes or Bill Watterson quote from my notes", + "query": "/automated_task Share a funny Calvin and Hobbes or Bill Watterson quote from my notes", "subject": "Your Calvin and Hobbes Quote for the Day" }} @@ -534,7 +534,7 @@ Khoj: {{ User: Every monday evening at 6 share the top posts on hacker news from last week. Format it as a newsletter Khoj: {{ "crontime": "0 18 * * 1", - "query": "/task Top posts last week on Hacker News", + "query": "/automated_task Top posts last week on Hacker News", "subject": "Your Weekly Top Hacker News Posts Newsletter" }} @@ -545,7 +545,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": "/task What is the latest released version of the Khoj python package?", + "query": "/automated_task What is the latest released version of the Khoj python package?", "subject": "Khoj Python Package Version 2.0.0 Release" }} @@ -554,7 +554,7 @@ Khoj: {{ User: Tell me the latest local tech news on the first sunday of every month Khoj: {{ "crontime": "0 8 1-7 * 0", - "query": "/task Find the latest local tech, AI and engineering news. Format it as a newsletter.", + "query": "/automated_task Find the latest local tech, AI and engineering news. Format it as a newsletter.", "subject": "Your Monthly Dose of Local Tech News" }} @@ -563,7 +563,7 @@ Khoj: {{ User: Inform me when the national election results are declared. Run task at 4pm every thursday. Khoj: {{ "crontime": "0 16 * * 4", - "query": "/task Check if the Indian national election results are officially declared", + "query": "/automated_task Check if the Indian national election results are officially declared", "subject": "Indian National Election Results Declared" }} diff --git a/src/khoj/processor/conversation/utils.py b/src/khoj/processor/conversation/utils.py index 6ef7016d..775848c8 100644 --- a/src/khoj/processor/conversation/utils.py +++ b/src/khoj/processor/conversation/utils.py @@ -102,7 +102,7 @@ def save_to_conversation_log( intent_type: str = "remember", client_application: ClientApplication = None, conversation_id: int = None, - job_id: str = None, + automation_id: str = None, ): user_message_time = user_message_time or datetime.now().strftime("%Y-%m-%d %H:%M:%S") updated_conversation = message_to_log( @@ -113,7 +113,7 @@ def save_to_conversation_log( "context": compiled_references, "intent": {"inferred-queries": inferred_queries, "type": intent_type}, "onlineContext": online_results, - "jobId": job_id, + "automationId": automation_id, }, conversation_log=meta_log.get("chat", []), ) diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 114e2f11..88148d78 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -391,59 +391,59 @@ def user_info(request: Request) -> Response: return Response(content=json.dumps(user_info), media_type="application/json", status_code=200) -@api.get("/tasks", response_class=Response) +@api.get("/automations", response_class=Response) @requires(["authenticated"]) -def get_jobs(request: Request) -> Response: +def get_automations(request: Request) -> Response: user: KhojUser = request.user.object - tasks: list[Job] = state.scheduler.get_jobs() + automations: list[Job] = state.scheduler.get_jobs() - # Collate all tasks assigned by user that are still active - tasks_info = [] - for task in tasks: - if task.id.startswith(f"job_{user.uuid}_"): - task_metadata = json.loads(task.name) - schedule = ( - f'{cron_descriptor.get_description(task_metadata["crontime"])} {task.next_run_time.strftime("%Z")}' - ) - tasks_info.append( + # Collate all automations created by user that are still active + automations_info = [] + for automation in automations: + if automation.id.startswith(f"automation_{user.uuid}_"): + automation_metadata = json.loads(automation.name) + crontime = automation_metadata["crontime"] + timezone = automation.next_run_time.strftime("%Z") + schedule = f"{cron_descriptor.get_description(crontime)} {timezone}" + automations_info.append( { - "id": task.id, - "subject": task_metadata["subject"], - "query_to_run": re.sub(r"^/task\s*", "", task_metadata["query_to_run"]), - "scheduling_request": task_metadata["scheduling_request"], + "id": automation.id, + "subject": automation_metadata["subject"], + "query_to_run": re.sub(r"^/automated_task\s*", "", automation_metadata["query_to_run"]), + "scheduling_request": automation_metadata["scheduling_request"], "schedule": schedule, - "next": task.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z"), + "next": automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z"), } ) # Return tasks information as a JSON response - return Response(content=json.dumps(tasks_info), media_type="application/json", status_code=200) + return Response(content=json.dumps(automations_info), media_type="application/json", status_code=200) -@api.delete("/task", response_class=Response) +@api.delete("/automation", response_class=Response) @requires(["authenticated"]) -def delete_job(request: Request, task_id: str) -> Response: +def delete_automation(request: Request, automation_id: str) -> Response: user: KhojUser = request.user.object # Perform validation checks - # Check if user is allowed to delete this task id - if not task_id.startswith(f"job_{user.uuid}_"): + # Check if user is allowed to delete this automation id + if not automation_id.startswith(f"automation_{user.uuid}_"): return Response(content="Unauthorized job deletion request", status_code=403) - # Check if task with this task id exist - task: Job = state.scheduler.get_job(job_id=task_id) - if not task: + # Check if automation with this id exist + automation: Job = state.scheduler.get_job(job_id=automation_id) + if not automation: return Response(content="Invalid job", status_code=403) # Collate info about user task to be deleted - task_metadata = json.loads(task.name) - task_info = { - "id": task.id, - "name": task_metadata["inferred_query"], - "next": task.next_run_time.strftime("%Y-%m-%d %H:%MS"), + automation_metadata = json.loads(automation.name) + automation_info = { + "id": automation.id, + "name": automation_metadata["query_to_run"], + "next": automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z"), } # Delete job - task.remove() + automation.remove() - # Return delete task information as a JSON response - return Response(content=json.dumps(task_info), media_type="application/json", status_code=200) + # Return deleted automation information as a JSON response + return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200) diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 5aee7ac0..e4b1ad2c 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -37,7 +37,7 @@ from khoj.routers.helpers import ( agenerate_chat_response, aget_relevant_information_sources, aget_relevant_output_modes, - create_scheduled_task, + create_automation, get_conversation_command, is_ready_to_chat, text_to_image, @@ -217,7 +217,8 @@ async def chat_options( ) -> Response: cmd_options = {} for cmd in ConversationCommand: - cmd_options[cmd.value] = command_descriptions[cmd] + if cmd in command_descriptions: + cmd_options[cmd.value] = command_descriptions[cmd] update_telemetry_state( request=request, @@ -373,14 +374,14 @@ async def websocket_endpoint( continue meta_log = conversation.conversation_log - is_task = conversation_commands == [ConversationCommand.Task] + is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask] - if conversation_commands == [ConversationCommand.Default] or is_task: + if conversation_commands == [ConversationCommand.Default] or is_automated_task: conversation_commands = await aget_relevant_information_sources(q, meta_log) conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands]) await send_status_update(f"**🗃️ Chose Data Sources to Search:** {conversation_commands_str}") - mode = await aget_relevant_output_modes(q, meta_log, is_task) + mode = await aget_relevant_output_modes(q, meta_log, is_automated_task) await send_status_update(f"**🧑🏾‍💻 Decided Response Mode:** {mode.value}") if mode not in conversation_commands: conversation_commands.append(mode) @@ -389,29 +390,31 @@ async def websocket_endpoint( await conversation_command_rate_limiter.update_and_check_if_valid(websocket, cmd) q = q.replace(f"/{cmd.value}", "").strip() - if ConversationCommand.Reminder in conversation_commands: + if ConversationCommand.Automation in conversation_commands: try: - job, crontime, inferred_query, subject = await create_scheduled_task( + automation, crontime, query_to_run, subject = await create_automation( q, location, timezone, user, websocket.url, meta_log ) except Exception as e: logger.error(f"Error scheduling task {q} for {user.email}: {e}") - await send_complete_llm_response(f"Unable to schedule task. Ensure the task doesn't already exist.") + await send_complete_llm_response( + f"Unable to create automation. Ensure the automation doesn't already exist." + ) continue # Display next run time in user timezone instead of UTC - schedule = f'{cron_descriptor.get_description(crontime)} {job.next_run_time.strftime("%Z")}' - next_run_time = job.next_run_time.strftime("%Y-%m-%d %H:%M %Z") - # Remove /task prefix from inferred_query - unprefixed_inferred_query = re.sub(r"^\/task\s*", "", inferred_query) - # Create the scheduled task response + schedule = f'{cron_descriptor.get_description(crontime)} {automation.next_run_time.strftime("%Z")}' + next_run_time = automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z") + # Remove /automated_task prefix from inferred_query + unprefixed_query_to_run = re.sub(r"^\/automated_task\s*", "", query_to_run) + # Create the automation response llm_response = f""" - ### 🕒 Scheduled Task + ### 🕒 Automation - Subject: **{subject}** -- Query: "{unprefixed_inferred_query}" +- Query to Run: "{unprefixed_query_to_run}" - Schedule: `{schedule}` - Next Run At: {next_run_time} -Manage your tasks [here](/config#tasks). +Manage your tasks [here](/config#automations). """.strip() await sync_to_async(save_to_conversation_log)( @@ -420,11 +423,11 @@ Manage your tasks [here](/config#tasks). user, meta_log, user_message_time, - intent_type="reminder", + intent_type="automation", client_application=websocket.user.client_app, conversation_id=conversation_id, - inferred_queries=[inferred_query], - job_id=job.id, + inferred_queries=[query_to_run], + automation_id=automation.id, ) common = CommonQueryParamsClass( client=websocket.user.client_app, @@ -621,7 +624,7 @@ async def chat( else: meta_log = conversation.conversation_log - is_task = conversation_commands == [ConversationCommand.Task] + is_task = conversation_commands == [ConversationCommand.AutomatedTask] if conversation_commands == [ConversationCommand.Default] or is_task: conversation_commands = await aget_relevant_information_sources(q, meta_log) @@ -640,32 +643,32 @@ async def chat( user_name = await aget_user_name(user) - if ConversationCommand.Reminder in conversation_commands: + if ConversationCommand.Automation in conversation_commands: try: - job, crontime, inferred_query, subject = await create_scheduled_task( + automation, crontime, query_to_run, subject = await create_automation( q, location, timezone, user, request.url, meta_log ) except Exception as e: - logger.error(f"Error scheduling task {q} for {user.email}: {e}") + logger.error(f"Error creating automation {q} for {user.email}: {e}") return Response( - content=f"Unable to schedule task. Ensure the task doesn't already exist.", + content=f"Unable to create automation. Ensure the automation doesn't already exist.", media_type="text/plain", status_code=500, ) # Display next run time in user timezone instead of UTC - schedule = f'{cron_descriptor.get_description(crontime)} {job.next_run_time.strftime("%Z")}' - next_run_time = job.next_run_time.strftime("%Y-%m-%d %H:%M %Z") - # Remove /task prefix from inferred_query - unprefixed_inferred_query = re.sub(r"^\/task\s*", "", inferred_query) - # Create the scheduled task response + schedule = f'{cron_descriptor.get_description(crontime)} {automation.next_run_time.strftime("%Z")}' + next_run_time = automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z") + # Remove /automated_task prefix from inferred_query + unprefixed_query_to_run = re.sub(r"^\/automated_task\s*", "", query_to_run) + # Create the Automation response llm_response = f""" - ### 🕒 Scheduled Task + ### 🕒 Automation - Subject: **{subject}** -- Query: "{unprefixed_inferred_query}" +- Query to Run: "{unprefixed_query_to_run}" - Schedule: `{schedule}` - Next Run At: {next_run_time} -Manage your tasks [here](/config#tasks). +Manage your automations [here](/config#automations). """.strip() await sync_to_async(save_to_conversation_log)( @@ -674,11 +677,11 @@ Manage your tasks [here](/config#tasks). user, meta_log, user_message_time, - intent_type="reminder", + intent_type="automation", client_application=request.user.client_app, conversation_id=conversation_id, - inferred_queries=[inferred_query], - job_id=job.id, + inferred_queries=[query_to_run], + automation_id=automation.id, ) if stream: diff --git a/src/khoj/routers/email.py b/src/khoj/routers/email.py index bb5cdd5c..cb0c39c7 100644 --- a/src/khoj/routers/email.py +++ b/src/khoj/routers/email.py @@ -58,7 +58,7 @@ def send_task_email(name, email, query, result, subject): template = env.get_template("task.html") html_result = markdown_it.MarkdownIt().render(result) - html_content = template.render(name=name, query=query, result=html_result) + html_content = template.render(name=name, subject=subject, query=query, result=html_result) r = resend.Emails.send( { diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index 358975b5..29cf95e6 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -170,8 +170,8 @@ def get_conversation_command(query: str, any_references: bool = False) -> Conver return ConversationCommand.Online elif query.startswith("/image"): return ConversationCommand.Image - elif query.startswith("/task"): - return ConversationCommand.Task + elif query.startswith("/automated_task"): + return ConversationCommand.AutomatedTask # If no relevant notes found for the given query elif not any_references: return ConversationCommand.General @@ -239,7 +239,7 @@ async def aget_relevant_output_modes(query: str, conversation_history: dict, is_ for mode, description in mode_descriptions_for_llm.items(): # Do not allow tasks to schedule another task - if is_task and mode == ConversationCommand.Reminder: + if is_task and mode == ConversationCommand.Automation: continue mode_options[mode.value] = description mode_options_str += f'- "{mode.value}": "{description}"\n' @@ -857,18 +857,14 @@ def should_notify(original_query: str, executed_query: str, ai_response: str) -> response=ai_response, ) - with timer("Chat actor: Decide to notify user of AI response", logger): + with timer("Chat actor: Decide to notify user of automation response", logger): try: response = send_message_to_model_wrapper_sync(to_notify_or_not) should_notify_result = "no" not in response.lower() - logger.info( - f'Decided to {"not " if not should_notify_result else ""}notify user of scheduled task response.' - ) + logger.info(f'Decided to {"not " if not should_notify_result else ""}notify user of automation response.') return should_notify_result except: - logger.warning( - f"Fallback to notify user of scheduled task 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.") return True @@ -904,7 +900,7 @@ def scheduled_chat(query_to_run: str, scheduling_request: str, subject: str, use return None # Extract the AI response from the chat API response - cleaned_query = re.sub(r"^/task\s*", "", query_to_run).strip() + cleaned_query = re.sub(r"^/automated_task\s*", "", query_to_run).strip() if raw_response.headers.get("Content-Type") == "application/json": response_map = raw_response.json() ai_response = response_map.get("response") or response_map.get("image") @@ -919,7 +915,7 @@ def scheduled_chat(query_to_run: str, scheduling_request: str, subject: str, use return raw_response -async def create_scheduled_task( +async def create_automation( q: str, location: LocationData, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {} ): user_timezone = pytz.timezone(timezone) @@ -930,7 +926,7 @@ async def create_scheduled_task( {"query_to_run": query_to_run, "scheduling_request": q, "subject": subject, "crontime": crontime_string} ) query_id = hashlib.md5(f"{query_to_run}".encode("utf-8")).hexdigest() - job_id = f"job_{user.uuid}_{crontime_string}_{query_id}" + job_id = f"automation_{user.uuid}_{crontime_string}_{query_id}" job = state.scheduler.add_job( run_with_process_lock, trigger=trigger, diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index 1e85b679..4b24b828 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -304,8 +304,8 @@ class ConversationCommand(str, Enum): Online = "online" Webpage = "webpage" Image = "image" - Reminder = "reminder" - Task = "task" + Automation = "automation" + AutomatedTask = "automated_task" command_descriptions = { @@ -315,8 +315,7 @@ command_descriptions = { ConversationCommand.Online: "Search for information on the internet.", ConversationCommand.Webpage: "Get information from webpage links provided by you.", ConversationCommand.Image: "Generate images by describing your imagination in words.", - ConversationCommand.Reminder: "Schedule your query to run at a specified time or interval.", - ConversationCommand.Task: "Scheduled task running at previously specified schedule.", + ConversationCommand.Automation: "Automatically run your query at a specified time or interval.", ConversationCommand.Help: "Display a help message with all available commands and other metadata.", } @@ -330,7 +329,7 @@ tool_descriptions_for_llm = { mode_descriptions_for_llm = { ConversationCommand.Image: "Use this if the user is requesting an image or visual response to their query.", - ConversationCommand.Reminder: "Use this if the user is requesting a response at a scheduled date or time.", + ConversationCommand.Automation: "Use this if the user is requesting a response at a scheduled date or time.", ConversationCommand.Default: "Use this if the other response modes don't seem to fit the query.", }