khoj 1.24.2.dev16__py3-none-any.whl → 1.25.1.dev34__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 +13 -4
- khoj/database/adapters/__init__.py +163 -49
- khoj/database/admin.py +18 -1
- khoj/database/migrations/0068_alter_agent_output_modes.py +24 -0
- khoj/database/migrations/0069_webscraper_serverchatsettings_web_scraper.py +89 -0
- khoj/database/models/__init__.py +78 -2
- khoj/interface/compiled/404/index.html +1 -1
- khoj/interface/compiled/_next/static/chunks/1603-fa3ee48860b9dc5c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/7762-79f2205740622b5c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/{layout-e71c8e913cccf792.js → layout-75636ab3a413fa8e.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-fa282831808ee536.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/{page-1688dead2f21270d.js → page-5480731341f34450.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/{layout-8102549127db3067.js → layout-96fcf62857bf8f30.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/{page-91abcb71846922b7.js → page-702057ccbcf27881.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/factchecker/{page-7ab093711c27041c.js → page-e7b34316ec6f44de.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/{layout-f3e40d346da53112.js → layout-d0f0a9067427fb20.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/{page-fada198096eab47f.js → page-10a5aad6e04f3cf8.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/search/{page-a7e036689b6507ff.js → page-d56541c746fded7d.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/{layout-6f9314b0d7a26046.js → layout-a8f33dfe92f997fb.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/{page-fa11cafaec7ab39f.js → page-e044a999468a7c5d.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{layout-39f03f9e32399f0f.js → layout-2df56074e42adaa0.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-c5d2b9076e5390b2.js → page-fbbd66a4d4633438.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{webpack-f52083d548d804fa.js → webpack-c0cd5a6afb1f0798.js} +1 -1
- khoj/interface/compiled/_next/static/css/2de69f0be774c768.css +1 -0
- khoj/interface/compiled/_next/static/css/3e1f1fdd70775091.css +1 -0
- khoj/interface/compiled/_next/static/css/467a524c75e7d7c0.css +1 -0
- khoj/interface/compiled/_next/static/css/b9a6bf04305d98d7.css +25 -0
- khoj/interface/compiled/agents/index.html +1 -1
- khoj/interface/compiled/agents/index.txt +2 -2
- khoj/interface/compiled/automations/index.html +1 -1
- khoj/interface/compiled/automations/index.txt +2 -2
- khoj/interface/compiled/chat/index.html +1 -1
- khoj/interface/compiled/chat/index.txt +2 -2
- khoj/interface/compiled/factchecker/index.html +1 -1
- khoj/interface/compiled/factchecker/index.txt +2 -2
- khoj/interface/compiled/index.html +1 -1
- khoj/interface/compiled/index.txt +2 -2
- khoj/interface/compiled/search/index.html +1 -1
- khoj/interface/compiled/search/index.txt +2 -2
- khoj/interface/compiled/settings/index.html +1 -1
- khoj/interface/compiled/settings/index.txt +3 -3
- khoj/interface/compiled/share/chat/index.html +1 -1
- khoj/interface/compiled/share/chat/index.txt +2 -2
- khoj/interface/web/assets/icons/agents.svg +1 -0
- khoj/interface/web/assets/icons/automation.svg +1 -0
- khoj/interface/web/assets/icons/chat.svg +24 -0
- khoj/interface/web/login.html +11 -22
- khoj/processor/conversation/google/gemini_chat.py +4 -19
- khoj/processor/conversation/google/utils.py +33 -15
- khoj/processor/conversation/prompts.py +14 -3
- khoj/processor/conversation/utils.py +3 -7
- khoj/processor/embeddings.py +6 -3
- khoj/processor/image/generate.py +1 -2
- khoj/processor/tools/online_search.py +135 -42
- khoj/routers/api.py +1 -1
- khoj/routers/api_agents.py +6 -3
- khoj/routers/api_chat.py +63 -520
- khoj/routers/api_model.py +1 -1
- khoj/routers/auth.py +9 -1
- khoj/routers/helpers.py +74 -61
- khoj/routers/subscription.py +18 -4
- khoj/search_type/text_search.py +7 -2
- khoj/utils/helpers.py +56 -13
- khoj/utils/initialization.py +0 -3
- {khoj-1.24.2.dev16.dist-info → khoj-1.25.1.dev34.dist-info}/METADATA +19 -14
- {khoj-1.24.2.dev16.dist-info → khoj-1.25.1.dev34.dist-info}/RECORD +71 -68
- khoj/interface/compiled/_next/static/chunks/1269-2e52d48e7d0e5c61.js +0 -1
- khoj/interface/compiled/_next/static/chunks/1603-67a89278e2c5dbe6.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-df26b497b7356151.js +0 -1
- khoj/interface/compiled/_next/static/css/1538cedb321e3a97.css +0 -1
- khoj/interface/compiled/_next/static/css/4cae6c0e5c72fb2d.css +0 -1
- khoj/interface/compiled/_next/static/css/50d972a8c787730b.css +0 -25
- khoj/interface/compiled/_next/static/css/dfb67a9287720a2b.css +0 -1
- /khoj/interface/compiled/_next/static/{MyYNlmGMz32TGV_-febR4 → Jid9q6Qg851ioDaaO_fth}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{MyYNlmGMz32TGV_-febR4 → Jid9q6Qg851ioDaaO_fth}/_ssgManifest.js +0 -0
- {khoj-1.24.2.dev16.dist-info → khoj-1.25.1.dev34.dist-info}/WHEEL +0 -0
- {khoj-1.24.2.dev16.dist-info → khoj-1.25.1.dev34.dist-info}/entry_points.txt +0 -0
- {khoj-1.24.2.dev16.dist-info → khoj-1.25.1.dev34.dist-info}/licenses/LICENSE +0 -0
khoj/routers/api_chat.py
CHANGED
@@ -3,7 +3,6 @@ import base64
|
|
3
3
|
import json
|
4
4
|
import logging
|
5
5
|
import time
|
6
|
-
import warnings
|
7
6
|
from datetime import datetime
|
8
7
|
from functools import partial
|
9
8
|
from typing import Dict, Optional
|
@@ -194,7 +193,7 @@ def chat_history(
|
|
194
193
|
n: Optional[int] = None,
|
195
194
|
):
|
196
195
|
user = request.user.object
|
197
|
-
validate_conversation_config()
|
196
|
+
validate_conversation_config(user)
|
198
197
|
|
199
198
|
# Load Conversation History
|
200
199
|
conversation = ConversationAdapters.get_conversation_by_user(
|
@@ -209,14 +208,17 @@ def chat_history(
|
|
209
208
|
|
210
209
|
agent_metadata = None
|
211
210
|
if conversation.agent:
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
211
|
+
if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE and conversation.agent.creator != user:
|
212
|
+
conversation.agent = None
|
213
|
+
else:
|
214
|
+
agent_metadata = {
|
215
|
+
"slug": conversation.agent.slug,
|
216
|
+
"name": conversation.agent.name,
|
217
|
+
"isCreator": conversation.agent.creator == user,
|
218
|
+
"color": conversation.agent.style_color,
|
219
|
+
"icon": conversation.agent.style_icon,
|
220
|
+
"persona": conversation.agent.personality,
|
221
|
+
}
|
220
222
|
|
221
223
|
meta_log = conversation.conversation_log
|
222
224
|
meta_log.update(
|
@@ -265,14 +267,17 @@ def get_shared_chat(
|
|
265
267
|
|
266
268
|
agent_metadata = None
|
267
269
|
if conversation.agent:
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
270
|
+
if conversation.agent.privacy_level == Agent.PrivacyLevel.PRIVATE:
|
271
|
+
conversation.agent = None
|
272
|
+
else:
|
273
|
+
agent_metadata = {
|
274
|
+
"slug": conversation.agent.slug,
|
275
|
+
"name": conversation.agent.name,
|
276
|
+
"isCreator": conversation.agent.creator == user,
|
277
|
+
"color": conversation.agent.style_color,
|
278
|
+
"icon": conversation.agent.style_icon,
|
279
|
+
"persona": conversation.agent.personality,
|
280
|
+
}
|
276
281
|
|
277
282
|
meta_log = conversation.conversation_log
|
278
283
|
scrubbed_title = conversation.title if conversation.title else conversation.slug
|
@@ -299,7 +304,7 @@ def get_shared_chat(
|
|
299
304
|
update_telemetry_state(
|
300
305
|
request=request,
|
301
306
|
telemetry_type="api",
|
302
|
-
api="
|
307
|
+
api="get_shared_chat_history",
|
303
308
|
**common.__dict__,
|
304
309
|
)
|
305
310
|
|
@@ -568,7 +573,6 @@ async def chat(
|
|
568
573
|
chat_metadata: dict = {}
|
569
574
|
connection_alive = True
|
570
575
|
user: KhojUser = request.user.object
|
571
|
-
subscribed: bool = has_required_scope(request, ["premium"])
|
572
576
|
event_delimiter = "␃🔚␗"
|
573
577
|
q = unquote(q)
|
574
578
|
nonlocal conversation_id
|
@@ -635,7 +639,7 @@ async def chat(
|
|
635
639
|
request=request,
|
636
640
|
telemetry_type="api",
|
637
641
|
api="chat",
|
638
|
-
client=
|
642
|
+
client=common.client,
|
639
643
|
user_agent=request.headers.get("user-agent"),
|
640
644
|
host=request.headers.get("host"),
|
641
645
|
metadata=chat_metadata,
|
@@ -688,7 +692,7 @@ async def chat(
|
|
688
692
|
q,
|
689
693
|
meta_log,
|
690
694
|
is_automated_task,
|
691
|
-
|
695
|
+
user=user,
|
692
696
|
uploaded_image_url=uploaded_image_url,
|
693
697
|
agent=agent,
|
694
698
|
)
|
@@ -698,7 +702,7 @@ async def chat(
|
|
698
702
|
):
|
699
703
|
yield result
|
700
704
|
|
701
|
-
mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url, agent)
|
705
|
+
mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, user, uploaded_image_url, agent)
|
702
706
|
async for result in send_event(ChatEvent.STATUS, f"**Decided Response Mode:** {mode.value}"):
|
703
707
|
yield result
|
704
708
|
if mode not in conversation_commands:
|
@@ -758,7 +762,12 @@ async def chat(
|
|
758
762
|
yield result
|
759
763
|
|
760
764
|
response = await extract_relevant_summary(
|
761
|
-
q,
|
765
|
+
q,
|
766
|
+
contextual_data,
|
767
|
+
conversation_history=meta_log,
|
768
|
+
uploaded_image_url=uploaded_image_url,
|
769
|
+
user=user,
|
770
|
+
agent=agent,
|
762
771
|
)
|
763
772
|
response_log = str(response)
|
764
773
|
async for result in send_llm_response(response_log):
|
@@ -829,505 +838,34 @@ async def chat(
|
|
829
838
|
# Gather Context
|
830
839
|
## Extract Document References
|
831
840
|
compiled_references, inferred_queries, defiltered_query = [], [], None
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
q,
|
836
|
-
(n or 7),
|
837
|
-
d,
|
838
|
-
conversation_id,
|
839
|
-
conversation_commands,
|
840
|
-
location,
|
841
|
-
partial(send_event, ChatEvent.STATUS),
|
842
|
-
uploaded_image_url=uploaded_image_url,
|
843
|
-
agent=agent,
|
844
|
-
):
|
845
|
-
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
846
|
-
yield result[ChatEvent.STATUS]
|
847
|
-
else:
|
848
|
-
compiled_references.extend(result[0])
|
849
|
-
inferred_queries.extend(result[1])
|
850
|
-
defiltered_query = result[2]
|
851
|
-
|
852
|
-
if not is_none_or_empty(compiled_references):
|
853
|
-
headings = "\n- " + "\n- ".join(set([c.get("compiled", c).split("\n")[0] for c in compiled_references]))
|
854
|
-
# Strip only leading # from headings
|
855
|
-
headings = headings.replace("#", "")
|
856
|
-
async for result in send_event(ChatEvent.STATUS, f"**Found Relevant Notes**: {headings}"):
|
857
|
-
yield result
|
858
|
-
|
859
|
-
online_results: Dict = dict()
|
860
|
-
|
861
|
-
if conversation_commands == [ConversationCommand.Notes] and not await EntryAdapters.auser_has_entries(user):
|
862
|
-
async for result in send_llm_response(f"{no_entries_found.format()}"):
|
863
|
-
yield result
|
864
|
-
return
|
865
|
-
|
866
|
-
if ConversationCommand.Notes in conversation_commands and is_none_or_empty(compiled_references):
|
867
|
-
conversation_commands.remove(ConversationCommand.Notes)
|
868
|
-
|
869
|
-
## Gather Online References
|
870
|
-
if ConversationCommand.Online in conversation_commands:
|
871
|
-
try:
|
872
|
-
async for result in search_online(
|
873
|
-
defiltered_query,
|
874
|
-
meta_log,
|
875
|
-
location,
|
876
|
-
user,
|
877
|
-
subscribed,
|
878
|
-
partial(send_event, ChatEvent.STATUS),
|
879
|
-
custom_filters,
|
880
|
-
uploaded_image_url=uploaded_image_url,
|
881
|
-
agent=agent,
|
882
|
-
):
|
883
|
-
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
884
|
-
yield result[ChatEvent.STATUS]
|
885
|
-
else:
|
886
|
-
online_results = result
|
887
|
-
except ValueError as e:
|
888
|
-
error_message = f"Error searching online: {e}. Attempting to respond without online results"
|
889
|
-
logger.warning(error_message)
|
890
|
-
async for result in send_llm_response(error_message):
|
891
|
-
yield result
|
892
|
-
return
|
893
|
-
|
894
|
-
## Gather Webpage References
|
895
|
-
if ConversationCommand.Webpage in conversation_commands:
|
896
|
-
try:
|
897
|
-
async for result in read_webpages(
|
898
|
-
defiltered_query,
|
899
|
-
meta_log,
|
900
|
-
location,
|
901
|
-
user,
|
902
|
-
subscribed,
|
903
|
-
partial(send_event, ChatEvent.STATUS),
|
904
|
-
uploaded_image_url=uploaded_image_url,
|
905
|
-
agent=agent,
|
906
|
-
):
|
907
|
-
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
908
|
-
yield result[ChatEvent.STATUS]
|
909
|
-
else:
|
910
|
-
direct_web_pages = result
|
911
|
-
webpages = []
|
912
|
-
for query in direct_web_pages:
|
913
|
-
if online_results.get(query):
|
914
|
-
online_results[query]["webpages"] = direct_web_pages[query]["webpages"]
|
915
|
-
else:
|
916
|
-
online_results[query] = {"webpages": direct_web_pages[query]["webpages"]}
|
917
|
-
|
918
|
-
for webpage in direct_web_pages[query]["webpages"]:
|
919
|
-
webpages.append(webpage["link"])
|
920
|
-
async for result in send_event(ChatEvent.STATUS, f"**Read web pages**: {webpages}"):
|
921
|
-
yield result
|
922
|
-
except ValueError as e:
|
923
|
-
logger.warning(
|
924
|
-
f"Error directly reading webpages: {e}. Attempting to respond without online results",
|
925
|
-
exc_info=True,
|
926
|
-
)
|
927
|
-
|
928
|
-
## Send Gathered References
|
929
|
-
async for result in send_event(
|
930
|
-
ChatEvent.REFERENCES,
|
931
|
-
{
|
932
|
-
"inferredQueries": inferred_queries,
|
933
|
-
"context": compiled_references,
|
934
|
-
"onlineContext": online_results,
|
935
|
-
},
|
936
|
-
):
|
937
|
-
yield result
|
938
|
-
|
939
|
-
# Generate Output
|
940
|
-
## Generate Image Output
|
941
|
-
if ConversationCommand.Image in conversation_commands:
|
942
|
-
async for result in text_to_image(
|
943
|
-
q,
|
944
|
-
user,
|
841
|
+
try:
|
842
|
+
async for result in extract_references_and_questions(
|
843
|
+
request,
|
945
844
|
meta_log,
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
845
|
+
q,
|
846
|
+
(n or 7),
|
847
|
+
d,
|
848
|
+
conversation_id,
|
849
|
+
conversation_commands,
|
850
|
+
location,
|
851
|
+
partial(send_event, ChatEvent.STATUS),
|
951
852
|
uploaded_image_url=uploaded_image_url,
|
952
853
|
agent=agent,
|
953
854
|
):
|
954
855
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
955
856
|
yield result[ChatEvent.STATUS]
|
956
857
|
else:
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
"detail": improved_image_prompt,
|
964
|
-
"image": image,
|
965
|
-
}
|
966
|
-
async for result in send_llm_response(json.dumps(content_obj)):
|
967
|
-
yield result
|
968
|
-
return
|
969
|
-
|
970
|
-
await sync_to_async(save_to_conversation_log)(
|
971
|
-
q,
|
972
|
-
image,
|
973
|
-
user,
|
974
|
-
meta_log,
|
975
|
-
user_message_time,
|
976
|
-
intent_type=intent_type,
|
977
|
-
inferred_queries=[improved_image_prompt],
|
978
|
-
client_application=request.user.client_app,
|
979
|
-
conversation_id=conversation_id,
|
980
|
-
compiled_references=compiled_references,
|
981
|
-
online_results=online_results,
|
982
|
-
uploaded_image_url=uploaded_image_url,
|
983
|
-
)
|
984
|
-
content_obj = {
|
985
|
-
"intentType": intent_type,
|
986
|
-
"inferredQueries": [improved_image_prompt],
|
987
|
-
"image": image,
|
988
|
-
}
|
989
|
-
async for result in send_llm_response(json.dumps(content_obj)):
|
990
|
-
yield result
|
991
|
-
return
|
992
|
-
|
993
|
-
## Generate Text Output
|
994
|
-
async for result in send_event(ChatEvent.STATUS, f"**Generating a well-informed response**"):
|
995
|
-
yield result
|
996
|
-
llm_response, chat_metadata = await agenerate_chat_response(
|
997
|
-
defiltered_query,
|
998
|
-
meta_log,
|
999
|
-
conversation,
|
1000
|
-
compiled_references,
|
1001
|
-
online_results,
|
1002
|
-
inferred_queries,
|
1003
|
-
conversation_commands,
|
1004
|
-
user,
|
1005
|
-
request.user.client_app,
|
1006
|
-
conversation_id,
|
1007
|
-
location,
|
1008
|
-
user_name,
|
1009
|
-
uploaded_image_url,
|
1010
|
-
)
|
1011
|
-
|
1012
|
-
# Send Response
|
1013
|
-
async for result in send_event(ChatEvent.START_LLM_RESPONSE, ""):
|
1014
|
-
yield result
|
1015
|
-
|
1016
|
-
continue_stream = True
|
1017
|
-
iterator = AsyncIteratorWrapper(llm_response)
|
1018
|
-
async for item in iterator:
|
1019
|
-
if item is None:
|
1020
|
-
async for result in send_event(ChatEvent.END_LLM_RESPONSE, ""):
|
1021
|
-
yield result
|
1022
|
-
logger.debug("Finished streaming response")
|
1023
|
-
return
|
1024
|
-
if not connection_alive or not continue_stream:
|
1025
|
-
continue
|
1026
|
-
try:
|
1027
|
-
async for result in send_event(ChatEvent.MESSAGE, f"{item}"):
|
1028
|
-
yield result
|
1029
|
-
except Exception as e:
|
1030
|
-
continue_stream = False
|
1031
|
-
logger.info(f"User {user} disconnected. Emitting rest of responses to clear thread: {e}")
|
1032
|
-
|
1033
|
-
## Stream Text Response
|
1034
|
-
if stream:
|
1035
|
-
return StreamingResponse(event_generator(q, image=image), media_type="text/plain")
|
1036
|
-
## Non-Streaming Text Response
|
1037
|
-
else:
|
1038
|
-
response_iterator = event_generator(q, image=image)
|
1039
|
-
response_data = await read_chat_stream(response_iterator)
|
1040
|
-
return Response(content=json.dumps(response_data), media_type="application/json", status_code=200)
|
1041
|
-
|
1042
|
-
|
1043
|
-
# Deprecated API. Remove by end of September 2024
|
1044
|
-
@api_chat.get("")
|
1045
|
-
@requires(["authenticated"])
|
1046
|
-
async def get_chat(
|
1047
|
-
request: Request,
|
1048
|
-
common: CommonQueryParams,
|
1049
|
-
q: str,
|
1050
|
-
n: int = 7,
|
1051
|
-
d: float = None,
|
1052
|
-
stream: Optional[bool] = False,
|
1053
|
-
title: Optional[str] = None,
|
1054
|
-
conversation_id: Optional[str] = None,
|
1055
|
-
city: Optional[str] = None,
|
1056
|
-
region: Optional[str] = None,
|
1057
|
-
country: Optional[str] = None,
|
1058
|
-
timezone: Optional[str] = None,
|
1059
|
-
image: Optional[str] = None,
|
1060
|
-
rate_limiter_per_minute=Depends(
|
1061
|
-
ApiUserRateLimiter(requests=60, subscribed_requests=60, window=60, slug="chat_minute")
|
1062
|
-
),
|
1063
|
-
rate_limiter_per_day=Depends(
|
1064
|
-
ApiUserRateLimiter(requests=600, subscribed_requests=600, window=60 * 60 * 24, slug="chat_day")
|
1065
|
-
),
|
1066
|
-
):
|
1067
|
-
# Issue a deprecation warning
|
1068
|
-
warnings.warn(
|
1069
|
-
"The 'get_chat' API endpoint is deprecated. It will be removed by the end of September 2024.",
|
1070
|
-
DeprecationWarning,
|
1071
|
-
stacklevel=2,
|
1072
|
-
)
|
1073
|
-
|
1074
|
-
async def event_generator(q: str, image: str):
|
1075
|
-
start_time = time.perf_counter()
|
1076
|
-
ttft = None
|
1077
|
-
chat_metadata: dict = {}
|
1078
|
-
connection_alive = True
|
1079
|
-
user: KhojUser = request.user.object
|
1080
|
-
subscribed: bool = has_required_scope(request, ["premium"])
|
1081
|
-
event_delimiter = "␃🔚␗"
|
1082
|
-
q = unquote(q)
|
1083
|
-
nonlocal conversation_id
|
1084
|
-
|
1085
|
-
uploaded_image_url = None
|
1086
|
-
if image:
|
1087
|
-
decoded_string = unquote(image)
|
1088
|
-
base64_data = decoded_string.split(",", 1)[1]
|
1089
|
-
image_bytes = base64.b64decode(base64_data)
|
1090
|
-
webp_image_bytes = convert_image_to_webp(image_bytes)
|
1091
|
-
try:
|
1092
|
-
uploaded_image_url = upload_image_to_bucket(webp_image_bytes, request.user.object.id)
|
1093
|
-
except:
|
1094
|
-
uploaded_image_url = None
|
1095
|
-
|
1096
|
-
async def send_event(event_type: ChatEvent, data: str | dict):
|
1097
|
-
nonlocal connection_alive, ttft
|
1098
|
-
if not connection_alive or await request.is_disconnected():
|
1099
|
-
connection_alive = False
|
1100
|
-
logger.warn(f"User {user} disconnected from {common.client} client")
|
1101
|
-
return
|
1102
|
-
try:
|
1103
|
-
if event_type == ChatEvent.END_LLM_RESPONSE:
|
1104
|
-
collect_telemetry()
|
1105
|
-
if event_type == ChatEvent.START_LLM_RESPONSE:
|
1106
|
-
ttft = time.perf_counter() - start_time
|
1107
|
-
if event_type == ChatEvent.MESSAGE:
|
1108
|
-
yield data
|
1109
|
-
elif event_type == ChatEvent.REFERENCES or stream:
|
1110
|
-
yield json.dumps({"type": event_type.value, "data": data}, ensure_ascii=False)
|
1111
|
-
except asyncio.CancelledError as e:
|
1112
|
-
connection_alive = False
|
1113
|
-
logger.warn(f"User {user} disconnected from {common.client} client: {e}")
|
1114
|
-
return
|
1115
|
-
except Exception as e:
|
1116
|
-
connection_alive = False
|
1117
|
-
logger.error(f"Failed to stream chat API response to {user} on {common.client}: {e}", exc_info=True)
|
1118
|
-
return
|
1119
|
-
finally:
|
1120
|
-
yield event_delimiter
|
1121
|
-
|
1122
|
-
async def send_llm_response(response: str):
|
1123
|
-
async for result in send_event(ChatEvent.START_LLM_RESPONSE, ""):
|
1124
|
-
yield result
|
1125
|
-
async for result in send_event(ChatEvent.MESSAGE, response):
|
1126
|
-
yield result
|
1127
|
-
async for result in send_event(ChatEvent.END_LLM_RESPONSE, ""):
|
1128
|
-
yield result
|
1129
|
-
|
1130
|
-
def collect_telemetry():
|
1131
|
-
# Gather chat response telemetry
|
1132
|
-
nonlocal chat_metadata
|
1133
|
-
latency = time.perf_counter() - start_time
|
1134
|
-
cmd_set = set([cmd.value for cmd in conversation_commands])
|
1135
|
-
chat_metadata = chat_metadata or {}
|
1136
|
-
chat_metadata["conversation_command"] = cmd_set
|
1137
|
-
chat_metadata["agent"] = conversation.agent.slug if conversation.agent else None
|
1138
|
-
chat_metadata["latency"] = f"{latency:.3f}"
|
1139
|
-
chat_metadata["ttft_latency"] = f"{ttft:.3f}"
|
1140
|
-
|
1141
|
-
logger.info(f"Chat response time to first token: {ttft:.3f} seconds")
|
1142
|
-
logger.info(f"Chat response total time: {latency:.3f} seconds")
|
1143
|
-
update_telemetry_state(
|
1144
|
-
request=request,
|
1145
|
-
telemetry_type="api",
|
1146
|
-
api="chat",
|
1147
|
-
client=request.user.client_app,
|
1148
|
-
user_agent=request.headers.get("user-agent"),
|
1149
|
-
host=request.headers.get("host"),
|
1150
|
-
metadata=chat_metadata,
|
1151
|
-
)
|
1152
|
-
|
1153
|
-
conversation_commands = [get_conversation_command(query=q, any_references=True)]
|
1154
|
-
|
1155
|
-
conversation = await ConversationAdapters.aget_conversation_by_user(
|
1156
|
-
user, client_application=request.user.client_app, conversation_id=conversation_id, title=title
|
1157
|
-
)
|
1158
|
-
if not conversation:
|
1159
|
-
async for result in send_llm_response(f"Conversation {conversation_id} not found"):
|
1160
|
-
yield result
|
1161
|
-
return
|
1162
|
-
conversation_id = conversation.id
|
1163
|
-
agent = conversation.agent if conversation.agent else None
|
1164
|
-
|
1165
|
-
await is_ready_to_chat(user)
|
1166
|
-
|
1167
|
-
user_name = await aget_user_name(user)
|
1168
|
-
location = None
|
1169
|
-
if city or region or country:
|
1170
|
-
location = LocationData(city=city, region=region, country=country)
|
1171
|
-
|
1172
|
-
if is_query_empty(q):
|
1173
|
-
async for result in send_llm_response("Please ask your query to get started."):
|
1174
|
-
yield result
|
1175
|
-
return
|
1176
|
-
|
1177
|
-
user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
1178
|
-
|
1179
|
-
meta_log = conversation.conversation_log
|
1180
|
-
is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask]
|
1181
|
-
|
1182
|
-
if conversation_commands == [ConversationCommand.Default] or is_automated_task:
|
1183
|
-
conversation_commands = await aget_relevant_information_sources(
|
1184
|
-
q, meta_log, is_automated_task, subscribed=subscribed, uploaded_image_url=uploaded_image_url
|
1185
|
-
)
|
1186
|
-
conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
|
858
|
+
compiled_references.extend(result[0])
|
859
|
+
inferred_queries.extend(result[1])
|
860
|
+
defiltered_query = result[2]
|
861
|
+
except Exception as e:
|
862
|
+
error_message = f"Error searching knowledge base: {e}. Attempting to respond without document references."
|
863
|
+
logger.warning(error_message)
|
1187
864
|
async for result in send_event(
|
1188
|
-
ChatEvent.STATUS,
|
865
|
+
ChatEvent.STATUS, "Document search failed. I'll try respond without document references"
|
1189
866
|
):
|
1190
867
|
yield result
|
1191
868
|
|
1192
|
-
mode = await aget_relevant_output_modes(q, meta_log, is_automated_task, uploaded_image_url)
|
1193
|
-
async for result in send_event(ChatEvent.STATUS, f"**Decided Response Mode:** {mode.value}"):
|
1194
|
-
yield result
|
1195
|
-
if mode not in conversation_commands:
|
1196
|
-
conversation_commands.append(mode)
|
1197
|
-
|
1198
|
-
for cmd in conversation_commands:
|
1199
|
-
await conversation_command_rate_limiter.update_and_check_if_valid(request, cmd)
|
1200
|
-
q = q.replace(f"/{cmd.value}", "").strip()
|
1201
|
-
|
1202
|
-
used_slash_summarize = conversation_commands == [ConversationCommand.Summarize]
|
1203
|
-
file_filters = conversation.file_filters if conversation else []
|
1204
|
-
# Skip trying to summarize if
|
1205
|
-
if (
|
1206
|
-
# summarization intent was inferred
|
1207
|
-
ConversationCommand.Summarize in conversation_commands
|
1208
|
-
# and not triggered via slash command
|
1209
|
-
and not used_slash_summarize
|
1210
|
-
# but we can't actually summarize
|
1211
|
-
and len(file_filters) != 1
|
1212
|
-
):
|
1213
|
-
conversation_commands.remove(ConversationCommand.Summarize)
|
1214
|
-
elif ConversationCommand.Summarize in conversation_commands:
|
1215
|
-
response_log = ""
|
1216
|
-
if len(file_filters) == 0:
|
1217
|
-
response_log = "No files selected for summarization. Please add files using the section on the left."
|
1218
|
-
async for result in send_llm_response(response_log):
|
1219
|
-
yield result
|
1220
|
-
elif len(file_filters) > 1:
|
1221
|
-
response_log = "Only one file can be selected for summarization."
|
1222
|
-
async for result in send_llm_response(response_log):
|
1223
|
-
yield result
|
1224
|
-
else:
|
1225
|
-
try:
|
1226
|
-
file_object = await FileObjectAdapters.async_get_file_objects_by_name(user, file_filters[0])
|
1227
|
-
if len(file_object) == 0:
|
1228
|
-
response_log = "Sorry, we couldn't find the full text of this file. Please re-upload the document and try again."
|
1229
|
-
async for result in send_llm_response(response_log):
|
1230
|
-
yield result
|
1231
|
-
return
|
1232
|
-
contextual_data = " ".join([file.raw_text for file in file_object])
|
1233
|
-
if not q:
|
1234
|
-
q = "Create a general summary of the file"
|
1235
|
-
async for result in send_event(
|
1236
|
-
ChatEvent.STATUS, f"**Constructing Summary Using:** {file_object[0].file_name}"
|
1237
|
-
):
|
1238
|
-
yield result
|
1239
|
-
|
1240
|
-
response = await extract_relevant_summary(
|
1241
|
-
q, contextual_data, subscribed=subscribed, uploaded_image_url=uploaded_image_url
|
1242
|
-
)
|
1243
|
-
response_log = str(response)
|
1244
|
-
async for result in send_llm_response(response_log):
|
1245
|
-
yield result
|
1246
|
-
except Exception as e:
|
1247
|
-
response_log = "Error summarizing file."
|
1248
|
-
logger.error(f"Error summarizing file for {user.email}: {e}", exc_info=True)
|
1249
|
-
async for result in send_llm_response(response_log):
|
1250
|
-
yield result
|
1251
|
-
await sync_to_async(save_to_conversation_log)(
|
1252
|
-
q,
|
1253
|
-
response_log,
|
1254
|
-
user,
|
1255
|
-
meta_log,
|
1256
|
-
user_message_time,
|
1257
|
-
intent_type="summarize",
|
1258
|
-
client_application=request.user.client_app,
|
1259
|
-
conversation_id=conversation_id,
|
1260
|
-
uploaded_image_url=uploaded_image_url,
|
1261
|
-
)
|
1262
|
-
return
|
1263
|
-
|
1264
|
-
custom_filters = []
|
1265
|
-
if conversation_commands == [ConversationCommand.Help]:
|
1266
|
-
if not q:
|
1267
|
-
conversation_config = await ConversationAdapters.aget_user_conversation_config(user)
|
1268
|
-
if conversation_config == None:
|
1269
|
-
conversation_config = await ConversationAdapters.aget_default_conversation_config()
|
1270
|
-
model_type = conversation_config.model_type
|
1271
|
-
formatted_help = help_message.format(model=model_type, version=state.khoj_version, device=get_device())
|
1272
|
-
async for result in send_llm_response(formatted_help):
|
1273
|
-
yield result
|
1274
|
-
return
|
1275
|
-
# Adding specification to search online specifically on khoj.dev pages.
|
1276
|
-
custom_filters.append("site:khoj.dev")
|
1277
|
-
conversation_commands.append(ConversationCommand.Online)
|
1278
|
-
|
1279
|
-
if ConversationCommand.Automation in conversation_commands:
|
1280
|
-
try:
|
1281
|
-
automation, crontime, query_to_run, subject = await create_automation(
|
1282
|
-
q, timezone, user, request.url, meta_log
|
1283
|
-
)
|
1284
|
-
except Exception as e:
|
1285
|
-
logger.error(f"Error scheduling task {q} for {user.email}: {e}")
|
1286
|
-
error_message = f"Unable to create automation. Ensure the automation doesn't already exist."
|
1287
|
-
async for result in send_llm_response(error_message):
|
1288
|
-
yield result
|
1289
|
-
return
|
1290
|
-
|
1291
|
-
llm_response = construct_automation_created_message(automation, crontime, query_to_run, subject)
|
1292
|
-
await sync_to_async(save_to_conversation_log)(
|
1293
|
-
q,
|
1294
|
-
llm_response,
|
1295
|
-
user,
|
1296
|
-
meta_log,
|
1297
|
-
user_message_time,
|
1298
|
-
intent_type="automation",
|
1299
|
-
client_application=request.user.client_app,
|
1300
|
-
conversation_id=conversation_id,
|
1301
|
-
inferred_queries=[query_to_run],
|
1302
|
-
automation_id=automation.id,
|
1303
|
-
uploaded_image_url=uploaded_image_url,
|
1304
|
-
)
|
1305
|
-
async for result in send_llm_response(llm_response):
|
1306
|
-
yield result
|
1307
|
-
return
|
1308
|
-
|
1309
|
-
# Gather Context
|
1310
|
-
## Extract Document References
|
1311
|
-
compiled_references, inferred_queries, defiltered_query = [], [], None
|
1312
|
-
async for result in extract_references_and_questions(
|
1313
|
-
request,
|
1314
|
-
meta_log,
|
1315
|
-
q,
|
1316
|
-
(n or 7),
|
1317
|
-
d,
|
1318
|
-
conversation_id,
|
1319
|
-
conversation_commands,
|
1320
|
-
location,
|
1321
|
-
partial(send_event, ChatEvent.STATUS),
|
1322
|
-
uploaded_image_url=uploaded_image_url,
|
1323
|
-
):
|
1324
|
-
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
1325
|
-
yield result[ChatEvent.STATUS]
|
1326
|
-
else:
|
1327
|
-
compiled_references.extend(result[0])
|
1328
|
-
inferred_queries.extend(result[1])
|
1329
|
-
defiltered_query = result[2]
|
1330
|
-
|
1331
869
|
if not is_none_or_empty(compiled_references):
|
1332
870
|
headings = "\n- " + "\n- ".join(set([c.get("compiled", c).split("\n")[0] for c in compiled_references]))
|
1333
871
|
# Strip only leading # from headings
|
@@ -1353,21 +891,22 @@ async def get_chat(
|
|
1353
891
|
meta_log,
|
1354
892
|
location,
|
1355
893
|
user,
|
1356
|
-
subscribed,
|
1357
894
|
partial(send_event, ChatEvent.STATUS),
|
1358
895
|
custom_filters,
|
1359
896
|
uploaded_image_url=uploaded_image_url,
|
897
|
+
agent=agent,
|
1360
898
|
):
|
1361
899
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
1362
900
|
yield result[ChatEvent.STATUS]
|
1363
901
|
else:
|
1364
902
|
online_results = result
|
1365
|
-
except
|
903
|
+
except Exception as e:
|
1366
904
|
error_message = f"Error searching online: {e}. Attempting to respond without online results"
|
1367
905
|
logger.warning(error_message)
|
1368
|
-
async for result in
|
906
|
+
async for result in send_event(
|
907
|
+
ChatEvent.STATUS, "Online search failed. I'll try respond without online references"
|
908
|
+
):
|
1369
909
|
yield result
|
1370
|
-
return
|
1371
910
|
|
1372
911
|
## Gather Webpage References
|
1373
912
|
if ConversationCommand.Webpage in conversation_commands:
|
@@ -1377,9 +916,9 @@ async def get_chat(
|
|
1377
916
|
meta_log,
|
1378
917
|
location,
|
1379
918
|
user,
|
1380
|
-
subscribed,
|
1381
919
|
partial(send_event, ChatEvent.STATUS),
|
1382
920
|
uploaded_image_url=uploaded_image_url,
|
921
|
+
agent=agent,
|
1383
922
|
):
|
1384
923
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
1385
924
|
yield result[ChatEvent.STATUS]
|
@@ -1396,11 +935,15 @@ async def get_chat(
|
|
1396
935
|
webpages.append(webpage["link"])
|
1397
936
|
async for result in send_event(ChatEvent.STATUS, f"**Read web pages**: {webpages}"):
|
1398
937
|
yield result
|
1399
|
-
except
|
938
|
+
except Exception as e:
|
1400
939
|
logger.warning(
|
1401
|
-
f"Error
|
940
|
+
f"Error reading webpages: {e}. Attempting to respond without webpage results",
|
1402
941
|
exc_info=True,
|
1403
942
|
)
|
943
|
+
async for result in send_event(
|
944
|
+
ChatEvent.STATUS, "Webpage read failed. I'll try respond without webpage references"
|
945
|
+
):
|
946
|
+
yield result
|
1404
947
|
|
1405
948
|
## Send Gathered References
|
1406
949
|
async for result in send_event(
|
@@ -1423,9 +966,9 @@ async def get_chat(
|
|
1423
966
|
location_data=location,
|
1424
967
|
references=compiled_references,
|
1425
968
|
online_results=online_results,
|
1426
|
-
subscribed=subscribed,
|
1427
969
|
send_status_func=partial(send_event, ChatEvent.STATUS),
|
1428
970
|
uploaded_image_url=uploaded_image_url,
|
971
|
+
agent=agent,
|
1429
972
|
):
|
1430
973
|
if isinstance(result, dict) and ChatEvent.STATUS in result:
|
1431
974
|
yield result[ChatEvent.STATUS]
|