Pass query params to chat API in POST body instead of URL query string

Closes #899, #678
This commit is contained in:
Debanjum Singh Solanky
2024-09-10 11:38:01 -07:00
parent fc6345e246
commit 596db603e0
6 changed files with 96 additions and 41 deletions

View File

@@ -103,7 +103,7 @@
let conversationID = chatBody.dataset.conversationId; let conversationID = chatBody.dataset.conversationId;
let hostURL = await window.hostURLAPI.getURL(); let hostURL = await window.hostURLAPI.getURL();
const khojToken = await window.tokenAPI.getToken(); const khojToken = await window.tokenAPI.getToken();
const headers = { 'Authorization': `Bearer ${khojToken}` }; const headers = { 'Authorization': `Bearer ${khojToken}`, 'Content-Type': 'application/json' };
if (!conversationID) { if (!conversationID) {
let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST", headers }); let response = await fetch(`${hostURL}/api/chat/sessions`, { method: "POST", headers });
@@ -149,12 +149,22 @@
document.getElementById("send-button").style.display = "none"; document.getElementById("send-button").style.display = "none";
// Call Khoj chat API // Call Khoj chat API
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&conversation_id=${conversationID}&stream=true&client=desktop`; const chatApi = `${hostURL}/api/chat?client=desktop`;
chatApi += (!!region && !!city && !!countryName && !!timezone) const chatApiBody = {
? `&region=${region}&city=${city}&country=${countryName}&timezone=${timezone}` 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 { try {
if (!response.ok) throw new Error(response.statusText); if (!response.ok) throw new Error(response.statusText);

View File

@@ -675,14 +675,15 @@ Optionally apply CALLBACK with JSON parsed response and CBARGS."
(json-parse-buffer :object-type 'alist)))) (json-parse-buffer :object-type 'alist))))
('file-error (message "Chat exception: [%s]" ex)))))) ('file-error (message "Chat exception: [%s]" ex))))))
(defun khoj--call-api-async (path &optional method params callback &rest cbargs) (defun khoj--call-api-async (path &optional method params body callback &rest cbargs)
"Async call to API at PATH with METHOD and query PARAMS as kv assoc list. "Async call to API at PATH with specified METHOD, query PARAMS and request BODY.
Optionally apply CALLBACK with JSON parsed response and CBARGS." Optionally apply CALLBACK with JSON parsed response and CBARGS."
(let* ((url-request-method (or method "GET")) (let* ((url-request-method (or method "GET"))
(url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key)))) (url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key)) ("Content-Type" . "application/json")))
(param-string (if params (url-build-query-string params) "")) (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 (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 (url-retrieve query-url
(lambda (status) (lambda (status)
(if (plist-get status :error) (if (plist-get status :error)
@@ -710,6 +711,7 @@ Filter out first similar result if IS-FIND-SIMILAR set."
(khoj--call-api-async path (khoj--call-api-async path
"GET" "GET"
params params
nil
'khoj--render-search-results 'khoj--render-search-results
content-type query buffer-name is-find-similar))) 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) (defun khoj--query-chat-api (query session-id callback &rest cbargs)
"Send QUERY for SESSION-ID to Khoj Chat API. "Send QUERY for SESSION-ID to Khoj Chat API.
Call CALLBACK func with response and CBARGS." Call CALLBACK func with response and CBARGS."
(let ((params `(("q" ,query) ("n" ,khoj-results-count)))) (let ((params `(("q" . ,query) ("n" . ,khoj-results-count))))
(when session-id (push `("conversation_id" ,session-id) params)) (when session-id (push `("conversation_id" . ,session-id) params))
(khoj--call-api-async "/api/chat" (khoj--call-api-async "/api/chat"
"POST" "POST"
nil
params params
callback cbargs))) callback cbargs)))

View File

@@ -1050,9 +1050,19 @@ export class KhojChatView extends KhojPaneView {
} }
// Get chat response from Khoj backend // Get chat response from Khoj backend
let encodedQuery = encodeURIComponent(query); const chatUrl = `${this.setting.khojUrl}/api/chat?client=obsidian`;
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&conversation_id=${conversationId}&n=${this.setting.resultsCount}&stream=true&client=obsidian`; const body = {
if (!!this.location) chatUrl += `&region=${this.location.region}&city=${this.location.city}&country=${this.location.countryName}&timezone=${this.location.timezone}`; 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 newResponseEl = this.createKhojResponseDiv();
let newResponseTextEl = newResponseEl.createDiv(); let newResponseTextEl = newResponseEl.createDiv();
@@ -1079,6 +1089,7 @@ export class KhojChatView extends KhojPaneView {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${this.setting.khojApiKey}`, "Authorization": `Bearer ${this.setting.khojApiKey}`,
}, },
body: JSON.stringify(body),
}) })
try { try {

View File

@@ -232,17 +232,26 @@ export default function Chat() {
async function chat() { async function chat() {
localStorage.removeItem("message"); localStorage.removeItem("message");
if (!queryToProcess || !conversationId) return; if (!queryToProcess || !conversationId) return;
let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`; const chatAPI = "/api/chat?client=web";
if (locationData) { const chatAPIBody = {
chatAPI += `&region=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`; 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, { const response = await fetch(chatAPI, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: image64 ? JSON.stringify({ image: image64 }) : undefined, body: JSON.stringify(chatAPIBody),
}); });
try { try {

View File

@@ -222,17 +222,26 @@ export default function SharedChat() {
async function chat() { async function chat() {
if (!queryToProcess || !conversationId) return; if (!queryToProcess || !conversationId) return;
let chatAPI = `/api/chat?q=${encodeURIComponent(queryToProcess)}&conversation_id=${conversationId}&stream=true&client=web`; const chatAPI = "/api/chat?client=web";
if (locationData) { const chatAPIBody = {
chatAPI += `&region=${locationData.region}&country=${locationData.country}&city=${locationData.city}&timezone=${locationData.timezone}`; 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, { const response = await fetch(chatAPI, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: image64 ? JSON.stringify({ image: image64 }) : undefined, body: JSON.stringify(chatAPIBody),
}); });
try { try {

View File

@@ -520,8 +520,18 @@ async def set_conversation_title(
) )
class ImageUploadObject(BaseModel): class ChatRequestBody(BaseModel):
image: str 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("") @api_chat.post("")
@@ -529,17 +539,7 @@ class ImageUploadObject(BaseModel):
async def chat( async def chat(
request: Request, request: Request,
common: CommonQueryParams, common: CommonQueryParams,
q: str, body: ChatRequestBody,
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,
rate_limiter_per_minute=Depends( rate_limiter_per_minute=Depends(
ApiUserRateLimiter(requests=60, subscribed_requests=60, window=60, slug="chat_minute") 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") 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() start_time = time.perf_counter()
ttft = None ttft = None
chat_metadata: dict = {} chat_metadata: dict = {}
@@ -560,7 +573,7 @@ async def chat(
uploaded_image_url = None uploaded_image_url = None
if image: if image:
decoded_string = unquote(image.image) decoded_string = unquote(image)
base64_data = decoded_string.split(",", 1)[1] base64_data = decoded_string.split(",", 1)[1]
image_bytes = base64.b64decode(base64_data) image_bytes = base64.b64decode(base64_data)
webp_image_bytes = convert_image_to_webp(image_bytes) webp_image_bytes = convert_image_to_webp(image_bytes)