khoj 1.42.2.dev1__py3-none-any.whl → 1.42.2.dev16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- khoj/configure.py +2 -0
- khoj/database/adapters/__init__.py +6 -6
- khoj/interface/compiled/404/index.html +2 -2
- khoj/interface/compiled/_next/static/chunks/{2327-f03b2a77f67b8f8c.js → 2327-aa22697ed9c8d54a.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/7127-79a3af5138960272.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{5138-81457f7f59956b56.js → 7211-7fedd2ee3655239c.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-4e2a134ec26aa606.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/page-ef89ac958e78aa81.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-ad4d1792ab1a4108.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/page-db0fbea54ccea62f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-9a167dc9b5fcd464.js → page-da90c78180a86040.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{webpack-1c900156837baf90.js → webpack-0f15e6b51732b337.js} +1 -1
- khoj/interface/compiled/_next/static/css/{c34713c98384ee87.css → 2945c4a857922f3b.css} +1 -1
- khoj/interface/compiled/_next/static/css/{9c223d337a984468.css → 7017ee76c2f2cd87.css} +1 -1
- khoj/interface/compiled/_next/static/css/9a460202d29476e5.css +1 -0
- khoj/interface/compiled/agents/index.html +2 -2
- khoj/interface/compiled/agents/index.txt +2 -2
- khoj/interface/compiled/automations/index.html +2 -2
- khoj/interface/compiled/automations/index.txt +3 -3
- khoj/interface/compiled/chat/index.html +2 -2
- khoj/interface/compiled/chat/index.txt +2 -2
- khoj/interface/compiled/index.html +2 -2
- khoj/interface/compiled/index.txt +2 -2
- khoj/interface/compiled/search/index.html +2 -2
- khoj/interface/compiled/search/index.txt +2 -2
- khoj/interface/compiled/settings/index.html +2 -2
- khoj/interface/compiled/settings/index.txt +4 -4
- khoj/interface/compiled/share/chat/index.html +2 -2
- khoj/interface/compiled/share/chat/index.txt +2 -2
- khoj/processor/conversation/anthropic/anthropic_chat.py +17 -132
- khoj/processor/conversation/anthropic/utils.py +1 -1
- khoj/processor/conversation/google/gemini_chat.py +18 -139
- khoj/processor/conversation/offline/chat_model.py +21 -151
- khoj/processor/conversation/openai/gpt.py +12 -126
- khoj/processor/conversation/prompts.py +2 -63
- khoj/routers/api.py +5 -533
- khoj/routers/api_automation.py +243 -0
- khoj/routers/api_chat.py +35 -116
- khoj/routers/helpers.py +329 -80
- khoj/routers/research.py +3 -33
- khoj/utils/helpers.py +0 -6
- {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/METADATA +1 -1
- {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/RECORD +54 -53
- khoj/interface/compiled/_next/static/chunks/7127-d3199617463d45f0.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-e00fb81dca656a10.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/page-465741d9149dfd48.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-1726184cf1c1b86e.js +0 -1
- khoj/interface/compiled/_next/static/css/fca983d49c3dd1a3.css +0 -1
- /khoj/interface/compiled/_next/static/{Dzg_ViqMwQEjqMgetZPRc → OTsOjbrtuaYMukpuJS4sy}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{Dzg_ViqMwQEjqMgetZPRc → OTsOjbrtuaYMukpuJS4sy}/_ssgManifest.js +0 -0
- /khoj/interface/compiled/_next/static/chunks/{1915-ab4353eaca76f690.js → 1915-1943ee8a628b893c.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{2117-1c18aa2098982bf9.js → 2117-5a41630a2bd2eae8.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{4363-4efaf12abe696251.js → 4363-e6ac2203564d1a3b.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{4447-5d44807c40355b1a.js → 4447-e038b251d626c340.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{8667-adbe6017a66cef10.js → 8667-8136f74e9a086fca.js} +0 -0
- /khoj/interface/compiled/_next/static/chunks/{9259-d8bcd9da9e80c81e.js → 9259-640fdd77408475df.js} +0 -0
- {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/WHEEL +0 -0
- {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/entry_points.txt +0 -0
- {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/licenses/LICENSE +0 -0
khoj/routers/api.py
CHANGED
@@ -1,19 +1,11 @@
|
|
1
|
-
import concurrent.futures
|
2
1
|
import json
|
3
2
|
import logging
|
4
3
|
import math
|
5
4
|
import os
|
6
|
-
import threading
|
7
|
-
import time
|
8
5
|
import uuid
|
9
|
-
from typing import
|
6
|
+
from typing import List, Optional, Union
|
10
7
|
|
11
|
-
import cron_descriptor
|
12
8
|
import openai
|
13
|
-
import pytz
|
14
|
-
from apscheduler.job import Job
|
15
|
-
from apscheduler.triggers.cron import CronTrigger
|
16
|
-
from asgiref.sync import sync_to_async
|
17
9
|
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
|
18
10
|
from fastapi.requests import Request
|
19
11
|
from fastapi.responses import Response
|
@@ -21,49 +13,20 @@ from starlette.authentication import has_required_scope, requires
|
|
21
13
|
|
22
14
|
from khoj.configure import initialize_content
|
23
15
|
from khoj.database import adapters
|
24
|
-
from khoj.database.adapters import
|
25
|
-
|
26
|
-
AutomationAdapters,
|
27
|
-
ConversationAdapters,
|
28
|
-
EntryAdapters,
|
29
|
-
get_default_search_model,
|
30
|
-
get_user_photo,
|
31
|
-
)
|
32
|
-
from khoj.database.models import (
|
33
|
-
Agent,
|
34
|
-
ChatMessageModel,
|
35
|
-
ChatModel,
|
36
|
-
KhojUser,
|
37
|
-
SpeechToTextModelOptions,
|
38
|
-
)
|
39
|
-
from khoj.processor.conversation import prompts
|
40
|
-
from khoj.processor.conversation.anthropic.anthropic_chat import (
|
41
|
-
extract_questions_anthropic,
|
42
|
-
)
|
43
|
-
from khoj.processor.conversation.google.gemini_chat import extract_questions_gemini
|
44
|
-
from khoj.processor.conversation.offline.chat_model import extract_questions_offline
|
16
|
+
from khoj.database.adapters import ConversationAdapters, EntryAdapters, get_user_photo
|
17
|
+
from khoj.database.models import KhojUser, SpeechToTextModelOptions
|
45
18
|
from khoj.processor.conversation.offline.whisper import transcribe_audio_offline
|
46
|
-
from khoj.processor.conversation.openai.gpt import extract_questions
|
47
19
|
from khoj.processor.conversation.openai.whisper import transcribe_audio
|
48
|
-
from khoj.processor.conversation.utils import clean_json, defilter_query
|
49
20
|
from khoj.routers.helpers import (
|
50
21
|
ApiUserRateLimiter,
|
51
|
-
ChatEvent,
|
52
22
|
CommonQueryParams,
|
53
23
|
ConversationCommandRateLimiter,
|
24
|
+
execute_search,
|
54
25
|
get_user_config,
|
55
|
-
schedule_automation,
|
56
|
-
schedule_query,
|
57
26
|
update_telemetry_state,
|
58
27
|
)
|
59
|
-
from khoj.search_filter.date_filter import DateFilter
|
60
|
-
from khoj.search_filter.file_filter import FileFilter
|
61
|
-
from khoj.search_filter.word_filter import WordFilter
|
62
|
-
from khoj.search_type import text_search
|
63
28
|
from khoj.utils import state
|
64
|
-
from khoj.utils.
|
65
|
-
from khoj.utils.helpers import ConversationCommand, is_none_or_empty, timer
|
66
|
-
from khoj.utils.rawconfig import LocationData, SearchResponse
|
29
|
+
from khoj.utils.rawconfig import SearchResponse
|
67
30
|
from khoj.utils.state import SearchType
|
68
31
|
|
69
32
|
# Initialize Router
|
@@ -116,98 +79,6 @@ async def search(
|
|
116
79
|
return results
|
117
80
|
|
118
81
|
|
119
|
-
async def execute_search(
|
120
|
-
user: KhojUser,
|
121
|
-
q: str,
|
122
|
-
n: Optional[int] = 5,
|
123
|
-
t: Optional[SearchType] = SearchType.All,
|
124
|
-
r: Optional[bool] = False,
|
125
|
-
max_distance: Optional[Union[float, None]] = None,
|
126
|
-
dedupe: Optional[bool] = True,
|
127
|
-
agent: Optional[Agent] = None,
|
128
|
-
):
|
129
|
-
# Run validation checks
|
130
|
-
results: List[SearchResponse] = []
|
131
|
-
|
132
|
-
start_time = time.time()
|
133
|
-
|
134
|
-
# Ensure the agent, if present, is accessible by the user
|
135
|
-
if user and agent and not await AgentAdapters.ais_agent_accessible(agent, user):
|
136
|
-
logger.error(f"Agent {agent.slug} is not accessible by user {user}")
|
137
|
-
return results
|
138
|
-
|
139
|
-
if q is None or q == "":
|
140
|
-
logger.warning(f"No query param (q) passed in API call to initiate search")
|
141
|
-
return results
|
142
|
-
|
143
|
-
# initialize variables
|
144
|
-
user_query = q.strip()
|
145
|
-
results_count = n or 5
|
146
|
-
search_futures: List[concurrent.futures.Future] = []
|
147
|
-
|
148
|
-
# return cached results, if available
|
149
|
-
if user:
|
150
|
-
query_cache_key = f"{user_query}-{n}-{t}-{r}-{max_distance}-{dedupe}"
|
151
|
-
if query_cache_key in state.query_cache[user.uuid]:
|
152
|
-
logger.debug(f"Return response from query cache")
|
153
|
-
return state.query_cache[user.uuid][query_cache_key]
|
154
|
-
|
155
|
-
# Encode query with filter terms removed
|
156
|
-
defiltered_query = user_query
|
157
|
-
for filter in [DateFilter(), WordFilter(), FileFilter()]:
|
158
|
-
defiltered_query = filter.defilter(defiltered_query)
|
159
|
-
|
160
|
-
encoded_asymmetric_query = None
|
161
|
-
if t != SearchType.Image:
|
162
|
-
with timer("Encoding query took", logger=logger):
|
163
|
-
search_model = await sync_to_async(get_default_search_model)()
|
164
|
-
encoded_asymmetric_query = state.embeddings_model[search_model.name].embed_query(defiltered_query)
|
165
|
-
|
166
|
-
with concurrent.futures.ThreadPoolExecutor() as executor:
|
167
|
-
if t in [
|
168
|
-
SearchType.All,
|
169
|
-
SearchType.Org,
|
170
|
-
SearchType.Markdown,
|
171
|
-
SearchType.Github,
|
172
|
-
SearchType.Notion,
|
173
|
-
SearchType.Plaintext,
|
174
|
-
SearchType.Pdf,
|
175
|
-
]:
|
176
|
-
# query markdown notes
|
177
|
-
search_futures += [
|
178
|
-
executor.submit(
|
179
|
-
text_search.query,
|
180
|
-
user_query,
|
181
|
-
user,
|
182
|
-
t,
|
183
|
-
question_embedding=encoded_asymmetric_query,
|
184
|
-
max_distance=max_distance,
|
185
|
-
agent=agent,
|
186
|
-
)
|
187
|
-
]
|
188
|
-
|
189
|
-
# Query across each requested content types in parallel
|
190
|
-
with timer("Query took", logger):
|
191
|
-
for search_future in concurrent.futures.as_completed(search_futures):
|
192
|
-
hits = await search_future.result()
|
193
|
-
# Collate results
|
194
|
-
results += text_search.collate_results(hits, dedupe=dedupe)
|
195
|
-
|
196
|
-
# Sort results across all content types and take top results
|
197
|
-
results = text_search.rerank_and_sort_results(
|
198
|
-
results, query=defiltered_query, rank_results=r, search_model_name=search_model.name
|
199
|
-
)[:results_count]
|
200
|
-
|
201
|
-
# Cache results
|
202
|
-
if user:
|
203
|
-
state.query_cache[user.uuid][query_cache_key] = results
|
204
|
-
|
205
|
-
end_time = time.time()
|
206
|
-
logger.debug(f"🔍 Search took: {end_time - start_time:.3f} seconds")
|
207
|
-
|
208
|
-
return results
|
209
|
-
|
210
|
-
|
211
82
|
@api.get("/update")
|
212
83
|
@requires(["authenticated"])
|
213
84
|
def update(
|
@@ -357,184 +228,6 @@ def set_user_name(
|
|
357
228
|
return {"status": "ok"}
|
358
229
|
|
359
230
|
|
360
|
-
async def extract_references_and_questions(
|
361
|
-
user: KhojUser,
|
362
|
-
chat_history: list[ChatMessageModel],
|
363
|
-
q: str,
|
364
|
-
n: int,
|
365
|
-
d: float,
|
366
|
-
conversation_id: str,
|
367
|
-
conversation_commands: List[ConversationCommand] = [ConversationCommand.Default],
|
368
|
-
location_data: LocationData = None,
|
369
|
-
send_status_func: Optional[Callable] = None,
|
370
|
-
query_images: Optional[List[str]] = None,
|
371
|
-
previous_inferred_queries: Set = set(),
|
372
|
-
agent: Agent = None,
|
373
|
-
query_files: str = None,
|
374
|
-
tracer: dict = {},
|
375
|
-
):
|
376
|
-
# Initialize Variables
|
377
|
-
compiled_references: List[dict[str, str]] = []
|
378
|
-
inferred_queries: List[str] = []
|
379
|
-
|
380
|
-
agent_has_entries = False
|
381
|
-
|
382
|
-
if agent:
|
383
|
-
agent_has_entries = await sync_to_async(EntryAdapters.agent_has_entries)(agent=agent)
|
384
|
-
|
385
|
-
if (
|
386
|
-
not ConversationCommand.Notes in conversation_commands
|
387
|
-
and not ConversationCommand.Default in conversation_commands
|
388
|
-
and not agent_has_entries
|
389
|
-
):
|
390
|
-
yield compiled_references, inferred_queries, q
|
391
|
-
return
|
392
|
-
|
393
|
-
# If Notes or Default is not in the conversation command, then the search should be restricted to the agent's knowledge base
|
394
|
-
should_limit_to_agent_knowledge = (
|
395
|
-
ConversationCommand.Notes not in conversation_commands
|
396
|
-
and ConversationCommand.Default not in conversation_commands
|
397
|
-
)
|
398
|
-
|
399
|
-
if not await sync_to_async(EntryAdapters.user_has_entries)(user=user):
|
400
|
-
if not agent_has_entries:
|
401
|
-
logger.debug("No documents in knowledge base. Use a Khoj client to sync and chat with your docs.")
|
402
|
-
yield compiled_references, inferred_queries, q
|
403
|
-
return
|
404
|
-
|
405
|
-
# Extract filter terms from user message
|
406
|
-
defiltered_query = defilter_query(q)
|
407
|
-
filters_in_query = q.replace(defiltered_query, "").strip()
|
408
|
-
conversation = await sync_to_async(ConversationAdapters.get_conversation_by_id)(conversation_id)
|
409
|
-
|
410
|
-
if not conversation:
|
411
|
-
logger.error(f"Conversation with id {conversation_id} not found when extracting references.")
|
412
|
-
yield compiled_references, inferred_queries, defiltered_query
|
413
|
-
return
|
414
|
-
|
415
|
-
filters_in_query += " ".join([f'file:"{filter}"' for filter in conversation.file_filters])
|
416
|
-
using_offline_chat = False
|
417
|
-
if is_none_or_empty(filters_in_query):
|
418
|
-
logger.debug(f"Filters in query: {filters_in_query}")
|
419
|
-
|
420
|
-
personality_context = prompts.personality_context.format(personality=agent.personality) if agent else ""
|
421
|
-
|
422
|
-
# Infer search queries from user message
|
423
|
-
with timer("Extracting search queries took", logger):
|
424
|
-
# If we've reached here, either the user has enabled offline chat or the openai model is enabled.
|
425
|
-
chat_model = await ConversationAdapters.aget_default_chat_model(user)
|
426
|
-
vision_enabled = chat_model.vision_enabled
|
427
|
-
|
428
|
-
if chat_model.model_type == ChatModel.ModelType.OFFLINE:
|
429
|
-
using_offline_chat = True
|
430
|
-
chat_model_name = chat_model.name
|
431
|
-
max_tokens = chat_model.max_prompt_size
|
432
|
-
if state.offline_chat_processor_config is None:
|
433
|
-
state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model_name, max_tokens)
|
434
|
-
|
435
|
-
loaded_model = state.offline_chat_processor_config.loaded_model
|
436
|
-
|
437
|
-
inferred_queries = extract_questions_offline(
|
438
|
-
defiltered_query,
|
439
|
-
model=chat_model,
|
440
|
-
loaded_model=loaded_model,
|
441
|
-
chat_history=chat_history,
|
442
|
-
should_extract_questions=True,
|
443
|
-
location_data=location_data,
|
444
|
-
user=user,
|
445
|
-
max_prompt_size=chat_model.max_prompt_size,
|
446
|
-
personality_context=personality_context,
|
447
|
-
query_files=query_files,
|
448
|
-
tracer=tracer,
|
449
|
-
)
|
450
|
-
elif chat_model.model_type == ChatModel.ModelType.OPENAI:
|
451
|
-
api_key = chat_model.ai_model_api.api_key
|
452
|
-
base_url = chat_model.ai_model_api.api_base_url
|
453
|
-
chat_model_name = chat_model.name
|
454
|
-
inferred_queries = extract_questions(
|
455
|
-
defiltered_query,
|
456
|
-
model=chat_model_name,
|
457
|
-
api_key=api_key,
|
458
|
-
api_base_url=base_url,
|
459
|
-
chat_history=chat_history,
|
460
|
-
location_data=location_data,
|
461
|
-
user=user,
|
462
|
-
query_images=query_images,
|
463
|
-
vision_enabled=vision_enabled,
|
464
|
-
personality_context=personality_context,
|
465
|
-
query_files=query_files,
|
466
|
-
tracer=tracer,
|
467
|
-
)
|
468
|
-
elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
|
469
|
-
api_key = chat_model.ai_model_api.api_key
|
470
|
-
api_base_url = chat_model.ai_model_api.api_base_url
|
471
|
-
chat_model_name = chat_model.name
|
472
|
-
inferred_queries = extract_questions_anthropic(
|
473
|
-
defiltered_query,
|
474
|
-
query_images=query_images,
|
475
|
-
model=chat_model_name,
|
476
|
-
api_key=api_key,
|
477
|
-
api_base_url=api_base_url,
|
478
|
-
chat_history=chat_history,
|
479
|
-
location_data=location_data,
|
480
|
-
user=user,
|
481
|
-
vision_enabled=vision_enabled,
|
482
|
-
personality_context=personality_context,
|
483
|
-
query_files=query_files,
|
484
|
-
tracer=tracer,
|
485
|
-
)
|
486
|
-
elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
|
487
|
-
api_key = chat_model.ai_model_api.api_key
|
488
|
-
api_base_url = chat_model.ai_model_api.api_base_url
|
489
|
-
chat_model_name = chat_model.name
|
490
|
-
inferred_queries = extract_questions_gemini(
|
491
|
-
defiltered_query,
|
492
|
-
query_images=query_images,
|
493
|
-
model=chat_model_name,
|
494
|
-
api_key=api_key,
|
495
|
-
api_base_url=api_base_url,
|
496
|
-
chat_history=chat_history,
|
497
|
-
location_data=location_data,
|
498
|
-
max_tokens=chat_model.max_prompt_size,
|
499
|
-
user=user,
|
500
|
-
vision_enabled=vision_enabled,
|
501
|
-
personality_context=personality_context,
|
502
|
-
query_files=query_files,
|
503
|
-
tracer=tracer,
|
504
|
-
)
|
505
|
-
|
506
|
-
# Collate search results as context for GPT
|
507
|
-
inferred_queries = list(set(inferred_queries) - previous_inferred_queries)
|
508
|
-
with timer("Searching knowledge base took", logger):
|
509
|
-
search_results = []
|
510
|
-
logger.info(f"🔍 Searching knowledge base with queries: {inferred_queries}")
|
511
|
-
if send_status_func:
|
512
|
-
inferred_queries_str = "\n- " + "\n- ".join(inferred_queries)
|
513
|
-
async for event in send_status_func(f"**Searching Documents for:** {inferred_queries_str}"):
|
514
|
-
yield {ChatEvent.STATUS: event}
|
515
|
-
for query in inferred_queries:
|
516
|
-
n_items = min(n, 3) if using_offline_chat else n
|
517
|
-
search_results.extend(
|
518
|
-
await execute_search(
|
519
|
-
user if not should_limit_to_agent_knowledge else None,
|
520
|
-
f"{query} {filters_in_query}",
|
521
|
-
n=n_items,
|
522
|
-
t=SearchType.All,
|
523
|
-
r=True,
|
524
|
-
max_distance=d,
|
525
|
-
dedupe=False,
|
526
|
-
agent=agent,
|
527
|
-
)
|
528
|
-
)
|
529
|
-
search_results = text_search.deduplicated_search_responses(search_results)
|
530
|
-
compiled_references = [
|
531
|
-
{"query": q, "compiled": item.additional["compiled"], "file": item.additional["file"]}
|
532
|
-
for q, item in zip(inferred_queries, search_results)
|
533
|
-
]
|
534
|
-
|
535
|
-
yield compiled_references, inferred_queries, defiltered_query
|
536
|
-
|
537
|
-
|
538
231
|
@api.get("/health", response_class=Response)
|
539
232
|
@requires(["authenticated"], status_code=200)
|
540
233
|
def health_check(request: Request) -> Response:
|
@@ -563,224 +256,3 @@ def user_info(request: Request) -> Response:
|
|
563
256
|
|
564
257
|
# Return user information as a JSON response
|
565
258
|
return Response(content=json.dumps(user_info), media_type="application/json", status_code=200)
|
566
|
-
|
567
|
-
|
568
|
-
@api.get("/automations", response_class=Response)
|
569
|
-
@requires(["authenticated"])
|
570
|
-
def get_automations(request: Request) -> Response:
|
571
|
-
user: KhojUser = request.user.object
|
572
|
-
|
573
|
-
# Collate all automations created by user that are still active
|
574
|
-
automations_info = [automation_info for automation_info in AutomationAdapters.get_automations_metadata(user)]
|
575
|
-
|
576
|
-
# Return tasks information as a JSON response
|
577
|
-
return Response(content=json.dumps(automations_info), media_type="application/json", status_code=200)
|
578
|
-
|
579
|
-
|
580
|
-
@api.delete("/automation", response_class=Response)
|
581
|
-
@requires(["authenticated"])
|
582
|
-
def delete_automation(request: Request, automation_id: str) -> Response:
|
583
|
-
user: KhojUser = request.user.object
|
584
|
-
|
585
|
-
try:
|
586
|
-
automation_info = AutomationAdapters.delete_automation(user, automation_id)
|
587
|
-
except ValueError:
|
588
|
-
return Response(status_code=204)
|
589
|
-
|
590
|
-
# Return deleted automation information as a JSON response
|
591
|
-
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|
592
|
-
|
593
|
-
|
594
|
-
@api.post("/automation", response_class=Response)
|
595
|
-
@requires(["authenticated"])
|
596
|
-
def post_automation(
|
597
|
-
request: Request,
|
598
|
-
q: str,
|
599
|
-
crontime: str,
|
600
|
-
subject: Optional[str] = None,
|
601
|
-
city: Optional[str] = None,
|
602
|
-
region: Optional[str] = None,
|
603
|
-
country: Optional[str] = None,
|
604
|
-
timezone: Optional[str] = None,
|
605
|
-
) -> Response:
|
606
|
-
user: KhojUser = request.user.object
|
607
|
-
|
608
|
-
# Perform validation checks
|
609
|
-
if is_none_or_empty(q) or is_none_or_empty(crontime):
|
610
|
-
return Response(content="A query and crontime is required", status_code=400)
|
611
|
-
if not cron_descriptor.get_description(crontime):
|
612
|
-
return Response(content="Invalid crontime", status_code=400)
|
613
|
-
|
614
|
-
# Infer subject, query to run
|
615
|
-
_, query_to_run, generated_subject = schedule_query(q, chat_history=[], user=user)
|
616
|
-
subject = subject or generated_subject
|
617
|
-
|
618
|
-
# Normalize query parameters
|
619
|
-
# Add /automated_task prefix to query if not present
|
620
|
-
query_to_run = query_to_run.strip()
|
621
|
-
if not query_to_run.startswith("/automated_task"):
|
622
|
-
query_to_run = f"/automated_task {query_to_run}"
|
623
|
-
|
624
|
-
# Normalize crontime for AP Scheduler CronTrigger
|
625
|
-
crontime = crontime.strip()
|
626
|
-
if len(crontime.split(" ")) > 5:
|
627
|
-
# Truncate crontime to 5 fields
|
628
|
-
crontime = " ".join(crontime.split(" ")[:5])
|
629
|
-
|
630
|
-
# Convert crontime to standard unix crontime
|
631
|
-
crontime = crontime.replace("?", "*")
|
632
|
-
|
633
|
-
# Disallow minute level automation recurrence
|
634
|
-
minute_value = crontime.split(" ")[0]
|
635
|
-
if not minute_value.isdigit():
|
636
|
-
return Response(
|
637
|
-
content="Minute level recurrence is unsupported. Please create a less frequent schedule.",
|
638
|
-
status_code=400,
|
639
|
-
)
|
640
|
-
|
641
|
-
# Create new Conversation Session associated with this new task
|
642
|
-
title = f"Automation: {subject}"
|
643
|
-
conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
|
644
|
-
|
645
|
-
# Schedule automation with query_to_run, timezone, subject directly provided by user
|
646
|
-
try:
|
647
|
-
# Use the query to run as the scheduling request if the scheduling request is unset
|
648
|
-
calling_url = request.url.replace(query=f"{request.url.query}")
|
649
|
-
automation = schedule_automation(
|
650
|
-
query_to_run, subject, crontime, timezone, q, user, calling_url, str(conversation.id)
|
651
|
-
)
|
652
|
-
except Exception as e:
|
653
|
-
logger.error(f"Error creating automation {q} for {user.email}: {e}", exc_info=True)
|
654
|
-
return Response(
|
655
|
-
content=f"Unable to create automation. Ensure the automation doesn't already exist.",
|
656
|
-
media_type="text/plain",
|
657
|
-
status_code=500,
|
658
|
-
)
|
659
|
-
|
660
|
-
# Collate info about the created user automation
|
661
|
-
automation_info = AutomationAdapters.get_automation_metadata(user, automation)
|
662
|
-
|
663
|
-
# Return information about the created automation as a JSON response
|
664
|
-
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|
665
|
-
|
666
|
-
|
667
|
-
@api.post("/trigger/automation", response_class=Response)
|
668
|
-
@requires(["authenticated"])
|
669
|
-
def trigger_manual_job(
|
670
|
-
request: Request,
|
671
|
-
automation_id: str,
|
672
|
-
):
|
673
|
-
user: KhojUser = request.user.object
|
674
|
-
|
675
|
-
# Check, get automation to edit
|
676
|
-
try:
|
677
|
-
automation: Job = AutomationAdapters.get_automation(user, automation_id)
|
678
|
-
except ValueError as e:
|
679
|
-
logger.error(f"Error triggering automation {automation_id} for {user.email}: {e}", exc_info=True)
|
680
|
-
return Response(content="Invalid automation", status_code=403)
|
681
|
-
|
682
|
-
# Trigger the job without waiting for the result.
|
683
|
-
scheduled_chat_func = automation.func
|
684
|
-
|
685
|
-
# Run the function in a separate thread
|
686
|
-
thread = threading.Thread(target=scheduled_chat_func, args=automation.args, kwargs=automation.kwargs)
|
687
|
-
thread.start()
|
688
|
-
|
689
|
-
return Response(content="Automation triggered", status_code=200)
|
690
|
-
|
691
|
-
|
692
|
-
@api.put("/automation", response_class=Response)
|
693
|
-
@requires(["authenticated"])
|
694
|
-
def edit_job(
|
695
|
-
request: Request,
|
696
|
-
automation_id: str,
|
697
|
-
q: Optional[str],
|
698
|
-
subject: Optional[str],
|
699
|
-
crontime: Optional[str],
|
700
|
-
city: Optional[str] = None,
|
701
|
-
region: Optional[str] = None,
|
702
|
-
country: Optional[str] = None,
|
703
|
-
timezone: Optional[str] = None,
|
704
|
-
) -> Response:
|
705
|
-
user: KhojUser = request.user.object
|
706
|
-
|
707
|
-
# Perform validation checks
|
708
|
-
if is_none_or_empty(q) or is_none_or_empty(subject) or is_none_or_empty(crontime):
|
709
|
-
return Response(content="A query, subject and crontime is required", status_code=400)
|
710
|
-
if not cron_descriptor.get_description(crontime):
|
711
|
-
return Response(content="Invalid crontime", status_code=400)
|
712
|
-
|
713
|
-
# Check, get automation to edit
|
714
|
-
try:
|
715
|
-
automation: Job = AutomationAdapters.get_automation(user, automation_id)
|
716
|
-
except ValueError as e:
|
717
|
-
logger.error(f"Error editing automation {automation_id} for {user.email}: {e}", exc_info=True)
|
718
|
-
return Response(content="Invalid automation", status_code=403)
|
719
|
-
|
720
|
-
# Infer subject, query to run
|
721
|
-
_, query_to_run, _ = schedule_query(q, chat_history=[], user=user)
|
722
|
-
subject = subject
|
723
|
-
|
724
|
-
# Normalize query parameters
|
725
|
-
# Add /automated_task prefix to query if not present
|
726
|
-
query_to_run = query_to_run.strip()
|
727
|
-
if not query_to_run.startswith("/automated_task"):
|
728
|
-
query_to_run = f"/automated_task {query_to_run}"
|
729
|
-
# Normalize crontime for AP Scheduler CronTrigger
|
730
|
-
crontime = crontime.strip()
|
731
|
-
if len(crontime.split(" ")) > 5:
|
732
|
-
# Truncate crontime to 5 fields
|
733
|
-
crontime = " ".join(crontime.split(" ")[:5])
|
734
|
-
# Convert crontime to standard unix crontime
|
735
|
-
crontime = crontime.replace("?", "*")
|
736
|
-
|
737
|
-
# Disallow minute level automation recurrence
|
738
|
-
minute_value = crontime.split(" ")[0]
|
739
|
-
if not minute_value.isdigit():
|
740
|
-
return Response(
|
741
|
-
content="Recurrence of every X minutes is unsupported. Please create a less frequent schedule.",
|
742
|
-
status_code=400,
|
743
|
-
)
|
744
|
-
|
745
|
-
# Construct updated automation metadata
|
746
|
-
automation_metadata: dict[str, str] = json.loads(clean_json(automation.name))
|
747
|
-
automation_metadata["scheduling_request"] = q
|
748
|
-
automation_metadata["query_to_run"] = query_to_run
|
749
|
-
automation_metadata["subject"] = subject.strip()
|
750
|
-
automation_metadata["crontime"] = crontime
|
751
|
-
conversation_id = automation_metadata.get("conversation_id")
|
752
|
-
|
753
|
-
if not conversation_id:
|
754
|
-
title = f"Automation: {subject}"
|
755
|
-
|
756
|
-
# Create new Conversation Session associated with this new task
|
757
|
-
conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
|
758
|
-
|
759
|
-
conversation_id = str(conversation.id)
|
760
|
-
automation_metadata["conversation_id"] = conversation_id
|
761
|
-
|
762
|
-
# Modify automation with updated query, subject
|
763
|
-
automation.modify(
|
764
|
-
name=json.dumps(automation_metadata),
|
765
|
-
kwargs={
|
766
|
-
"query_to_run": query_to_run,
|
767
|
-
"subject": subject,
|
768
|
-
"scheduling_request": q,
|
769
|
-
"user": user,
|
770
|
-
"calling_url": request.url,
|
771
|
-
"conversation_id": conversation_id,
|
772
|
-
},
|
773
|
-
)
|
774
|
-
|
775
|
-
# Reschedule automation if crontime updated
|
776
|
-
user_timezone = pytz.timezone(timezone)
|
777
|
-
trigger = CronTrigger.from_crontab(crontime, user_timezone)
|
778
|
-
if automation.trigger != trigger:
|
779
|
-
automation.reschedule(trigger=trigger)
|
780
|
-
|
781
|
-
# Collate info about the updated user automation
|
782
|
-
automation = AutomationAdapters.get_automation(user, automation.id)
|
783
|
-
automation_info = AutomationAdapters.get_automation_metadata(user, automation)
|
784
|
-
|
785
|
-
# Return modified automation information as a JSON response
|
786
|
-
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
|