diff --git a/pyproject.toml b/pyproject.toml
index 498be35e..4c32a1d2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -82,6 +82,7 @@ dependencies = [
"huggingface-hub >= 0.22.2",
"apscheduler ~= 3.10.0",
"pytz ~= 2024.1",
+ "cron-descriptor == 1.4.3",
]
dynamic = ["version"]
diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html
index 96e4fa55..a17cd0f9 100644
--- a/src/khoj/interface/web/config.html
+++ b/src/khoj/interface/web/config.html
@@ -286,7 +286,9 @@
| Name |
- Next Run |
+ Scheduling Request |
+ Query to Run |
+ Schedule |
Actions |
@@ -674,12 +676,17 @@
function generateTaskRow(taskObj) {
let taskId = taskObj.id;
- let taskName = taskObj.name;
- let taskNextRun = taskObj.next;
+ 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;
return `
- | ${taskName} |
- ${taskNextRun} |
+ ${taskSubject} |
+ ${taskSchedulingRequest} |
+ ${taskQuery} |
+ ${taskSchedule} |
|
diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py
index 60042815..114e2f11 100644
--- a/src/khoj/routers/api.py
+++ b/src/khoj/routers/api.py
@@ -8,6 +8,7 @@ import time
import uuid
from typing import Any, Callable, List, Optional, Union
+import cron_descriptor
from apscheduler.job import Job
from asgiref.sync import sync_to_async
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
@@ -401,11 +402,17 @@ def get_jobs(request: Request) -> Response:
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(
{
"id": task.id,
- "name": re.sub(r"^/task\s*", "", task_metadata["inferred_query"]),
- "next": task.next_run_time.strftime("%Y-%m-%d %H:%M"),
+ "subject": task_metadata["subject"],
+ "query_to_run": re.sub(r"^/task\s*", "", task_metadata["query_to_run"]),
+ "scheduling_request": task_metadata["scheduling_request"],
+ "schedule": schedule,
+ "next": task.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z"),
}
)
diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py
index fe628e69..5aee7ac0 100644
--- a/src/khoj/routers/api_chat.py
+++ b/src/khoj/routers/api_chat.py
@@ -6,6 +6,7 @@ from datetime import datetime
from typing import Dict, Optional
from urllib.parse import unquote
+import cron_descriptor
from asgiref.sync import sync_to_async
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket
from fastapi.requests import Request
@@ -398,16 +399,19 @@ async def websocket_endpoint(
await send_complete_llm_response(f"Unable to schedule task. Ensure the task doesn't already exist.")
continue
# Display next run time in user timezone instead of UTC
- next_run_time = job.next_run_time.strftime("%Y-%m-%d %H:%M %Z (%z)")
+ 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
llm_response = f"""
### 🕒 Scheduled Task
-- Query: **"{unprefixed_inferred_query}"**
- Subject: **{subject}**
-- Schedule: `{crontime}`
-- Next Run At: **{next_run_time}**.
+- Query: "{unprefixed_inferred_query}"
+- Schedule: `{schedule}`
+- Next Run At: {next_run_time}
+
+Manage your tasks [here](/config#tasks).
""".strip()
await sync_to_async(save_to_conversation_log)(
@@ -649,16 +653,19 @@ async def chat(
status_code=500,
)
# Display next run time in user timezone instead of UTC
- next_run_time = job.next_run_time.strftime("%Y-%m-%d %H:%M %Z (%z)")
+ 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
llm_response = f"""
### 🕒 Scheduled Task
-- Query: **"{unprefixed_inferred_query}"**
- Subject: **{subject}**
-- Schedule: `{crontime}`
-- Next Run At: **{next_run_time}**.'
+- Query: "{unprefixed_inferred_query}"
+- Schedule: `{schedule}`
+- Next Run At: {next_run_time}
+
+Manage your tasks [here](/config#tasks).
""".strip()
await sync_to_async(save_to_conversation_log)(
diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py
index 10067216..358975b5 100644
--- a/src/khoj/routers/helpers.py
+++ b/src/khoj/routers/helpers.py
@@ -872,13 +872,13 @@ def should_notify(original_query: str, executed_query: str, ai_response: str) ->
return True
-def scheduled_chat(executing_query: str, scheduling_query: str, subject: str, user: KhojUser, calling_url: URL):
+def scheduled_chat(query_to_run: str, scheduling_request: str, subject: str, user: KhojUser, calling_url: URL):
# Extract relevant params from the original URL
scheme = "http" if not calling_url.is_secure else "https"
query_dict = parse_qs(calling_url.query)
# Replace the original scheduling query with the scheduled query
- query_dict["q"] = [executing_query]
+ query_dict["q"] = [query_to_run]
# Construct the URL to call the chat API with the scheduled query string
encoded_query = urlencode(query_dict, doseq=True)
@@ -904,7 +904,7 @@ def scheduled_chat(executing_query: str, scheduling_query: str, subject: str, us
return None
# Extract the AI response from the chat API response
- cleaned_query = re.sub(r"^/task\s*", "", scheduling_query).strip()
+ cleaned_query = re.sub(r"^/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")
@@ -912,9 +912,9 @@ def scheduled_chat(executing_query: str, scheduling_query: str, subject: str, us
ai_response = raw_response.text
# Notify user if the AI response is satisfactory
- if should_notify(original_query=scheduling_query, executed_query=cleaned_query, ai_response=ai_response):
+ if should_notify(original_query=scheduling_request, executed_query=cleaned_query, ai_response=ai_response):
if is_resend_enabled():
- send_task_email(user.get_short_name(), user.email, scheduling_query, ai_response, subject)
+ send_task_email(user.get_short_name(), user.email, scheduling_request, ai_response, subject)
else:
return raw_response
@@ -923,14 +923,14 @@ async def create_scheduled_task(
q: str, location: LocationData, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}
):
user_timezone = pytz.timezone(timezone)
- crontime, inferred_query, subject = await schedule_query(q, location, meta_log)
- trigger = CronTrigger.from_crontab(crontime, user_timezone)
+ crontime_string, query_to_run, subject = await schedule_query(q, location, meta_log)
+ trigger = CronTrigger.from_crontab(crontime_string, user_timezone)
# Generate id and metadata used by task scheduler and process locks for the task runs
job_metadata = json.dumps(
- {"inferred_query": inferred_query, "original_query": q, "subject": subject, "crontime": crontime}
+ {"query_to_run": query_to_run, "scheduling_request": q, "subject": subject, "crontime": crontime_string}
)
- query_id = hashlib.md5(f"{inferred_query}".encode("utf-8")).hexdigest()
- job_id = f"job_{user.uuid}_{crontime}_{query_id}"
+ query_id = hashlib.md5(f"{query_to_run}".encode("utf-8")).hexdigest()
+ job_id = f"job_{user.uuid}_{crontime_string}_{query_id}"
job = state.scheduler.add_job(
run_with_process_lock,
trigger=trigger,
@@ -939,8 +939,8 @@ async def create_scheduled_task(
f"{ProcessLock.Operation.SCHEDULED_JOB}_{user.uuid}_{query_id}",
),
kwargs={
- "executing_query": inferred_query,
- "scheduling_query": q,
+ "query_to_run": query_to_run,
+ "scheduling_request": q,
"subject": subject,
"user": user,
"calling_url": calling_url,
@@ -950,4 +950,4 @@ async def create_scheduled_task(
max_instances=2, # Allow second instance to kill any previous instance with stale lock
jitter=30,
)
- return job, crontime, inferred_query, subject
+ return job, crontime_string, query_to_run, subject