Merge pull request #732 from khoj-ai/fit-and-finish/schedule-tasks

Fixes and improves for scheduled tasks
This commit is contained in:
sabaimran
2024-05-01 03:16:09 -07:00
committed by GitHub
9 changed files with 116 additions and 47 deletions

View File

@@ -1509,7 +1509,7 @@
#chat-input { #chat-input {
font-family: var(--font-family); font-family: var(--font-family);
font-size: small; font-size: small;
height: 36px; height: 48px;
border-radius: 16px; border-radius: 16px;
resize: none; resize: none;
overflow-y: hidden; overflow-y: hidden;

View File

@@ -11,20 +11,18 @@
</a> </a>
<div class="calls-to-action" style="margin-top: 20px;"> <div class="calls-to-action" style="margin-top: 20px;">
<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 Automation, From Your 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;">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;">
<a href="https://app.khoj.dev/config#tasks" style="text-decoration: none; text-decoration: underline dotted;"> <a href="https://app.khoj.dev/automations" style="text-decoration: none; text-decoration: underline dotted;">
<h3 style="color: #333; font-size: large; margin: 0; padding: 0; line-height: 2.0; background-color: #b8f1c7; padding: 8px; ">{{subject}}</h3> <h3 style="color: #333; font-size: large; margin: 0; padding: 0; line-height: 2.0; background-color: #b8f1c7; padding: 8px; ">{{subject}}</h3>
</a> </a>
<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 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;">The automation 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 your automations 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 manage your automations via <a href="https://app.khoj.dev/automations">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

@@ -1170,7 +1170,7 @@ To get started, just start typing below. You can also type / to see a list of co
chat_log.message, chat_log.message,
chat_log.by, chat_log.by,
chat_log.context, chat_log.context,
new Date(chat_log.created), new Date(chat_log.created + "Z"),
chat_log.onlineContext, chat_log.onlineContext,
chat_log.intent?.type, chat_log.intent?.type,
chat_log.intent?.["inferred-queries"]); chat_log.intent?.["inferred-queries"]);
@@ -1265,7 +1265,7 @@ To get started, just start typing below. You can also type / to see a list of co
chat_log.message, chat_log.message,
chat_log.by, chat_log.by,
chat_log.context, chat_log.context,
new Date(chat_log.created), new Date(chat_log.created + "Z"),
chat_log.onlineContext, chat_log.onlineContext,
chat_log.intent?.type, chat_log.intent?.type,
chat_log.intent?.["inferred-queries"] chat_log.intent?.["inferred-queries"]
@@ -2164,7 +2164,7 @@ To get started, just start typing below. You can also type / to see a list of co
#chat-input { #chat-input {
font-family: var(--font-family); font-family: var(--font-family);
font-size: medium; font-size: medium;
height: 36px; height: 48px;
border-radius: 16px; border-radius: 16px;
resize: none; resize: none;
overflow-y: hidden; overflow-y: hidden;

View File

