mirror of
https://github.com/khoj-ai/khoj.git
synced 2025-02-17 08:04:21 +00:00
Move automations into independent page. Allow direct automation
- Previously it was a section in the settings page. Move it to independent, top-level page to improve visibility of feature - Calculate crontime from natural language on web client before sending it to to server for saving new/updated schedule to disk. - Avoids round-trip of call to chat model - Convert POST /api/automation API endpoint into a direct request for automation with query_to_run, subject and schedule provided via the automation page. This allows more granular control to create automation - Make the POST automations endpoint more robust; runs validation checks, normalizes parameters
This commit is contained in:
parent
cbc8a02179
commit
bd5008136a
6 changed files with 371 additions and 59 deletions
1
src/khoj/interface/web/assets/natural-cron.min.js
vendored
Normal file
1
src/khoj/interface/web/assets/natural-cron.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -272,39 +272,6 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="automations" class="section">
|
||||
<h2 class="section-title">Automations</h2>
|
||||
<div id="automations" class="api-settings">
|
||||
<div class="card-title-row">
|
||||
<img class="card-icon" src="/static/assets/icons/automation.svg" alt="Automations">
|
||||
<h3 class="card-title">
|
||||
<span>Automations</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-description-row">
|
||||
<p id="tasks-settings-card-description" class="card-description">Manage your automations</p>
|
||||
</div>
|
||||
<table id="automations-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Scheduling Request</th>
|
||||
<th scope="col">Query to Run</th>
|
||||
<th scope="col">Schedule</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="automations-list"></tbody>
|
||||
</table>
|
||||
<div class="card-action-row">
|
||||
<button class="card-button happy" id="create-automation">
|
||||
<img class="automation-action-icon" src="/static/assets/icons/new.svg" alt="Automations">
|
||||
Create Automation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if billing_enabled %}
|
||||
<div id="billing" class="section">
|
||||
<h2 class="section-title">Billing</h2>
|
||||
|
|
251
src/khoj/interface/web/config_automation.html
Normal file
251
src/khoj/interface/web/config_automation.html
Normal file
|
@ -0,0 +1,251 @@
|
|||
{% extends "base_config.html" %}
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
<div class="section">
|
||||
<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>
|
||||
</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>
|
||||
</button>
|
||||
<div id="automations" class="section-cards"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/assets/natural-cron.min.js"></script>
|
||||
<style>
|
||||
td {
|
||||
padding: 10px 0;
|
||||
}
|
||||
div.automation {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
grid-template-rows: none;
|
||||
}
|
||||
#create-automation-button {
|
||||
width: auto;
|
||||
}
|
||||
div#automations {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
button.negative-button {
|
||||
background-color: gainsboro;
|
||||
}
|
||||
.positive-button {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
.positive-button:hover {
|
||||
background-color: var(--summer-sun);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function deleteAutomation(automationId) {
|
||||
const AutomationList = document.getElementById("automations");
|
||||
fetch(`/api/automation?automation_id=${automationId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status == 200 || response.status == 204) {
|
||||
const AutomationItem = document.getElementById(`automation-card-${automationId}`);
|
||||
AutomationList.removeChild(AutomationItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAutomationRow(automation) {
|
||||
let automationId = automation.id;
|
||||
let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`;
|
||||
|
||||
let scheduleEl = document.getElementById(`automation-schedule-${automationId}`);
|
||||
scheduleEl.setAttribute('data-original', automation.schedule);
|
||||
scheduleEl.setAttribute('data-cron', automation.crontime);
|
||||
scheduleEl.setAttribute('title', automationNextRun);
|
||||
scheduleEl.value = automation.schedule;
|
||||
|
||||
let subjectEl = document.getElementById(`automation-subject-${automationId}`);
|
||||
subjectEl.setAttribute('data-original', automation.subject);
|
||||
subjectEl.value = automation.subject;
|
||||
|
||||
let queryEl = document.getElementById(`automation-queryToRun-${automationId}`);
|
||||
queryEl.setAttribute('data-original', automation.query_to_run);
|
||||
queryEl.value = automation.query_to_run;
|
||||
}
|
||||
|
||||
function generateAutomationRow(automation) {
|
||||
let automationId = automation.id;
|
||||
let automationNextRun = `Next run at ${automation.next}\nCron: ${automation.crontime}`;
|
||||
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>
|
||||
<textarea id="automation-queryToRun-${automationId}"
|
||||
data-original="${automation.query_to_run}"
|
||||
name="query-to-run">${automation.query_to_run}</textarea>
|
||||
<label for="schedule">Schedule</label>
|
||||
<input type="text"
|
||||
id="automation-schedule-${automationId}"
|
||||
name="schedule"
|
||||
data-cron="${automation.crontime}"
|
||||
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 id="automation-success-${automationId}" style="display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let saveAutomationButtonEl = automationEl.querySelector(`#save-automation-button-${automation.id}`);
|
||||
saveAutomationButtonEl.addEventListener("click", async () => { await saveAutomation(automation.id); });
|
||||
let deleteAutomationButtonEl = automationEl.querySelector(`#delete-automation-button-${automation.id}`);
|
||||
deleteAutomationButtonEl.addEventListener("click", () => { deleteAutomation(automation.id); });
|
||||
|
||||
return automationEl.firstElementChild;
|
||||
}
|
||||
|
||||
function listAutomations() {
|
||||
const AutomationsList = document.getElementById("automations");
|
||||
fetch('/api/automations')
|
||||
.then(response => response.json())
|
||||
.then(automations => {
|
||||
if (!automations?.length > 0) return;
|
||||
AutomationsList.innerHTML = ''; // Clear existing content
|
||||
AutomationsList.append(...automations.map(automation => generateAutomationRow(automation)))
|
||||
});
|
||||
}
|
||||
listAutomations();
|
||||
|
||||
function enableSaveOnlyWhenInputsChanged() {
|
||||
const inputs = document.querySelectorAll('input[name="schedule"], textarea[name="query-to-run"], input[name="subject"]');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
// Get automation id by splitting the id by "-" and taking all elements after the second one
|
||||
const automationId = this.id.split("-").slice(2).join("-");
|
||||
let anyChanged = false;
|
||||
let inputNameStubs = ["subject", "query-to-run", "schedule"]
|
||||
for (let stub of inputNameStubs) {
|
||||
let el = document.getElementById(`automation-${stub}-${automationId}`);
|
||||
let originalValue = el.getAttribute('data-original');
|
||||
let currentValue = el.value;
|
||||
if (originalValue !== currentValue) {
|
||||
anyChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
document.getElementById(`save-automation-button-${automationId}`).disabled = !anyChanged;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get client location information from IP
|
||||
const ip_response = await fetch("https://ipapi.co/json")
|
||||
const ip_data = await ip_response.json();
|
||||
|
||||
// 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');
|
||||
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}®ion=${ip_data.region}&country=${ip_data.country_name}&timezone=${ip_data.timezone}`;
|
||||
let method = "POST";
|
||||
if (!create) {
|
||||
query_string += `&automation_id=${automationId}`;
|
||||
method = "PUT"
|
||||
}
|
||||
|
||||
fetch(`/api/automation?${query_string}`, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.ok ? response.json() : Promise.reject(data))
|
||||
.then(automation => {
|
||||
if (create) {
|
||||
const automationEl = document.getElementById(`automation-card-${automationId}`);
|
||||
automationEl.replaceWith(generateAutomationRow(automation));
|
||||
} else {
|
||||
updateAutomationRow(automation);
|
||||
}
|
||||
|
||||
notificationEl.style.display = "none";
|
||||
saveButtonEl.textContent = `✅ Automation ${actOn}d`;
|
||||
setTimeout(function() {
|
||||
saveButtonEl.textContent = "Save";
|
||||
}, 2000);
|
||||
})
|
||||
.catch(error => {
|
||||
notificationEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()} automations.`;
|
||||
notificationEl.style.display = "block";
|
||||
saveButtonEl.textContent = `⚠️ Failed to ${actOn.toLowerCase()} automations`;
|
||||
setTimeout(function() {
|
||||
saveButtonEl.textContent = actOn;
|
||||
}, 2000);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
const create_automation_button = document.getElementById("create-automation-button");
|
||||
create_automation_button.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
var automationEl = document.createElement("div");
|
||||
automationEl.classList.add("card");
|
||||
automationEl.classList.add("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>
|
||||
<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 id="automation-success-${placeholderId}" style="display: none;"></div>
|
||||
`;
|
||||
document.getElementById("automations").insertBefore(automationEl, document.getElementById("automations").firstChild);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -8,7 +8,9 @@ import uuid
|
|||
from typing import Any, Callable, List, Optional, Union
|
||||
|
||||
import cron_descriptor
|
||||
import pytz
|
||||
from apscheduler.job import Job
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from asgiref.sync import sync_to_async
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
|
||||
from fastapi.requests import Request
|
||||
|
@ -33,6 +35,7 @@ from khoj.routers.helpers import (
|
|||
CommonQueryParams,
|
||||
ConversationCommandRateLimiter,
|
||||
create_automation,
|
||||
schedule_automation,
|
||||
update_telemetry_state,
|
||||
)
|
||||
from khoj.search_filter.date_filter import DateFilter
|
||||
|
@ -41,7 +44,7 @@ from khoj.search_filter.word_filter import WordFilter
|
|||
from khoj.search_type import text_search
|
||||
from khoj.utils import state
|
||||
from khoj.utils.config import OfflineChatProcessorModel
|
||||
from khoj.utils.helpers import ConversationCommand, timer
|
||||
from khoj.utils.helpers import ConversationCommand, is_none_or_empty, timer
|
||||
from khoj.utils.rawconfig import LocationData, SearchResponse
|
||||
from khoj.utils.state import SearchType
|
||||
|
||||
|
@ -411,8 +414,8 @@ def delete_automation(request: Request, automation_id: str) -> Response:
|
|||
|
||||
try:
|
||||
automation_info = AutomationAdapters.delete_automation(user, automation_id)
|
||||
except ValueError as e:
|
||||
return Response(content="Could not find automation", status_code=403)
|
||||
except ValueError:
|
||||
return Response(status_code=204)
|
||||
|
||||
# Return deleted automation information as a JSON response
|
||||
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|
||||
|
@ -420,21 +423,44 @@ def delete_automation(request: Request, automation_id: str) -> Response:
|
|||
|
||||
@api.post("/automation", response_class=Response)
|
||||
@requires(["authenticated"])
|
||||
async def make_automation(
|
||||
async def post_automation(
|
||||
request: Request,
|
||||
q: str,
|
||||
subject: str,
|
||||
crontime: str,
|
||||
city: Optional[str] = None,
|
||||
region: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
timezone: Optional[str] = None,
|
||||
) -> Response:
|
||||
user: KhojUser = request.user.object
|
||||
if city or region or country:
|
||||
location = LocationData(city=city, region=region, country=country)
|
||||
|
||||
# Create automation with scheduling query and location data
|
||||
# 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 not cron_descriptor.get_description(crontime):
|
||||
return Response(content="Invalid crontime", status_code=400)
|
||||
|
||||
# Normalize query parameters
|
||||
# Add /automated_task prefix to query if not present
|
||||
q = q.strip()
|
||||
if not q.startswith("/automated_task"):
|
||||
query_to_run = f"/automated_task {q}"
|
||||
# Normalize crontime for AP Scheduler CronTrigger
|
||||
crontime = crontime.strip()
|
||||
if len(crontime.split(" ")) > 5:
|
||||
# Truncate crontime to 5 fields
|
||||
crontime = " ".join(crontime.split(" ")[:5])
|
||||
# Convert crontime to standard unix crontime
|
||||
crontime = crontime.replace("?", "*")
|
||||
subject = subject.strip()
|
||||
|
||||
# Schedule automation with query_to_run, timezone, subject directly provided by user
|
||||
try:
|
||||
automation, crontime, query_to_run, subject = await create_automation(q, location, timezone, user, request.url)
|
||||
# Get user timezone
|
||||
user_timezone = pytz.timezone(timezone)
|
||||
# Use the query to run as the scheduling request if the scheduling request is unset
|
||||
automation = await schedule_automation(query_to_run, subject, crontime, user_timezone, q, user, request.url)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating automation {q} for {user.email}: {e}")
|
||||
return Response(
|
||||
|
@ -449,25 +475,36 @@ async def make_automation(
|
|||
"id": automation.id,
|
||||
"subject": subject,
|
||||
"query_to_run": query_to_run,
|
||||
"scheduling_request": crontime,
|
||||
"scheduling_request": query_to_run,
|
||||
"schedule": schedule,
|
||||
"crontime": crontime,
|
||||
"next": automation.next_run_time.strftime("%Y-%m-%d %I:%M %p %Z"),
|
||||
}
|
||||
|
||||
# Return information about the created automation as a JSON response
|
||||
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|
||||
|
||||
|
||||
@api.patch("/automation", response_class=Response)
|
||||
@api.put("/automation", response_class=Response)
|
||||
@requires(["authenticated"])
|
||||
def edit_job(
|
||||
request: Request, automation_id: str, query_to_run: Optional[str] = None, crontime: Optional[str] = None
|
||||
request: Request,
|
||||
automation_id: str,
|
||||
q: Optional[str],
|
||||
subject: Optional[str],
|
||||
crontime: Optional[str],
|
||||
city: Optional[str] = None,
|
||||
region: Optional[str] = None,
|
||||
country: Optional[str] = None,
|
||||
timezone: Optional[str] = None,
|
||||
) -> Response:
|
||||
user: KhojUser = request.user.object
|
||||
|
||||
# Perform validation checks
|
||||
# Check at least one of query or crontime is provided
|
||||
if not query_to_run and not crontime:
|
||||
return Response(content="A query or crontime is required", status_code=400)
|
||||
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 not cron_descriptor.get_description(crontime):
|
||||
return Response(content="Invalid crontime", status_code=400)
|
||||
|
||||
# Check, get automation to edit
|
||||
try:
|
||||
|
@ -475,14 +512,31 @@ def edit_job(
|
|||
except ValueError as e:
|
||||
return Response(content="Invalid automation", status_code=403)
|
||||
|
||||
# Normalize query parameters
|
||||
# Add /automated_task prefix to query if not present
|
||||
if not query_to_run.startswith("/automated_task"):
|
||||
query_to_run = f"/automated_task {query_to_run}"
|
||||
q = q.strip()
|
||||
if not q.startswith("/automated_task"):
|
||||
query_to_run = f"/automated_task {q}"
|
||||
# Normalize crontime for AP Scheduler CronTrigger
|
||||
crontime = crontime.strip()
|
||||
if len(crontime.split(" ")) > 5:
|
||||
# Truncate crontime to 5 fields
|
||||
crontime = " ".join(crontime.split(" ")[:5])
|
||||
# Convert crontime to standard unix crontime
|
||||
crontime = crontime.replace("?", "*")
|
||||
|
||||
# Update automation with new query
|
||||
# Construct updated automation metadata
|
||||
automation_metadata = json.loads(automation.name)
|
||||
automation_metadata["query_to_run"] = query_to_run
|
||||
automation.modify(kwargs={"query_to_run": query_to_run}, name=json.dumps(automation_metadata))
|
||||
automation_metadata["subject"] = subject.strip()
|
||||
|
||||
# Modify automation with updated query, subject, crontime
|
||||
automation.modify(kwargs={"query_to_run": query_to_run, "subject": subject}, name=json.dumps(automation_metadata))
|
||||
|
||||
# Reschedule automation if crontime updated
|
||||
trigger = CronTrigger.from_crontab(crontime)
|
||||
if automation.trigger != trigger:
|
||||
automation.reschedule(trigger=trigger)
|
||||
|
||||
# Collate info about the modified user automation
|
||||
automation_info = {
|
||||
|
|
|
@ -922,14 +922,32 @@ async def create_automation(
|
|||
q: str, location: LocationData, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}
|
||||
):
|
||||
user_timezone = pytz.timezone(timezone)
|
||||
crontime_string, query_to_run, subject = await schedule_query(q, location, meta_log)
|
||||
trigger = CronTrigger.from_crontab(crontime_string, user_timezone)
|
||||
crontime, query_to_run, subject = await schedule_query(q, location, meta_log)
|
||||
job = await schedule_automation(query_to_run, subject, crontime, user_timezone, q, user, calling_url)
|
||||
return job, crontime, query_to_run, subject
|
||||
|
||||
|
||||
async def schedule_automation(
|
||||
query_to_run: str,
|
||||
subject: str,
|
||||
crontime: str,
|
||||
user_timezone,
|
||||
scheduling_request: str,
|
||||
user: KhojUser,
|
||||
calling_url: URL,
|
||||
):
|
||||
trigger = CronTrigger.from_crontab(crontime, user_timezone)
|
||||
# Generate id and metadata used by task scheduler and process locks for the task runs
|
||||
job_metadata = json.dumps(
|
||||
{"query_to_run": query_to_run, "scheduling_request": q, "subject": subject, "crontime": crontime_string}
|
||||
{
|
||||
"query_to_run": query_to_run,
|
||||
"scheduling_request": scheduling_request,
|
||||
"subject": subject,
|
||||
"crontime": crontime,
|
||||
}
|
||||
)
|
||||
query_id = hashlib.md5(f"{query_to_run}{crontime_string}".encode("utf-8")).hexdigest()
|
||||
job_id = f"automation_{user.uuid}_{crontime_string}_{query_id}"
|
||||
query_id = hashlib.md5(f"{query_to_run}_{crontime}".encode("utf-8")).hexdigest()
|
||||
job_id = f"automation_{user.uuid}_{query_id}"
|
||||
job = await sync_to_async(state.scheduler.add_job)(
|
||||
run_with_process_lock,
|
||||
trigger=trigger,
|
||||
|
@ -939,7 +957,7 @@ async def create_automation(
|
|||
),
|
||||
kwargs={
|
||||
"query_to_run": query_to_run,
|
||||
"scheduling_request": q,
|
||||
"scheduling_request": scheduling_request,
|
||||
"subject": subject,
|
||||
"user": user,
|
||||
"calling_url": calling_url,
|
||||
|
@ -949,7 +967,7 @@ async def create_automation(
|
|||
max_instances=2, # Allow second instance to kill any previous instance with stale lock
|
||||
jitter=30,
|
||||
)
|
||||
return job, crontime_string, query_to_run, subject
|
||||
return job
|
||||
|
||||
|
||||
def construct_automation_created_message(automation: Job, crontime: str, query_to_run: str, subject: str, url: URL):
|
||||
|
@ -968,5 +986,5 @@ def construct_automation_created_message(automation: Job, crontime: str, query_t
|
|||
- Schedule: `{schedule}`
|
||||
- Next Run At: {next_run_time}
|
||||
|
||||
Manage your tasks [here](/config#automations).
|
||||
Manage your automations [here](/automations).
|
||||
""".strip()
|
||||
|
|
|
@ -11,6 +11,7 @@ from starlette.authentication import has_required_scope, requires
|
|||
from khoj.database import adapters
|
||||
from khoj.database.adapters import (
|
||||
AgentAdapters,
|
||||
AutomationAdapters,
|
||||
ConversationAdapters,
|
||||
EntryAdapters,
|
||||
get_user_github_config,
|
||||
|
@ -364,3 +365,23 @@ def computer_config_page(request: Request):
|
|||
"khoj_version": state.khoj_version,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@web_client.get("/automations", response_class=HTMLResponse)
|
||||
@requires(["authenticated"], redirect="login_page")
|
||||
def automations_config_page(request: Request):
|
||||
user = request.user.object
|
||||
user_picture = request.session.get("user", {}).get("picture")
|
||||
has_documents = EntryAdapters.user_has_entries(user=user)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"config_automation.html",
|
||||
context={
|
||||
"request": request,
|
||||
"username": user.username,
|
||||
"user_photo": user_picture,
|
||||
"is_active": has_required_scope(request, ["premium"]),
|
||||
"has_documents": has_documents,
|
||||
"khoj_version": state.khoj_version,
|
||||
},
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue