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