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.
Files changed (60) hide show
  1. khoj/configure.py +2 -0
  2. khoj/database/adapters/__init__.py +6 -6
  3. khoj/interface/compiled/404/index.html +2 -2
  4. khoj/interface/compiled/_next/static/chunks/{2327-f03b2a77f67b8f8c.js → 2327-aa22697ed9c8d54a.js} +1 -1
  5. khoj/interface/compiled/_next/static/chunks/7127-79a3af5138960272.js +1 -0
  6. khoj/interface/compiled/_next/static/chunks/{5138-81457f7f59956b56.js → 7211-7fedd2ee3655239c.js} +1 -1
  7. khoj/interface/compiled/_next/static/chunks/app/agents/layout-4e2a134ec26aa606.js +1 -0
  8. khoj/interface/compiled/_next/static/chunks/app/automations/page-ef89ac958e78aa81.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/app/chat/layout-ad4d1792ab1a4108.js +1 -0
  10. khoj/interface/compiled/_next/static/chunks/app/chat/page-db0fbea54ccea62f.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-9a167dc9b5fcd464.js → page-da90c78180a86040.js} +1 -1
  12. khoj/interface/compiled/_next/static/chunks/{webpack-1c900156837baf90.js → webpack-0f15e6b51732b337.js} +1 -1
  13. khoj/interface/compiled/_next/static/css/{c34713c98384ee87.css → 2945c4a857922f3b.css} +1 -1
  14. khoj/interface/compiled/_next/static/css/{9c223d337a984468.css → 7017ee76c2f2cd87.css} +1 -1
  15. khoj/interface/compiled/_next/static/css/9a460202d29476e5.css +1 -0
  16. khoj/interface/compiled/agents/index.html +2 -2
  17. khoj/interface/compiled/agents/index.txt +2 -2
  18. khoj/interface/compiled/automations/index.html +2 -2
  19. khoj/interface/compiled/automations/index.txt +3 -3
  20. khoj/interface/compiled/chat/index.html +2 -2
  21. khoj/interface/compiled/chat/index.txt +2 -2
  22. khoj/interface/compiled/index.html +2 -2
  23. khoj/interface/compiled/index.txt +2 -2
  24. khoj/interface/compiled/search/index.html +2 -2
  25. khoj/interface/compiled/search/index.txt +2 -2
  26. khoj/interface/compiled/settings/index.html +2 -2
  27. khoj/interface/compiled/settings/index.txt +4 -4
  28. khoj/interface/compiled/share/chat/index.html +2 -2
  29. khoj/interface/compiled/share/chat/index.txt +2 -2
  30. khoj/processor/conversation/anthropic/anthropic_chat.py +17 -132
  31. khoj/processor/conversation/anthropic/utils.py +1 -1
  32. khoj/processor/conversation/google/gemini_chat.py +18 -139
  33. khoj/processor/conversation/offline/chat_model.py +21 -151
  34. khoj/processor/conversation/openai/gpt.py +12 -126
  35. khoj/processor/conversation/prompts.py +2 -63
  36. khoj/routers/api.py +5 -533
  37. khoj/routers/api_automation.py +243 -0
  38. khoj/routers/api_chat.py +35 -116
  39. khoj/routers/helpers.py +329 -80
  40. khoj/routers/research.py +3 -33
  41. khoj/utils/helpers.py +0 -6
  42. {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/METADATA +1 -1
  43. {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/RECORD +54 -53
  44. khoj/interface/compiled/_next/static/chunks/7127-d3199617463d45f0.js +0 -1
  45. khoj/interface/compiled/_next/static/chunks/app/agents/layout-e00fb81dca656a10.js +0 -1
  46. khoj/interface/compiled/_next/static/chunks/app/automations/page-465741d9149dfd48.js +0 -1
  47. khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +0 -1
  48. khoj/interface/compiled/_next/static/chunks/app/chat/page-1726184cf1c1b86e.js +0 -1
  49. khoj/interface/compiled/_next/static/css/fca983d49c3dd1a3.css +0 -1
  50. /khoj/interface/compiled/_next/static/{Dzg_ViqMwQEjqMgetZPRc → OTsOjbrtuaYMukpuJS4sy}/_buildManifest.js +0 -0
  51. /khoj/interface/compiled/_next/static/{Dzg_ViqMwQEjqMgetZPRc → OTsOjbrtuaYMukpuJS4sy}/_ssgManifest.js +0 -0
  52. /khoj/interface/compiled/_next/static/chunks/{1915-ab4353eaca76f690.js → 1915-1943ee8a628b893c.js} +0 -0
  53. /khoj/interface/compiled/_next/static/chunks/{2117-1c18aa2098982bf9.js → 2117-5a41630a2bd2eae8.js} +0 -0
  54. /khoj/interface/compiled/_next/static/chunks/{4363-4efaf12abe696251.js → 4363-e6ac2203564d1a3b.js} +0 -0
  55. /khoj/interface/compiled/_next/static/chunks/{4447-5d44807c40355b1a.js → 4447-e038b251d626c340.js} +0 -0
  56. /khoj/interface/compiled/_next/static/chunks/{8667-adbe6017a66cef10.js → 8667-8136f74e9a086fca.js} +0 -0
  57. /khoj/interface/compiled/_next/static/chunks/{9259-d8bcd9da9e80c81e.js → 9259-640fdd77408475df.js} +0 -0
  58. {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/WHEEL +0 -0
  59. {khoj-1.42.2.dev1.dist-info → khoj-1.42.2.dev16.dist-info}/entry_points.txt +0 -0
  60. {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 Any, Callable, List, Optional, Set, Union
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
- AgentAdapters,
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.config import OfflineChatProcessorModel
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)