Rename scheduled task to automations across code and UX

- Fix query, subject parameters passed to email template
- Show 12 hour scheduled time in automation created chat message
This commit is contained in:
Debanjum Singh Solanky 2024-04-29 20:41:07 +05:30
parent 230d160602
commit 2f9241b5a3
9 changed files with 128 additions and 134 deletions

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Khoj AI - Task</title> <title>Khoj AI - Automation</title>
</head> </head>
<body> <body>
<body style="font-family: 'Verdana', sans-serif; font-weight: 400; font-style: normal; padding: 0; text-align: left; width: 600px; margin: 20px auto;"> <body style="font-family: 'Verdana', sans-serif; font-weight: 400; font-style: normal; padding: 0; text-align: left; width: 600px; margin: 20px auto;">
@ -13,7 +13,7 @@
<div> <div>
<h1 style="color: #333; font-size: large; font-weight: bold; margin: 0; line-height: 1.5; background-color: #fee285; padding: 8px; box-shadow: 6px 6px rgba(0, 0, 0, 1.5);">Your Open, Personal AI</h1> <h1 style="color: #333; font-size: large; font-weight: bold; margin: 0; line-height: 1.5; background-color: #fee285; padding: 8px; box-shadow: 6px 6px rgba(0, 0, 0, 1.5);">Your Open, Personal AI</h1>
<p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">Hey {{name}}! </p> <p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">Hey {{name}}! </p>
<p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">I've shared your scheduled task results below:</p> <p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">I've shared your automation results below:</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; grid-gap: 12px; margin-top: 20px;"> <div style="display: grid; grid-template-columns: 1fr 1fr; grid-gap: 12px; margin-top: 20px;">
<div style="border: 1px solid black; border-radius: 8px; padding: 8px; box-shadow: 6px 6px rgba(0, 0, 0, 1.0); margin-top: 20px;"> <div style="border: 1px solid black; border-radius: 8px; padding: 8px; box-shadow: 6px 6px rgba(0, 0, 0, 1.0); margin-top: 20px;">
@ -23,8 +23,8 @@
<p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">{{result}}</p> <p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">{{result}}</p>
</div> </div>
</div> </div>
<p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">The scheduled query I ran on your behalf: {query}</p> <p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">The automation query I ran on your behalf: {{query}}</p>
<p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">You can view, delete and manage your scheduled tasks via <a href="https://app.khoj.dev/configure#tasks">the settings page</a></p> <p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">You can view, delete your automations via <a href="https://app.khoj.dev/configure#tasks">the settings page</a></p>
</div> </div>
</div> </div>
<p style="color: #333; font-size: large; margin-top: 20px; padding: 0; line-height: 1.5;">- Khoj</p> <p style="color: #333; font-size: large; margin-top: 20px; padding: 0; line-height: 1.5;">- Khoj</p>

View file

