khoj 1.16.1.dev15__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/__init__.py +0 -0
- khoj/app/README.md +94 -0
- khoj/app/__init__.py +0 -0
- khoj/app/asgi.py +16 -0
- khoj/app/settings.py +192 -0
- khoj/app/urls.py +25 -0
- khoj/configure.py +424 -0
- khoj/database/__init__.py +0 -0
- khoj/database/adapters/__init__.py +1234 -0
- khoj/database/admin.py +290 -0
- khoj/database/apps.py +6 -0
- khoj/database/management/__init__.py +0 -0
- khoj/database/management/commands/__init__.py +0 -0
- khoj/database/management/commands/change_generated_images_url.py +61 -0
- khoj/database/management/commands/convert_images_png_to_webp.py +99 -0
- khoj/database/migrations/0001_khojuser.py +98 -0
- khoj/database/migrations/0002_googleuser.py +32 -0
- khoj/database/migrations/0003_vector_extension.py +10 -0
- khoj/database/migrations/0004_content_types_and_more.py +181 -0
- khoj/database/migrations/0005_embeddings_corpus_id.py +19 -0
- khoj/database/migrations/0006_embeddingsdates.py +33 -0
- khoj/database/migrations/0007_add_conversation.py +27 -0
- khoj/database/migrations/0008_alter_conversation_conversation_log.py +17 -0
- khoj/database/migrations/0009_khojapiuser.py +24 -0
- khoj/database/migrations/0010_chatmodeloptions_and_more.py +83 -0
- khoj/database/migrations/0010_rename_embeddings_entry_and_more.py +30 -0
- khoj/database/migrations/0011_merge_20231102_0138.py +14 -0
- khoj/database/migrations/0012_entry_file_source.py +21 -0
- khoj/database/migrations/0013_subscription.py +37 -0
- khoj/database/migrations/0014_alter_googleuser_picture.py +17 -0
- khoj/database/migrations/0015_alter_subscription_user.py +21 -0
- khoj/database/migrations/0016_alter_subscription_renewal_date.py +17 -0
- khoj/database/migrations/0017_searchmodel.py +32 -0
- khoj/database/migrations/0018_searchmodelconfig_delete_searchmodel.py +30 -0
- khoj/database/migrations/0019_alter_googleuser_family_name_and_more.py +27 -0
- khoj/database/migrations/0020_reflectivequestion.py +36 -0
- khoj/database/migrations/0021_speechtotextmodeloptions_and_more.py +42 -0
- khoj/database/migrations/0022_texttoimagemodelconfig.py +25 -0
- khoj/database/migrations/0023_usersearchmodelconfig.py +33 -0
- khoj/database/migrations/0024_alter_entry_embeddings.py +18 -0
- khoj/database/migrations/0025_clientapplication_khojuser_phone_number_and_more.py +46 -0
- khoj/database/migrations/0025_searchmodelconfig_embeddings_inference_endpoint_and_more.py +22 -0
- khoj/database/migrations/0026_searchmodelconfig_cross_encoder_inference_endpoint_and_more.py +22 -0
- khoj/database/migrations/0027_merge_20240118_1324.py +13 -0
- khoj/database/migrations/0028_khojuser_verified_phone_number.py +17 -0
- khoj/database/migrations/0029_userrequests.py +27 -0
- khoj/database/migrations/0030_conversation_slug_and_title.py +38 -0
- khoj/database/migrations/0031_agent_conversation_agent.py +53 -0
- khoj/database/migrations/0031_alter_googleuser_locale.py +30 -0
- khoj/database/migrations/0032_merge_20240322_0427.py +14 -0
- khoj/database/migrations/0033_rename_tuning_agent_personality.py +17 -0
- khoj/database/migrations/0034_alter_chatmodeloptions_chat_model.py +32 -0
- khoj/database/migrations/0035_processlock.py +26 -0
- khoj/database/migrations/0036_alter_processlock_name.py +19 -0
- khoj/database/migrations/0036_delete_offlinechatprocessorconversationconfig.py +15 -0
- khoj/database/migrations/0036_publicconversation.py +42 -0
- khoj/database/migrations/0037_chatmodeloptions_openai_config_and_more.py +51 -0
- khoj/database/migrations/0037_searchmodelconfig_bi_encoder_docs_encode_config_and_more.py +32 -0
- khoj/database/migrations/0038_merge_20240425_0857.py +14 -0
- khoj/database/migrations/0038_merge_20240426_1640.py +12 -0
- khoj/database/migrations/0039_merge_20240501_0301.py +12 -0
- khoj/database/migrations/0040_alter_processlock_name.py +26 -0
- khoj/database/migrations/0040_merge_20240504_1010.py +14 -0
- khoj/database/migrations/0041_merge_20240505_1234.py +14 -0
- khoj/database/migrations/0042_serverchatsettings.py +46 -0
- khoj/database/migrations/0043_alter_chatmodeloptions_model_type.py +21 -0
- khoj/database/migrations/0044_conversation_file_filters.py +17 -0
- khoj/database/migrations/0045_fileobject.py +37 -0
- khoj/database/migrations/0046_khojuser_email_verification_code_and_more.py +22 -0
- khoj/database/migrations/0047_alter_entry_file_type.py +31 -0
- khoj/database/migrations/0048_voicemodeloption_uservoicemodelconfig.py +52 -0
- khoj/database/migrations/0049_datastore.py +38 -0
- khoj/database/migrations/0049_texttoimagemodelconfig_api_key_and_more.py +58 -0
- khoj/database/migrations/0050_alter_processlock_name.py +25 -0
- khoj/database/migrations/0051_merge_20240702_1220.py +14 -0
- khoj/database/migrations/0052_alter_searchmodelconfig_bi_encoder_docs_encode_config_and_more.py +27 -0
- khoj/database/migrations/__init__.py +0 -0
- khoj/database/models/__init__.py +402 -0
- khoj/database/tests.py +3 -0
- khoj/interface/email/feedback.html +34 -0
- khoj/interface/email/magic_link.html +17 -0
- khoj/interface/email/task.html +40 -0
- khoj/interface/email/welcome.html +61 -0
- khoj/interface/web/404.html +56 -0
- khoj/interface/web/agent.html +312 -0
- khoj/interface/web/agents.html +276 -0
- khoj/interface/web/assets/icons/agents.svg +6 -0
- khoj/interface/web/assets/icons/automation.svg +37 -0
- khoj/interface/web/assets/icons/cancel.svg +3 -0
- khoj/interface/web/assets/icons/chat.svg +24 -0
- khoj/interface/web/assets/icons/collapse.svg +17 -0
- khoj/interface/web/assets/icons/computer.png +0 -0
- khoj/interface/web/assets/icons/confirm-icon.svg +1 -0
- khoj/interface/web/assets/icons/copy-button-success.svg +6 -0
- khoj/interface/web/assets/icons/copy-button.svg +5 -0
- khoj/interface/web/assets/icons/credit-card.png +0 -0
- khoj/interface/web/assets/icons/delete.svg +26 -0
- khoj/interface/web/assets/icons/docx.svg +7 -0
- khoj/interface/web/assets/icons/edit.svg +4 -0
- khoj/interface/web/assets/icons/favicon-128x128.ico +0 -0
- khoj/interface/web/assets/icons/favicon-128x128.png +0 -0
- khoj/interface/web/assets/icons/favicon-256x256.png +0 -0
- khoj/interface/web/assets/icons/favicon.icns +0 -0
- khoj/interface/web/assets/icons/github.svg +1 -0
- khoj/interface/web/assets/icons/key.svg +4 -0
- khoj/interface/web/assets/icons/khoj-logo-sideways-200.png +0 -0
- khoj/interface/web/assets/icons/khoj-logo-sideways-500.png +0 -0
- khoj/interface/web/assets/icons/khoj-logo-sideways.svg +5385 -0
- khoj/interface/web/assets/icons/logotype.svg +1 -0
- khoj/interface/web/assets/icons/markdown.svg +1 -0
- khoj/interface/web/assets/icons/new.svg +23 -0
- khoj/interface/web/assets/icons/notion.svg +4 -0
- khoj/interface/web/assets/icons/openai-logomark.svg +1 -0
- khoj/interface/web/assets/icons/org.svg +1 -0
- khoj/interface/web/assets/icons/pdf.svg +23 -0
- khoj/interface/web/assets/icons/pencil-edit.svg +5 -0
- khoj/interface/web/assets/icons/plaintext.svg +1 -0
- khoj/interface/web/assets/icons/question-mark-icon.svg +1 -0
- khoj/interface/web/assets/icons/search.svg +25 -0
- khoj/interface/web/assets/icons/send.svg +1 -0
- khoj/interface/web/assets/icons/share.svg +8 -0
- khoj/interface/web/assets/icons/speaker.svg +4 -0
- khoj/interface/web/assets/icons/stop-solid.svg +37 -0
- khoj/interface/web/assets/icons/sync.svg +4 -0
- khoj/interface/web/assets/icons/thumbs-down-svgrepo-com.svg +6 -0
- khoj/interface/web/assets/icons/thumbs-up-svgrepo-com.svg +6 -0
- khoj/interface/web/assets/icons/user-silhouette.svg +4 -0
- khoj/interface/web/assets/icons/voice.svg +8 -0
- khoj/interface/web/assets/icons/web.svg +2 -0
- khoj/interface/web/assets/icons/whatsapp.svg +17 -0
- khoj/interface/web/assets/khoj.css +237 -0
- khoj/interface/web/assets/markdown-it.min.js +8476 -0
- khoj/interface/web/assets/natural-cron.min.js +1 -0
- khoj/interface/web/assets/org.min.js +1823 -0
- khoj/interface/web/assets/pico.min.css +5 -0
- khoj/interface/web/assets/purify.min.js +3 -0
- khoj/interface/web/assets/samples/desktop-browse-draw-sample.png +0 -0
- khoj/interface/web/assets/samples/desktop-plain-chat-sample.png +0 -0
- khoj/interface/web/assets/samples/desktop-remember-plan-sample.png +0 -0
- khoj/interface/web/assets/samples/phone-browse-draw-sample.png +0 -0
- khoj/interface/web/assets/samples/phone-plain-chat-sample.png +0 -0
- khoj/interface/web/assets/samples/phone-remember-plan-sample.png +0 -0
- khoj/interface/web/assets/utils.js +33 -0
- khoj/interface/web/base_config.html +445 -0
- khoj/interface/web/chat.html +3546 -0
- khoj/interface/web/config.html +1011 -0
- khoj/interface/web/config_automation.html +1103 -0
- khoj/interface/web/content_source_computer_input.html +139 -0
- khoj/interface/web/content_source_github_input.html +216 -0
- khoj/interface/web/content_source_notion_input.html +94 -0
- khoj/interface/web/khoj.webmanifest +51 -0
- khoj/interface/web/login.html +219 -0
- khoj/interface/web/public_conversation.html +2006 -0
- khoj/interface/web/search.html +470 -0
- khoj/interface/web/utils.html +48 -0
- khoj/main.py +241 -0
- khoj/manage.py +22 -0
- khoj/migrations/__init__.py +0 -0
- khoj/migrations/migrate_offline_chat_default_model.py +69 -0
- khoj/migrations/migrate_offline_chat_default_model_2.py +71 -0
- khoj/migrations/migrate_offline_chat_schema.py +83 -0
- khoj/migrations/migrate_offline_model.py +29 -0
- khoj/migrations/migrate_processor_config_openai.py +67 -0
- khoj/migrations/migrate_server_pg.py +138 -0
- khoj/migrations/migrate_version.py +17 -0
- khoj/processor/__init__.py +0 -0
- khoj/processor/content/__init__.py +0 -0
- khoj/processor/content/docx/__init__.py +0 -0
- khoj/processor/content/docx/docx_to_entries.py +110 -0
- khoj/processor/content/github/__init__.py +0 -0
- khoj/processor/content/github/github_to_entries.py +224 -0
- khoj/processor/content/images/__init__.py +0 -0
- khoj/processor/content/images/image_to_entries.py +118 -0
- khoj/processor/content/markdown/__init__.py +0 -0
- khoj/processor/content/markdown/markdown_to_entries.py +165 -0
- khoj/processor/content/notion/notion_to_entries.py +260 -0
- khoj/processor/content/org_mode/__init__.py +0 -0
- khoj/processor/content/org_mode/org_to_entries.py +231 -0
- khoj/processor/content/org_mode/orgnode.py +532 -0
- khoj/processor/content/pdf/__init__.py +0 -0
- khoj/processor/content/pdf/pdf_to_entries.py +116 -0
- khoj/processor/content/plaintext/__init__.py +0 -0
- khoj/processor/content/plaintext/plaintext_to_entries.py +122 -0
- khoj/processor/content/text_to_entries.py +297 -0
- khoj/processor/conversation/__init__.py +0 -0
- khoj/processor/conversation/anthropic/__init__.py +0 -0
- khoj/processor/conversation/anthropic/anthropic_chat.py +206 -0
- khoj/processor/conversation/anthropic/utils.py +114 -0
- khoj/processor/conversation/offline/__init__.py +0 -0
- khoj/processor/conversation/offline/chat_model.py +231 -0
- khoj/processor/conversation/offline/utils.py +78 -0
- khoj/processor/conversation/offline/whisper.py +15 -0
- khoj/processor/conversation/openai/__init__.py +0 -0
- khoj/processor/conversation/openai/gpt.py +187 -0
- khoj/processor/conversation/openai/utils.py +129 -0
- khoj/processor/conversation/openai/whisper.py +13 -0
- khoj/processor/conversation/prompts.py +758 -0
- khoj/processor/conversation/utils.py +262 -0
- khoj/processor/embeddings.py +117 -0
- khoj/processor/speech/__init__.py +0 -0
- khoj/processor/speech/text_to_speech.py +51 -0
- khoj/processor/tools/__init__.py +0 -0
- khoj/processor/tools/online_search.py +225 -0
- khoj/routers/__init__.py +0 -0
- khoj/routers/api.py +626 -0
- khoj/routers/api_agents.py +43 -0
- khoj/routers/api_chat.py +1180 -0
- khoj/routers/api_config.py +434 -0
- khoj/routers/api_phone.py +86 -0
- khoj/routers/auth.py +181 -0
- khoj/routers/email.py +133 -0
- khoj/routers/helpers.py +1188 -0
- khoj/routers/indexer.py +349 -0
- khoj/routers/notion.py +91 -0
- khoj/routers/storage.py +35 -0
- khoj/routers/subscription.py +104 -0
- khoj/routers/twilio.py +36 -0
- khoj/routers/web_client.py +471 -0
- khoj/search_filter/__init__.py +0 -0
- khoj/search_filter/base_filter.py +15 -0
- khoj/search_filter/date_filter.py +217 -0
- khoj/search_filter/file_filter.py +30 -0
- khoj/search_filter/word_filter.py +29 -0
- khoj/search_type/__init__.py +0 -0
- khoj/search_type/text_search.py +241 -0
- khoj/utils/__init__.py +0 -0
- khoj/utils/cli.py +93 -0
- khoj/utils/config.py +81 -0
- khoj/utils/constants.py +24 -0
- khoj/utils/fs_syncer.py +249 -0
- khoj/utils/helpers.py +418 -0
- khoj/utils/initialization.py +146 -0
- khoj/utils/jsonl.py +43 -0
- khoj/utils/models.py +47 -0
- khoj/utils/rawconfig.py +160 -0
- khoj/utils/state.py +46 -0
- khoj/utils/yaml.py +43 -0
- khoj-1.16.1.dev15.dist-info/METADATA +178 -0
- khoj-1.16.1.dev15.dist-info/RECORD +242 -0
- khoj-1.16.1.dev15.dist-info/WHEEL +4 -0
- khoj-1.16.1.dev15.dist-info/entry_points.txt +2 -0
- khoj-1.16.1.dev15.dist-info/licenses/LICENSE +661 -0
khoj/routers/api_chat.py
ADDED
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import math
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
from urllib.parse import unquote
|
|
7
|
+
|
|
8
|
+
from asgiref.sync import sync_to_async
|
|
9
|
+
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket
|
|
10
|
+
from fastapi.requests import Request
|
|
11
|
+
from fastapi.responses import Response, StreamingResponse
|
|
12
|
+
from starlette.authentication import requires
|
|
13
|
+
from starlette.websockets import WebSocketDisconnect
|
|
14
|
+
from websockets import ConnectionClosedOK
|
|
15
|
+
|
|
16
|
+
from khoj.app.settings import ALLOWED_HOSTS
|
|
17
|
+
from khoj.database.adapters import (
|
|
18
|
+
ConversationAdapters,
|
|
19
|
+
DataStoreAdapters,
|
|
20
|
+
EntryAdapters,
|
|
21
|
+
FileObjectAdapters,
|
|
22
|
+
PublicConversationAdapters,
|
|
23
|
+
aget_user_name,
|
|
24
|
+
)
|
|
25
|
+
from khoj.database.models import KhojUser
|
|
26
|
+
from khoj.processor.conversation.prompts import (
|
|
27
|
+
help_message,
|
|
28
|
+
no_entries_found,
|
|
29
|
+
no_notes_found,
|
|
30
|
+
)
|
|
31
|
+
from khoj.processor.conversation.utils import save_to_conversation_log
|
|
32
|
+
from khoj.processor.speech.text_to_speech import generate_text_to_speech
|
|
33
|
+
from khoj.processor.tools.online_search import read_webpages, search_online
|
|
34
|
+
from khoj.routers.api import extract_references_and_questions
|
|
35
|
+
from khoj.routers.helpers import (
|
|
36
|
+
ApiUserRateLimiter,
|
|
37
|
+
CommonQueryParams,
|
|
38
|
+
CommonQueryParamsClass,
|
|
39
|
+
ConversationCommandRateLimiter,
|
|
40
|
+
agenerate_chat_response,
|
|
41
|
+
aget_relevant_information_sources,
|
|
42
|
+
aget_relevant_output_modes,
|
|
43
|
+
construct_automation_created_message,
|
|
44
|
+
create_automation,
|
|
45
|
+
extract_relevant_summary,
|
|
46
|
+
get_conversation_command,
|
|
47
|
+
is_query_empty,
|
|
48
|
+
is_ready_to_chat,
|
|
49
|
+
text_to_image,
|
|
50
|
+
update_telemetry_state,
|
|
51
|
+
validate_conversation_config,
|
|
52
|
+
)
|
|
53
|
+
from khoj.utils import state
|
|
54
|
+
from khoj.utils.helpers import (
|
|
55
|
+
AsyncIteratorWrapper,
|
|
56
|
+
ConversationCommand,
|
|
57
|
+
command_descriptions,
|
|
58
|
+
get_device,
|
|
59
|
+
is_none_or_empty,
|
|
60
|
+
)
|
|
61
|
+
from khoj.utils.rawconfig import FilterRequest, LocationData
|
|
62
|
+
|
|
63
|
+
# Initialize Router
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
conversation_command_rate_limiter = ConversationCommandRateLimiter(
|
|
66
|
+
trial_rate_limit=2, subscribed_rate_limit=100, slug="command"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
api_chat = APIRouter()
|
|
71
|
+
|
|
72
|
+
from pydantic import BaseModel
|
|
73
|
+
|
|
74
|
+
from khoj.routers.email import send_query_feedback
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@api_chat.get("/conversation/file-filters/{conversation_id}", response_class=Response)
|
|
78
|
+
@requires(["authenticated"])
|
|
79
|
+
def get_file_filter(request: Request, conversation_id: str) -> Response:
|
|
80
|
+
conversation = ConversationAdapters.get_conversation_by_user(
|
|
81
|
+
request.user.object, conversation_id=int(conversation_id)
|
|
82
|
+
)
|
|
83
|
+
if not conversation:
|
|
84
|
+
return Response(content=json.dumps({"status": "error", "message": "Conversation not found"}), status_code=404)
|
|
85
|
+
|
|
86
|
+
# get all files from "computer"
|
|
87
|
+
file_list = EntryAdapters.get_all_filenames_by_source(request.user.object, "computer")
|
|
88
|
+
file_filters = []
|
|
89
|
+
for file in conversation.file_filters:
|
|
90
|
+
if file in file_list:
|
|
91
|
+
file_filters.append(file)
|
|
92
|
+
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class FactCheckerStoreDataFormat(BaseModel):
|
|
96
|
+
factToVerify: str
|
|
97
|
+
response: str
|
|
98
|
+
references: Any
|
|
99
|
+
childReferences: List[Any]
|
|
100
|
+
runId: str
|
|
101
|
+
modelUsed: Dict[str, Any]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class FactCheckerStoreData(BaseModel):
|
|
105
|
+
runId: str
|
|
106
|
+
storeData: FactCheckerStoreDataFormat
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@api_chat.post("/store/factchecker", response_class=Response)
|
|
110
|
+
@requires(["authenticated"])
|
|
111
|
+
async def store_factchecker(request: Request, common: CommonQueryParams, data: FactCheckerStoreData):
|
|
112
|
+
user = request.user.object
|
|
113
|
+
|
|
114
|
+
update_telemetry_state(
|
|
115
|
+
request=request,
|
|
116
|
+
telemetry_type="api",
|
|
117
|
+
api="store_factchecker",
|
|
118
|
+
**common.__dict__,
|
|
119
|
+
)
|
|
120
|
+
fact_checker_key = f"factchecker_{data.runId}"
|
|
121
|
+
await DataStoreAdapters.astore_data(data.storeData.model_dump_json(), fact_checker_key, user, private=False)
|
|
122
|
+
return Response(content=json.dumps({"status": "ok"}), media_type="application/json", status_code=200)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@api_chat.get("/store/factchecker", response_class=Response)
|
|
126
|
+
async def get_factchecker(request: Request, common: CommonQueryParams, runId: str):
|
|
127
|
+
update_telemetry_state(
|
|
128
|
+
request=request,
|
|
129
|
+
telemetry_type="api",
|
|
130
|
+
api="read_factchecker",
|
|
131
|
+
**common.__dict__,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
fact_checker_key = f"factchecker_{runId}"
|
|
135
|
+
|
|
136
|
+
data = await DataStoreAdapters.aretrieve_public_data(fact_checker_key)
|
|
137
|
+
if data is None:
|
|
138
|
+
return Response(status_code=404)
|
|
139
|
+
return Response(content=json.dumps(data.value), media_type="application/json", status_code=200)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@api_chat.post("/conversation/file-filters", response_class=Response)
|
|
143
|
+
@requires(["authenticated"])
|
|
144
|
+
def add_file_filter(request: Request, filter: FilterRequest):
|
|
145
|
+
try:
|
|
146
|
+
conversation = ConversationAdapters.get_conversation_by_user(
|
|
147
|
+
request.user.object, conversation_id=int(filter.conversation_id)
|
|
148
|
+
)
|
|
149
|
+
file_list = EntryAdapters.get_all_filenames_by_source(request.user.object, "computer")
|
|
150
|
+
if filter.filename in file_list and filter.filename not in conversation.file_filters:
|
|
151
|
+
conversation.file_filters.append(filter.filename)
|
|
152
|
+
conversation.save()
|
|
153
|
+
# remove files from conversation.file_filters that are not in file_list
|
|
154
|
+
conversation.file_filters = [file for file in conversation.file_filters if file in file_list]
|
|
155
|
+
conversation.save()
|
|
156
|
+
return Response(content=json.dumps(conversation.file_filters), media_type="application/json", status_code=200)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Error adding file filter {filter.filename}: {e}", exc_info=True)
|
|
159
|
+
raise HTTPException(status_code=422, detail=str(e))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@api_chat.delete("/conversation/file-filters", response_class=Response)
|
|
163
|
+
@requires(["authenticated"])
|
|
164
|
+
def remove_file_filter(request: Request, filter: FilterRequest) -> Response:
|
|
165
|
+
conversation = ConversationAdapters.get_conversation_by_user(
|
|
166
|
+
request.user.object, conversation_id=int(filter.conversation_id)
|
|
167
|
+
)
|
|
168
|
+
if filter.filename in conversation.file_filters:
|
|
169
|
+
conversation.file_filters.remove(filter.filename)
|
|
170
|
+
conversation.save()
|
|
171
|
+
# remove files from conversation.file_filters that are not in file_list
|
|
172
|
+
file_list = EntryAdapters.get_all_filenames_by_source(request.user.object, "computer")
|
|
173
|
+
conversation.file_filters = [file for file in conversation.file_filters if file in file_list]
|
|
174
|
+
conversation.save()
|
|
175
|
+
return Response(content=json.dumps(conversation.file_filters), media_type="application/json", status_code=200)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class FeedbackData(BaseModel):
|
|
179
|
+
uquery: str
|
|
180
|
+
kquery: str
|
|
181
|
+
sentiment: str
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@api_chat.post("/feedback")
|
|
185
|
+
@requires(["authenticated"])
|
|
186
|
+
async def sendfeedback(request: Request, data: FeedbackData):
|
|
187
|
+
user: KhojUser = request.user.object
|
|
188
|
+
await send_query_feedback(data.uquery, data.kquery, data.sentiment, user.email)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@api_chat.post("/speech")
|
|
192
|
+
@requires(["authenticated", "premium"])
|
|
193
|
+
async def text_to_speech(
|
|
194
|
+
request: Request,
|
|
195
|
+
common: CommonQueryParams,
|
|
196
|
+
text: str,
|
|
197
|
+
rate_limiter_per_minute=Depends(
|
|
198
|
+
ApiUserRateLimiter(requests=5, subscribed_requests=20, window=60, slug="chat_minute")
|
|
199
|
+
),
|
|
200
|
+
rate_limiter_per_day=Depends(
|
|
201
|
+
ApiUserRateLimiter(requests=5, subscribed_requests=300, window=60 * 60 * 24, slug="chat_day")
|
|
202
|
+
),
|
|
203
|
+
) -> Response:
|
|
204
|
+
voice_model = await ConversationAdapters.aget_voice_model_config(request.user.object)
|
|
205
|
+
|
|
206
|
+
params = {"text_to_speak": text}
|
|
207
|
+
|
|
208
|
+
if voice_model:
|
|
209
|
+
params["voice_id"] = voice_model.model_id
|
|
210
|
+
|
|
211
|
+
speech_stream = generate_text_to_speech(**params)
|
|
212
|
+
return StreamingResponse(speech_stream.iter_content(chunk_size=1024), media_type="audio/mpeg")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@api_chat.get("/starters", response_class=Response)
|
|
216
|
+
@requires(["authenticated"])
|
|
217
|
+
async def chat_starters(
|
|
218
|
+
request: Request,
|
|
219
|
+
common: CommonQueryParams,
|
|
220
|
+
) -> Response:
|
|
221
|
+
user: KhojUser = request.user.object
|
|
222
|
+
starter_questions = await ConversationAdapters.aget_conversation_starters(user)
|
|
223
|
+
return Response(content=json.dumps(starter_questions), media_type="application/json", status_code=200)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@api_chat.get("/history")
|
|
227
|
+
@requires(["authenticated"])
|
|
228
|
+
def chat_history(
|
|
229
|
+
request: Request,
|
|
230
|
+
common: CommonQueryParams,
|
|
231
|
+
conversation_id: Optional[int] = None,
|
|
232
|
+
n: Optional[int] = None,
|
|
233
|
+
):
|
|
234
|
+
user = request.user.object
|
|
235
|
+
validate_conversation_config()
|
|
236
|
+
|
|
237
|
+
# Load Conversation History
|
|
238
|
+
conversation = ConversationAdapters.get_conversation_by_user(
|
|
239
|
+
user=user, client_application=request.user.client_app, conversation_id=conversation_id
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if conversation is None:
|
|
243
|
+
return Response(
|
|
244
|
+
content=json.dumps({"status": "error", "message": f"Conversation: {conversation_id} not found"}),
|
|
245
|
+
status_code=404,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
agent_metadata = None
|
|
249
|
+
if conversation.agent:
|
|
250
|
+
agent_metadata = {
|
|
251
|
+
"slug": conversation.agent.slug,
|
|
252
|
+
"name": conversation.agent.name,
|
|
253
|
+
"avatar": conversation.agent.avatar,
|
|
254
|
+
"isCreator": conversation.agent.creator == user,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
meta_log = conversation.conversation_log
|
|
258
|
+
meta_log.update(
|
|
259
|
+
{
|
|
260
|
+
"conversation_id": conversation.id,
|
|
261
|
+
"slug": conversation.title if conversation.title else conversation.slug,
|
|
262
|
+
"agent": agent_metadata,
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if n:
|
|
267
|
+
# Get latest N messages if N > 0
|
|
268
|
+
if n > 0 and meta_log.get("chat"):
|
|
269
|
+
meta_log["chat"] = meta_log["chat"][-n:]
|
|
270
|
+
# Else return all messages except latest N
|
|
271
|
+
elif n < 0 and meta_log.get("chat"):
|
|
272
|
+
meta_log["chat"] = meta_log["chat"][:n]
|
|
273
|
+
|
|
274
|
+
update_telemetry_state(
|
|
275
|
+
request=request,
|
|
276
|
+
telemetry_type="api",
|
|
277
|
+
api="chat_history",
|
|
278
|
+
**common.__dict__,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
return {"status": "ok", "response": meta_log}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@api_chat.get("/share/history")
|
|
285
|
+
def get_shared_chat(
|
|
286
|
+
request: Request,
|
|
287
|
+
common: CommonQueryParams,
|
|
288
|
+
public_conversation_slug: str,
|
|
289
|
+
n: Optional[int] = None,
|
|
290
|
+
):
|
|
291
|
+
user = request.user.object if request.user.is_authenticated else None
|
|
292
|
+
|
|
293
|
+
# Load Conversation History
|
|
294
|
+
conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
|
|
295
|
+
|
|
296
|
+
if conversation is None:
|
|
297
|
+
return Response(
|
|
298
|
+
content=json.dumps({"status": "error", "message": f"Conversation: {public_conversation_slug} not found"}),
|
|
299
|
+
status_code=404,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
agent_metadata = None
|
|
303
|
+
if conversation.agent:
|
|
304
|
+
agent_metadata = {
|
|
305
|
+
"slug": conversation.agent.slug,
|
|
306
|
+
"name": conversation.agent.name,
|
|
307
|
+
"avatar": conversation.agent.avatar,
|
|
308
|
+
"isCreator": conversation.agent.creator == user,
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
meta_log = conversation.conversation_log
|
|
312
|
+
meta_log.update(
|
|
313
|
+
{
|
|
314
|
+
"conversation_id": conversation.id,
|
|
315
|
+
"slug": conversation.title if conversation.title else conversation.slug,
|
|
316
|
+
"agent": agent_metadata,
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if n:
|
|
321
|
+
# Get latest N messages if N > 0
|
|
322
|
+
if n > 0 and meta_log.get("chat"):
|
|
323
|
+
meta_log["chat"] = meta_log["chat"][-n:]
|
|
324
|
+
# Else return all messages except latest N
|
|
325
|
+
elif n < 0 and meta_log.get("chat"):
|
|
326
|
+
meta_log["chat"] = meta_log["chat"][:n]
|
|
327
|
+
|
|
328
|
+
update_telemetry_state(
|
|
329
|
+
request=request,
|
|
330
|
+
telemetry_type="api",
|
|
331
|
+
api="public_conversation_history",
|
|
332
|
+
**common.__dict__,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return {"status": "ok", "response": meta_log}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@api_chat.delete("/history")
|
|
339
|
+
@requires(["authenticated"])
|
|
340
|
+
async def clear_chat_history(
|
|
341
|
+
request: Request,
|
|
342
|
+
common: CommonQueryParams,
|
|
343
|
+
conversation_id: Optional[int] = None,
|
|
344
|
+
):
|
|
345
|
+
user = request.user.object
|
|
346
|
+
|
|
347
|
+
# Clear Conversation History
|
|
348
|
+
await ConversationAdapters.adelete_conversation_by_user(user, request.user.client_app, conversation_id)
|
|
349
|
+
|
|
350
|
+
update_telemetry_state(
|
|
351
|
+
request=request,
|
|
352
|
+
telemetry_type="api",
|
|
353
|
+
api="clear_chat_history",
|
|
354
|
+
**common.__dict__,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return {"status": "ok", "message": "Conversation history cleared"}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@api_chat.post("/share/fork")
|
|
361
|
+
@requires(["authenticated"])
|
|
362
|
+
def fork_public_conversation(
|
|
363
|
+
request: Request,
|
|
364
|
+
common: CommonQueryParams,
|
|
365
|
+
public_conversation_slug: str,
|
|
366
|
+
):
|
|
367
|
+
user = request.user.object
|
|
368
|
+
|
|
369
|
+
# Load Conversation History
|
|
370
|
+
public_conversation = PublicConversationAdapters.get_public_conversation_by_slug(public_conversation_slug)
|
|
371
|
+
|
|
372
|
+
# Duplicate Public Conversation to User's Private Conversation
|
|
373
|
+
ConversationAdapters.create_conversation_from_public_conversation(
|
|
374
|
+
user, public_conversation, request.user.client_app
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
chat_metadata = {"forked_conversation": public_conversation.slug}
|
|
378
|
+
|
|
379
|
+
update_telemetry_state(
|
|
380
|
+
request=request,
|
|
381
|
+
telemetry_type="api",
|
|
382
|
+
api="fork_public_conversation",
|
|
383
|
+
**common.__dict__,
|
|
384
|
+
metadata=chat_metadata,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
redirect_uri = str(request.app.url_path_for("chat_page"))
|
|
388
|
+
|
|
389
|
+
return Response(status_code=200, content=json.dumps({"status": "ok", "next_url": redirect_uri}))
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
@api_chat.post("/share")
|
|
393
|
+
@requires(["authenticated"])
|
|
394
|
+
def duplicate_chat_history_public_conversation(
|
|
395
|
+
request: Request,
|
|
396
|
+
common: CommonQueryParams,
|
|
397
|
+
conversation_id: int,
|
|
398
|
+
):
|
|
399
|
+
user = request.user.object
|
|
400
|
+
domain = request.headers.get("host")
|
|
401
|
+
scheme = request.url.scheme
|
|
402
|
+
|
|
403
|
+
# Throw unauthorized exception if domain not in ALLOWED_HOSTS
|
|
404
|
+
host_domain = domain.split(":")[0]
|
|
405
|
+
if host_domain not in ALLOWED_HOSTS:
|
|
406
|
+
raise HTTPException(status_code=401, detail="Unauthorized domain")
|
|
407
|
+
|
|
408
|
+
# Duplicate Conversation History to Public Conversation
|
|
409
|
+
conversation = ConversationAdapters.get_conversation_by_user(user, request.user.client_app, conversation_id)
|
|
410
|
+
public_conversation = ConversationAdapters.make_public_conversation_copy(conversation)
|
|
411
|
+
public_conversation_url = PublicConversationAdapters.get_public_conversation_url(public_conversation)
|
|
412
|
+
|
|
413
|
+
update_telemetry_state(
|
|
414
|
+
request=request,
|
|
415
|
+
telemetry_type="api",
|
|
416
|
+
api="post_chat_share",
|
|
417
|
+
**common.__dict__,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return Response(
|
|
421
|
+
status_code=200, content=json.dumps({"status": "ok", "url": f"{scheme}://{domain}{public_conversation_url}"})
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@api_chat.get("/sessions")
|
|
426
|
+
@requires(["authenticated"])
|
|
427
|
+
def chat_sessions(
|
|
428
|
+
request: Request,
|
|
429
|
+
common: CommonQueryParams,
|
|
430
|
+
):
|
|
431
|
+
user = request.user.object
|
|
432
|
+
|
|
433
|
+
# Load Conversation Sessions
|
|
434
|
+
sessions = ConversationAdapters.get_conversation_sessions(user, request.user.client_app).values_list(
|
|
435
|
+
"id", "slug", "title"
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
session_values = [{"conversation_id": session[0], "slug": session[2] or session[1]} for session in sessions]
|
|
439
|
+
|
|
440
|
+
update_telemetry_state(
|
|
441
|
+
request=request,
|
|
442
|
+
telemetry_type="api",
|
|
443
|
+
api="chat_sessions",
|
|
444
|
+
**common.__dict__,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return Response(content=json.dumps(session_values), media_type="application/json", status_code=200)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
@api_chat.post("/sessions")
|
|
451
|
+
@requires(["authenticated"])
|
|
452
|
+
async def create_chat_session(
|
|
453
|
+
request: Request,
|
|
454
|
+
common: CommonQueryParams,
|
|
455
|
+
agent_slug: Optional[str] = None,
|
|
456
|
+
):
|
|
457
|
+
user = request.user.object
|
|
458
|
+
|
|
459
|
+
# Create new Conversation Session
|
|
460
|
+
conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app, agent_slug)
|
|
461
|
+
|
|
462
|
+
response = {"conversation_id": conversation.id}
|
|
463
|
+
|
|
464
|
+
conversation_metadata = {
|
|
465
|
+
"agent": agent_slug,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
update_telemetry_state(
|
|
469
|
+
request=request,
|
|
470
|
+
telemetry_type="api",
|
|
471
|
+
api="create_chat_sessions",
|
|
472
|
+
metadata=conversation_metadata,
|
|
473
|
+
**common.__dict__,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return Response(content=json.dumps(response), media_type="application/json", status_code=200)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@api_chat.get("/options", response_class=Response)
|
|
480
|
+
@requires(["authenticated"])
|
|
481
|
+
async def chat_options(
|
|
482
|
+
request: Request,
|
|
483
|
+
common: CommonQueryParams,
|
|
484
|
+
) -> Response:
|
|
485
|
+
cmd_options = {}
|
|
486
|
+
for cmd in ConversationCommand:
|
|
487
|
+
if cmd in command_descriptions:
|
|
488
|
+
cmd_options[cmd.value] = command_descriptions[cmd]
|
|
489
|
+
|
|
490
|
+
update_telemetry_state(
|
|
491
|
+
request=request,
|
|
492
|
+
telemetry_type="api",
|
|
493
|
+
api="chat_options",
|
|
494
|
+
**common.__dict__,
|
|
495
|
+
)
|
|
496
|
+
return Response(content=json.dumps(cmd_options), media_type="application/json", status_code=200)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@api_chat.patch("/title", response_class=Response)
|
|
500
|
+
@requires(["authenticated"])
|
|
501
|
+
async def set_conversation_title(
|
|
502
|
+
request: Request,
|
|
503
|
+
common: CommonQueryParams,
|
|
504
|
+
title: str,
|
|
505
|
+
conversation_id: Optional[int] = None,
|
|
506
|
+
) -> Response:
|
|
507
|
+
user = request.user.object
|
|
508
|
+
title = title.strip()[:200]
|
|
509
|
+
|
|
510
|
+
# Set Conversation Title
|
|
511
|
+
conversation = await ConversationAdapters.aset_conversation_title(
|
|
512
|
+
user, request.user.client_app, conversation_id, title
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
success = True if conversation else False
|
|
516
|
+
|
|
517
|
+
update_telemetry_state(
|
|
518
|
+
request=request,
|
|
519
|
+
telemetry_type="api",
|
|
520
|
+
api="set_conversation_title",
|
|
521
|
+
**common.__dict__,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return Response(
|
|
525
|
+
content=json.dumps({"status": "ok", "success": success}), media_type="application/json", status_code=200
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
@api_chat.websocket("/ws")
|
|
530
|
+
async def websocket_endpoint(
|
|
531
|
+
websocket: WebSocket,
|
|
532
|
+
conversation_id: int,
|
|
533
|
+
city: Optional[str] = None,
|
|
534
|
+
region: Optional[str] = None,
|
|
535
|
+
country: Optional[str] = None,
|
|
536
|
+
timezone: Optional[str] = None,
|
|
537
|
+
):
|
|
538
|
+
connection_alive = True
|
|
539
|
+
|
|
540
|
+
async def send_status_update(message: str):
|
|
541
|
+
nonlocal connection_alive
|
|
542
|
+
if not connection_alive:
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
status_packet = {
|
|
546
|
+
"type": "status",
|
|
547
|
+
"message": message,
|
|
548
|
+
"content-type": "application/json",
|
|
549
|
+
}
|
|
550
|
+
try:
|
|
551
|
+
await websocket.send_text(json.dumps(status_packet))
|
|
552
|
+
except ConnectionClosedOK:
|
|
553
|
+
connection_alive = False
|
|
554
|
+
logger.info(f"User {user} disconnected web socket. Emitting rest of responses to clear thread")
|
|
555
|
+
|
|
556
|
+
async def send_complete_llm_response(llm_response: str):
|
|
557
|
+
nonlocal connection_alive
|
|
558
|
+
if not connection_alive:
|
|
559
|
+
return
|
|
560
|
+
try:
|
|
561
|
+
await websocket.send_text("start_llm_response")
|
|
562
|
+
await websocket.send_text(llm_response)
|
|
563
|
+
await websocket.send_text("end_llm_response")
|
|
564
|
+
except ConnectionClosedOK:
|
|
565
|
+
connection_alive = False
|
|
566
|
+
logger.info(f"User {user} disconnected web socket. Emitting rest of responses to clear thread")
|
|
567
|
+
|
|
568
|
+
async def send_message(message: str):
|
|
569
|
+
nonlocal connection_alive
|
|
570
|
+
if not connection_alive:
|
|
571
|
+
return
|
|
572
|
+
try:
|
|
573
|
+
await websocket.send_text(message)
|
|
574
|
+
except ConnectionClosedOK:
|
|
575
|
+
connection_alive = False
|
|
576
|
+
logger.info(f"User {user} disconnected web socket. Emitting rest of responses to clear thread")
|
|
577
|
+
|
|
578
|
+
async def send_rate_limit_message(message: str):
|
|
579
|
+
nonlocal connection_alive
|
|
580
|
+
if not connection_alive:
|
|
581
|
+
return
|
|
582
|
+
|
|
583
|
+
status_packet = {
|
|
584
|
+
"type": "rate_limit",
|
|
585
|
+
"message": message,
|
|
586
|
+
"content-type": "application/json",
|
|
587
|
+
}
|
|
588
|
+
try:
|
|
589
|
+
await websocket.send_text(json.dumps(status_packet))
|
|
590
|
+
except ConnectionClosedOK:
|
|
591
|
+
connection_alive = False
|
|
592
|
+
logger.info(f"User {user} disconnected web socket. Emitting rest of responses to clear thread")
|
|
593
|
+
|
|
594
|
+
user: KhojUser = websocket.user.object
|
|
595
|
+
conversation = await ConversationAdapters.aget_conversation_by_user(
|
|
596
|
+
user, client_application=websocket.user.client_app, conversation_id=conversation_id
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
hourly_limiter = ApiUserRateLimiter(requests=5, subscribed_requests=60, window=60, slug="chat_minute")
|
|
600
|
+
|
|
601
|
+
daily_limiter = ApiUserRateLimiter(requests=5, subscribed_requests=600, window=60 * 60 * 24, slug="chat_day")
|
|
602
|
+
|
|
603
|
+
await is_ready_to_chat(user)
|
|
604
|
+
|
|
605
|
+
user_name = await aget_user_name(user)
|
|
606
|
+
|
|
607
|
+
location = None
|
|
608
|
+
|
|
609
|
+
if city or region or country:
|
|
610
|
+
location = LocationData(city=city, region=region, country=country)
|
|
611
|
+
|
|
612
|
+
await websocket.accept()
|
|
613
|
+
while connection_alive:
|
|
614
|
+
try:
|
|
615
|
+
if conversation:
|
|
616
|
+
await sync_to_async(conversation.refresh_from_db)(fields=["conversation_log"])
|
|
617
|
+
q = await websocket.receive_text()
|
|
618
|
+
|
|
619
|
+
# Refresh these because the connection to the database might have been closed
|
|
620
|
+
await conversation.arefresh_from_db()
|
|
621
|
+
|
|
622
|
+
except WebSocketDisconnect:
|
|
623
|
+
logger.debug(f"User {user} disconnected web socket")
|
|
624
|
+
break
|
|
625
|
+
|
|
626
|
+
try:
|
|
627
|
+
await sync_to_async(hourly_limiter)(websocket)
|
|
628
|
+
await sync_to_async(daily_limiter)(websocket)
|
|
629
|
+
except HTTPException as e:
|
|
630
|
+
await send_rate_limit_message(e.detail)
|
|
631
|
+
break
|
|
632
|
+
|
|
633
|
+
if is_query_empty(q):
|
|
634
|
+
await send_message("start_llm_response")
|
|
635
|
+
await send_message(
|
|
636
|
+
"It seems like your query is incomplete. Could you please provide more details or specify what you need help with?"
|
|
637
|
+
)
|
|
638
|
+
await send_message("end_llm_response")
|
|
639
|
+
continue
|
|
640
|
+
|
|
641
|
+
user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
642
|
+
conversation_commands = [get_conversation_command(query=q, any_references=True)]
|
|
643
|
+
|
|
644
|
+
await send_status_update(f"**👀 Understanding Query**: {q}")
|
|
645
|
+
|
|
646
|
+
meta_log = conversation.conversation_log
|
|
647
|
+
is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask]
|
|
648
|
+
used_slash_summarize = conversation_commands == [ConversationCommand.Summarize]
|
|
649
|
+
|
|
650
|
+
if conversation_commands == [ConversationCommand.Default] or is_automated_task:
|
|
651
|
+
conversation_commands = await aget_relevant_information_sources(q, meta_log, is_automated_task)
|
|
652
|
+
conversation_commands_str = ", ".join([cmd.value for cmd in conversation_commands])
|
|
653
|
+
await send_status_update(f"**🗃️ Chose Data Sources to Search:** {conversation_commands_str}")
|
|
654
|
+
|
|
655
|
+
mode = await aget_relevant_output_modes(q, meta_log, is_automated_task)
|
|
656
|
+
await send_status_update(f"**🧑🏾💻 Decided Response Mode:** {mode.value}")
|
|
657
|
+
if mode not in conversation_commands:
|
|
658
|
+
conversation_commands.append(mode)
|
|
659
|
+
|
|
660
|
+
for cmd in conversation_commands:
|
|
661
|
+
await conversation_command_rate_limiter.update_and_check_if_valid(websocket, cmd)
|
|
662
|
+
q = q.replace(f"/{cmd.value}", "").strip()
|
|
663
|
+
|
|
664
|
+
file_filters = conversation.file_filters if conversation else []
|
|
665
|
+
# Skip trying to summarize if
|
|
666
|
+
if (
|
|
667
|
+
# summarization intent was inferred
|
|
668
|
+
ConversationCommand.Summarize in conversation_commands
|
|
669
|
+
# and not triggered via slash command
|
|
670
|
+
and not used_slash_summarize
|
|
671
|
+
# but we can't actually summarize
|
|
672
|
+
and len(file_filters) != 1
|
|
673
|
+
):
|
|
674
|
+
conversation_commands.remove(ConversationCommand.Summarize)
|
|
675
|
+
elif ConversationCommand.Summarize in conversation_commands:
|
|
676
|
+
response_log = ""
|
|
677
|
+
if len(file_filters) == 0:
|
|
678
|
+
response_log = "No files selected for summarization. Please add files using the section on the left."
|
|
679
|
+
await send_complete_llm_response(response_log)
|
|
680
|
+
elif len(file_filters) > 1:
|
|
681
|
+
response_log = "Only one file can be selected for summarization."
|
|
682
|
+
await send_complete_llm_response(response_log)
|
|
683
|
+
else:
|
|
684
|
+
try:
|
|
685
|
+
file_object = await FileObjectAdapters.async_get_file_objects_by_name(user, file_filters[0])
|
|
686
|
+
if len(file_object) == 0:
|
|
687
|
+
response_log = "Sorry, we couldn't find the full text of this file. Please re-upload the document and try again."
|
|
688
|
+
await send_complete_llm_response(response_log)
|
|
689
|
+
continue
|
|
690
|
+
contextual_data = " ".join([file.raw_text for file in file_object])
|
|
691
|
+
if not q:
|
|
692
|
+
q = "Create a general summary of the file"
|
|
693
|
+
await send_status_update(f"**🧑🏾💻 Constructing Summary Using:** {file_object[0].file_name}")
|
|
694
|
+
response = await extract_relevant_summary(q, contextual_data)
|
|
695
|
+
response_log = str(response)
|
|
696
|
+
await send_complete_llm_response(response_log)
|
|
697
|
+
except Exception as e:
|
|
698
|
+
response_log = "Error summarizing file."
|
|
699
|
+
logger.error(f"Error summarizing file for {user.email}: {e}", exc_info=True)
|
|
700
|
+
await send_complete_llm_response(response_log)
|
|
701
|
+
await sync_to_async(save_to_conversation_log)(
|
|
702
|
+
q,
|
|
703
|
+
response_log,
|
|
704
|
+
user,
|
|
705
|
+
meta_log,
|
|
706
|
+
user_message_time,
|
|
707
|
+
intent_type="summarize",
|
|
708
|
+
client_application=websocket.user.client_app,
|
|
709
|
+
conversation_id=conversation_id,
|
|
710
|
+
)
|
|
711
|
+
update_telemetry_state(
|
|
712
|
+
request=websocket,
|
|
713
|
+
telemetry_type="api",
|
|
714
|
+
api="chat",
|
|
715
|
+
metadata={"conversation_command": conversation_commands[0].value},
|
|
716
|
+
)
|
|
717
|
+
continue
|
|
718
|
+
|
|
719
|
+
custom_filters = []
|
|
720
|
+
if conversation_commands == [ConversationCommand.Help]:
|
|
721
|
+
if not q:
|
|
722
|
+
conversation_config = await ConversationAdapters.aget_user_conversation_config(user)
|
|
723
|
+
if conversation_config == None:
|
|
724
|
+
conversation_config = await ConversationAdapters.aget_default_conversation_config()
|
|
725
|
+
model_type = conversation_config.model_type
|
|
726
|
+
formatted_help = help_message.format(model=model_type, version=state.khoj_version, device=get_device())
|
|
727
|
+
await send_complete_llm_response(formatted_help)
|
|
728
|
+
continue
|
|
729
|
+
# Adding specification to search online specifically on khoj.dev pages.
|
|
730
|
+
custom_filters.append("site:khoj.dev")
|
|
731
|
+
conversation_commands.append(ConversationCommand.Online)
|
|
732
|
+
|
|
733
|
+
if ConversationCommand.Automation in conversation_commands:
|
|
734
|
+
try:
|
|
735
|
+
automation, crontime, query_to_run, subject = await create_automation(
|
|
736
|
+
q, timezone, user, websocket.url, meta_log
|
|
737
|
+
)
|
|
738
|
+
except Exception as e:
|
|
739
|
+
logger.error(f"Error scheduling task {q} for {user.email}: {e}")
|
|
740
|
+
await send_complete_llm_response(
|
|
741
|
+
f"Unable to create automation. Ensure the automation doesn't already exist."
|
|
742
|
+
)
|
|
743
|
+
continue
|
|
744
|
+
|
|
745
|
+
llm_response = construct_automation_created_message(automation, crontime, query_to_run, subject)
|
|
746
|
+
await sync_to_async(save_to_conversation_log)(
|
|
747
|
+
q,
|
|
748
|
+
llm_response,
|
|
749
|
+
user,
|
|
750
|
+
meta_log,
|
|
751
|
+
user_message_time,
|
|
752
|
+
intent_type="automation",
|
|
753
|
+
client_application=websocket.user.client_app,
|
|
754
|
+
conversation_id=conversation_id,
|
|
755
|
+
inferred_queries=[query_to_run],
|
|
756
|
+
automation_id=automation.id,
|
|
757
|
+
)
|
|
758
|
+
common = CommonQueryParamsClass(
|
|
759
|
+
client=websocket.user.client_app,
|
|
760
|
+
user_agent=websocket.headers.get("user-agent"),
|
|
761
|
+
host=websocket.headers.get("host"),
|
|
762
|
+
)
|
|
763
|
+
update_telemetry_state(
|
|
764
|
+
request=websocket,
|
|
765
|
+
telemetry_type="api",
|
|
766
|
+
api="chat",
|
|
767
|
+
**common.__dict__,
|
|
768
|
+
)
|
|
769
|
+
await send_complete_llm_response(llm_response)
|
|
770
|
+
continue
|
|
771
|
+
|
|
772
|
+
compiled_references, inferred_queries, defiltered_query = await extract_references_and_questions(
|
|
773
|
+
websocket, meta_log, q, 7, 0.18, conversation_id, conversation_commands, location, send_status_update
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
if compiled_references:
|
|
777
|
+
headings = "\n- " + "\n- ".join(set([c.get("compiled", c).split("\n")[0] for c in compiled_references]))
|
|
778
|
+
await send_status_update(f"**📜 Found Relevant Notes**: {headings}")
|
|
779
|
+
|
|
780
|
+
online_results: Dict = dict()
|
|
781
|
+
|
|
782
|
+
if conversation_commands == [ConversationCommand.Notes] and not await EntryAdapters.auser_has_entries(user):
|
|
783
|
+
await send_complete_llm_response(f"{no_entries_found.format()}")
|
|
784
|
+
continue
|
|
785
|
+
|
|
786
|
+
if ConversationCommand.Notes in conversation_commands and is_none_or_empty(compiled_references):
|
|
787
|
+
conversation_commands.remove(ConversationCommand.Notes)
|
|
788
|
+
|
|
789
|
+
if ConversationCommand.Online in conversation_commands:
|
|
790
|
+
try:
|
|
791
|
+
online_results = await search_online(
|
|
792
|
+
defiltered_query, meta_log, location, send_status_update, custom_filters
|
|
793
|
+
)
|
|
794
|
+
except ValueError as e:
|
|
795
|
+
logger.warning(f"Error searching online: {e}. Attempting to respond without online results")
|
|
796
|
+
await send_complete_llm_response(
|
|
797
|
+
f"Error searching online: {e}. Attempting to respond without online results"
|
|
798
|
+
)
|
|
799
|
+
continue
|
|
800
|
+
|
|
801
|
+
if ConversationCommand.Webpage in conversation_commands:
|
|
802
|
+
try:
|
|
803
|
+
direct_web_pages = await read_webpages(defiltered_query, meta_log, location, send_status_update)
|
|
804
|
+
webpages = []
|
|
805
|
+
for query in direct_web_pages:
|
|
806
|
+
if online_results.get(query):
|
|
807
|
+
online_results[query]["webpages"] = direct_web_pages[query]["webpages"]
|
|
808
|
+
else:
|
|
809
|
+
online_results[query] = {"webpages": direct_web_pages[query]["webpages"]}
|
|
810
|
+
|
|
811
|
+
for webpage in direct_web_pages[query]["webpages"]:
|
|
812
|
+
webpages.append(webpage["link"])
|
|
813
|
+
|
|
814
|
+
await send_status_update(f"**📚 Read web pages**: {webpages}")
|
|
815
|
+
except ValueError as e:
|
|
816
|
+
logger.warning(
|
|
817
|
+
f"Error directly reading webpages: {e}. Attempting to respond without online results", exc_info=True
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
if ConversationCommand.Image in conversation_commands:
|
|
821
|
+
update_telemetry_state(
|
|
822
|
+
request=websocket,
|
|
823
|
+
telemetry_type="api",
|
|
824
|
+
api="chat",
|
|
825
|
+
metadata={"conversation_command": conversation_commands[0].value},
|
|
826
|
+
)
|
|
827
|
+
image, status_code, improved_image_prompt, intent_type = await text_to_image(
|
|
828
|
+
q,
|
|
829
|
+
user,
|
|
830
|
+
meta_log,
|
|
831
|
+
location_data=location,
|
|
832
|
+
references=compiled_references,
|
|
833
|
+
online_results=online_results,
|
|
834
|
+
send_status_func=send_status_update,
|
|
835
|
+
)
|
|
836
|
+
if image is None or status_code != 200:
|
|
837
|
+
content_obj = {
|
|
838
|
+
"image": image,
|
|
839
|
+
"intentType": intent_type,
|
|
840
|
+
"detail": improved_image_prompt,
|
|
841
|
+
"content-type": "application/json",
|
|
842
|
+
}
|
|
843
|
+
await send_complete_llm_response(json.dumps(content_obj))
|
|
844
|
+
continue
|
|
845
|
+
|
|
846
|
+
await sync_to_async(save_to_conversation_log)(
|
|
847
|
+
q,
|
|
848
|
+
image,
|
|
849
|
+
user,
|
|
850
|
+
meta_log,
|
|
851
|
+
user_message_time,
|
|
852
|
+
intent_type=intent_type,
|
|
853
|
+
inferred_queries=[improved_image_prompt],
|
|
854
|
+
client_application=websocket.user.client_app,
|
|
855
|
+
conversation_id=conversation_id,
|
|
856
|
+
compiled_references=compiled_references,
|
|
857
|
+
online_results=online_results,
|
|
858
|
+
)
|
|
859
|
+
content_obj = {"image": image, "intentType": intent_type, "inferredQueries": [improved_image_prompt], "context": compiled_references, "content-type": "application/json", "online_results": online_results} # type: ignore
|
|
860
|
+
|
|
861
|
+
await send_complete_llm_response(json.dumps(content_obj))
|
|
862
|
+
continue
|
|
863
|
+
|
|
864
|
+
await send_status_update(f"**💭 Generating a well-informed response**")
|
|
865
|
+
llm_response, chat_metadata = await agenerate_chat_response(
|
|
866
|
+
defiltered_query,
|
|
867
|
+
meta_log,
|
|
868
|
+
conversation,
|
|
869
|
+
compiled_references,
|
|
870
|
+
online_results,
|
|
871
|
+
inferred_queries,
|
|
872
|
+
conversation_commands,
|
|
873
|
+
user,
|
|
874
|
+
websocket.user.client_app,
|
|
875
|
+
conversation_id,
|
|
876
|
+
location,
|
|
877
|
+
user_name,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
chat_metadata["agent"] = conversation.agent.slug if conversation.agent else None
|
|
881
|
+
|
|
882
|
+
update_telemetry_state(
|
|
883
|
+
request=websocket,
|
|
884
|
+
telemetry_type="api",
|
|
885
|
+
api="chat",
|
|
886
|
+
metadata=chat_metadata,
|
|
887
|
+
)
|
|
888
|
+
iterator = AsyncIteratorWrapper(llm_response)
|
|
889
|
+
|
|
890
|
+
await send_message("start_llm_response")
|
|
891
|
+
|
|
892
|
+
async for item in iterator:
|
|
893
|
+
if item is None:
|
|
894
|
+
break
|
|
895
|
+
if connection_alive:
|
|
896
|
+
try:
|
|
897
|
+
await send_message(f"{item}")
|
|
898
|
+
except ConnectionClosedOK:
|
|
899
|
+
connection_alive = False
|
|
900
|
+
logger.info(f"User {user} disconnected web socket. Emitting rest of responses to clear thread")
|
|
901
|
+
|
|
902
|
+
await send_message("end_llm_response")
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
@api_chat.get("", response_class=Response)
|
|
906
|
+
@requires(["authenticated"])
|
|
907
|
+
async def chat(
|
|
908
|
+
request: Request,
|
|
909
|
+
common: CommonQueryParams,
|
|
910
|
+
q: str,
|
|
911
|
+
n: Optional[int] = 5,
|
|
912
|
+
d: Optional[float] = 0.22,
|
|
913
|
+
stream: Optional[bool] = False,
|
|
914
|
+
title: Optional[str] = None,
|
|
915
|
+
conversation_id: Optional[int] = None,
|
|
916
|
+
city: Optional[str] = None,
|
|
917
|
+
region: Optional[str] = None,
|
|
918
|
+
country: Optional[str] = None,
|
|
919
|
+
timezone: Optional[str] = None,
|
|
920
|
+
rate_limiter_per_minute=Depends(
|
|
921
|
+
ApiUserRateLimiter(requests=5, subscribed_requests=60, window=60, slug="chat_minute")
|
|
922
|
+
),
|
|
923
|
+
rate_limiter_per_day=Depends(
|
|
924
|
+
ApiUserRateLimiter(requests=5, subscribed_requests=600, window=60 * 60 * 24, slug="chat_day")
|
|
925
|
+
),
|
|
926
|
+
) -> Response:
|
|
927
|
+
user: KhojUser = request.user.object
|
|
928
|
+
q = unquote(q)
|
|
929
|
+
if is_query_empty(q):
|
|
930
|
+
return Response(
|
|
931
|
+
content="It seems like your query is incomplete. Could you please provide more details or specify what you need help with?",
|
|
932
|
+
media_type="text/plain",
|
|
933
|
+
status_code=400,
|
|
934
|
+
)
|
|
935
|
+
user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
936
|
+
logger.info(f"Chat request by {user.username}: {q}")
|
|
937
|
+
|
|
938
|
+
await is_ready_to_chat(user)
|
|
939
|
+
conversation_commands = [get_conversation_command(query=q, any_references=True)]
|
|
940
|
+
|
|
941
|
+
_custom_filters = []
|
|
942
|
+
if conversation_commands == [ConversationCommand.Help]:
|
|
943
|
+
help_str = "/" + ConversationCommand.Help
|
|
944
|
+
if q.strip() == help_str:
|
|
945
|
+
conversation_config = await ConversationAdapters.aget_user_conversation_config(user)
|
|
946
|
+
if conversation_config == None:
|
|
947
|
+
conversation_config = await ConversationAdapters.aget_default_conversation_config()
|
|
948
|
+
model_type = conversation_config.model_type
|
|
949
|
+
formatted_help = help_message.format(model=model_type, version=state.khoj_version, device=get_device())
|
|
950
|
+
return StreamingResponse(iter([formatted_help]), media_type="text/event-stream", status_code=200)
|
|
951
|
+
# Adding specification to search online specifically on khoj.dev pages.
|
|
952
|
+
_custom_filters.append("site:khoj.dev")
|
|
953
|
+
conversation_commands.append(ConversationCommand.Online)
|
|
954
|
+
|
|
955
|
+
conversation = await ConversationAdapters.aget_conversation_by_user(
|
|
956
|
+
user, request.user.client_app, conversation_id, title
|
|
957
|
+
)
|
|
958
|
+
conversation_id = conversation.id if conversation else None
|
|
959
|
+
|
|
960
|
+
if not conversation:
|
|
961
|
+
return Response(
|
|
962
|
+
content=f"No conversation found with requested id, title", media_type="text/plain", status_code=400
|
|
963
|
+
)
|
|
964
|
+
else:
|
|
965
|
+
meta_log = conversation.conversation_log
|
|
966
|
+
|
|
967
|
+
if ConversationCommand.Summarize in conversation_commands:
|
|
968
|
+
file_filters = conversation.file_filters
|
|
969
|
+
llm_response = ""
|
|
970
|
+
if len(file_filters) == 0:
|
|
971
|
+
llm_response = "No files selected for summarization. Please add files using the section on the left."
|
|
972
|
+
elif len(file_filters) > 1:
|
|
973
|
+
llm_response = "Only one file can be selected for summarization."
|
|
974
|
+
else:
|
|
975
|
+
try:
|
|
976
|
+
file_object = await FileObjectAdapters.async_get_file_objects_by_name(user, file_filters[0])
|
|
977
|
+
if len(file_object) == 0:
|
|
978
|
+
llm_response = "Sorry, we couldn't find the full text of this file. Please re-upload the document and try again."
|
|
979
|
+
return StreamingResponse(content=llm_response, media_type="text/event-stream", status_code=200)
|
|
980
|
+
contextual_data = " ".join([file.raw_text for file in file_object])
|
|
981
|
+
summarizeStr = "/" + ConversationCommand.Summarize
|
|
982
|
+
if q.strip() == summarizeStr:
|
|
983
|
+
q = "Create a general summary of the file"
|
|
984
|
+
response = await extract_relevant_summary(q, contextual_data)
|
|
985
|
+
llm_response = str(response)
|
|
986
|
+
except Exception as e:
|
|
987
|
+
logger.error(f"Error summarizing file for {user.email}: {e}")
|
|
988
|
+
llm_response = "Error summarizing file."
|
|
989
|
+
await sync_to_async(save_to_conversation_log)(
|
|
990
|
+
q,
|
|
991
|
+
llm_response,
|
|
992
|
+
user,
|
|
993
|
+
conversation.conversation_log,
|
|
994
|
+
user_message_time,
|
|
995
|
+
intent_type="summarize",
|
|
996
|
+
client_application=request.user.client_app,
|
|
997
|
+
conversation_id=conversation_id,
|
|
998
|
+
)
|
|
999
|
+
update_telemetry_state(
|
|
1000
|
+
request=request,
|
|
1001
|
+
telemetry_type="api",
|
|
1002
|
+
api="chat",
|
|
1003
|
+
metadata={"conversation_command": conversation_commands[0].value},
|
|
1004
|
+
**common.__dict__,
|
|
1005
|
+
)
|
|
1006
|
+
return StreamingResponse(content=llm_response, media_type="text/event-stream", status_code=200)
|
|
1007
|
+
|
|
1008
|
+
is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask]
|
|
1009
|
+
|
|
1010
|
+
if conversation_commands == [ConversationCommand.Default] or is_automated_task:
|
|
1011
|
+
conversation_commands = await aget_relevant_information_sources(q, meta_log, is_automated_task)
|
|
1012
|
+
mode = await aget_relevant_output_modes(q, meta_log, is_automated_task)
|
|
1013
|
+
if mode not in conversation_commands:
|
|
1014
|
+
conversation_commands.append(mode)
|
|
1015
|
+
|
|
1016
|
+
for cmd in conversation_commands:
|
|
1017
|
+
await conversation_command_rate_limiter.update_and_check_if_valid(request, cmd)
|
|
1018
|
+
q = q.replace(f"/{cmd.value}", "").strip()
|
|
1019
|
+
|
|
1020
|
+
location = None
|
|
1021
|
+
|
|
1022
|
+
if city or region or country:
|
|
1023
|
+
location = LocationData(city=city, region=region, country=country)
|
|
1024
|
+
|
|
1025
|
+
user_name = await aget_user_name(user)
|
|
1026
|
+
|
|
1027
|
+
if ConversationCommand.Automation in conversation_commands:
|
|
1028
|
+
try:
|
|
1029
|
+
automation, crontime, query_to_run, subject = await create_automation(
|
|
1030
|
+
q, timezone, user, request.url, meta_log
|
|
1031
|
+
)
|
|
1032
|
+
except Exception as e:
|
|
1033
|
+
logger.error(f"Error creating automation {q} for {user.email}: {e}", exc_info=True)
|
|
1034
|
+
return Response(
|
|
1035
|
+
content=f"Unable to create automation. Ensure the automation doesn't already exist.",
|
|
1036
|
+
media_type="text/plain",
|
|
1037
|
+
status_code=500,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
llm_response = construct_automation_created_message(automation, crontime, query_to_run, subject)
|
|
1041
|
+
await sync_to_async(save_to_conversation_log)(
|
|
1042
|
+
q,
|
|
1043
|
+
llm_response,
|
|
1044
|
+
user,
|
|
1045
|
+
meta_log,
|
|
1046
|
+
user_message_time,
|
|
1047
|
+
intent_type="automation",
|
|
1048
|
+
client_application=request.user.client_app,
|
|
1049
|
+
conversation_id=conversation_id,
|
|
1050
|
+
inferred_queries=[query_to_run],
|
|
1051
|
+
automation_id=automation.id,
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
if stream:
|
|
1055
|
+
return StreamingResponse(llm_response, media_type="text/event-stream", status_code=200)
|
|
1056
|
+
else:
|
|
1057
|
+
return Response(content=llm_response, media_type="text/plain", status_code=200)
|
|
1058
|
+
|
|
1059
|
+
compiled_references, inferred_queries, defiltered_query = await extract_references_and_questions(
|
|
1060
|
+
request, meta_log, q, (n or 5), (d or math.inf), conversation_id, conversation_commands, location
|
|
1061
|
+
)
|
|
1062
|
+
online_results: Dict[str, Dict] = {}
|
|
1063
|
+
|
|
1064
|
+
if conversation_commands == [ConversationCommand.Notes] and not await EntryAdapters.auser_has_entries(user):
|
|
1065
|
+
no_entries_found_format = no_entries_found.format()
|
|
1066
|
+
if stream:
|
|
1067
|
+
return StreamingResponse(iter([no_entries_found_format]), media_type="text/event-stream", status_code=200)
|
|
1068
|
+
else:
|
|
1069
|
+
response_obj = {"response": no_entries_found_format}
|
|
1070
|
+
return Response(content=json.dumps(response_obj), media_type="text/plain", status_code=200)
|
|
1071
|
+
|
|
1072
|
+
if conversation_commands == [ConversationCommand.Notes] and is_none_or_empty(compiled_references):
|
|
1073
|
+
no_notes_found_format = no_notes_found.format()
|
|
1074
|
+
if stream:
|
|
1075
|
+
return StreamingResponse(iter([no_notes_found_format]), media_type="text/event-stream", status_code=200)
|
|
1076
|
+
else:
|
|
1077
|
+
response_obj = {"response": no_notes_found_format}
|
|
1078
|
+
return Response(content=json.dumps(response_obj), media_type="text/plain", status_code=200)
|
|
1079
|
+
|
|
1080
|
+
if ConversationCommand.Notes in conversation_commands and is_none_or_empty(compiled_references):
|
|
1081
|
+
conversation_commands.remove(ConversationCommand.Notes)
|
|
1082
|
+
|
|
1083
|
+
if ConversationCommand.Online in conversation_commands:
|
|
1084
|
+
try:
|
|
1085
|
+
online_results = await search_online(defiltered_query, meta_log, location, custom_filters=_custom_filters)
|
|
1086
|
+
except ValueError as e:
|
|
1087
|
+
logger.warning(f"Error searching online: {e}. Attempting to respond without online results")
|
|
1088
|
+
|
|
1089
|
+
if ConversationCommand.Webpage in conversation_commands:
|
|
1090
|
+
try:
|
|
1091
|
+
online_results = await read_webpages(defiltered_query, meta_log, location)
|
|
1092
|
+
except ValueError as e:
|
|
1093
|
+
logger.warning(
|
|
1094
|
+
f"Error directly reading webpages: {e}. Attempting to respond without online results", exc_info=True
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
if ConversationCommand.Image in conversation_commands:
|
|
1098
|
+
update_telemetry_state(
|
|
1099
|
+
request=request,
|
|
1100
|
+
telemetry_type="api",
|
|
1101
|
+
api="chat",
|
|
1102
|
+
metadata={"conversation_command": conversation_commands[0].value},
|
|
1103
|
+
**common.__dict__,
|
|
1104
|
+
)
|
|
1105
|
+
image, status_code, improved_image_prompt, intent_type = await text_to_image(
|
|
1106
|
+
q, user, meta_log, location_data=location, references=compiled_references, online_results=online_results
|
|
1107
|
+
)
|
|
1108
|
+
if image is None:
|
|
1109
|
+
content_obj = {"image": image, "intentType": intent_type, "detail": improved_image_prompt}
|
|
1110
|
+
return Response(content=json.dumps(content_obj), media_type="application/json", status_code=status_code)
|
|
1111
|
+
|
|
1112
|
+
await sync_to_async(save_to_conversation_log)(
|
|
1113
|
+
q,
|
|
1114
|
+
image,
|
|
1115
|
+
user,
|
|
1116
|
+
meta_log,
|
|
1117
|
+
user_message_time,
|
|
1118
|
+
intent_type=intent_type,
|
|
1119
|
+
inferred_queries=[improved_image_prompt],
|
|
1120
|
+
client_application=request.user.client_app,
|
|
1121
|
+
conversation_id=conversation.id,
|
|
1122
|
+
compiled_references=compiled_references,
|
|
1123
|
+
online_results=online_results,
|
|
1124
|
+
)
|
|
1125
|
+
content_obj = {"image": image, "intentType": intent_type, "inferredQueries": [improved_image_prompt], "context": compiled_references, "online_results": online_results} # type: ignore
|
|
1126
|
+
return Response(content=json.dumps(content_obj), media_type="application/json", status_code=status_code)
|
|
1127
|
+
|
|
1128
|
+
# Get the (streamed) chat response from the LLM of choice.
|
|
1129
|
+
llm_response, chat_metadata = await agenerate_chat_response(
|
|
1130
|
+
defiltered_query,
|
|
1131
|
+
meta_log,
|
|
1132
|
+
conversation,
|
|
1133
|
+
compiled_references,
|
|
1134
|
+
online_results,
|
|
1135
|
+
inferred_queries,
|
|
1136
|
+
conversation_commands,
|
|
1137
|
+
user,
|
|
1138
|
+
request.user.client_app,
|
|
1139
|
+
conversation.id,
|
|
1140
|
+
location,
|
|
1141
|
+
user_name,
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
cmd_set = set([cmd.value for cmd in conversation_commands])
|
|
1145
|
+
chat_metadata["conversation_command"] = cmd_set
|
|
1146
|
+
chat_metadata["agent"] = conversation.agent.slug if conversation.agent else None
|
|
1147
|
+
|
|
1148
|
+
update_telemetry_state(
|
|
1149
|
+
request=request,
|
|
1150
|
+
telemetry_type="api",
|
|
1151
|
+
api="chat",
|
|
1152
|
+
metadata=chat_metadata,
|
|
1153
|
+
**common.__dict__,
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
if llm_response is None:
|
|
1157
|
+
return Response(content=llm_response, media_type="text/plain", status_code=500)
|
|
1158
|
+
|
|
1159
|
+
if stream:
|
|
1160
|
+
return StreamingResponse(llm_response, media_type="text/event-stream", status_code=200)
|
|
1161
|
+
|
|
1162
|
+
iterator = AsyncIteratorWrapper(llm_response)
|
|
1163
|
+
|
|
1164
|
+
# Get the full response from the generator if the stream is not requested.
|
|
1165
|
+
aggregated_gpt_response = ""
|
|
1166
|
+
async for item in iterator:
|
|
1167
|
+
if item is None:
|
|
1168
|
+
break
|
|
1169
|
+
aggregated_gpt_response += item
|
|
1170
|
+
|
|
1171
|
+
actual_response = aggregated_gpt_response.split("### compiled references:")[0]
|
|
1172
|
+
|
|
1173
|
+
response_obj = {
|
|
1174
|
+
"response": actual_response,
|
|
1175
|
+
"inferredQueries": inferred_queries,
|
|
1176
|
+
"context": compiled_references,
|
|
1177
|
+
"online_results": online_results,
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return Response(content=json.dumps(response_obj), media_type="application/json", status_code=200)
|