khoj 1.22.3__py3-none-any.whl → 1.22.4.dev6__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 (50) hide show
  1. khoj/database/adapters/__init__.py +7 -2
  2. khoj/database/migrations/0061_alter_chatmodeloptions_model_type.py +26 -0
  3. khoj/database/models/__init__.py +1 -0
  4. khoj/interface/compiled/404/index.html +1 -1
  5. khoj/interface/compiled/_next/static/chunks/app/agents/{page-6ade083d5e27a023.js → page-989a824c640bc532.js} +1 -1
  6. khoj/interface/compiled/_next/static/chunks/app/automations/{page-6ea3381528603372.js → page-3f4b6ff0261e19b7.js} +1 -1
  7. khoj/interface/compiled/_next/static/chunks/app/chat/{page-132e5199f954559f.js → page-cc71b18feddf80d6.js} +1 -1
  8. khoj/interface/compiled/_next/static/chunks/app/factchecker/{page-04a19ab1a988976f.js → page-828cf3c5b8e3af79.js} +1 -1
  9. khoj/interface/compiled/_next/static/chunks/app/{page-8465c90401833c39.js → page-2f423a763e2ee0c7.js} +1 -1
  10. khoj/interface/compiled/_next/static/chunks/app/search/{page-fa15807b1ad7e30b.js → page-dcd385f03255ef36.js} +1 -1
  11. khoj/interface/compiled/_next/static/chunks/app/settings/{page-1a2acc46cdabaf4a.js → page-ddcd51147d18c694.js} +1 -1
  12. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-e20f54450d3ce6c0.js → page-a84001b4724b5463.js} +1 -1
  13. khoj/interface/compiled/_next/static/chunks/{webpack-d83aaba66623b485.js → webpack-d4781cada9b58e75.js} +1 -1
  14. khoj/interface/compiled/_next/static/css/4cae6c0e5c72fb2d.css +1 -0
  15. khoj/interface/compiled/_next/static/css/8d802d0ad3a555d8.css +1 -0
  16. khoj/interface/compiled/agents/index.html +1 -1
  17. khoj/interface/compiled/agents/index.txt +2 -2
  18. khoj/interface/compiled/automations/index.html +1 -1
  19. khoj/interface/compiled/automations/index.txt +2 -2
  20. khoj/interface/compiled/chat/index.html +1 -1
  21. khoj/interface/compiled/chat/index.txt +2 -2
  22. khoj/interface/compiled/factchecker/index.html +1 -1
  23. khoj/interface/compiled/factchecker/index.txt +2 -2
  24. khoj/interface/compiled/index.html +1 -1
  25. khoj/interface/compiled/index.txt +2 -2
  26. khoj/interface/compiled/search/index.html +1 -1
  27. khoj/interface/compiled/search/index.txt +2 -2
  28. khoj/interface/compiled/settings/index.html +1 -1
  29. khoj/interface/compiled/settings/index.txt +2 -2
  30. khoj/interface/compiled/share/chat/index.html +1 -1
  31. khoj/interface/compiled/share/chat/index.txt +2 -2
  32. khoj/processor/conversation/google/__init__.py +0 -0
  33. khoj/processor/conversation/google/gemini_chat.py +221 -0
  34. khoj/processor/conversation/google/utils.py +93 -0
  35. khoj/processor/conversation/openai/gpt.py +2 -0
  36. khoj/processor/conversation/openai/utils.py +35 -10
  37. khoj/processor/conversation/prompts.py +2 -2
  38. khoj/processor/conversation/utils.py +15 -4
  39. khoj/processor/tools/online_search.py +2 -1
  40. khoj/routers/api.py +13 -0
  41. khoj/routers/helpers.py +77 -30
  42. {khoj-1.22.3.dist-info → khoj-1.22.4.dev6.dist-info}/METADATA +2 -1
  43. {khoj-1.22.3.dist-info → khoj-1.22.4.dev6.dist-info}/RECORD +48 -44
  44. khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +0 -1
  45. khoj/interface/compiled/_next/static/css/9eec70b61e3de1ba.css +0 -1
  46. /khoj/interface/compiled/_next/static/{HGHeV7_enScOK08fsj1eR → 1C2MwhlH-dUGJ3iv5t17P}/_buildManifest.js +0 -0
  47. /khoj/interface/compiled/_next/static/{HGHeV7_enScOK08fsj1eR → 1C2MwhlH-dUGJ3iv5t17P}/_ssgManifest.js +0 -0
  48. {khoj-1.22.3.dist-info → khoj-1.22.4.dev6.dist-info}/WHEEL +0 -0
  49. {khoj-1.22.3.dist-info → khoj-1.22.4.dev6.dist-info}/entry_points.txt +0 -0
  50. {khoj-1.22.3.dist-info → khoj-1.22.4.dev6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,93 @@
1
+ import logging
2
+ from threading import Thread
3
+
4
+ import google.generativeai as genai
5
+ from tenacity import (
6
+ before_sleep_log,
7
+ retry,
8
+ stop_after_attempt,
9
+ wait_exponential,
10
+ wait_random_exponential,
11
+ )
12
+
13
+ from khoj.processor.conversation.utils import ThreadedGenerator
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ DEFAULT_MAX_TOKENS_GEMINI = 8192
19
+
20
+
21
+ @retry(
22
+ wait=wait_random_exponential(min=1, max=10),
23
+ stop=stop_after_attempt(2),
24
+ before_sleep=before_sleep_log(logger, logging.DEBUG),
25
+ reraise=True,
26
+ )
27
+ def gemini_completion_with_backoff(
28
+ messages, system_prompt, model_name, temperature=0, api_key=None, model_kwargs=None, max_tokens=None
29
+ ) -> str:
30
+ genai.configure(api_key=api_key)
31
+ max_tokens = max_tokens or DEFAULT_MAX_TOKENS_GEMINI
32
+ model_kwargs = model_kwargs or dict()
33
+ model_kwargs["temperature"] = temperature
34
+ model_kwargs["max_output_tokens"] = max_tokens
35
+ model = genai.GenerativeModel(model_name, generation_config=model_kwargs, system_instruction=system_prompt)
36
+
37
+ formatted_messages = [{"role": message.role, "parts": [message.content]} for message in messages]
38
+ # all messages up to the last are considered to be part of the chat history
39
+ chat_session = model.start_chat(history=formatted_messages[0:-1])
40
+ # the last message is considered to be the current prompt
41
+ aggregated_response = chat_session.send_message(formatted_messages[-1]["parts"][0])
42
+ return aggregated_response.text
43
+
44
+
45
+ @retry(
46
+ wait=wait_exponential(multiplier=1, min=4, max=10),
47
+ stop=stop_after_attempt(2),
48
+ before_sleep=before_sleep_log(logger, logging.DEBUG),
49
+ reraise=True,
50
+ )
51
+ def gemini_chat_completion_with_backoff(
52
+ messages,
53
+ compiled_references,
54
+ online_results,
55
+ model_name,
56
+ temperature,
57
+ api_key,
58
+ system_prompt,
59
+ max_prompt_size=None,
60
+ completion_func=None,
61
+ model_kwargs=None,
62
+ ):
63
+ g = ThreadedGenerator(compiled_references, online_results, completion_func=completion_func)
64
+ t = Thread(
65
+ target=gemini_llm_thread,
66
+ args=(g, messages, system_prompt, model_name, temperature, api_key, max_prompt_size, model_kwargs),
67
+ )
68
+ t.start()
69
+ return g
70
+
71
+
72
+ def gemini_llm_thread(
73
+ g, messages, system_prompt, model_name, temperature, api_key, max_prompt_size=None, model_kwargs=None
74
+ ):
75
+ try:
76
+ genai.configure(api_key=api_key)
77
+ max_tokens = max_prompt_size or DEFAULT_MAX_TOKENS_GEMINI
78
+ model_kwargs = model_kwargs or dict()
79
+ model_kwargs["temperature"] = temperature
80
+ model_kwargs["max_output_tokens"] = max_tokens
81
+ model_kwargs["stop_sequences"] = ["Notes:\n["]
82
+ model = genai.GenerativeModel(model_name, generation_config=model_kwargs, system_instruction=system_prompt)
83
+
84
+ formatted_messages = [{"role": message.role, "parts": [message.content]} for message in messages]
85
+ # all messages up to the last are considered to be part of the chat history
86
+ chat_session = model.start_chat(history=formatted_messages[0:-1])
87
+ # the last message is considered to be the current prompt
88
+ for chunk in chat_session.send_message(formatted_messages[-1]["parts"][0], stream=True):
89
+ g.send(chunk.text)
90
+ except Exception as e:
91
+ logger.error(f"Error in gemini_llm_thread: {e}", exc_info=True)
92
+ finally:
93
+ g.close()
@@ -14,6 +14,7 @@ from khoj.processor.conversation.openai.utils import (
14
14
  from khoj.processor.conversation.utils import (
15
15
  construct_structured_message,
16
16
  generate_chatml_messages_with_context,
17
+ remove_json_codeblock,
17
18
  )
18
19
  from khoj.utils.helpers import ConversationCommand, is_none_or_empty
19
20
  from khoj.utils.rawconfig import LocationData
@@ -85,6 +86,7 @@ def extract_questions(
85
86
  # Extract, Clean Message from GPT's Response
86
87
  try:
87
88
  response = response.strip()
89
+ response = remove_json_codeblock(response)
88
90
  response = json.loads(response)
89
91
  response = [q.strip() for q in response["queries"] if q.strip()]
90
92
  if not isinstance(response, list) or not response:
@@ -45,15 +45,28 @@ def completion_with_backoff(
45
45
  openai_clients[client_key] = client
46
46
 
47
47
  formatted_messages = [{"role": message.role, "content": message.content} for message in messages]
48
+ stream = True
49
+
50
+ # Update request parameters for compatability with o1 model series
51
+ # Refer: https://platform.openai.com/docs/guides/reasoning/beta-limitations
52
+ if model.startswith("o1"):
53
+ stream = False
54
+ temperature = 1
55
+ model_kwargs.pop("stop", None)
56
+ model_kwargs.pop("response_format", None)
48
57
 
49
58
  chat = client.chat.completions.create(
50
- stream=True,
59
+ stream=stream,
51
60
  messages=formatted_messages, # type: ignore
52
61
  model=model, # type: ignore
53
62
  temperature=temperature,
54
63
  timeout=20,
55
64
  **(model_kwargs or dict()),
56
65
  )
66
+
67
+ if not stream:
68
+ return chat.choices[0].message.content
69
+
57
70
  aggregated_response = ""
58
71
  for chunk in chat:
59
72
  if len(chunk.choices) == 0:
@@ -112,9 +125,18 @@ def llm_thread(g, messages, model_name, temperature, openai_api_key=None, api_ba
112
125
  client: openai.OpenAI = openai_clients[client_key]
113
126
 
114
127
  formatted_messages = [{"role": message.role, "content": message.content} for message in messages]
128
+ stream = True
129
+
130
+ # Update request parameters for compatability with o1 model series
131
+ # Refer: https://platform.openai.com/docs/guides/reasoning/beta-limitations
132
+ if model_name.startswith("o1"):
133
+ stream = False
134
+ temperature = 1
135
+ model_kwargs.pop("stop", None)
136
+ model_kwargs.pop("response_format", None)
115
137
 
116
138
  chat = client.chat.completions.create(
117
- stream=True,
139
+ stream=stream,
118
140
  messages=formatted_messages,
119
141
  model=model_name, # type: ignore
120
142
  temperature=temperature,
@@ -122,14 +144,17 @@ def llm_thread(g, messages, model_name, temperature, openai_api_key=None, api_ba
122
144
  **(model_kwargs or dict()),
123
145
  )
124
146
 
125
- for chunk in chat:
126
- if len(chunk.choices) == 0:
127
- continue
128
- delta_chunk = chunk.choices[0].delta
129
- if isinstance(delta_chunk, str):
130
- g.send(delta_chunk)
131
- elif delta_chunk.content:
132
- g.send(delta_chunk.content)
147
+ if not stream:
148
+ g.send(chat.choices[0].message.content)
149
+ else:
150
+ for chunk in chat:
151
+ if len(chunk.choices) == 0:
152
+ continue
153
+ delta_chunk = chunk.choices[0].delta
154
+ if isinstance(delta_chunk, str):
155
+ g.send(delta_chunk)
156
+ elif delta_chunk.content:
157
+ g.send(delta_chunk.content)
133
158
  except Exception as e:
134
159
  logger.error(f"Error in llm_thread: {e}", exc_info=True)
135
160
  finally:
@@ -13,8 +13,8 @@ You were created by Khoj Inc. with the following capabilities:
13
13
  - You *CAN* generate images, look-up real-time information from the internet, set reminders and answer questions based on the user's notes.
14
14
  - Say "I don't know" or "I don't understand" if you don't know what to say or if you don't know the answer to a question.
15
15
  - Make sure to use the specific LaTeX math mode delimiters for your response. LaTex math mode specific delimiters as following
16
- - inline math mode : `\\(` and `\\)`
17
- - display math mode: insert linebreak after opening `$$`, `\\[` and before closing `$$`, `\\]`
16
+ - inline math mode : \\( and \\)
17
+ - display math mode: insert linebreak after opening $$, \\[ and before closing $$, \\]
18
18
  - Ask crisp follow-up questions to get additional context, when the answer cannot be inferred from the provided notes or past conversations.
19
19
  - Sometimes the user will share personal information that needs to be remembered, like an account ID or a residential address. These can be acknowledged with a simple "Got it" or "Okay".
20
20
  - Provide inline references to quotes from the user's notes or any web pages you refer to in your responses in markdown format. For example, "The farmer had ten sheep. [1](https://example.com)". *ALWAYS CITE YOUR SOURCES AND PROVIDE REFERENCES*. Add them inline to directly support your claim.
@@ -1,4 +1,3 @@
1
- import json
2
1
  import logging
3
2
  import math
4
3
  import queue
@@ -24,6 +23,8 @@ model_to_prompt_size = {
24
23
  "gpt-4-0125-preview": 20000,
25
24
  "gpt-4-turbo-preview": 20000,
26
25
  "gpt-4o-mini": 20000,
26
+ "o1-preview": 20000,
27
+ "o1-mini": 20000,
27
28
  "TheBloke/Mistral-7B-Instruct-v0.2-GGUF": 3500,
28
29
  "NousResearch/Hermes-2-Pro-Mistral-7B-GGUF": 3500,
29
30
  "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF": 20000,
@@ -220,8 +221,9 @@ def truncate_messages(
220
221
  try:
221
222
  if loaded_model:
222
223
  encoder = loaded_model.tokenizer()
223
- elif model_name.startswith("gpt-"):
224
- encoder = tiktoken.encoding_for_model(model_name)
224
+ elif model_name.startswith("gpt-") or model_name.startswith("o1"):
225
+ # as tiktoken doesn't recognize o1 model series yet
226
+ encoder = tiktoken.encoding_for_model("gpt-4o" if model_name.startswith("o1") else model_name)
225
227
  elif tokenizer_name:
226
228
  if tokenizer_name in state.pretrained_tokenizers:
227
229
  encoder = state.pretrained_tokenizers[tokenizer_name]
@@ -278,10 +280,19 @@ def truncate_messages(
278
280
  )
279
281
 
280
282
  if system_message:
281
- system_message.role = "user" if "gemma-2" in model_name else "system"
283
+ # Default system message role is system.
284
+ # Fallback to system message role of user for models that do not support this role like gemma-2 and openai's o1 model series.
285
+ system_message.role = "user" if "gemma-2" in model_name or model_name.startswith("o1") else "system"
282
286
  return messages + [system_message] if system_message else messages
283
287
 
284
288
 
285
289
  def reciprocal_conversation_to_chatml(message_pair):
286
290
  """Convert a single back and forth between user and assistant to chatml format"""
287
291
  return [ChatMessage(content=message, role=role) for message, role in zip(message_pair, ["user", "assistant"])]
292
+
293
+
294
+ def remove_json_codeblock(response):
295
+ """Remove any markdown json codeblock formatting if present. Useful for non schema enforceable models"""
296
+ if response.startswith("```json") and response.endswith("```"):
297
+ response = response[7:-3]
298
+ return response
@@ -7,6 +7,7 @@ from collections import defaultdict
7
7
  from typing import Callable, Dict, List, Optional, Tuple, Union
8
8
 
9
9
  import aiohttp
10
+ import requests
10
11
  from bs4 import BeautifulSoup
11
12
  from markdownify import markdownify
12
13
 
@@ -94,7 +95,7 @@ async def search_online(
94
95
 
95
96
  # Read, extract relevant info from the retrieved web pages
96
97
  if webpages:
97
- webpage_links = [link for link, _, _ in webpages]
98
+ webpage_links = set([link for link, _, _ in webpages])
98
99
  logger.info(f"Reading web pages at: {list(webpage_links)}")
99
100
  if send_status_func:
100
101
  webpage_links_str = "\n- " + "\n- ".join(list(webpage_links))
khoj/routers/api.py CHANGED
@@ -31,6 +31,7 @@ from khoj.database.models import ChatModelOptions, KhojUser, SpeechToTextModelOp
31
31
  from khoj.processor.conversation.anthropic.anthropic_chat import (
32
32
  extract_questions_anthropic,
33
33
  )
34
+ from khoj.processor.conversation.google.gemini_chat import extract_questions_gemini
34
35
  from khoj.processor.conversation.offline.chat_model import extract_questions_offline
35
36
  from khoj.processor.conversation.offline.whisper import transcribe_audio_offline
36
37
  from khoj.processor.conversation.openai.gpt import extract_questions
@@ -419,6 +420,18 @@ async def extract_references_and_questions(
419
420
  location_data=location_data,
420
421
  user=user,
421
422
  )
423
+ elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
424
+ api_key = conversation_config.openai_config.api_key
425
+ chat_model = conversation_config.chat_model
426
+ inferred_queries = extract_questions_gemini(
427
+ defiltered_query,
428
+ model=chat_model,
429
+ api_key=api_key,
430
+ conversation_log=meta_log,
431
+ location_data=location_data,
432
+ max_tokens=conversation_config.max_prompt_size,
433
+ user=user,
434
+ )
422
435
 
423
436
  # Collate search results as context for GPT
424
437
  with timer("Searching knowledge base took", logger):
khoj/routers/helpers.py CHANGED
@@ -76,6 +76,10 @@ from khoj.processor.conversation.anthropic.anthropic_chat import (
76
76
  anthropic_send_message_to_model,
77
77
  converse_anthropic,
78
78
  )
79
+ from khoj.processor.conversation.google.gemini_chat import (
80
+ converse_gemini,
81
+ gemini_send_message_to_model,
82
+ )
79
83
  from khoj.processor.conversation.offline.chat_model import (
80
84
  converse_offline,
81
85
  send_message_to_model_offline,
@@ -84,6 +88,7 @@ from khoj.processor.conversation.openai.gpt import converse, send_message_to_mod
84
88
  from khoj.processor.conversation.utils import (
85
89
  ThreadedGenerator,
86
90
  generate_chatml_messages_with_context,
91
+ remove_json_codeblock,
87
92
  save_to_conversation_log,
88
93
  )
89
94
  from khoj.processor.speech.text_to_speech import is_eleven_labs_enabled
@@ -136,7 +141,7 @@ async def is_ready_to_chat(user: KhojUser):
136
141
  await ConversationAdapters.aget_default_conversation_config()
137
142
  )
138
143
 
139
- if user_conversation_config and user_conversation_config.model_type == "offline":
144
+ if user_conversation_config and user_conversation_config.model_type == ChatModelOptions.ModelType.OFFLINE:
140
145
  chat_model = user_conversation_config.chat_model
141
146
  max_tokens = user_conversation_config.max_prompt_size
142
147
  if state.offline_chat_processor_config is None:
@@ -146,7 +151,14 @@ async def is_ready_to_chat(user: KhojUser):
146
151
 
147
152
  if (
148
153
  user_conversation_config
149
- and (user_conversation_config.model_type == "openai" or user_conversation_config.model_type == "anthropic")
154
+ and (
155
+ user_conversation_config.model_type
156
+ in [
157
+ ChatModelOptions.ModelType.OPENAI,
158
+ ChatModelOptions.ModelType.ANTHROPIC,
159
+ ChatModelOptions.ModelType.GOOGLE,
160
+ ]
161
+ )
150
162
  and user_conversation_config.openai_config
151
163
  ):
152
164
  return True
@@ -287,9 +299,7 @@ async def aget_relevant_information_sources(
287
299
 
288
300
  try:
289
301
  response = response.strip()
290
- # Remove any markdown json codeblock formatting if present (useful for gemma-2)
291
- if response.startswith("```json"):
292
- response = response[7:-3]
302
+ response = remove_json_codeblock(response)
293
303
  response = json.loads(response)
294
304
  response = [q.strip() for q in response["source"] if q.strip()]
295
305
  if not isinstance(response, list) or not response or len(response) == 0:
@@ -342,7 +352,9 @@ async def aget_relevant_output_modes(
342
352
  response = await send_message_to_model_wrapper(relevant_mode_prompt, response_type="json_object")
343
353
 
344
354
  try:
345
- response = json.loads(response.strip())
355
+ response = response.strip()
356
+ response = remove_json_codeblock(response)
357
+ response = json.loads(response)
346
358
 
347
359
  if is_none_or_empty(response):
348
360
  return ConversationCommand.Text
@@ -422,9 +434,7 @@ async def generate_online_subqueries(
422
434
  # Validate that the response is a non-empty, JSON-serializable list
423
435
  try:
424
436
  response = response.strip()
425
- # Remove any markdown json codeblock formatting if present (useful for gemma-2)
426
- if response.startswith("```json") and response.endswith("```"):
427
- response = response[7:-3]
437
+ response = remove_json_codeblock(response)
428
438
  response = json.loads(response)
429
439
  response = [q.strip() for q in response["queries"] if q.strip()]
430
440
  if not isinstance(response, list) or not response or len(response) == 0:
@@ -607,9 +617,10 @@ async def send_message_to_model_wrapper(
607
617
  else conversation_config.max_prompt_size
608
618
  )
609
619
  tokenizer = conversation_config.tokenizer
620
+ model_type = conversation_config.model_type
610
621
  vision_available = conversation_config.vision_enabled
611
622
 
612
- if conversation_config.model_type == "offline":
623
+ if model_type == ChatModelOptions.ModelType.OFFLINE:
613
624
  if state.offline_chat_processor_config is None or state.offline_chat_processor_config.loaded_model is None:
614
625
  state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model, max_tokens)
615
626
 
@@ -633,7 +644,7 @@ async def send_message_to_model_wrapper(
633
644
  response_type=response_type,
634
645
  )
635
646
 
636
- elif conversation_config.model_type == "openai":
647
+ elif model_type == ChatModelOptions.ModelType.OPENAI:
637
648
  openai_chat_config = conversation_config.openai_config
638
649
  api_key = openai_chat_config.api_key
639
650
  api_base_url = openai_chat_config.api_base_url
@@ -657,7 +668,7 @@ async def send_message_to_model_wrapper(
657
668
  )
658
669
 
659
670
  return openai_response
660
- elif conversation_config.model_type == "anthropic":
671
+ elif model_type == ChatModelOptions.ModelType.ANTHROPIC:
661
672
  api_key = conversation_config.openai_config.api_key
662
673
  truncated_messages = generate_chatml_messages_with_context(
663
674
  user_message=message,
@@ -666,6 +677,7 @@ async def send_message_to_model_wrapper(
666
677
  max_prompt_size=max_tokens,
667
678
  tokenizer_name=tokenizer,
668
679
  vision_enabled=vision_available,
680
+ uploaded_image_url=uploaded_image_url,
669
681
  model_type=conversation_config.model_type,
670
682
  )
671
683
 
@@ -674,6 +686,21 @@ async def send_message_to_model_wrapper(
674
686
  api_key=api_key,
675
687
  model=chat_model,
676
688
  )
689
+ elif model_type == ChatModelOptions.ModelType.GOOGLE:
690
+ api_key = conversation_config.openai_config.api_key
691
+ truncated_messages = generate_chatml_messages_with_context(
692
+ user_message=message,
693
+ system_message=system_message,
694
+ model_name=chat_model,
695
+ max_prompt_size=max_tokens,
696
+ tokenizer_name=tokenizer,
697
+ vision_enabled=vision_available,
698
+ uploaded_image_url=uploaded_image_url,
699
+ )
700
+
701
+ return gemini_send_message_to_model(
702
+ messages=truncated_messages, api_key=api_key, model=chat_model, response_type=response_type
703
+ )
677
704
  else:
678
705
  raise HTTPException(status_code=500, detail="Invalid conversation config")
679
706
 
@@ -692,7 +719,7 @@ def send_message_to_model_wrapper_sync(
692
719
  max_tokens = conversation_config.max_prompt_size
693
720
  vision_available = conversation_config.vision_enabled
694
721
 
695
- if conversation_config.model_type == "offline":
722
+ if conversation_config.model_type == ChatModelOptions.ModelType.OFFLINE:
696
723
  if state.offline_chat_processor_config is None or state.offline_chat_processor_config.loaded_model is None:
697
724
  state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model, max_tokens)
698
725
 
@@ -714,7 +741,7 @@ def send_message_to_model_wrapper_sync(
714
741
  response_type=response_type,
715
742
  )
716
743
 
717
- elif conversation_config.model_type == "openai":
744
+ elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
718
745
  api_key = conversation_config.openai_config.api_key
719
746
  truncated_messages = generate_chatml_messages_with_context(
720
747
  user_message=message,
@@ -730,7 +757,7 @@ def send_message_to_model_wrapper_sync(
730
757
 
731
758
  return openai_response
732
759
 
733
- elif conversation_config.model_type == "anthropic":
760
+ elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
734
761
  api_key = conversation_config.openai_config.api_key
735
762
  truncated_messages = generate_chatml_messages_with_context(
736
763
  user_message=message,
@@ -746,6 +773,22 @@ def send_message_to_model_wrapper_sync(
746
773
  api_key=api_key,
747
774
  model=chat_model,
748
775
  )
776
+
777
+ elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
778
+ api_key = conversation_config.openai_config.api_key
779
+ truncated_messages = generate_chatml_messages_with_context(
780
+ user_message=message,
781
+ system_message=system_message,
782
+ model_name=chat_model,
783
+ max_prompt_size=max_tokens,
784
+ vision_enabled=vision_available,
785
+ )
786
+
787
+ return gemini_send_message_to_model(
788
+ messages=truncated_messages,
789
+ api_key=api_key,
790
+ model=chat_model,
791
+ )
749
792
  else:
750
793
  raise HTTPException(status_code=500, detail="Invalid conversation config")
751
794
 
@@ -811,7 +854,7 @@ def generate_chat_response(
811
854
  agent=agent,
812
855
  )
813
856
 
814
- elif conversation_config.model_type == "openai":
857
+ elif conversation_config.model_type == ChatModelOptions.ModelType.OPENAI:
815
858
  openai_chat_config = conversation_config.openai_config
816
859
  api_key = openai_chat_config.api_key
817
860
  chat_model = conversation_config.chat_model
@@ -834,7 +877,7 @@ def generate_chat_response(
834
877
  vision_available=vision_available,
835
878
  )
836
879
 
837
- elif conversation_config.model_type == "anthropic":
880
+ elif conversation_config.model_type == ChatModelOptions.ModelType.ANTHROPIC:
838
881
  api_key = conversation_config.openai_config.api_key
839
882
  chat_response = converse_anthropic(
840
883
  compiled_references,
@@ -851,6 +894,23 @@ def generate_chat_response(
851
894
  user_name=user_name,
852
895
  agent=agent,
853
896
  )
897
+ elif conversation_config.model_type == ChatModelOptions.ModelType.GOOGLE:
898
+ api_key = conversation_config.openai_config.api_key
899
+ chat_response = converse_gemini(
900
+ compiled_references,
901
+ q,
902
+ online_results,
903
+ meta_log,
904
+ model=conversation_config.chat_model,
905
+ api_key=api_key,
906
+ completion_func=partial_completion,
907
+ conversation_commands=conversation_commands,
908
+ max_prompt_size=conversation_config.max_prompt_size,
909
+ tokenizer_name=conversation_config.tokenizer,
910
+ location_data=location_data,
911
+ user_name=user_name,
912
+ agent=agent,
913
+ )
854
914
 
855
915
  metadata.update({"chat_model": conversation_config.chat_model})
856
916
 
@@ -1217,11 +1277,6 @@ def scheduled_chat(
1217
1277
  token = token[0].token
1218
1278
  headers["Authorization"] = f"Bearer {token}"
1219
1279
 
1220
- # Log request details
1221
- logger.info(f"POST URL: {url}")
1222
- logger.info(f"Headers: {headers}")
1223
- logger.info(f"Payload: {json_payload}")
1224
-
1225
1280
  # Call the chat API endpoint with authenticated user token and query
1226
1281
  raw_response = requests.post(url, headers=headers, json=json_payload, allow_redirects=False)
1227
1282
 
@@ -1231,14 +1286,6 @@ def scheduled_chat(
1231
1286
  logger.info(f"Redirecting to {redirect_url}")
1232
1287
  raw_response = requests.post(redirect_url, headers=headers, json=json_payload)
1233
1288
 
1234
- # Log response details
1235
- logger.info(f"Response status code: {raw_response.status_code}")
1236
- logger.info(f"Response headers: {raw_response.headers}")
1237
- logger.info(f"Response text: {raw_response.text}")
1238
- if raw_response.history:
1239
- for resp in raw_response.history:
1240
- logger.info(f"Redirected from {resp.url} with status code {resp.status_code}")
1241
-
1242
1289
  # Stop if the chat API call was not successful
1243
1290
  if raw_response.status_code != 200:
1244
1291
  logger.error(f"Failed to run schedule chat: {raw_response.text}, user: {user}, query: {query_to_run}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: khoj
3
- Version: 1.22.3
3
+ Version: 1.22.4.dev6
4
4
  Summary: Your Second Brain
5
5
  Project-URL: Homepage, https://khoj.dev
6
6
  Project-URL: Documentation, https://docs.khoj.dev
@@ -36,6 +36,7 @@ Requires-Dist: django==5.0.8
36
36
  Requires-Dist: docx2txt==0.8
37
37
  Requires-Dist: einops==0.8.0
38
38
  Requires-Dist: fastapi>=0.110.0
39
+ Requires-Dist: google-generativeai==0.7.2
39
40
  Requires-Dist: httpx==0.25.0
40
41
  Requires-Dist: huggingface-hub>=0.22.2
41
42
  Requires-Dist: itsdangerous==2.1.2