@ -272,17 +272,17 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div id="tasks" class="section"> <div id="automations" class="section">
<h2 class="section-title">Scheduled Tasks</h2> <h2 class="section-title">Automations</h2>
<div id="scheduled-tasks" class="api-settings"> <div id="automations" class="api-settings">
<div class="card-title-row"> <div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/key.svg" alt="Scheduled Tasks"> <img class="card-icon" src="/static/assets/icons/key.svg" alt="Automations">
<h3 class="card-title">Tasks</h3> <h3 class="card-title">Automations</h3>
</div> </div>
<div class="card-description-row"> <div class="card-description-row">
<p id="tasks-settings-card-description" class="card-description">Manage your scheduled tasks</p> <p id="tasks-settings-card-description" class="card-description">Manage your automations</p>
</div> </div>
<table id="scheduled-tasks-table"> <table id="automations-table">
<thead> <thead>
<tr> <tr>
<th scope="col">Name</th> <th scope="col">Name</th>
@ -292,10 +292,10 @@
<th scope="col">Actions</th> <th scope="col">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="scheduled-tasks-list"></tbody> <tbody id="automations-list"></tbody>
</table> </table>
<div class="card-action-row"> <div class="card-action-row">
<button class="card-button happy" id="create-scheduled-task" onclick="createScheduledTask()"> <button class="card-button happy" id="create-automation" onclick="createAutomation()">
Create Task Create Task
</button> </button>
</div> </div>
@ -661,46 +661,42 @@
// List user's API keys on page load // List user's API keys on page load
listApiKeys(); listApiKeys();
function deleteTask(taskId) { function deleteAutomation(automationId) {
const scheduledTaskList = document.getElementById("scheduled-tasks-list"); const AutomationList = document.getElementById("automations-list");
fetch(`/api/task?task_id=${taskId}`, { fetch(`/api/automation?automation_id=${automationId}`, {
method: 'DELETE', method: 'DELETE',
}) })
.then(response => { .then(response => {
if (response.status == 200) { if (response.status == 200) {
const scheduledTaskItem = document.getElementById(`scheduled-task-item-${taskId}`); const AutomationItem = document.getElementById(`automation-item-${automationId}`);
scheduledTaskList.removeChild(scheduledTaskItem); AutomationList.removeChild(AutomationItem);
} }
}); });
} }
function generateTaskRow(taskObj) { function generateAutomationRow(automationObj) {
let taskId = taskObj.id; let automationId = automationObj.id;
let taskSchedulingRequest = taskObj.scheduling_request; let automationNextRun = `Next run at ${automationObj.next}`;
let taskQuery = taskObj.query_to_run;
let taskSubject = taskObj.subject;
let taskNextRun = `Next run at ${taskObj.next}`;
let taskSchedule = taskObj.schedule;
return ` return `
<tr id="scheduled-task-item-${taskId}"> <tr id="automation-item-${automationId}">
<td><b>${taskSubject}</b></td> <td><b>${automationObj.subject}</b></td>
<td><b>${taskSchedulingRequest}</b></td> <td><b>${automationObj.scheduling_request}</b></td>
<td><b>${taskQuery}</b></td> <td><b>${automationObj.query_to_run}</b></td>
<td id="scheduled-task-${taskId}" title="${taskNextRun}">${taskSchedule}</td> <td id="automation-${automationId}" title="${automationNextRun}">${automationObj.schedule}</td>
<td> <td>
<img onclick="deleteTask('${taskId}')" class="configured-icon api-key-action enabled" src="/static/assets/icons/trash-solid.svg" alt="Delete Task" title="Delete Task"> <img onclick="deleteAutomation('${automationId}')" class="configured-icon api-key-action enabled" src="/static/assets/icons/trash-solid.svg" alt="Delete Automation" title="Delete Automation">
</td> </td>
</tr> </tr>
`; `;
} }
function listScheduledTasks() { function listAutomations() {
const scheduledTasksList = document.getElementById("scheduled-tasks-list"); const AutomationsList = document.getElementById("automations-list");
fetch('/api/tasks') fetch('/api/automations')
.then(response => response.json()) .then(response => response.json())
.then(tasks => { .then(automations => {
if (!tasks?.length > 0) return; if (!automations?.length > 0) return;
scheduledTasksList.innerHTML = tasks.map(generateTaskRow).join(""); AutomationsList.innerHTML = automations.map(generateAutomationRow).join("");
}); });
} }
@ -713,8 +709,8 @@
}); });
} }
// List user's scheduled tasks on page load // List user's automations on page load
listScheduledTasks(); listAutomations();
function removeFile(path) { function removeFile(path) {
fetch('/api/config/data/file?filename=' + path, { fetch('/api/config/data/file?filename=' + path, {

View file

@ -507,7 +507,7 @@ Khoj:
""".strip() """.strip()
) )
# Schedule task # Automations
# -- # --
crontime_prompt = PromptTemplate.from_template( 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. User: Hahah, nice! Show a new one every morning.
Khoj: {{ Khoj: {{
"crontime": "0 9 * * *", "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" "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 User: Every monday evening at 6 share the top posts on hacker news from last week. Format it as a newsletter
Khoj: {{ Khoj: {{
"crontime": "0 18 * * 1", "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" "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 User: Notify me when version 2.0.0 is released
Khoj: {{ Khoj: {{
"crontime": "0 10 * * *", "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" "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 User: Tell me the latest local tech news on the first sunday of every month
Khoj: {{ Khoj: {{
"crontime": "0 8 1-7 * 0", "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" "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. User: Inform me when the national election results are declared. Run task at 4pm every thursday.
Khoj: {{ Khoj: {{
"crontime": "0 16 * * 4", "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" "subject": "Indian National Election Results Declared"
}} }}

View file

@ -102,7 +102,7 @@ def save_to_conversation_log(
intent_type: str = "remember", intent_type: str = "remember",
client_application: ClientApplication = None, client_application: ClientApplication = None,
conversation_id: int = 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") user_message_time = user_message_time or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
updated_conversation = message_to_log( updated_conversation = message_to_log(
@ -113,7 +113,7 @@ def save_to_conversation_log(
"context": compiled_references, "context": compiled_references,
"intent": {"inferred-queries": inferred_queries, "type": intent_type}, "intent": {"inferred-queries": inferred_queries, "type": intent_type},
"onlineContext": online_results, "onlineContext": online_results,
"jobId": job_id, "automationId": automation_id,
}, },
conversation_log=meta_log.get("chat", []), conversation_log=meta_log.get("chat", []),
) )

View file

@ -391,59 +391,59 @@ def user_info(request: Request) -> Response:
return Response(content=json.dumps(user_info), media_type="application/json", status_code=200) 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"]) @requires(["authenticated"])
def get_jobs(request: Request) -> Response: def get_automations(request: Request) -> Response:
user: KhojUser = request.user.object 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 # Collate all automations created by user that are still active
tasks_info = [] automations_info = []
for task in tasks: for automation in automations:
if task.id.startswith(f"job_{user.uuid}_"): if automation.id.startswith(f"automation_{user.uuid}_"):
task_metadata = json.loads(task.name) automation_metadata = json.loads(automation.name)
schedule = ( crontime = automation_metadata["crontime"]
f'{cron_descriptor.get_description(task_metadata["crontime"])} {task.next_run_time.strftime("%Z")}' timezone = automation.next_run_time.strftime("%Z")
) schedule = f"{cron_descriptor.get_description(crontime)} {timezone}"
tasks_info.append( automations_info.append(
{ {
"id": task.id, "id": automation.id,
"subject": task_metadata["subject"], "subject": automation_metadata["subject"],
"query_to_run": re.sub(r"^/task\s*", "", task_metadata["query_to_run"]), "query_to_run": re.sub(r"^/automated_task\s*", "", automation_metadata["query_to_run"]),
"scheduling_request": task_metadata["scheduling_request"], "scheduling_request": automation_metadata["scheduling_request"],
"schedule": schedule, "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 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"]) @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 user: KhojUser = request.user.object
# Perform validation checks # Perform validation checks
# Check if user is allowed to delete this task id # Check if user is allowed to delete this automation id
if not task_id.startswith(f"job_{user.uuid}_"): if not automation_id.startswith(f"automation_{user.uuid}_"):
return Response(content="Unauthorized job deletion request", status_code=403) return Response(content="Unauthorized job deletion request", status_code=403)
# Check if task with this task id exist # Check if automation with this id exist
task: Job = state.scheduler.get_job(job_id=task_id) automation: Job = state.scheduler.get_job(job_id=automation_id)
if not task: if not automation:
return Response(content="Invalid job", status_code=403) return Response(content="Invalid job", status_code=403)
# Collate info about user task to be deleted # Collate info about user task to be deleted
task_metadata = json.loads(task.name) automation_metadata = json.loads(automation.name)
task_info = { automation_info = {
"id": task.id, "id": automation.id,
"name": task_metadata["inferred_query"], "name": automation_metadata["query_to_run"],
"next": task.next_run_time.strftime("%Y-%m-%d %H:%MS"), "next": automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z"),
} }
# Delete job # Delete job
task.remove() automation.remove()
# Return delete task information as a JSON response # Return deleted automation information as a JSON response
return Response(content=json.dumps(task_info), media_type="application/json", status_code=200) return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)

View file

@ -37,7 +37,7 @@ from khoj.routers.helpers import (
agenerate_chat_response, agenerate_chat_response,
aget_relevant_information_sources, aget_relevant_information_sources,
aget_relevant_output_modes, aget_relevant_output_modes,
create_scheduled_task, create_automation,
get_conversation_command, get_conversation_command,
is_ready_to_chat, is_ready_to_chat,
text_to_image, text_to_image,
@ -217,7 +217,8 @@ async def chat_options(
) -> Response: ) -> Response:
cmd_options = {} cmd_options = {}
for cmd in ConversationCommand: 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( update_telemetry_state(
request=request, request=request,
@ -373,14 +374,14 @@ async def websocket_endpoint(
continue continue
meta_log = conversation.conversation_log 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 = await aget_relevant_information_sources(q, meta_log)
conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands]) conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
await send_status_update(f"**🗃️ Chose Data Sources to Search:** {conversation_commands_str}") 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}") await send_status_update(f"**🧑🏾‍💻 Decided Response Mode:** {mode.value}")
if mode not in conversation_commands: if mode not in conversation_commands:
conversation_commands.append(mode) conversation_commands.append(mode)
@ -389,29 +390,31 @@ async def websocket_endpoint(
await conversation_command_rate_limiter.update_and_check_if_valid(websocket, cmd) await conversation_command_rate_limiter.update_and_check_if_valid(websocket, cmd)
q = q.replace(f"/{cmd.value}", "").strip() q = q.replace(f"/{cmd.value}", "").strip()
if ConversationCommand.Reminder in conversation_commands: if ConversationCommand.Automation in conversation_commands:
try: 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 q, location, timezone, user, websocket.url, meta_log
) )
except Exception as e: except Exception as e:
logger.error(f"Error scheduling task {q} for {user.email}: {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 continue
# Display next run time in user timezone instead of UTC # Display next run time in user timezone instead of UTC
schedule = f'{cron_descriptor.get_description(crontime)} {job.next_run_time.strftime("%Z")}' schedule = f'{cron_descriptor.get_description(crontime)} {automation.next_run_time.strftime("%Z")}'
next_run_time = job.next_run_time.strftime("%Y-%m-%d %H:%M %Z") next_run_time = automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z")
# Remove /task prefix from inferred_query # Remove /automated_task prefix from inferred_query
unprefixed_inferred_query = re.sub(r"^\/task\s*", "", inferred_query) unprefixed_query_to_run = re.sub(r"^\/automated_task\s*", "", query_to_run)
# Create the scheduled task response # Create the automation response
llm_response = f""" llm_response = f"""
### 🕒 Scheduled Task ### 🕒 Automation
- Subject: **{subject}** - Subject: **{subject}**
- Query: "{unprefixed_inferred_query}" - Query to Run: "{unprefixed_query_to_run}"
- Schedule: `{schedule}` - Schedule: `{schedule}`
- Next Run At: {next_run_time} - Next Run At: {next_run_time}
Manage your tasks [here](/config#tasks). Manage your tasks [here](/config#automations).
""".strip() """.strip()
await sync_to_async(save_to_conversation_log)( await sync_to_async(save_to_conversation_log)(
@ -420,11 +423,11 @@ Manage your tasks [here](/config#tasks).
user, user,
meta_log, meta_log,
user_message_time, user_message_time,
intent_type="reminder", intent_type="automation",
client_application=websocket.user.client_app, client_application=websocket.user.client_app,
conversation_id=conversation_id, conversation_id=conversation_id,
inferred_queries=[inferred_query], inferred_queries=[query_to_run],
job_id=job.id, automation_id=automation.id,
) )
common = CommonQueryParamsClass( common = CommonQueryParamsClass(
client=websocket.user.client_app, client=websocket.user.client_app,
@ -621,7 +624,7 @@ async def chat(
else: else:
meta_log = conversation.conversation_log 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: if conversation_commands == [ConversationCommand.Default] or is_task:
conversation_commands = await aget_relevant_information_sources(q, meta_log) conversation_commands = await aget_relevant_information_sources(q, meta_log)
@ -640,32 +643,32 @@ async def chat(
user_name = await aget_user_name(user) user_name = await aget_user_name(user)
if ConversationCommand.Reminder in conversation_commands: if ConversationCommand.Automation in conversation_commands:
try: 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 q, location, timezone, user, request.url, meta_log
) )
except Exception as e: 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( 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", media_type="text/plain",
status_code=500, status_code=500,
) )
# Display next run time in user timezone instead of UTC # Display next run time in user timezone instead of UTC
schedule = f'{cron_descriptor.get_description(crontime)} {job.next_run_time.strftime("%Z")}' schedule = f'{cron_descriptor.get_description(crontime)} {automation.next_run_time.strftime("%Z")}'
next_run_time = job.next_run_time.strftime("%Y-%m-%d %H:%M %Z") next_run_time = automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z")
# Remove /task prefix from inferred_query # Remove /automated_task prefix from inferred_query
unprefixed_inferred_query = re.sub(r"^\/task\s*", "", inferred_query) unprefixed_query_to_run = re.sub(r"^\/automated_task\s*", "", query_to_run)
# Create the scheduled task response # Create the Automation response
llm_response = f""" llm_response = f"""
### 🕒 Scheduled Task ### 🕒 Automation
- Subject: **{subject}** - Subject: **{subject}**
- Query: "{unprefixed_inferred_query}" - Query to Run: "{unprefixed_query_to_run}"
- Schedule: `{schedule}` - Schedule: `{schedule}`
- Next Run At: {next_run_time} - Next Run At: {next_run_time}
Manage your tasks [here](/config#tasks). Manage your automations [here](/config#automations).
""".strip() """.strip()
await sync_to_async(save_to_conversation_log)( await sync_to_async(save_to_conversation_log)(
@ -674,11 +677,11 @@ Manage your tasks [here](/config#tasks).
user, user,
meta_log, meta_log,
user_message_time, user_message_time,
intent_type="reminder", intent_type="automation",
client_application=request.user.client_app, client_application=request.user.client_app,
conversation_id=conversation_id, conversation_id=conversation_id,
inferred_queries=[inferred_query], inferred_queries=[query_to_run],
job_id=job.id, automation_id=automation.id,
) )
if stream: if stream:

View file

@ -58,7 +58,7 @@ def send_task_email(name, email, query, result, subject):
template = env.get_template("task.html") template = env.get_template("task.html")
html_result = markdown_it.MarkdownIt().render(result) 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( r = resend.Emails.send(
{ {

View file

@ -170,8 +170,8 @@ def get_conversation_command(query: str, any_references: bool = False) -> Conver
return ConversationCommand.Online return ConversationCommand.Online
elif query.startswith("/image"): elif query.startswith("/image"):
return ConversationCommand.Image return ConversationCommand.Image
elif query.startswith("/task"): elif query.startswith("/automated_task"):
return ConversationCommand.Task return ConversationCommand.AutomatedTask
# If no relevant notes found for the given query # If no relevant notes found for the given query
elif not any_references: elif not any_references:
return ConversationCommand.General 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(): for mode, description in mode_descriptions_for_llm.items():
# Do not allow tasks to schedule another task # Do not allow tasks to schedule another task
if is_task and mode == ConversationCommand.Reminder: if is_task and mode == ConversationCommand.Automation:
continue continue
mode_options[mode.value] = description mode_options[mode.value] = description
mode_options_str += f'- "{mode.value}": "{description}"\n' 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, 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: try:
response = send_message_to_model_wrapper_sync(to_notify_or_not) response = send_message_to_model_wrapper_sync(to_notify_or_not)
should_notify_result = "no" not in response.lower() should_notify_result = "no" not in response.lower()
logger.info( logger.info(f'Decided to {"not " if not should_notify_result else ""}notify user of automation response.')
f'Decided to {"not " if not should_notify_result else ""}notify user of scheduled task response.'
)
return should_notify_result return should_notify_result
except: except:
logger.warning( logger.warning(f"Fallback to notify user of automation response as failed to infer should notify or not.")
f"Fallback to notify user of scheduled task response as failed to infer should notify or not."
)
return True return True
@ -904,7 +900,7 @@ def scheduled_chat(query_to_run: str, scheduling_request: str, subject: str, use
return None return None
# Extract the AI response from the chat API response # 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": if raw_response.headers.get("Content-Type") == "application/json":
response_map = raw_response.json() response_map = raw_response.json()
ai_response = response_map.get("response") or response_map.get("image") 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 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 = {} q: str, location: LocationData, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}
): ):
user_timezone = pytz.timezone(timezone) 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_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() 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( job = state.scheduler.add_job(
run_with_process_lock, run_with_process_lock,
trigger=trigger, trigger=trigger,

View file

@ -304,8 +304,8 @@ class ConversationCommand(str, Enum):
Online = "online" Online = "online"
Webpage = "webpage" Webpage = "webpage"
Image = "image" Image = "image"
Reminder = "reminder" Automation = "automation"
Task = "task" AutomatedTask = "automated_task"
command_descriptions = { command_descriptions = {
@ -315,8 +315,7 @@ command_descriptions = {
ConversationCommand.Online: "Search for information on the internet.", ConversationCommand.Online: "Search for information on the internet.",
ConversationCommand.Webpage: "Get information from webpage links provided by you.", ConversationCommand.Webpage: "Get information from webpage links provided by you.",
ConversationCommand.Image: "Generate images by describing your imagination in words.", ConversationCommand.Image: "Generate images by describing your imagination in words.",
ConversationCommand.Reminder: "Schedule your query to run at a specified time or interval.", ConversationCommand.Automation: "Automatically run your query at a specified time or interval.",
ConversationCommand.Task: "Scheduled task running at previously specified schedule.",
ConversationCommand.Help: "Display a help message with all available commands and other metadata.", 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 = { mode_descriptions_for_llm = {
ConversationCommand.Image: "Use this if the user is requesting an image or visual response to their query.", 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.", ConversationCommand.Default: "Use this if the other response modes don't seem to fit the query.",
} }