Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes and improvements for scheduled tasks #732

Merged
merged 7 commits into from
May 1, 2024
2 changes: 1 addition & 1 deletion src/interface/desktop/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -1509,7 +1509,7 @@
#chat-input {
font-family: var(--font-family);
font-size: small;
height: 36px;
height: 48px;
border-radius: 16px;
resize: none;
overflow-y: hidden;
Expand Down
10 changes: 4 additions & 6 deletions src/khoj/interface/email/task.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,18 @@
</a>
<div class="calls-to-action" style="margin-top: 20px;">
<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>
<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>
<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>

<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;">
<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>
</a>
<p style="color: #333; font-size: medium; margin-top: 20px; padding: 0; line-height: 1.5;">{{result}}</p>
</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;">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;">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 manage your automations via <a href="https://app.khoj.dev/automations">the settings page</a>.</p>
</div>
</div>
<p style="color: #333; font-size: large; margin-top: 20px; padding: 0; line-height: 1.5;">- Khoj</p>
Expand Down
6 changes: 3 additions & 3 deletions src/khoj/interface/web/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -1170,7 +1170,7 @@
chat_log.message,
chat_log.by,
chat_log.context,
new Date(chat_log.created),
new Date(chat_log.created + "Z"),
chat_log.onlineContext,
chat_log.intent?.type,
chat_log.intent?.["inferred-queries"]);
Expand Down Expand Up @@ -1265,7 +1265,7 @@
chat_log.message,
chat_log.by,
chat_log.context,
new Date(chat_log.created),
new Date(chat_log.created + "Z"),
chat_log.onlineContext,
chat_log.intent?.type,
chat_log.intent?.["inferred-queries"]
Expand Down Expand Up @@ -2164,7 +2164,7 @@
#chat-input {
font-family: var(--font-family);
font-size: medium;
height: 36px;
height: 48px;
border-radius: 16px;
resize: none;
overflow-y: hidden;
Expand Down
87 changes: 59 additions & 28 deletions src/khoj/interface/web/config_automation.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ <h2 class="section-title">
<img class="card-icon" src="/static/assets/icons/automation.svg?v={{ khoj_version }}" alt="Automate">
<span class="card-title-text">Automate</span>
<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>
</h2>
<div class="section-body">
<h4>Automations</h4>
<button id="create-automation-button" type="button" class="positive-button">
<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>
<div id="automations" class="section-cards"></div>
</div>
Expand All @@ -28,12 +27,15 @@ <h4>Automations</h4>
width: 100%;
height: 100%;
grid-template-rows: none;
background-color: var(--frosted-background-color);
padding: 12px;
}
#create-automation-button {
width: auto;
}
div#automations {
margin-bottom: 12px;
grid-template-columns: 1fr;
}
button.negative-button {
background-color: gainsboro;
Expand All @@ -44,6 +46,34 @@ <h4>Automations</h4>
.positive-button:hover {
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>
<script>
function deleteAutomation(automationId) {
Expand Down Expand Up @@ -84,13 +114,12 @@ <h4>Automations</h4>
let automationEl = document.createElement("div");
automationEl.innerHTML = `
<div class="card automation" id="automation-card-${automationId}">
<label for="subject">Subject</label>
<input type="text"
id="automation-subject-${automationId}"
name="subject"
data-original="${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}"
data-original="${automation.query_to_run}"
name="query-to-run">${automation.query_to_run}</textarea>
Expand All @@ -102,12 +131,14 @@ <h4>Automations</h4>
data-original="${automation.schedule}"
title="${automationNextRun}"
value="${automation.schedule}">
<button type="button"
class="save-automation-button positive-button"
id="save-automation-button-${automationId}">Save</button>
<button type="button"
class="delete-automation-button negative-button"
id="delete-automation-button-${automationId}">Delete</button>
<div class="automation-buttons">
<button type="button"
class="delete-automation-button negative-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>
`;
Expand Down Expand Up @@ -155,14 +186,13 @@ <h4>Automations</h4>
}

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 scheduleEl = document.getElementById(`automation-schedule-${automationId}`);
const notificationEl = document.getElementById(`automation-success-${automationId}`);
const saveButtonEl = document.getElementById(`save-automation-button-${automationId}`);
const actOn = create ? "Create" : "Save";

if (subject === "" || queryToRun == "" || scheduleEl.value == "") {
if (queryToRun == "" || scheduleEl.value == "") {
return;
}

Expand All @@ -186,10 +216,13 @@ <h4>Automations</h4>
const encodedCrontime = encodeURIComponent(crontime);

// 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";
if (!create) {
const subject = encodeURIComponent(document.getElementById(`automation-subject-${automationId}`).value);
query_string += `&automation_id=${automationId}`;
query_string += `&subject=${subject}`;
method = "PUT"
}

Expand Down Expand Up @@ -231,29 +264,27 @@ <h4>Automations</h4>
var automationEl = document.createElement("div");
automationEl.classList.add("card");
automationEl.classList.add("automation");
automationEl.classList.add("new-automation")
const placeholderId = Date.now();
automationEl.id = "automation-card-" + placeholderId;
automationEl.innerHTML = `
<label for="subject">Subject</label>
<input type="text"
id="automation-subject-${placeholderId}"
name="subject"
placeholder="My Personal Newsletter">
<label for="query-to-run">Query to Run</label>
<label for="query-to-run">Your new automation</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>
<label for="schedule">Schedule</label>
<input type="text"
id="automation-schedule-${placeholderId}"
name="schedule"
placeholder="9AM every morning">
<button type="button"
class="save-automation-button"
onclick="saveAutomation(${placeholderId}, true)"
id="save-automation-button-${placeholderId}">Create</button>
<button type="button"
class="delete-automation-button"
onclick="deleteAutomation(${placeholderId}, true)"
id="delete-automation-button-${placeholderId}">Delete</button>
<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"
class="save-automation-button"
onclick="saveAutomation(${placeholderId}, true)"
id="save-automation-button-${placeholderId}">Create</button>
</div>
<div id="automation-success-${placeholderId}" style="display: none;"></div>
`;
document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
Expand Down
12 changes: 9 additions & 3 deletions src/khoj/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging
import io
import os
import atexit
import sys
import locale

Expand Down Expand Up @@ -93,6 +94,11 @@
from khoj.utils.initialization import initialization


def shutdown_scheduler():
logger.info("🌑 Shutting down Khoj")
state.scheduler.shutdown()


def run(should_start_server=True):
# Turn Tokenizers Parallelism Off. App does not support it.
os.environ["TOKENIZERS_PARALLELISM"] = "false"
Expand Down Expand Up @@ -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 should_start_server:
start_server(app, host=args.host, port=args.port, socket=args.socket)

# Teardown
state.scheduler.shutdown()
# Teardown
shutdown_scheduler()


def set_state(args):
Expand Down Expand Up @@ -202,3 +207,4 @@ def poll_task_scheduler():
run()
else:
run(should_start_server=False)
atexit.register(shutdown_scheduler)
20 changes: 20 additions & 0 deletions src/khoj/processor/conversation/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,26 @@
""".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(
"""
You are Khoj, an extremely smart and discerning notification assistant.
Expand Down
8 changes: 4 additions & 4 deletions src/khoj/routers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
ApiUserRateLimiter,
CommonQueryParams,
ConversationCommandRateLimiter,
acreate_title_from_query,
schedule_automation,
update_telemetry_state,
)
Expand Down Expand Up @@ -425,7 +426,6 @@ def delete_automation(request: Request, automation_id: str) -> Response:
async def post_automation(
request: Request,
q: str,
subject: str,
crontime: str,
city: Optional[str] = None,
region: Optional[str] = None,
Expand All @@ -435,8 +435,8 @@ async def post_automation(
user: KhojUser = request.user.object

# Perform validation checks
if is_none_or_empty(q) or is_none_or_empty(subject) or is_none_or_empty(crontime):
return Response(content="A query, subject and crontime is required", status_code=400)
if is_none_or_empty(q) or is_none_or_empty(crontime):
return Response(content="A query and crontime is required", status_code=400)
if not cron_descriptor.get_description(crontime):
return Response(content="Invalid crontime", status_code=400)

Expand All @@ -452,7 +452,7 @@ async def post_automation(
crontime = " ".join(crontime.split(" ")[:5])
# Convert crontime to standard unix crontime
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
try:
Expand Down
4 changes: 3 additions & 1 deletion src/khoj/routers/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def send_welcome_email(name, email):
{
"from": "team@khoj.dev",
"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,
}
)
Expand All @@ -55,6 +55,8 @@ def send_task_email(name, email, query, result, subject):
logger.debug("Email sending disabled")
return

logger.info(f"Sending email to {email} for task {subject}")

template = env.get_template("task.html")

html_result = markdown_it.MarkdownIt().render(result)
Expand Down
14 changes: 13 additions & 1 deletion src/khoj/routers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,18 @@ async def agenerate_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):
"""
Given a query, determine which of the available tools the agent should use in order to answer appropriately.
Expand Down Expand Up @@ -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
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_request, ai_response, subject)
send_task_email(user.get_short_name(), user.email, cleaned_query, ai_response, subject)
else:
return raw_response

Expand Down