mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-30 19:03:01 +01:00
Add a schedule picker and automations preview func (#747)
* Update suggested automations * add a schedule picker when creating an automation * Create a new conversation in flow of the automation scheduling in order to send a preview and deliver more consistent results * Start adding in scaffolding to manually trigger a test job for an automation * Add support for manually triggering automations for testing * Schedule automation asynchronously * Update styling of the preview button * Improve admin lookup experience and prevent jobs from being scheduled to run every minute of everyday * Ignore mypy issues on job info short description
This commit is contained in:
parent
ac3e5089a2
commit
cbbbe2da9a
8 changed files with 496 additions and 48 deletions
|
@ -1,9 +1,13 @@
|
||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from apscheduler.job import Job
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django_apscheduler.admin import DjangoJobAdmin
|
||||||
|
from django_apscheduler.jobstores import DjangoJobStore
|
||||||
|
from django_apscheduler.models import DjangoJob
|
||||||
|
|
||||||
from khoj.database.models import (
|
from khoj.database.models import (
|
||||||
Agent,
|
Agent,
|
||||||
|
@ -25,6 +29,35 @@ from khoj.database.models import (
|
||||||
)
|
)
|
||||||
from khoj.utils.helpers import ImageIntentType
|
from khoj.utils.helpers import ImageIntentType
|
||||||
|
|
||||||
|
admin.site.unregister(DjangoJob)
|
||||||
|
|
||||||
|
|
||||||
|
class KhojDjangoJobAdmin(DjangoJobAdmin):
|
||||||
|
list_display = (
|
||||||
|
"id",
|
||||||
|
"next_run_time",
|
||||||
|
"job_info",
|
||||||
|
)
|
||||||
|
search_fields = ("id", "next_run_time")
|
||||||
|
ordering = ("-next_run_time",)
|
||||||
|
job_store = DjangoJobStore()
|
||||||
|
|
||||||
|
def job_info(self, obj):
|
||||||
|
job: Job = self.job_store.lookup_job(obj.id)
|
||||||
|
return f"{job.func_ref} {job.args} {job.kwargs}" if job else "None"
|
||||||
|
|
||||||
|
job_info.short_description = "Job Info" # type: ignore
|
||||||
|
|
||||||
|
def get_search_results(self, request, queryset, search_term):
|
||||||
|
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
|
||||||
|
if search_term:
|
||||||
|
jobs = [job.id for job in self.job_store.get_all_jobs() if search_term in str(job)]
|
||||||
|
queryset |= self.model.objects.filter(id__in=jobs)
|
||||||
|
return queryset, use_distinct
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(DjangoJob, KhojDjangoJobAdmin)
|
||||||
|
|
||||||
|
|
||||||
class KhojUserAdmin(UserAdmin):
|
class KhojUserAdmin(UserAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
|
|
|
@ -193,9 +193,55 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
.loader {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
border-top: 4px solid var(--primary-color);
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: rotation 1s linear infinite;
|
||||||
|
}
|
||||||
|
.loader::after {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-left: 4px solid var(--summer-sun);
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
animation: rotation 0.5s linear infinite reverse;
|
||||||
|
}
|
||||||
|
@keyframes rotation {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
async function openChat(agentSlug) {
|
async function openChat(agentSlug) {
|
||||||
|
// Create a loading animation
|
||||||
|
let loading = document.createElement("div");
|
||||||
|
loading.innerHTML = '<div>Booting your agent...</div><span class="loader"></span>';
|
||||||
|
loading.style.position = "fixed";
|
||||||
|
loading.style.top = "0";
|
||||||
|
loading.style.right = "0";
|
||||||
|
loading.style.bottom = "0";
|
||||||
|
loading.style.left = "0";
|
||||||
|
loading.style.display = "flex";
|
||||||
|
loading.style.justifyContent = "center";
|
||||||
|
loading.style.alignItems = "center";
|
||||||
|
loading.style.backgroundColor = "rgba(0, 0, 0, 0.5)"; // Semi-transparent black
|
||||||
|
document.body.appendChild(loading);
|
||||||
|
|
||||||
let response = await fetch(`/api/chat/sessions?agent_slug=${agentSlug}`, { method: "POST" });
|
let response = await fetch(`/api/chat/sessions?agent_slug=${agentSlug}`, { method: "POST" });
|
||||||
let data = await response.json();
|
let data = await response.json();
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
|
|
3
src/khoj/interface/web/assets/icons/cancel.svg
Normal file
3
src/khoj/interface/web/assets/icons/cancel.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M704 288h-281.6l177.6-202.88a32 32 0 0 0-48.32-42.24l-224 256a30.08 30.08 0 0 0-2.24 3.84 32 32 0 0 0-2.88 4.16v1.92a32 32 0 0 0 0 5.12A32 32 0 0 0 320 320a32 32 0 0 0 0 4.8 32 32 0 0 0 0 5.12v1.92a32 32 0 0 0 2.88 4.16 30.08 30.08 0 0 0 2.24 3.84l224 256a32 32 0 1 0 48.32-42.24L422.4 352H704a224 224 0 0 1 224 224v128a224 224 0 0 1-224 224H320a232 232 0 0 1-28.16-1.6 32 32 0 0 0-35.84 27.84 32 32 0 0 0 27.84 35.52A295.04 295.04 0 0 0 320 992h384a288 288 0 0 0 288-288v-128a288 288 0 0 0-288-288zM103.04 760a32 32 0 0 0-62.08 16A289.92 289.92 0 0 0 140.16 928a32 32 0 0 0 40-49.92 225.6 225.6 0 0 1-77.12-118.08zM64 672a32 32 0 0 0 22.72-9.28 37.12 37.12 0 0 0 6.72-10.56A32 32 0 0 0 96 640a33.6 33.6 0 0 0-9.28-22.72 32 32 0 0 0-10.56-6.72 32 32 0 0 0-34.88 6.72A32 32 0 0 0 32 640a32 32 0 0 0 2.56 12.16 37.12 37.12 0 0 0 6.72 10.56A32 32 0 0 0 64 672z" fill="#231815" /></svg>
|
After Width: | Height: | Size: 1.1 KiB |
5
src/khoj/interface/web/assets/icons/pencil-edit.svg
Normal file
5
src/khoj/interface/web/assets/icons/pencil-edit.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M21.2799 6.40005L11.7399 15.94C10.7899 16.89 7.96987 17.33 7.33987 16.7C6.70987 16.07 7.13987 13.25 8.08987 12.3L17.6399 2.75002C17.8754 2.49308 18.1605 2.28654 18.4781 2.14284C18.7956 1.99914 19.139 1.92124 19.4875 1.9139C19.8359 1.90657 20.1823 1.96991 20.5056 2.10012C20.8289 2.23033 21.1225 2.42473 21.3686 2.67153C21.6147 2.91833 21.8083 3.21243 21.9376 3.53609C22.0669 3.85976 22.1294 4.20626 22.1211 4.55471C22.1128 4.90316 22.0339 5.24635 21.8894 5.5635C21.7448 5.88065 21.5375 6.16524 21.2799 6.40005V6.40005Z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M11 4H6C4.93913 4 3.92178 4.42142 3.17163 5.17157C2.42149 5.92172 2 6.93913 2 8V18C2 19.0609 2.42149 20.0783 3.17163 20.8284C3.92178 21.5786 4.93913 22 6 22H17C19.21 22 20 20.2 20 18V13" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -62,6 +62,15 @@
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img.automation-edit-cancel-icon,
|
||||||
|
img.automation-edit-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
object-fit: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
textarea.fake-input,
|
textarea.fake-input,
|
||||||
input.fake-input {
|
input.fake-input {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -108,11 +117,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
button.save-automation-button,
|
button.save-automation-button,
|
||||||
|
button.cancel-edit-automation-button,
|
||||||
|
button.send-preview-automation-button,
|
||||||
button.delete-automation-button {
|
button.delete-automation-button {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.send-preview-automation-button {
|
||||||
|
border-color: var(--summer-sun);
|
||||||
|
}
|
||||||
|
|
||||||
button.save-automation-button:hover {
|
button.save-automation-button:hover {
|
||||||
background-color: var(--primary-hover);
|
background-color: var(--primary-hover);
|
||||||
}
|
}
|
||||||
|
@ -122,10 +137,16 @@
|
||||||
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2);
|
box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: auto;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 2;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.automation:hover,
|
div.automation:not(.new-automation):hover {
|
||||||
div.new-automation:hover {
|
|
||||||
box-shadow: 0 10px 15px 0 hsla(0, 0%, 0%, 0.1);
|
box-shadow: 0 10px 15px 0 hsla(0, 0%, 0%, 0.1);
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
|
@ -181,6 +202,16 @@
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.subject-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.subject-wrapper p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes confirmation {
|
@keyframes confirmation {
|
||||||
0% { background-color: normal; transform: scale(1); }
|
0% { background-color: normal; transform: scale(1); }
|
||||||
50% { background-color: var(--primary); transform: scale(1.1); }
|
50% { background-color: var(--primary); transform: scale(1.1); }
|
||||||
|
@ -199,6 +230,10 @@
|
||||||
div.automation-buttons {
|
div.automation-buttons {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
div.new-automation {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -235,28 +270,88 @@
|
||||||
queryEl.value = automation.query_to_run;
|
queryEl.value = automation.query_to_run;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickAutomationCard(automationId) {
|
function onClickEditAutomationCard(automationId) {
|
||||||
const automationIDElements = document.querySelectorAll(`.${automationId}`);
|
const automationIDElements = document.querySelectorAll(`.${automationId}`);
|
||||||
automationIDElements.forEach(el => {
|
automationIDElements.forEach(el => {
|
||||||
// Only toggle the first time the button is clicked
|
if (el.classList.contains("automation-edit-icon")) {
|
||||||
|
el.classList.remove("automation-edit-icon");
|
||||||
|
el.classList.add("automation-edit-cancel-icon");
|
||||||
|
el.src = "/static/assets/icons/cancel.svg";
|
||||||
|
el.onclick = function(event) { clickCancelEdit(event, automationId); };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.classList.contains("hide-details")) {
|
||||||
|
el.classList.add("hide-details-placeholder");
|
||||||
el.classList.remove("hide-details");
|
el.classList.remove("hide-details");
|
||||||
|
}
|
||||||
|
if (el.classList.contains("fake-input")) {
|
||||||
|
el.classList.add("fake-input-placeholder");
|
||||||
el.classList.remove("fake-input");
|
el.classList.remove("fake-input");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendAPreviewAutomation(automationId) {
|
||||||
|
const notificationEl = document.getElementById(`automation-success-${automationId}`);
|
||||||
|
|
||||||
|
fetch(`/api/trigger/automation?automation_id=${automationId}`, { method: 'POST' })
|
||||||
|
.then(response =>
|
||||||
|
{
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(automations => {
|
||||||
|
notificationEl.style.display = 'block';
|
||||||
|
notificationEl.textContent = "Automation triggered. Check your inbox in a few minutes!";
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
notificationEl.style.display = 'block';
|
||||||
|
notificationEl.textContent = "Sorry, something went wrong. Try again later."
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickCancelEdit(event, automationId) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const automationIDElements = document.querySelectorAll(`.${automationId}`);
|
||||||
|
automationIDElements.forEach(el => {
|
||||||
|
if (el.classList.contains("automation-edit-cancel-icon")) {
|
||||||
|
el.classList.remove("automation-edit-cancel-icon");
|
||||||
|
el.classList.add("automation-edit-icon");
|
||||||
|
el.src = "/static/assets/icons/pencil-edit.svg";
|
||||||
|
el.onclick = function() { onClickEditAutomationCard(automationId); };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.classList.contains("hide-details-placeholder")) {
|
||||||
|
el.classList.remove("hide-details-placeholder");
|
||||||
|
el.classList.add("hide-details");
|
||||||
|
}
|
||||||
|
if (el.classList.contains("fake-input-placeholder")) {
|
||||||
|
el.classList.remove("fake-input-placeholder");
|
||||||
|
el.classList.add("fake-input");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function generateAutomationRow(automation, isSuggested=false) {
|
function generateAutomationRow(automation, isSuggested=false) {
|
||||||
let automationId = automation.id;
|
let automationId = automation.id;
|
||||||
let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`;
|
let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`;
|
||||||
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}">
|
||||||
<div class="card-header" onclick="onClickAutomationCard('${automationId}')">
|
<div class="card-header" onclick="onClickEditAutomationCard('${automationId}')">
|
||||||
|
<div class="subject-wrapper">
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="automation-subject-${automationId}"
|
id="automation-subject-${automationId}"
|
||||||
class="${automationId} fake-input"
|
class="${automationId} fake-input"
|
||||||
name="subject"
|
name="subject"
|
||||||
data-original="${automation.subject}"
|
data-original="${automation.subject}"
|
||||||
value="${automation.subject}">
|
value="${automation.subject}">
|
||||||
|
<img class="automation-edit-icon ${automationId}" src="/static/assets/icons/pencil-edit.svg" onclick="onClickEditAutomationCard('${automationId}')" alt="Automations">
|
||||||
|
</div>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
id="automation-schedule-${automationId}"
|
id="automation-schedule-${automationId}"
|
||||||
name="schedule"
|
name="schedule"
|
||||||
|
@ -276,13 +371,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="automation-buttons-wrapper">
|
<div id="automation-buttons-wrapper">
|
||||||
<div class="automation-buttons">
|
<div class="automation-buttons">
|
||||||
<div id="empty-div"></div>
|
|
||||||
${isSuggested ?
|
${isSuggested ?
|
||||||
`<div id="empty-div"></div>`:
|
`<div id="empty-div"></div>
|
||||||
|
<div id="empty-div"></div>`:
|
||||||
`
|
`
|
||||||
<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="send-preview-automation-button positive-button"
|
||||||
|
title="Immediately get a preview of this automation"
|
||||||
|
onclick="sendAPreviewAutomation('${automationId}')">Preview</button>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
@ -308,6 +407,10 @@
|
||||||
if (deleteAutomationButtonEl) {
|
if (deleteAutomationButtonEl) {
|
||||||
deleteAutomationButtonEl.addEventListener("click", () => { deleteAutomation(automation.id); });
|
deleteAutomationButtonEl.addEventListener("click", () => { deleteAutomation(automation.id); });
|
||||||
}
|
}
|
||||||
|
let cancelEditAutomationButtonEl = automationEl.querySelector(`#cancel-edit-automation-button-${automation.id}`);
|
||||||
|
if (cancelEditAutomationButtonEl) {
|
||||||
|
cancelEditAutomationButtonEl.addEventListener("click", (event) => { clickCancelEdit(event, automation.id); });
|
||||||
|
}
|
||||||
|
|
||||||
return automationEl.firstElementChild;
|
return automationEl.firstElementChild;
|
||||||
}
|
}
|
||||||
|
@ -317,7 +420,7 @@
|
||||||
let suggestedAutomationsMetadata = [
|
let suggestedAutomationsMetadata = [
|
||||||
{
|
{
|
||||||
"subject": "Weekly Newsletter",
|
"subject": "Weekly Newsletter",
|
||||||
"query_to_run": "Share a Newsletter including: 1. Weather forecast for this week. 2. A recap of news from last week 3. A nice quote to start the week.",
|
"query_to_run": "Compile a message including: 1. A recap of news from last week 2. A reminder to work out and stay hydrated 3. A quote to inspire me for the week ahead",
|
||||||
"schedule": "9AM every Monday",
|
"schedule": "9AM every Monday",
|
||||||
"next": "Next run at 9AM on Monday",
|
"next": "Next run at 9AM on Monday",
|
||||||
"crontime": "0 9 * * 1",
|
"crontime": "0 9 * * 1",
|
||||||
|
@ -326,7 +429,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"subject": "Daily Weather Update",
|
"subject": "Daily Weather Update",
|
||||||
"query_to_run": "Get the weather forecast for today and tomorrow",
|
"query_to_run": "Get the weather forecast for today",
|
||||||
"schedule": "9AM every morning",
|
"schedule": "9AM every morning",
|
||||||
"next": "Next run at 9AM today",
|
"next": "Next run at 9AM today",
|
||||||
"crontime": "0 9 * * *",
|
"crontime": "0 9 * * *",
|
||||||
|
@ -413,15 +516,220 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createScheduleSelector(automationId) {
|
||||||
|
var scheduleContainer = document.createElement('div');
|
||||||
|
scheduleContainer.id = `schedule-container-${automationId}`;
|
||||||
|
|
||||||
|
var frequencyLabel = document.createElement('label');
|
||||||
|
frequencyLabel.for = `frequency-selector-${automationId}`;
|
||||||
|
frequencyLabel.textContent = 'Every';
|
||||||
|
var frequencySelector = document.createElement('select')
|
||||||
|
frequencySelector.id = `frequency-selector-${automationId}`;
|
||||||
|
var dayLabel = document.createElement('label');
|
||||||
|
dayLabel.id = `day-selector-label-${automationId}`;
|
||||||
|
dayLabel.for = `day-selector-${automationId}`;
|
||||||
|
dayLabel.textContent = 'on';
|
||||||
|
var daySelector = document.createElement('select');
|
||||||
|
daySelector.id = `day-selector-${automationId}`;
|
||||||
|
var dateLabel = document.createElement('label');
|
||||||
|
dateLabel.id = `date-label-${automationId}`;
|
||||||
|
dateLabel.for = `date-selector-${automationId}`;
|
||||||
|
dateLabel.textContent = 'on the';
|
||||||
|
var dateSelector = document.createElement('select');
|
||||||
|
dateSelector.id = `date-selector-${automationId}`;
|
||||||
|
var timeLabel = document.createElement('label');
|
||||||
|
timeLabel.for = `time-selector-${automationId}`;
|
||||||
|
timeLabel.textContent = 'at';
|
||||||
|
var timeSelector = document.createElement('select');
|
||||||
|
timeSelector.id = `time-selector-${automationId}`;
|
||||||
|
|
||||||
|
|
||||||
|
// Populate frequency selector with options for day, week, and month
|
||||||
|
var frequencies = ['day', 'week', 'month'];
|
||||||
|
for (var i = 0; i < frequencies.length; i++) {
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = frequencies[i];
|
||||||
|
option.text = frequencies[i];
|
||||||
|
frequencySelector.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listener for frequency selector change
|
||||||
|
frequencySelector.addEventListener('change', function() {
|
||||||
|
switch (this.value) {
|
||||||
|
case 'day':
|
||||||
|
daySelector.style.display = 'none';
|
||||||
|
dateSelector.style.display = 'none';
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
daySelector.style.display = 'block';
|
||||||
|
dateSelector.style.display = 'none';
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
daySelector.style.display = 'none';
|
||||||
|
dateSelector.style.display = 'block';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Populate the date selector with options for each day of the month
|
||||||
|
for (var i = 1; i <= 31; i++) {
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = i;
|
||||||
|
option.text = i;
|
||||||
|
dateSelector.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the day selector with options for each day of the week
|
||||||
|
var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
for (var i = 0; i < days.length; i++) {
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = i;
|
||||||
|
option.text = days[i];
|
||||||
|
daySelector.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
var timePeriods = ['AM', 'PM'];
|
||||||
|
// Populate the time selector with options for each hour of the day
|
||||||
|
for (var i = 0; i < timePeriods.length; i++) {
|
||||||
|
for (var hour = 0; hour < 12; hour++) {
|
||||||
|
for (var minute = 0; minute < 60; minute+=15) {
|
||||||
|
// Ensure all minutes are two digits
|
||||||
|
var paddedMinute = String(minute).padStart(2, '0');
|
||||||
|
var option = document.createElement('option');
|
||||||
|
var friendlyHour = hour === 0 ? 12 : hour;
|
||||||
|
option.value = `${friendlyHour}:${paddedMinute} ${timePeriods[i]}`;
|
||||||
|
option.text = `${friendlyHour}:${paddedMinute} ${timePeriods[i]}`;
|
||||||
|
timeSelector.appendChild(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate date selector with options 1 through 31
|
||||||
|
for (var i = 1; i <= 31; i++) {
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = i;
|
||||||
|
option.text = i;
|
||||||
|
dateSelector.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hoursMinutesSelectorContainer = document.createElement('div');
|
||||||
|
hoursMinutesSelectorContainer.classList.add('hours-minutes-selector-container');
|
||||||
|
hoursMinutesSelectorContainer.appendChild(timeLabel);
|
||||||
|
hoursMinutesSelectorContainer.appendChild(timeSelector);
|
||||||
|
|
||||||
|
scheduleContainer.appendChild(frequencyLabel);
|
||||||
|
scheduleContainer.appendChild(frequencySelector);
|
||||||
|
scheduleContainer.appendChild(dayLabel);
|
||||||
|
scheduleContainer.appendChild(daySelector);
|
||||||
|
scheduleContainer.appendChild(dateLabel);
|
||||||
|
scheduleContainer.appendChild(dateSelector);
|
||||||
|
scheduleContainer.appendChild(hoursMinutesSelectorContainer);
|
||||||
|
|
||||||
|
return scheduleContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupScheduleViewListener(cronString, automationId) {
|
||||||
|
// Parse the cron string
|
||||||
|
var cronParts = cronString.split(' ');
|
||||||
|
var minutes = cronParts[0];
|
||||||
|
var hours = cronParts[1];
|
||||||
|
var dayOfMonth = cronParts[2];
|
||||||
|
var month = cronParts[3];
|
||||||
|
var dayOfWeek = cronParts[4];
|
||||||
|
|
||||||
|
var timeSelector = document.getElementById(`time-selector-${automationId}`);
|
||||||
|
|
||||||
|
// Set the initial value of the time selector based on the cron string. Convert 24-hour time to 12-hour time
|
||||||
|
if (hours === '*' && minutes === '*') {
|
||||||
|
var currentTime = new Date();
|
||||||
|
hours = currentTime.getHours();
|
||||||
|
minutes = currentTime.getMinutes();
|
||||||
|
}
|
||||||
|
var hours = parseInt(hours);
|
||||||
|
var minutes = parseInt(minutes);
|
||||||
|
var timePeriod = hours >= 12 ? 'PM' : 'AM';
|
||||||
|
hours = hours % 12;
|
||||||
|
hours = hours ? hours : 12; // 0 should be 12
|
||||||
|
minutes = Math.round(minutes / 15) * 15;
|
||||||
|
minutes = String(minutes).padStart(2, '0');
|
||||||
|
// Resolve minutes to the nearest 15 minute interval
|
||||||
|
|
||||||
|
timeSelector.value = `${hours}:${minutes} ${timePeriod}`;
|
||||||
|
|
||||||
|
const frequencySelector = document.getElementById(`frequency-selector-${automationId}`);
|
||||||
|
const daySelector = document.getElementById(`day-selector-${automationId}`);
|
||||||
|
const daySelectorLabel = document.getElementById(`day-selector-label-${automationId}`);
|
||||||
|
const dateSelector = document.getElementById(`date-selector-${automationId}`);
|
||||||
|
const dateLabel = document.getElementById(`date-label-${automationId}`);
|
||||||
|
|
||||||
|
// Event listener for frequency selector change
|
||||||
|
frequencySelector.addEventListener('change', function() {
|
||||||
|
processFrequencySelector(frequencySelector, daySelector, daySelectorLabel, dateSelector, dateLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the initial value based on the frequency selector value
|
||||||
|
processFrequencySelector(frequencySelector, daySelector, daySelectorLabel, dateSelector, dateLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFrequencySelector(frequencySelector, daySelector, daySelectorLabel, dateSelector, dateLabel) {
|
||||||
|
switch (frequencySelector.value) {
|
||||||
|
case 'day':
|
||||||
|
daySelector.style.display = 'none';
|
||||||
|
dateSelector.style.display = 'none';
|
||||||
|
daySelectorLabel.style.display = 'none';
|
||||||
|
dateLabel.style.display = 'none';
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
daySelector.style.display = 'block';
|
||||||
|
dateSelector.style.display = 'none';
|
||||||
|
daySelectorLabel.style.display = 'block';
|
||||||
|
dateLabel.style.display = 'none';
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
daySelector.style.display = 'none';
|
||||||
|
dateSelector.style.display = 'block';
|
||||||
|
daySelectorLabel.style.display = 'none';
|
||||||
|
dateLabel.style.display = 'block';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertFrequencyToCron(automationId) {
|
||||||
|
var frequencySelector = document.getElementById(`frequency-selector-${automationId}`);
|
||||||
|
var daySelector = document.getElementById(`day-selector-${automationId}`);
|
||||||
|
var dateSelector = document.getElementById(`date-selector-${automationId}`);
|
||||||
|
var timeSelector = document.getElementById(`time-selector-${automationId}`);
|
||||||
|
|
||||||
|
var hours = timeSelector.value.split(':')[0];
|
||||||
|
var minutes = timeSelector.value.split(':')[1].split(' ')[0];
|
||||||
|
|
||||||
|
var cronString = '';
|
||||||
|
switch (frequencySelector.value) {
|
||||||
|
case 'day':
|
||||||
|
cronString = `${minutes} ${hours} * * *`;
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
cronString = `${minutes} ${hours} * * ${daySelector.value}`;
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
cronString = `${minutes} ${hours} ${dateSelector.value} * *`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cronString;
|
||||||
|
}
|
||||||
|
|
||||||
async function saveAutomation(automationId, create=false) {
|
async function saveAutomation(automationId, create=false) {
|
||||||
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 queryToRunEl = document.getElementById(`automation-queryToRun-${automationId}`);
|
const queryToRunEl = document.getElementById(`automation-queryToRun-${automationId}`);
|
||||||
const queryToRun = encodeURIComponent(queryToRunEl.value);
|
const queryToRun = encodeURIComponent(queryToRunEl.value);
|
||||||
const actOn = create ? "Create" : "Save";
|
const actOn = create ? "Creat" : "Sav";
|
||||||
|
var cronTime = null;
|
||||||
|
|
||||||
if (queryToRun == "" || scheduleEl.value == "") {
|
if (queryToRun == "") {
|
||||||
|
if (!create && scheduleEl.value == "") {
|
||||||
notificationEl.textContent = `⚠️ Failed to automate. All input fields need to be filled.`;
|
notificationEl.textContent = `⚠️ Failed to automate. All input fields need to be filled.`;
|
||||||
notificationEl.style.display = "block";
|
notificationEl.style.display = "block";
|
||||||
let originalQueryToRunElBorder = queryToRunEl.style.border;
|
let originalQueryToRunElBorder = queryToRunEl.style.border;
|
||||||
|
@ -436,12 +744,22 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Get client location information from IP
|
// Get client location information from IP
|
||||||
const ip_response = await fetch("https://ipapi.co/json")
|
const ip_response = await fetch("https://ipapi.co/json");
|
||||||
const ip_data = await ip_response.json();
|
let ip_data = null;
|
||||||
|
if (ip_response.ok) {
|
||||||
|
ip_data = await ip_response.json();
|
||||||
|
}
|
||||||
|
|
||||||
// Get cron string from natural language user schedule, if changed
|
// Get cron string from natural language user schedule, if changed
|
||||||
const crontime = scheduleEl.getAttribute('data-original') !== scheduleEl.value ? getCronString(scheduleEl.value) : scheduleEl.getAttribute('data-cron');
|
if (create && !scheduleEl) {
|
||||||
|
crontime = convertFrequencyToCron(automationId);
|
||||||
|
} else {
|
||||||
|
crontime = scheduleEl.getAttribute('data-original') !== scheduleEl.value ? getCronString(scheduleEl.value) : scheduleEl.getAttribute('data-cron');
|
||||||
|
}
|
||||||
|
|
||||||
if (crontime.startsWith("ERROR:")) {
|
if (crontime.startsWith("ERROR:")) {
|
||||||
notificationEl.textContent = `⚠️ Failed to automate. Fix or simplify Schedule input field.`;
|
notificationEl.textContent = `⚠️ Failed to automate. Fix or simplify Schedule input field.`;
|
||||||
notificationEl.style.display = "block";
|
notificationEl.style.display = "block";
|
||||||
|
@ -456,7 +774,10 @@
|
||||||
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}&crontime=${encodedCrontime}&city=${ip_data.city}®ion=${ip_data.region}&country=${ip_data.country_name}&timezone=${ip_data.timezone}`;
|
let query_string = `q=${queryToRun}&crontime=${encodedCrontime}`;
|
||||||
|
if (ip_data) {
|
||||||
|
query_string += `&city=${ip_data.city}®ion=${ip_data.region}&country=${ip_data.country_name}&timezone=${ip_data.timezone}`;
|
||||||
|
}
|
||||||
|
|
||||||
let method = "POST";
|
let method = "POST";
|
||||||
if (!create) {
|
if (!create) {
|
||||||
|
@ -466,6 +787,11 @@
|
||||||
method = "PUT"
|
method = "PUT"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a loading animation while waiting for the API response
|
||||||
|
// TODO add a more pleasant loading symbol here.
|
||||||
|
notificationEl.textContent = `⏳ ${actOn}ing automation...`;
|
||||||
|
notificationEl.style.display = "block";
|
||||||
|
|
||||||
fetch(`/api/automation?${query_string}`, {
|
fetch(`/api/automation?${query_string}`, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -477,9 +803,8 @@
|
||||||
if (create) {
|
if (create) {
|
||||||
const automationEl = document.getElementById(`automation-card-${automationId}`);
|
const automationEl = document.getElementById(`automation-card-${automationId}`);
|
||||||
// Create a more interesting confirmation animation.
|
// Create a more interesting confirmation animation.
|
||||||
automationEl.classList.add("confirmation")
|
automationEl.classList.remove("new-automation");
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
|
||||||
// Check if automationEl is a child of #automations or #suggested-automations-list
|
// Check if automationEl is a child of #automations or #suggested-automations-list
|
||||||
// If #suggested-automations-list, remove the element from the list and add it to #automations
|
// If #suggested-automations-list, remove the element from the list and add it to #automations
|
||||||
let parentEl = automationEl.parentElement;
|
let parentEl = automationEl.parentElement;
|
||||||
|
@ -495,7 +820,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationEl.style.display = "none";
|
notificationEl.style.display = "none";
|
||||||
saveButtonEl.textContent = `✅ Automation ${actOn}d`;
|
saveButtonEl.textContent = `✅ Automation ${actOn}ed`;
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
const automationIDElements = document.querySelectorAll(`.${automationId}`);
|
const automationIDElements = document.querySelectorAll(`.${automationId}`);
|
||||||
automationIDElements.forEach(el => {
|
automationIDElements.forEach(el => {
|
||||||
|
@ -514,11 +839,11 @@
|
||||||
}, 2000);
|
}, 2000);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()} automations.`;
|
notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automations.`;
|
||||||
notificationEl.style.display = "block";
|
notificationEl.style.display = "block";
|
||||||
saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()} automations`;
|
saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()}e automations`;
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
saveButtonEl.textContent = actOn;
|
saveButtonEl.textContent = `${actOn}e`;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
@ -533,13 +858,11 @@
|
||||||
automationEl.classList.add("new-automation")
|
automationEl.classList.add("new-automation")
|
||||||
const placeholderId = Date.now();
|
const placeholderId = Date.now();
|
||||||
automationEl.id = "automation-card-" + placeholderId;
|
automationEl.id = "automation-card-" + placeholderId;
|
||||||
|
var scheduleSelector = createScheduleSelector(placeholderId);
|
||||||
automationEl.innerHTML = `
|
automationEl.innerHTML = `
|
||||||
<label for="schedule">Schedule</label>
|
<label for="schedule">New Automation</label>
|
||||||
<input type="text"
|
${scheduleSelector.outerHTML}
|
||||||
id="automation-schedule-${placeholderId}"
|
<label for="query-to-run">What would you like to receive in your automation?</label>
|
||||||
name="schedule"
|
|
||||||
placeholder="9AM every morning">
|
|
||||||
<label for="query-to-run">Instructions</label>
|
|
||||||
<textarea id="automation-queryToRun-${placeholderId}" placeholder="Provide me with a mindful moment, reminding me to be centered."></textarea>
|
<textarea id="automation-queryToRun-${placeholderId}" placeholder="Provide me with a mindful moment, reminding me to be centered."></textarea>
|
||||||
<div class="automation-buttons">
|
<div class="automation-buttons">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
@ -554,6 +877,7 @@
|
||||||
<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);
|
||||||
|
setupScheduleViewListener("* * * * *", placeholderId);
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any, Callable, List, Optional, Union
|
from typing import Any, Callable, List, Optional, Union
|
||||||
|
@ -36,6 +37,7 @@ from khoj.routers.helpers import (
|
||||||
ConversationCommandRateLimiter,
|
ConversationCommandRateLimiter,
|
||||||
acreate_title_from_query,
|
acreate_title_from_query,
|
||||||
schedule_automation,
|
schedule_automation,
|
||||||
|
scheduled_chat,
|
||||||
update_telemetry_state,
|
update_telemetry_state,
|
||||||
)
|
)
|
||||||
from khoj.search_filter.date_filter import DateFilter
|
from khoj.search_filter.date_filter import DateFilter
|
||||||
|
@ -452,12 +454,19 @@ 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("?", "*")
|
||||||
|
if crontime == "* * * * *":
|
||||||
|
return Response(content="Invalid crontime. Please create a more specific schedule.", status_code=400)
|
||||||
subject = await acreate_title_from_query(q)
|
subject = await acreate_title_from_query(q)
|
||||||
|
|
||||||
|
# Create new Conversation Session associated with this new task
|
||||||
|
conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app)
|
||||||
|
|
||||||
|
calling_url = request.url.replace(query=f"{request.url.query}&conversation_id={conversation.id}")
|
||||||
|
|
||||||
# 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:
|
||||||
# Use the query to run as the scheduling request if the scheduling request is unset
|
# Use the query to run as the scheduling request if the scheduling request is unset
|
||||||
automation = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, request.url)
|
automation = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error creating automation {q} for {user.email}: {e}", exc_info=True)
|
logger.error(f"Error creating automation {q} for {user.email}: {e}", exc_info=True)
|
||||||
return Response(
|
return Response(
|
||||||
|
@ -473,6 +482,31 @@ async def post_automation(
|
||||||
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("/trigger/automation", response_class=Response)
|
||||||
|
@requires(["authenticated"])
|
||||||
|
def trigger_manual_job(
|
||||||
|
request: Request,
|
||||||
|
automation_id: str,
|
||||||
|
):
|
||||||
|
user: KhojUser = request.user.object
|
||||||
|
|
||||||
|
# Check, get automation to edit
|
||||||
|
try:
|
||||||
|
automation: Job = AutomationAdapters.get_automation(user, automation_id)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Error triggering automation {automation_id} for {user.email}: {e}", exc_info=True)
|
||||||
|
return Response(content="Invalid automation", status_code=403)
|
||||||
|
|
||||||
|
# Trigger the job without waiting for the result.
|
||||||
|
scheduled_chat_func = automation.func
|
||||||
|
|
||||||
|
# Run the function in a separate thread
|
||||||
|
thread = threading.Thread(target=scheduled_chat_func, args=automation.args, kwargs=automation.kwargs)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return Response(content="Automation triggered", status_code=200)
|
||||||
|
|
||||||
|
|
||||||
@api.put("/automation", response_class=Response)
|
@api.put("/automation", response_class=Response)
|
||||||
@requires(["authenticated"])
|
@requires(["authenticated"])
|
||||||
def edit_job(
|
def edit_job(
|
||||||
|
|
|
@ -956,6 +956,8 @@ def scheduled_chat(
|
||||||
|
|
||||||
async def create_automation(q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}):
|
async def create_automation(q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}):
|
||||||
crontime, query_to_run, subject = await schedule_query(q, meta_log)
|
crontime, query_to_run, subject = await schedule_query(q, meta_log)
|
||||||
|
if crontime == "* * * * *":
|
||||||
|
raise HTTPException(status_code=400, detail="Cannot run jobs constantly. Please provide a valid crontime.")
|
||||||
job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url)
|
job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url)
|
||||||
return job, crontime, query_to_run, subject
|
return job, crontime, query_to_run, subject
|
||||||
|
|
||||||
|
@ -970,6 +972,8 @@ async def schedule_automation(
|
||||||
calling_url: URL,
|
calling_url: URL,
|
||||||
):
|
):
|
||||||
user_timezone = pytz.timezone(timezone)
|
user_timezone = pytz.timezone(timezone)
|
||||||
|
if crontime == "* * * * *":
|
||||||
|
raise HTTPException(status_code=400, detail="Cannot run jobs constantly. Please provide a valid crontime.")
|
||||||
trigger = CronTrigger.from_crontab(crontime, user_timezone)
|
trigger = CronTrigger.from_crontab(crontime, user_timezone)
|
||||||
trigger.jitter = 60
|
trigger.jitter = 60
|
||||||
# Generate id and metadata used by task scheduler and process locks for the task runs
|
# Generate id and metadata used by task scheduler and process locks for the task runs
|
||||||
|
|
|
@ -11,7 +11,6 @@ from starlette.authentication import has_required_scope, requires
|
||||||
from khoj.database import adapters
|
from khoj.database import adapters
|
||||||
from khoj.database.adapters import (
|
from khoj.database.adapters import (
|
||||||
AgentAdapters,
|
AgentAdapters,
|
||||||
AutomationAdapters,
|
|
||||||
ConversationAdapters,
|
ConversationAdapters,
|
||||||
EntryAdapters,
|
EntryAdapters,
|
||||||
PublicConversationAdapters,
|
PublicConversationAdapters,
|
||||||
|
|
Loading…
Reference in a new issue