mirror of
https://github.com/khoaliber/khoj.git
synced 2026-03-09 13:25:11 +00:00
[Breaking] Move automation api into new router with consistent routes
- Rename GET /api/automations to GET /api/automation - Rename POST /api/trigger/automation to POST /api/automation/trigger - Update calls to the automations API from the web app.
This commit is contained in:
@@ -74,7 +74,7 @@ import { KhojLogoType } from "../components/logo/khojLogo";
|
|||||||
|
|
||||||
const automationsFetcher = () =>
|
const automationsFetcher = () =>
|
||||||
window
|
window
|
||||||
.fetch("/api/automations")
|
.fetch("/api/automation")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ function deleteAutomation(automationId: string, setIsDeleted: (isDeleted: boolea
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendAPreview(automationId: string, setToastMessage: (toastMessage: string) => void) {
|
function sendAPreview(automationId: string, setToastMessage: (toastMessage: string) => void) {
|
||||||
fetch(`/api/trigger/automation?automation_id=${automationId}`, { method: "POST" })
|
fetch(`/api/automation/trigger?automation_id=${automationId}`, { method: "POST" })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Network response was not ok");
|
throw new Error("Network response was not ok");
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ def configure_routes(app):
|
|||||||
# Import APIs here to setup search types before while configuring server
|
# Import APIs here to setup search types before while configuring server
|
||||||
from khoj.routers.api import api
|
from khoj.routers.api import api
|
||||||
from khoj.routers.api_agents import api_agents
|
from khoj.routers.api_agents import api_agents
|
||||||
|
from khoj.routers.api_automation import api_automation
|
||||||
from khoj.routers.api_chat import api_chat
|
from khoj.routers.api_chat import api_chat
|
||||||
from khoj.routers.api_content import api_content
|
from khoj.routers.api_content import api_content
|
||||||
from khoj.routers.api_model import api_model
|
from khoj.routers.api_model import api_model
|
||||||
@@ -321,6 +322,7 @@ def configure_routes(app):
|
|||||||
app.include_router(api, prefix="/api")
|
app.include_router(api, prefix="/api")
|
||||||
app.include_router(api_chat, prefix="/api/chat")
|
app.include_router(api_chat, prefix="/api/chat")
|
||||||
app.include_router(api_agents, prefix="/api/agents")
|
app.include_router(api_agents, prefix="/api/agents")
|
||||||
|
app.include_router(api_automation, prefix="/api/automation")
|
||||||
app.include_router(api_model, prefix="/api/model")
|
app.include_router(api_model, prefix="/api/model")
|
||||||
app.include_router(api_content, prefix="/api/content")
|
app.include_router(api_content, prefix="/api/content")
|
||||||
app.include_router(notion_router, prefix="/api/notion")
|
app.include_router(notion_router, prefix="/api/notion")
|
||||||
|
|||||||
@@ -2,15 +2,10 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import threading
|
|
||||||
import uuid
|
import uuid
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
import cron_descriptor
|
|
||||||
import openai
|
import openai
|
||||||
import pytz
|
|
||||||
from apscheduler.job import Job
|
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
|
||||||
from fastapi.requests import Request
|
from fastapi.requests import Request
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
@@ -18,28 +13,19 @@ from starlette.authentication import has_required_scope, requires
|
|||||||
|
|
||||||
from khoj.configure import initialize_content
|
from khoj.configure import initialize_content
|
||||||
from khoj.database import adapters
|
from khoj.database import adapters
|
||||||
from khoj.database.adapters import (
|
from khoj.database.adapters import ConversationAdapters, EntryAdapters, get_user_photo
|
||||||
AutomationAdapters,
|
|
||||||
ConversationAdapters,
|
|
||||||
EntryAdapters,
|
|
||||||
get_user_photo,
|
|
||||||
)
|
|
||||||
from khoj.database.models import KhojUser, SpeechToTextModelOptions
|
from khoj.database.models import KhojUser, SpeechToTextModelOptions
|
||||||
from khoj.processor.conversation.offline.whisper import transcribe_audio_offline
|
from khoj.processor.conversation.offline.whisper import transcribe_audio_offline
|
||||||
from khoj.processor.conversation.openai.whisper import transcribe_audio
|
from khoj.processor.conversation.openai.whisper import transcribe_audio
|
||||||
from khoj.processor.conversation.utils import clean_json
|
|
||||||
from khoj.routers.helpers import (
|
from khoj.routers.helpers import (
|
||||||
ApiUserRateLimiter,
|
ApiUserRateLimiter,
|
||||||
CommonQueryParams,
|
CommonQueryParams,
|
||||||
ConversationCommandRateLimiter,
|
ConversationCommandRateLimiter,
|
||||||
execute_search,
|
execute_search,
|
||||||
get_user_config,
|
get_user_config,
|
||||||
schedule_automation,
|
|
||||||
schedule_query,
|
|
||||||
update_telemetry_state,
|
update_telemetry_state,
|
||||||
)
|
)
|
||||||
from khoj.utils import state
|
from khoj.utils import state
|
||||||
from khoj.utils.helpers import is_none_or_empty
|
|
||||||
from khoj.utils.rawconfig import SearchResponse
|
from khoj.utils.rawconfig import SearchResponse
|
||||||
from khoj.utils.state import SearchType
|
from khoj.utils.state import SearchType
|
||||||
|
|
||||||
@@ -270,224 +256,3 @@ def user_info(request: Request) -> Response:
|
|||||||
|
|
||||||
# Return user information as a JSON response
|
# Return user information as a JSON response
|
||||||
return Response(content=json.dumps(user_info), media_type="application/json", status_code=200)
|
return Response(content=json.dumps(user_info), media_type="application/json", status_code=200)
|
||||||
|
|
||||||
|
|
||||||
@api.get("/automations", response_class=Response)
|
|
||||||
@requires(["authenticated"])
|
|
||||||
def get_automations(request: Request) -> Response:
|
|
||||||
user: KhojUser = request.user.object
|
|
||||||
|
|
||||||
# Collate all automations created by user that are still active
|
|
||||||
automations_info = [automation_info for automation_info in AutomationAdapters.get_automations_metadata(user)]
|
|
||||||
|
|
||||||
# Return tasks information as a JSON response
|
|
||||||
return Response(content=json.dumps(automations_info), media_type="application/json", status_code=200)
|
|
||||||
|
|
||||||
|
|
||||||
@api.delete("/automation", response_class=Response)
|
|
||||||
@requires(["authenticated"])
|
|
||||||
def delete_automation(request: Request, automation_id: str) -> Response:
|
|
||||||
user: KhojUser = request.user.object
|
|
||||||
|
|
||||||
try:
|
|
||||||
automation_info = AutomationAdapters.delete_automation(user, automation_id)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@api.post("/automation", response_class=Response)
|
|
||||||
@requires(["authenticated"])
|
|
||||||
def post_automation(
|
|
||||||
request: Request,
|
|
||||||
q: str,
|
|
||||||
crontime: str,
|
|
||||||
subject: Optional[str] = None,
|
|
||||||
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
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Infer subject, query to run
|
|
||||||
_, query_to_run, generated_subject = schedule_query(q, chat_history=[], user=user)
|
|
||||||
subject = subject or generated_subject
|
|
||||||
|
|
||||||
# Normalize query parameters
|
|
||||||
# Add /automated_task prefix to query if not present
|
|
||||||
query_to_run = query_to_run.strip()
|
|
||||||
if not query_to_run.startswith("/automated_task"):
|
|
||||||
query_to_run = f"/automated_task {query_to_run}"
|
|
||||||
|
|
||||||
# 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("?", "*")
|
|
||||||
|
|
||||||
# Disallow minute level automation recurrence
|
|
||||||
minute_value = crontime.split(" ")[0]
|
|
||||||
if not minute_value.isdigit():
|
|
||||||
return Response(
|
|
||||||
content="Minute level recurrence is unsupported. Please create a less frequent schedule.",
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create new Conversation Session associated with this new task
|
|
||||||
title = f"Automation: {subject}"
|
|
||||||
conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
|
|
||||||
|
|
||||||
# Schedule automation with query_to_run, timezone, subject directly provided by user
|
|
||||||
try:
|
|
||||||
# Use the query to run as the scheduling request if the scheduling request is unset
|
|
||||||
calling_url = request.url.replace(query=f"{request.url.query}")
|
|
||||||
automation = schedule_automation(
|
|
||||||
query_to_run, subject, crontime, timezone, q, user, calling_url, str(conversation.id)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error creating automation {q} for {user.email}: {e}", exc_info=True)
|
|
||||||
return Response(
|
|
||||||
content=f"Unable to create automation. Ensure the automation doesn't already exist.",
|
|
||||||
media_type="text/plain",
|
|
||||||
status_code=500,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Collate info about the created user automation
|
|
||||||
automation_info = AutomationAdapters.get_automation_metadata(user, automation)
|
|
||||||
|
|
||||||
# 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.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)
|
|
||||||
@requires(["authenticated"])
|
|
||||||
def edit_job(
|
|
||||||
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
|
|
||||||
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:
|
|
||||||
automation: Job = AutomationAdapters.get_automation(user, automation_id)
|
|
||||||
except ValueError as e:
|
|
||||||
logger.error(f"Error editing automation {automation_id} for {user.email}: {e}", exc_info=True)
|
|
||||||
return Response(content="Invalid automation", status_code=403)
|
|
||||||
|
|
||||||
# Infer subject, query to run
|
|
||||||
_, query_to_run, _ = schedule_query(q, chat_history=[], user=user)
|
|
||||||
subject = subject
|
|
||||||
|
|
||||||
# Normalize query parameters
|
|
||||||
# Add /automated_task prefix to query if not present
|
|
||||||
query_to_run = query_to_run.strip()
|
|
||||||
if not query_to_run.startswith("/automated_task"):
|
|
||||||
query_to_run = f"/automated_task {query_to_run}"
|
|
||||||
# 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("?", "*")
|
|
||||||
|
|
||||||
# Disallow minute level automation recurrence
|
|
||||||
minute_value = crontime.split(" ")[0]
|
|
||||||
if not minute_value.isdigit():
|
|
||||||
return Response(
|
|
||||||
content="Recurrence of every X minutes is unsupported. Please create a less frequent schedule.",
|
|
||||||
status_code=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Construct updated automation metadata
|
|
||||||
automation_metadata: dict[str, str] = json.loads(clean_json(automation.name))
|
|
||||||
automation_metadata["scheduling_request"] = q
|
|
||||||
automation_metadata["query_to_run"] = query_to_run
|
|
||||||
automation_metadata["subject"] = subject.strip()
|
|
||||||
automation_metadata["crontime"] = crontime
|
|
||||||
conversation_id = automation_metadata.get("conversation_id")
|
|
||||||
|
|
||||||
if not conversation_id:
|
|
||||||
title = f"Automation: {subject}"
|
|
||||||
|
|
||||||
# Create new Conversation Session associated with this new task
|
|
||||||
conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
|
|
||||||
|
|
||||||
conversation_id = str(conversation.id)
|
|
||||||
automation_metadata["conversation_id"] = conversation_id
|
|
||||||
|
|
||||||
# Modify automation with updated query, subject
|
|
||||||
automation.modify(
|
|
||||||
name=json.dumps(automation_metadata),
|
|
||||||
kwargs={
|
|
||||||
"query_to_run": query_to_run,
|
|
||||||
"subject": subject,
|
|
||||||
"scheduling_request": q,
|
|
||||||
"user": user,
|
|
||||||
"calling_url": request.url,
|
|
||||||
"conversation_id": conversation_id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# Reschedule automation if crontime updated
|
|
||||||
user_timezone = pytz.timezone(timezone)
|
|
||||||
trigger = CronTrigger.from_crontab(crontime, user_timezone)
|
|
||||||
if automation.trigger != trigger:
|
|
||||||
automation.reschedule(trigger=trigger)
|
|
||||||
|
|
||||||
# Collate info about the updated user automation
|
|
||||||
automation = AutomationAdapters.get_automation(user, automation.id)
|
|
||||||
automation_info = AutomationAdapters.get_automation_metadata(user, automation)
|
|
||||||
|
|
||||||
# Return modified automation information as a JSON response
|
|
||||||
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|
|
||||||
|
|||||||
243
src/khoj/routers/api_automation.py
Normal file
243
src/khoj/routers/api_automation.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import cron_descriptor
|
||||||
|
import pytz
|
||||||
|
from apscheduler.job import Job
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
from fastapi import APIRouter, Request
|
||||||
|
from fastapi.responses import Response
|
||||||
|
from starlette.authentication import requires
|
||||||
|
|
||||||
|
from khoj.database.adapters import AutomationAdapters, ConversationAdapters
|
||||||
|
from khoj.database.models import KhojUser
|
||||||
|
from khoj.processor.conversation.utils import clean_json
|
||||||
|
from khoj.routers.helpers import schedule_automation, schedule_query
|
||||||
|
from khoj.utils.helpers import is_none_or_empty
|
||||||
|
|
||||||
|
# Initialize Router
|
||||||
|
api_automation = APIRouter()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@api_automation.get("", response_class=Response)
|
||||||
|
@requires(["authenticated"])
|
||||||
|
def get_automations(request: Request) -> Response:
|
||||||
|
user: KhojUser = request.user.object
|
||||||
|
|
||||||
|
# Collate all automations created by user that are still active
|
||||||
|
automations_info = [automation_info for automation_info in AutomationAdapters.get_automations_metadata(user)]
|
||||||
|
|
||||||
|
# Return tasks information as a JSON response
|
||||||
|
return Response(content=json.dumps(automations_info), media_type="application/json", status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
@api_automation.delete("", response_class=Response)
|
||||||
|
@requires(["authenticated"])
|
||||||
|
def delete_automation(request: Request, automation_id: str) -> Response:
|
||||||
|
user: KhojUser = request.user.object
|
||||||
|
|
||||||
|
try:
|
||||||
|
automation_info = AutomationAdapters.delete_automation(user, automation_id)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@api_automation.post("", response_class=Response)
|
||||||
|
@requires(["authenticated"])
|
||||||
|
def post_automation(
|
||||||
|
request: Request,
|
||||||
|
q: str,
|
||||||
|
crontime: str,
|
||||||
|
subject: Optional[str] = None,
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Infer subject, query to run
|
||||||
|
_, query_to_run, generated_subject = schedule_query(q, chat_history=[], user=user)
|
||||||
|
subject = subject or generated_subject
|
||||||
|
|
||||||
|
# Normalize query parameters
|
||||||
|
# Add /automated_task prefix to query if not present
|
||||||
|
query_to_run = query_to_run.strip()
|
||||||
|
if not query_to_run.startswith("/automated_task"):
|
||||||
|
query_to_run = f"/automated_task {query_to_run}"
|
||||||
|
|
||||||
|
# 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("?", "*")
|
||||||
|
|
||||||
|
# Disallow minute level automation recurrence
|
||||||
|
minute_value = crontime.split(" ")[0]
|
||||||
|
if not minute_value.isdigit():
|
||||||
|
return Response(
|
||||||
|
content="Minute level recurrence is unsupported. Please create a less frequent schedule.",
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create new Conversation Session associated with this new task
|
||||||
|
title = f"Automation: {subject}"
|
||||||
|
conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
|
||||||
|
|
||||||
|
# Schedule automation with query_to_run, timezone, subject directly provided by user
|
||||||
|
try:
|
||||||
|
# Use the query to run as the scheduling request if the scheduling request is unset
|
||||||
|
calling_url = request.url.replace(query=f"{request.url.query}")
|
||||||
|
automation = schedule_automation(
|
||||||
|
query_to_run, subject, crontime, timezone, q, user, calling_url, str(conversation.id)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating automation {q} for {user.email}: {e}", exc_info=True)
|
||||||
|
return Response(
|
||||||
|
content=f"Unable to create automation. Ensure the automation doesn't already exist.",
|
||||||
|
media_type="text/plain",
|
||||||
|
status_code=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Collate info about the created user automation
|
||||||
|
automation_info = AutomationAdapters.get_automation_metadata(user, automation)
|
||||||
|
|
||||||
|
# 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_automation.post("/trigger", 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_automation.put("", response_class=Response)
|
||||||
|
@requires(["authenticated"])
|
||||||
|
def edit_job(
|
||||||
|
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
|
||||||
|
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:
|
||||||
|
automation: Job = AutomationAdapters.get_automation(user, automation_id)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Error editing automation {automation_id} for {user.email}: {e}", exc_info=True)
|
||||||
|
return Response(content="Invalid automation", status_code=403)
|
||||||
|
|
||||||
|
# Infer subject, query to run
|
||||||
|
_, query_to_run, _ = schedule_query(q, chat_history=[], user=user)
|
||||||
|
subject = subject
|
||||||
|
|
||||||
|
# Normalize query parameters
|
||||||
|
# Add /automated_task prefix to query if not present
|
||||||
|
query_to_run = query_to_run.strip()
|
||||||
|
if not query_to_run.startswith("/automated_task"):
|
||||||
|
query_to_run = f"/automated_task {query_to_run}"
|
||||||
|
# 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("?", "*")
|
||||||
|
|
||||||
|
# Disallow minute level automation recurrence
|
||||||
|
minute_value = crontime.split(" ")[0]
|
||||||
|
if not minute_value.isdigit():
|
||||||
|
return Response(
|
||||||
|
content="Recurrence of every X minutes is unsupported. Please create a less frequent schedule.",
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Construct updated automation metadata
|
||||||
|
automation_metadata: dict[str, str] = json.loads(clean_json(automation.name))
|
||||||
|
automation_metadata["scheduling_request"] = q
|
||||||
|
automation_metadata["query_to_run"] = query_to_run
|
||||||
|
automation_metadata["subject"] = subject.strip()
|
||||||
|
automation_metadata["crontime"] = crontime
|
||||||
|
conversation_id = automation_metadata.get("conversation_id")
|
||||||
|
|
||||||
|
if not conversation_id:
|
||||||
|
title = f"Automation: {subject}"
|
||||||
|
|
||||||
|
# Create new Conversation Session associated with this new task
|
||||||
|
conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
|
||||||
|
|
||||||
|
conversation_id = str(conversation.id)
|
||||||
|
automation_metadata["conversation_id"] = conversation_id
|
||||||
|
|
||||||
|
# Modify automation with updated query, subject
|
||||||
|
automation.modify(
|
||||||
|
name=json.dumps(automation_metadata),
|
||||||
|
kwargs={
|
||||||
|
"query_to_run": query_to_run,
|
||||||
|
"subject": subject,
|
||||||
|
"scheduling_request": q,
|
||||||
|
"user": user,
|
||||||
|
"calling_url": request.url,
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reschedule automation if crontime updated
|
||||||
|
user_timezone = pytz.timezone(timezone)
|
||||||
|
trigger = CronTrigger.from_crontab(crontime, user_timezone)
|
||||||
|
if automation.trigger != trigger:
|
||||||
|
automation.reschedule(trigger=trigger)
|
||||||
|
|
||||||
|
# Collate info about the updated user automation
|
||||||
|
automation = AutomationAdapters.get_automation(user, automation.id)
|
||||||
|
automation_info = AutomationAdapters.get_automation_metadata(user, automation)
|
||||||
|
|
||||||
|
# Return modified automation information as a JSON response
|
||||||
|
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|
||||||
Reference in New Issue
Block a user