@@ -6,14 +6,13 @@
<img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate"> <img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate">
<span class="card-title-text">Automate</span> <span class="card-title-text">Automate</span>
<div class="instructions"> <div class="instructions">
<a href="https://docs.khoj.dev/features/automations">ⓘ Help</a> You can automate queries to run on a schedule using Khoj's automations. Results will be sent straight to your inbox.
</div> </div>
</h2> </h2>
<div class="section-body"> <div class="section-body">
<h4>Automations</h4>
<button id="create-automation-button" type="button" class="positive-button"> <button id="create-automation-button" type="button" class="positive-button">
<img class="automation-action-icon" src="/static/assets/icons/new.svg" alt="Automations"> <img class="automation-action-icon" src="/static/assets/icons/new.svg" alt="Automations">
<span id="create-automation-button-text">Create</span> <span id="create-automation-button-text">Build</span>
</button> </button>
<div id="automations" class="section-cards"></div> <div id="automations" class="section-cards"></div>
</div> </div>
@@ -28,12 +27,15 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
grid-template-rows: none; grid-template-rows: none;
background-color: var(--frosted-background-color);
padding: 12px;
} }
#create-automation-button { #create-automation-button {
width: auto; width: auto;
} }
div#automations { div#automations {
margin-bottom: 12px; margin-bottom: 12px;
grid-template-columns: 1fr;
} }
button.negative-button { button.negative-button {
background-color: gainsboro; background-color: gainsboro;
@@ -44,6 +46,34 @@
.positive-button:hover { .positive-button:hover {
background-color: var(--summer-sun); background-color: var(--summer-sun);
} }
div.automation-buttons {
display: grid;
grid-gap: 8px;
grid-template-columns: 1fr 3fr;
}
button.save-automation-button {
background-color: var(--summer-sun);
}
button.save-automation-button:hover {
background-color: var(--primary-hover);
}
div.new-automation {
background-color: var(--frosted-background-color);
border-radius: 10px;
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2);
margin-bottom: 20px;
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
div.new-automation:hover {
box-shadow: 0 10px 15px 0 hsla(0, 0%, 0%, 0.1);
transform: translateY(-5px);
}
</style> </style>
<script> <script>
function deleteAutomation(automationId) { function deleteAutomation(automationId) {
@@ -84,13 +114,12 @@
let automationEl = document.createElement("div"); let automationEl = document.createElement("div");
automationEl.innerHTML = ` automationEl.innerHTML = `
<div class="card automation" id="automation-card-${automationId}"> <div class="card automation" id="automation-card-${automationId}">
<label for="subject">Subject</label>
<input type="text" <input type="text"
id="automation-subject-${automationId}" id="automation-subject-${automationId}"
name="subject" name="subject"
data-original="${automation.subject}" data-original="${automation.subject}"
value="${automation.subject}"> value="${automation.subject}">
<label for="query-to-run">Query to Run</label> <label for="query-to-run">Your automation</label>
<textarea id="automation-queryToRun-${automationId}" <textarea id="automation-queryToRun-${automationId}"
data-original="${automation.query_to_run}" data-original="${automation.query_to_run}"
name="query-to-run">${automation.query_to_run}</textarea> name="query-to-run">${automation.query_to_run}</textarea>
@@ -102,12 +131,14 @@
data-original="${automation.schedule}" data-original="${automation.schedule}"
title="${automationNextRun}" title="${automationNextRun}"
value="${automation.schedule}"> value="${automation.schedule}">
<button type="button" <div class="automation-buttons">
class="save-automation-button positive-button"
id="save-automation-button-${automationId}">Save</button>
<button type="button" <button type="button"
class="delete-automation-button negative-button" class="delete-automation-button negative-button"
id="delete-automation-button-${automationId}">Delete</button> id="delete-automation-button-${automationId}">Delete</button>
<button type="button"
class="save-automation-button positive-button"
id="save-automation-button-${automationId}">Save</button>
</div>
<div id="automation-success-${automationId}" style="display: none;"></div> <div id="automation-success-${automationId}" style="display: none;"></div>
</div> </div>
`; `;
@@ -155,14 +186,13 @@
} }
async function saveAutomation(automationId, create=false) { async function saveAutomation(automationId, create=false) {
const subject = encodeURIComponent(document.getElementById(`automation-subject-${automationId}`).value);
const queryToRun = encodeURIComponent(document.getElementById(`automation-queryToRun-${automationId}`).value); const queryToRun = encodeURIComponent(document.getElementById(`automation-queryToRun-${automationId}`).value);
const scheduleEl = document.getElementById(`automation-schedule-${automationId}`); const scheduleEl = document.getElementById(`automation-schedule-${automationId}`);
const notificationEl = document.getElementById(`automation-success-${automationId}`); const notificationEl = document.getElementById(`automation-success-${automationId}`);
const saveButtonEl = document.getElementById(`save-automation-button-${automationId}`); const saveButtonEl = document.getElementById(`save-automation-button-${automationId}`);
const actOn = create ? "Create" : "Save"; const actOn = create ? "Create" : "Save";
if (subject === "" || queryToRun == "" || scheduleEl.value == "") { if (queryToRun == "" || scheduleEl.value == "") {
return; return;
} }
@@ -186,10 +216,13 @@
const encodedCrontime = encodeURIComponent(crontime); const encodedCrontime = encodeURIComponent(crontime);
// Construct query string and select method for API call // Construct query string and select method for API call
let query_string = `q=${queryToRun}&subject=${subject}&crontime=${encodedCrontime}&city=${ip_data.city}&region=${ip_data.region}&country=${ip_data.country_name}&timezone=${ip_data.timezone}`; let query_string = `q=${queryToRun}&crontime=${encodedCrontime}&city=${ip_data.city}&region=${ip_data.region}&country=${ip_data.country_name}&timezone=${ip_data.timezone}`;
let method = "POST"; let method = "POST";
if (!create) { if (!create) {
const subject = encodeURIComponent(document.getElementById(`automation-subject-${automationId}`).value);
query_string += `&automation_id=${automationId}`; query_string += `&automation_id=${automationId}`;
query_string += `&subject=${subject}`;
method = "PUT" method = "PUT"
} }
@@ -231,29 +264,27 @@
var automationEl = document.createElement("div"); var automationEl = document.createElement("div");
automationEl.classList.add("card"); automationEl.classList.add("card");
automationEl.classList.add("automation"); automationEl.classList.add("automation");
automationEl.classList.add("new-automation")
const placeholderId = Date.now(); const placeholderId = Date.now();
automationEl.id = "automation-card-" + placeholderId; automationEl.id = "automation-card-" + placeholderId;
automationEl.innerHTML = ` automationEl.innerHTML = `
<label for="subject">Subject</label> <label for="query-to-run">Your new automation</label>
<input type="text"
id="automation-subject-${placeholderId}"
name="subject"
placeholder="My Personal Newsletter">
<label for="query-to-run">Query to Run</label>
<textarea id="automation-queryToRun-${placeholderId}" placeholder="Share a Newsletter including: 1. Weather forecast for this Week. 2. A Book Highlight from my Notes. 3. Recap News from Last Week"></textarea> <textarea id="automation-queryToRun-${placeholderId}" placeholder="Share a Newsletter including: 1. Weather forecast for this Week. 2. A Book Highlight from my Notes. 3. Recap News from Last Week"></textarea>
<label for="schedule">Schedule</label> <label for="schedule">Schedule</label>
<input type="text" <input type="text"
id="automation-schedule-${placeholderId}" id="automation-schedule-${placeholderId}"
name="schedule" name="schedule"
placeholder="9AM every morning"> placeholder="9AM every morning">
<div class="automation-buttons">
<button type="button"
class="delete-automation-button negative-button"
onclick="deleteAutomation(${placeholderId}, true)"
id="delete-automation-button-${placeholderId}">Cancel</button>
<button type="button" <button type="button"
class="save-automation-button" class="save-automation-button"
onclick="saveAutomation(${placeholderId}, true)" onclick="saveAutomation(${placeholderId}, true)"
id="save-automation-button-${placeholderId}">Create</button> id="save-automation-button-${placeholderId}">Create</button>
<button type="button" </div>
class="delete-automation-button"
onclick="deleteAutomation(${placeholderId}, true)"
id="delete-automation-button-${placeholderId}">Delete</button>
<div id="automation-success-${placeholderId}" style="display: none;"></div> <div id="automation-success-${placeholderId}" style="display: none;"></div>
`; `;
document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild); document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);

View File

@@ -6,6 +6,7 @@ from contextlib import redirect_stdout
import logging import logging
import io import io
import os import os
import atexit
import sys import sys
import locale import locale
@@ -93,6 +94,11 @@ from khoj.utils.cli import cli
from khoj.utils.initialization import initialization from khoj.utils.initialization import initialization
def shutdown_scheduler():
logger.info("🌑 Shutting down Khoj")
state.scheduler.shutdown()
def run(should_start_server=True): def run(should_start_server=True):
# Turn Tokenizers Parallelism Off. App does not support it. # Turn Tokenizers Parallelism Off. App does not support it.
os.environ["TOKENIZERS_PARALLELISM"] = "false" os.environ["TOKENIZERS_PARALLELISM"] = "false"
@@ -158,9 +164,8 @@ def run(should_start_server=True):
# If the server is started through gunicorn (external to the script), don't start the server # If the server is started through gunicorn (external to the script), don't start the server
if should_start_server: if should_start_server:
start_server(app, host=args.host, port=args.port, socket=args.socket) start_server(app, host=args.host, port=args.port, socket=args.socket)
# Teardown # Teardown
state.scheduler.shutdown() shutdown_scheduler()
def set_state(args): def set_state(args):
@@ -202,3 +207,4 @@ if __name__ == "__main__":
run() run()
else: else:
run(should_start_server=False) run(should_start_server=False)
atexit.register(shutdown_scheduler)

View File

@@ -575,6 +575,26 @@ Khoj:
""".strip() """.strip()
) )
subject_generation = PromptTemplate.from_template(
"""
You are an extremely smart and helpful title generator assistant. Given a user query, extract the subject or title of the task to be performed.
- Use the user query to infer the subject or title of the task.
# Examples:
User: Show a new Calvin and Hobbes quote every morning at 9am. My Current Location: Shanghai, China
Khoj: Your daily Calvin and Hobbes Quote
User: Notify me when version 2.0.0 of the sentence transformers python package is released. My Current Location: Mexico City, Mexico
Khoj: Sentence Transformers Python Package Version 2.0.0 Release
User: Gather the latest tech news on the first sunday of every month.
Khoj: Your Monthly Dose of Tech News
User Query: {query}
Khoj:
""".strip()
)
to_notify_or_not = PromptTemplate.from_template( to_notify_or_not = PromptTemplate.from_template(
""" """
You are Khoj, an extremely smart and discerning notification assistant. You are Khoj, an extremely smart and discerning notification assistant.

View File

@@ -34,6 +34,7 @@ from khoj.routers.helpers import (
ApiUserRateLimiter, ApiUserRateLimiter,
CommonQueryParams, CommonQueryParams,
ConversationCommandRateLimiter, ConversationCommandRateLimiter,
acreate_title_from_query,
schedule_automation, schedule_automation,
update_telemetry_state, update_telemetry_state,
) )
@@ -425,7 +426,6 @@ def delete_automation(request: Request, automation_id: str) -> Response:
async def post_automation( async def post_automation(
request: Request, request: Request,
q: str, q: str,
subject: str,
crontime: str, crontime: str,
city: Optional[str] = None, city: Optional[str] = None,
region: Optional[str] = None, region: Optional[str] = None,
@@ -435,8 +435,8 @@ async def post_automation(
user: KhojUser = request.user.object user: KhojUser = request.user.object
# Perform validation checks # Perform validation checks
if is_none_or_empty(q) or is_none_or_empty(subject) or is_none_or_empty(crontime): if is_none_or_empty(q) or is_none_or_empty(crontime):
return Response(content="A query, subject and crontime is required", status_code=400) return Response(content="A query and crontime is required", status_code=400)
if not cron_descriptor.get_description(crontime): if not cron_descriptor.get_description(crontime):
return Response(content="Invalid crontime", status_code=400) return Response(content="Invalid crontime", status_code=400)
@@ -452,7 +452,7 @@ async def post_automation(
crontime = " ".join(crontime.split(" ")[:5]) crontime = " ".join(crontime.split(" ")[:5])
# Convert crontime to standard unix crontime # Convert crontime to standard unix crontime
crontime = crontime.replace("?", "*") crontime = crontime.replace("?", "*")
subject = subject.strip() subject = await acreate_title_from_query(q)
# Schedule automation with query_to_run, timezone, subject directly provided by user # Schedule automation with query_to_run, timezone, subject directly provided by user
try: try:

View File

@@ -44,7 +44,7 @@ async def send_welcome_email(name, email):
{ {
"from": "team@khoj.dev", "from": "team@khoj.dev",
"to": email, "to": email,
"subject": f"Welcome to Khoj, {name}!" if name else "Welcome to Khoj!", "subject": f"{name}, four ways to use Khoj!" if name else "Four ways to use Khoj!",
"html": html_content, "html": html_content,
} }
) )
@@ -55,6 +55,8 @@ def send_task_email(name, email, query, result, subject):
logger.debug("Email sending disabled") logger.debug("Email sending disabled")
return return
logger.info(f"Sending email to {email} for task {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)

View File

@@ -187,6 +187,18 @@ async def agenerate_chat_response(*args):
return await loop.run_in_executor(executor, generate_chat_response, *args) return await loop.run_in_executor(executor, generate_chat_response, *args)
async def acreate_title_from_query(query: str) -> str:
"""
Create a title from the given query
"""
title_generation_prompt = prompts.subject_generation.format(query=query)
with timer("Chat actor: Generate title from query", logger):
response = await send_message_to_model_wrapper(title_generation_prompt)
return response.strip()
async def aget_relevant_information_sources(query: str, conversation_history: dict, is_task: bool): async def aget_relevant_information_sources(query: str, conversation_history: dict, is_task: bool):
""" """
Given a query, determine which of the available tools the agent should use in order to answer appropriately. Given a query, determine which of the available tools the agent should use in order to answer appropriately.
@@ -913,7 +925,7 @@ def scheduled_chat(query_to_run: str, scheduling_request: str, subject: str, use
# Notify user if the AI response is satisfactory # Notify user if the AI response is satisfactory
if should_notify(original_query=scheduling_request, 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(): if is_resend_enabled():
send_task_email(user.get_short_name(), user.email, scheduling_request, ai_response, subject) send_task_email(user.get_short_name(), user.email, cleaned_query, ai_response, subject)
else: else:
return raw_response return raw_response