diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 7f0fc8a4..ac7cc42b 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -103,7 +103,7 @@ let conversationID = chatBody.dataset.conversationId; let hostURL = await window.hostURLAPI.getURL(); const khojToken = await window.tokenAPI.getToken(); - const headers = { 'Authorization': `Bearer ${khojToken}` }; + const headers = { 'Authorization': `Bearer ${khojToken}`, 'Content-Type': 'application/json' }; if (!conversationID) { let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST", headers }); @@ -149,12 +149,22 @@ document.getElementById("send-button").style.display = "none"; // Call Khoj chat API - let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`; - chatApi += (!!region && !!city && !!countryName && !!timezone) - ? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}` - : ''; + const chatApi = `${hostURL}/api/chat?client=desktop`; + const chatApiBody = { + q: query, + conversation_id: parseInt(conversationID), + stream: true, + ...(!!city && { city: city }), + ...(!!region && { region: region }), + ...(!!countryName && { country: countryName }), + ...(!!timezone && { timezone: timezone }), + }; - const response = await fetch(chatApi, { method: 'POST', headers }); + const response = await fetch(chatApi, { + method: "POST", + headers: headers, + body: JSON.stringify(chatApiBody), + }); try { if (!response.ok) throw new Error(response.statusText); diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index b984519a..00e16400 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -675,14 +675,15 @@ Optionally apply CALLBACK with JSON parsed response and CBARGS." (json-parse-buffer :object-type 'alist)))) ('file-error (message "Chat exception: [%s]" ex)))))) -(defun khoj--call-api-async (path &optional method params callback &rest cbargs) - "Async call to API at PATH with METHOD and query PARAMS as kv assoc list. +(defun khoj--call-api-async (path &optional method params body callback &rest cbargs) + "Async call to API at PATH with specified METHOD, query PARAMS and request BODY. Optionally apply CALLBACK with JSON parsed response and CBARGS." (let* ((url-request-method (or method "GET")) - (url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key)))) - (param-string (if params (url-build-query-string params) "")) + (url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key)) ("Content-Type" . "application/json"))) + (url-request-data (if body (json-encode body) nil)) + (param-string (url-build-query-string (append params '((client "emacs"))))) (cbargs (if (and (listp cbargs) (listp (car cbargs))) (car cbargs) cbargs)) ; normalize cbargs to (a b) from ((a b)) if required - (query-url (format "%s%s?%s&client=emacs" khoj-server-url path param-string))) + (query-url (format "%s%s?%s" khoj-server-url path param-string))) (url-retrieve query-url (lambda (status) (if (plist-get status :error) @@ -710,6 +711,7 @@ Filter out first similar result if IS-FIND-SIMILAR set." (khoj--call-api-async path "GET" params + nil 'khoj--render-search-results content-type query buffer-name is-find-similar))) @@ -875,10 +877,11 @@ Filter out first similar result if IS-FIND-SIMILAR set." (defun khoj--query-chat-api (query session-id callback &rest cbargs) "Send QUERY for SESSION-ID to Khoj Chat API. Call CALLBACK func with response and CBARGS." - (let ((params `(("q" ,query) ("n" ,khoj-results-count)))) - (when session-id (push `("conversation_id" ,session-id) params)) + (let ((params `(("q" . ,query) ("n" . ,khoj-results-count)))) + (when session-id (push `("conversation_id" . ,session-id) params)) (khoj--call-api-async "/api/chat" "POST" + nil params callback cbargs))) diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index f55ad46d..365548f5 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -1050,9 +1050,19 @@ export class KhojChatView extends KhojPaneView { } // Get chat response from Khoj backend - let encodedQuery = encodeURIComponent(query); - let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&conversation_id=${conversationId}&n=${this.setting.resultsCount}&stream=true&client=obsidian`; - if (!!this.location) chatUrl += `®ion=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}`; + const chatUrl = `${this.setting.khojUrl}/api/chat?client=obsidian`; + const body = { + q: query, + n: this.setting.resultsCount, + stream: true, + ...(!!conversationId && { conversation_id: parseInt(conversationId) }), + ...(!!this.location && { + city: this.location.city, + region: this.location.region, + country: this.location.countryName, + timezone: this.location.timezone, + }), + }; let newResponseEl = this.createKhojResponseDiv(); let newResponseTextEl = newResponseEl.createDiv(); @@ -1079,6 +1089,7 @@ export class KhojChatView extends KhojPaneView { "Content-Type": "application/json", "Authorization": `Bearer ${this.setting.khojApiKey}`, }, + body: JSON.stringify(body), }) try { diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx index 2b9155d2..1a9611f6 100644 --- a/src/interface/web/app/chat/page.tsx +++ b/src/interface/web/app/chat/page.tsx @@ -232,17 +232,26 @@ export default function Chat() { async function chat() { localStorage.removeItem("message"); if (!queryToProcess || !conversationId) return; - let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`; - if (locationData) { - chatAPI += `®ion=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`; - } + const chatAPI = "/api/chat?client=web"; + const chatAPIBody = { + q: queryToProcess, + conversation_id: parseInt(conversationId), + stream: true, + ...(locationData && { + region: locationData.region, + country: locationData.country, + city: locationData.city, + timezone: locationData.timezone, + }), + ...(image64 && { image: image64 }), + }; const response = await fetch(chatAPI, { method: "POST", headers: { "Content-Type": "application/json", }, - body: image64 ? JSON.stringify({ image: image64 }) : undefined, + body: JSON.stringify(chatAPIBody), }); try { diff --git a/src/interface/web/app/share/chat/page.tsx b/src/interface/web/app/share/chat/page.tsx index eefc159c..f6788948 100644 --- a/src/interface/web/app/share/chat/page.tsx +++ b/src/interface/web/app/share/chat/page.tsx @@ -222,17 +222,26 @@ export default function SharedChat() { async function chat() { if (!queryToProcess || !conversationId) return; - let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`; - if (locationData) { - chatAPI += `®ion=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`; - } + const chatAPI = "/api/chat?client=web"; + const chatAPIBody = { + q: queryToProcess, + conversation_id: parseInt(conversationId), + stream: true, + ...(locationData && { + region: locationData.region, + country: locationData.country, + city: locationData.city, + timezone: locationData.timezone, + }), + ...(image64 && { image: image64 }), + }; const response = await fetch(chatAPI, { method: "POST", headers: { "Content-Type": "application/json", }, - body: image64 ? JSON.stringify({ image: image64 }) : undefined, + body: JSON.stringify(chatAPIBody), }); try { diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 38cf5d3d..0e83b9c2 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -520,8 +520,18 @@ async def set_conversation_title( ) -class ImageUploadObject(BaseModel): - image: str +class ChatRequestBody(BaseModel): + q: str + n: Optional[int] = 7 + d: Optional[float] = None + stream: Optional[bool] = False + title: Optional[str] = None + conversation_id: Optional[int] = None + city: Optional[str] = None + region: Optional[str] = None + country: Optional[str] = None + timezone: Optional[str] = None + image: Optional[str] = None @api_chat.post("") @@ -529,17 +539,7 @@ class ImageUploadObject(BaseModel): async def chat( request: Request, common: CommonQueryParams, - q: str, - n: int = 7, - d: float = None, - stream: Optional[bool] = False, - title: Optional[str] = None, - conversation_id: Optional[int] = None, - city: Optional[str] = None, - region: Optional[str] = None, - country: Optional[str] = None, - timezone: Optional[str] = None, - image: Optional[ImageUploadObject] = None, + body: ChatRequestBody, rate_limiter_per_minute=Depends( ApiUserRateLimiter(requests=60, subscribed_requests=60, window=60, slug="chat_minute") ), @@ -547,7 +547,20 @@ async def chat( ApiUserRateLimiter(requests=600, subscribed_requests=600, window=60 * 60 * 24, slug="chat_day") ), ): - async def event_generator(q: str, image: ImageUploadObject): + # Access the parameters from the body + q = body.q + n = body.n + d = body.d + stream = body.stream + title = body.title + conversation_id = body.conversation_id + city = body.city + region = body.region + country = body.country + timezone = body.timezone + image = body.image + + async def event_generator(q: str, image: str): start_time = time.perf_counter() ttft = None chat_metadata: dict = {} @@ -560,7 +573,7 @@ async def chat( uploaded_image_url = None if image: - decoded_string = unquote(image.image) + decoded_string = unquote(image) base64_data = decoded_string.split(",", 1)[1] image_bytes = base64.b64decode(base64_data) webp_image_bytes = convert_image_to_webp(image_bytes)