mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-27 17:35:07 +01:00
Miscellaneous bugs and fixes for chat sessions (#646)
* Display given_name field only if it is not None * Add default slugs in the migration script * Ensure that updated_at is saved appropriately, make sure most recent chat is returned for default history * Remove the bin button from the chat interface, given deletion is handled in the drop-down menus * Refresh the side panel when a new chat is created * Improveme tool retrieval prompt, don't let /online fail, and improve parsing of extract questions * Fix ending chat response by offline chat on hitting a stop phrase Previously the whole phrase wouldn't be in the same response chunk, so chat response wouldn't stop on hitting a stop phrase Now use a queue to keep track of last 3 chunks, and to stop responding when hit a stop phrase * Make chat on Obsidian backward compatible post chat session API updates - Make chat on Obsidian get chat history from `responseJson.response.chat' when available (i.e when using new api) - Else fallback to loading chat history from responseJson.response (i.e when using old api) * Fix detecting success of indexing update in khoj.el When khoj.el attempts to index on a Khoj server served behind an https endpoint, the success reponse status contains plist with certs. This doesn't mean the update failed. Look for :errors key in status instead to determine if indexing API call failed. This fixes detecting indexing API call success on the Khoj Emacs client, even for Khoj servers running behind SSL/HTTPS * Fix the mechanism for populating notes references in the conversation primer for both offline and online chat * Return conversation.default when empty list for dynamic prompt selection, send all cmds in telemetry * Fix making chat on Obsidian backward compatible post chat session API updates New API always has conversation_id set, not `chat' which can be unset when chat session is empty. So use conversation_id to decide whether to get chat logs from `responseJson.response.chat' or `responseJson.response' instead --------- Co-authored-by: Debanjum Singh Solanky <debanjum@gmail.com>
This commit is contained in:
parent
138f5223bd
commit
44f8f20ea7
19 changed files with 348 additions and 331 deletions
|
@ -65,12 +65,18 @@
|
|||
font-weight: 300;
|
||||
}
|
||||
|
||||
.khoj-header {
|
||||
div.khoj-header {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 20px;
|
||||
padding: 16px 0;
|
||||
padding: 24px 16px 0px 0px;
|
||||
margin: 0 0 16px 0;
|
||||
-webkit-user-select: none;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
a.khoj-nav {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
nav.khoj-nav {
|
||||
|
@ -117,7 +123,7 @@ img.khoj-logo {
|
|||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 20px;
|
||||
padding: 12px 10px;
|
||||
padding: 24px 10px 10px 10px;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -349,6 +349,7 @@
|
|||
let data = await response.json();
|
||||
conversationID = data.conversation_id;
|
||||
chat_body.dataset.conversationId = conversationID;
|
||||
await refreshChatSessionsPanel();
|
||||
}
|
||||
|
||||
|
||||
|
@ -665,6 +666,107 @@
|
|||
return;
|
||||
});
|
||||
|
||||
await refreshChatSessionsPanel();
|
||||
|
||||
fetch(`${hostURL}/api/chat/starters?client=desktop`, { headers })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Render chat options, if any
|
||||
if (data.length > 0) {
|
||||
let questionStarterSuggestions = document.getElementById("question-starters");
|
||||
for (let index in data) {
|
||||
let questionStarter = data[index];
|
||||
let questionStarterButton = document.createElement('button');
|
||||
questionStarterButton.innerHTML = questionStarter;
|
||||
questionStarterButton.classList.add("question-starter");
|
||||
questionStarterButton.addEventListener('click', function() {
|
||||
questionStarterSuggestions.style.display = "none";
|
||||
document.getElementById("chat-input").value = questionStarter;
|
||||
chat();
|
||||
});
|
||||
questionStarterSuggestions.appendChild(questionStarterButton);
|
||||
}
|
||||
questionStarterSuggestions.style.display = "grid";
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
|
||||
fetch(`${hostURL}/api/chat/options`, { headers })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Render chat options, if any
|
||||
if (data) {
|
||||
chatOptions = data;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
|
||||
// Fill query field with value passed in URL query parameters, if any.
|
||||
var query_via_url = new URLSearchParams(window.location.search).get("q");
|
||||
if (query_via_url) {
|
||||
document.getElementById("chat-input").value = query_via_url;
|
||||
chat();
|
||||
}
|
||||
}
|
||||
|
||||
function flashStatusInChatInput(message) {
|
||||
// Get chat input element and original placeholder
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
let originalPlaceholder = chatInput.placeholder;
|
||||
// Set placeholder to message
|
||||
chatInput.placeholder = message;
|
||||
// Reset placeholder after 2 seconds
|
||||
setTimeout(() => {
|
||||
chatInput.placeholder = originalPlaceholder;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function createNewConversation() {
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
flashStatusInChatInput("📝 New conversation started");
|
||||
chatBody.dataset.conversationId = "";
|
||||
chatBody.dataset.conversationTitle = "";
|
||||
renderMessage("Hey 👋🏾, what's up?", "khoj");
|
||||
}
|
||||
|
||||
async function clearConversationHistory() {
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
let originalPlaceholder = chatInput.placeholder;
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
let conversationId = chatBody.dataset.conversationId;
|
||||
|
||||
let deleteURL = `/api/chat/history?client=desktop`;
|
||||
if (conversationId) {
|
||||
deleteURL += `&conversation_id=${conversationId}`;
|
||||
}
|
||||
|
||||
const hostURL = await window.hostURLAPI.getURL();
|
||||
const khojToken = await window.tokenAPI.getToken();
|
||||
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
||||
|
||||
fetch(`${hostURL}${deleteURL}`, { method: "DELETE", headers })
|
||||
.then(response => response.ok ? response.json() : Promise.reject(response))
|
||||
.then(data => {
|
||||
chatBody.innerHTML = "";
|
||||
chatBody.dataset.conversationId = "";
|
||||
chatBody.dataset.conversationTitle = "";
|
||||
loadChat();
|
||||
flashStatusInChatInput("🗑 Cleared conversation history");
|
||||
})
|
||||
.catch(err => {
|
||||
flashStatusInChatInput("⛔️ Failed to clear conversation history");
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshChatSessionsPanel() {
|
||||
const hostURL = await window.hostURLAPI.getURL();
|
||||
const khojToken = await window.tokenAPI.getToken();
|
||||
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
||||
|
||||
fetch(`${hostURL}/api/chat/sessions`, { method: "GET", headers })
|
||||
.then(response => response.json())
|
||||
|
@ -799,101 +901,9 @@
|
|||
conversationListBody.appendChild(conversationButton);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fetch(`${hostURL}/api/chat/starters?client=desktop`, { headers })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Render chat options, if any
|
||||
if (data.length > 0) {
|
||||
let questionStarterSuggestions = document.getElementById("question-starters");
|
||||
for (let index in data) {
|
||||
let questionStarter = data[index];
|
||||
let questionStarterButton = document.createElement('button');
|
||||
questionStarterButton.innerHTML = questionStarter;
|
||||
questionStarterButton.classList.add("question-starter");
|
||||
questionStarterButton.addEventListener('click', function() {
|
||||
questionStarterSuggestions.style.display = "none";
|
||||
document.getElementById("chat-input").value = questionStarter;
|
||||
chat();
|
||||
});
|
||||
questionStarterSuggestions.appendChild(questionStarterButton);
|
||||
}
|
||||
questionStarterSuggestions.style.display = "grid";
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
}).catch(err => {
|
||||
return;
|
||||
});
|
||||
|
||||
fetch(`${hostURL}/api/chat/options`, { headers })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Render chat options, if any
|
||||
if (data) {
|
||||
chatOptions = data;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
|
||||
// Fill query field with value passed in URL query parameters, if any.
|
||||
var query_via_url = new URLSearchParams(window.location.search).get("q");
|
||||
if (query_via_url) {
|
||||
document.getElementById("chat-input").value = query_via_url;
|
||||
chat();
|
||||
}
|
||||
}
|
||||
|
||||
function flashStatusInChatInput(message) {
|
||||
// Get chat input element and original placeholder
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
let originalPlaceholder = chatInput.placeholder;
|
||||
// Set placeholder to message
|
||||
chatInput.placeholder = message;
|
||||
// Reset placeholder after 2 seconds
|
||||
setTimeout(() => {
|
||||
chatInput.placeholder = originalPlaceholder;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function createNewConversation() {
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
flashStatusInChatInput("📝 New conversation started");
|
||||
chatBody.dataset.conversationId = "";
|
||||
chatBody.dataset.conversationTitle = "";
|
||||
renderMessage("Hey 👋🏾, what's up?", "khoj");
|
||||
}
|
||||
|
||||
async function clearConversationHistory() {
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
let originalPlaceholder = chatInput.placeholder;
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
let conversationId = chatBody.dataset.conversationId;
|
||||
|
||||
let deleteURL = `/api/chat/history?client=desktop`;
|
||||
if (conversationId) {
|
||||
deleteURL += `&conversation_id=${conversationId}`;
|
||||
}
|
||||
|
||||
const hostURL = await window.hostURLAPI.getURL();
|
||||
const khojToken = await window.tokenAPI.getToken();
|
||||
const headers = { 'Authorization': `Bearer ${khojToken}` };
|
||||
|
||||
fetch(`${hostURL}${deleteURL}`, { method: "DELETE", headers })
|
||||
.then(response => response.ok ? response.json() : Promise.reject(response))
|
||||
.then(data => {
|
||||
chatBody.innerHTML = "";
|
||||
chatBody.dataset.conversationId = "";
|
||||
chatBody.dataset.conversationTitle = "";
|
||||
loadChat();
|
||||
flashStatusInChatInput("🗑 Cleared conversation history");
|
||||
})
|
||||
.catch(err => {
|
||||
flashStatusInChatInput("⛔️ Failed to clear conversation history");
|
||||
})
|
||||
}
|
||||
|
||||
let sendMessageTimeout;
|
||||
|
@ -1065,16 +1075,6 @@
|
|||
<div id="chat-footer">
|
||||
<div id="chat-tooltip" style="display: none;"></div>
|
||||
<div id="input-row">
|
||||
<button id="clear-chat-button" class="input-row-button" onclick="clearConversationHistory()">
|
||||
<svg class="input-row-button-img" alt="Clear Chat History" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<rect width="128" height="128" fill="none"/>
|
||||
<line x1="216" y1="56" x2="40" y2="56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<line x1="104" y1="104" x2="104" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<line x1="152" y1="104" x2="152" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<path d="M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<path d="M168,56V40a16,16,0,0,0-16-16H104A16,16,0,0,0,88,40V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands"></textarea>
|
||||
<button id="speak-button" class="input-row-button"
|
||||
ontouchstart="speechToText(event)" ontouchend="speechToText(event)" ontouchcancel="speechToText(event)" onmousedown="speechToText(event)">
|
||||
|
@ -1292,7 +1292,7 @@
|
|||
}
|
||||
#input-row {
|
||||
display: grid;
|
||||
grid-template-columns: 32px auto 32px 40px;
|
||||
grid-template-columns: auto 32px 40px;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
background: #f9fafc;
|
||||
|
|
|
@ -356,7 +356,7 @@ const createWindow = (tab = 'chat.html') => {
|
|||
width: 800,
|
||||
height: 800,
|
||||
show: false,
|
||||
// titleBarStyle: 'hidden',
|
||||
titleBarStyle: 'hidden',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: true,
|
||||
|
|
|
@ -426,7 +426,7 @@ Auto invokes setup steps on calling main entrypoint."
|
|||
(url-retrieve (format "%s/api/v1/index/update?%s&force=%s&client=emacs" khoj-server-url type-query (or force "false"))
|
||||
;; render response from indexing API endpoint on server
|
||||
(lambda (status)
|
||||
(if (not status)
|
||||
(if (not (plist-get status :error))
|
||||
(message "khoj.el: %scontent index %supdated" (if content-type (format "%s " content-type) "all ") (if force "force " ""))
|
||||
(progn
|
||||
(khoj--delete-open-network-connections-to-server)
|
||||
|
|
|
@ -285,7 +285,7 @@ export class KhojChatModal extends Modal {
|
|||
|
||||
return false;
|
||||
} else if (responseJson.response) {
|
||||
let chatLogs = responseJson.response.chat;
|
||||
let chatLogs = responseJson.response?.conversation_id ? responseJson.response.chat ?? [] : responseJson.response;
|
||||
chatLogs.forEach((chatLog: any) => {
|
||||
this.renderMessageWithReferences(chatBodyEl, chatLog.message, chatLog.by, chatLog.context, new Date(chatLog.created), chatLog.intent?.type);
|
||||
});
|
||||
|
|
|
@ -392,7 +392,7 @@ class ConversationAdapters:
|
|||
if conversation_id:
|
||||
conversation = Conversation.objects.filter(user=user, client=client_application, id=conversation_id)
|
||||
if not conversation_id or not conversation.exists():
|
||||
conversation = Conversation.objects.filter(user=user, client=client_application)
|
||||
conversation = Conversation.objects.filter(user=user, client=client_application).order_by("-updated_at")
|
||||
if conversation.exists():
|
||||
return conversation.first()
|
||||
return Conversation.objects.create(user=user, client=client_application)
|
||||
|
@ -514,7 +514,7 @@ class ConversationAdapters:
|
|||
else:
|
||||
conversation = Conversation.objects.filter(user=user, client=client_application)
|
||||
if conversation.exists():
|
||||
conversation.update(conversation_log=conversation_log, slug=slug)
|
||||
conversation.update(conversation_log=conversation_log, slug=slug, updated_at=datetime.now(tz=timezone.utc))
|
||||
else:
|
||||
Conversation.objects.create(
|
||||
user=user, conversation_log=conversation_log, client=client_application, slug=slug
|
||||
|
|
|
@ -3,6 +3,21 @@
|
|||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_default_slug(apps, schema_editor):
|
||||
Conversation = apps.get_model("database", "Conversation")
|
||||
for conversation in Conversation.objects.all():
|
||||
formatted_date = conversation.created_at.strftime("%Y-%m-%d")
|
||||
conversation.slug = f"Conversation from {formatted_date}"
|
||||
conversation.save()
|
||||
|
||||
|
||||
def reverse_set_default_slug(apps, schema_editor):
|
||||
Conversation = apps.get_model("database", "Conversation")
|
||||
for conversation in Conversation.objects.all():
|
||||
conversation.slug = None
|
||||
conversation.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("database", "0029_userrequests"),
|
||||
|
@ -19,4 +34,5 @@ class Migration(migrations.Migration):
|
|||
name="title",
|
||||
field=models.CharField(blank=True, default=None, max_length=200, null=True),
|
||||
),
|
||||
migrations.RunPython(set_default_slug, reverse_code=reverse_set_default_slug),
|
||||
]
|
||||
|
|
|
@ -358,6 +358,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
let data = await response.json();
|
||||
conversationID = data.conversation_id;
|
||||
chat_body.dataset.conversationId = conversationID;
|
||||
refreshChatSessionsPanel();
|
||||
}
|
||||
|
||||
// Generate backend API URL to execute query
|
||||
|
@ -626,144 +627,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
return;
|
||||
});
|
||||
|
||||
fetch('/api/chat/sessions', { method: "GET" })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let conversationListBody = document.getElementById("conversation-list-body");
|
||||
conversationListBody.innerHTML = "";
|
||||
let conversationListBodyHeader = document.getElementById("conversation-list-header");
|
||||
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
conversationId = chatBody.dataset.conversationId;
|
||||
|
||||
if (data.length > 0) {
|
||||
conversationListBodyHeader.style.display = "block";
|
||||
for (let index in data) {
|
||||
let conversation = data[index];
|
||||
let conversationButton = document.createElement('div');
|
||||
let incomingConversationId = conversation["conversation_id"];
|
||||
const conversationTitle = conversation["slug"] || `New conversation 🌱`;
|
||||
conversationButton.innerHTML = conversationTitle;
|
||||
conversationButton.classList.add("conversation-button");
|
||||
if (incomingConversationId == conversationId) {
|
||||
conversationButton.classList.add("selected-conversation");
|
||||
}
|
||||
conversationButton.addEventListener('click', function() {
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
chatBody.dataset.conversationId = incomingConversationId;
|
||||
chatBody.dataset.conversationTitle = conversationTitle;
|
||||
loadChat();
|
||||
});
|
||||
let threeDotMenu = document.createElement('div');
|
||||
threeDotMenu.classList.add("three-dot-menu");
|
||||
let threeDotMenuButton = document.createElement('button');
|
||||
threeDotMenuButton.innerHTML = "⋮";
|
||||
threeDotMenuButton.classList.add("three-dot-menu-button");
|
||||
threeDotMenuButton.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
let existingChildren = threeDotMenu.children;
|
||||
|
||||
if (existingChildren.length > 1) {
|
||||
// Skip deleting the first, since that's the menu button.
|
||||
for (let i = 1; i < existingChildren.length; i++) {
|
||||
existingChildren[i].remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let conversationMenu = document.createElement('div');
|
||||
conversationMenu.classList.add("conversation-menu");
|
||||
|
||||
let deleteButton = document.createElement('button');
|
||||
deleteButton.innerHTML = "Delete";
|
||||
deleteButton.classList.add("delete-conversation-button");
|
||||
deleteButton.classList.add("three-dot-menu-button-item");
|
||||
deleteButton.addEventListener('click', function() {
|
||||
let deleteURL = `/api/chat/history?client=web&conversation_id=${incomingConversationId}`;
|
||||
fetch(deleteURL , { method: "DELETE" })
|
||||
.then(response => response.ok ? response.json() : Promise.reject(response))
|
||||
.then(data => {
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
chatBody.dataset.conversationId = "";
|
||||
chatBody.dataset.conversationTitle = "";
|
||||
loadChat();
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
});
|
||||
conversationMenu.appendChild(deleteButton);
|
||||
threeDotMenu.appendChild(conversationMenu);
|
||||
|
||||
let editTitleButton = document.createElement('button');
|
||||
editTitleButton.innerHTML = "Rename";
|
||||
editTitleButton.classList.add("edit-title-button");
|
||||
editTitleButton.classList.add("three-dot-menu-button-item");
|
||||
editTitleButton.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
let conversationMenuChildren = conversationMenu.children;
|
||||
|
||||
let totalItems = conversationMenuChildren.length;
|
||||
|
||||
for (let i = totalItems - 1; i >= 0; i--) {
|
||||
conversationMenuChildren[i].remove();
|
||||
}
|
||||
|
||||
// Create a dialog box to get new title for conversation
|
||||
let conversationTitleInputBox = document.createElement('div');
|
||||
conversationTitleInputBox.classList.add("conversation-title-input-box");
|
||||
let conversationTitleInput = document.createElement('input');
|
||||
conversationTitleInput.classList.add("conversation-title-input");
|
||||
|
||||
conversationTitleInput.value = conversationTitle;
|
||||
|
||||
conversationTitleInput.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
conversationTitleInputButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
conversationTitleInputBox.appendChild(conversationTitleInput);
|
||||
let conversationTitleInputButton = document.createElement('button');
|
||||
conversationTitleInputButton.innerHTML = "Save";
|
||||
conversationTitleInputButton.classList.add("three-dot-menu-button-item");
|
||||
conversationTitleInputButton.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
let newTitle = conversationTitleInput.value;
|
||||
if (newTitle != null) {
|
||||
let editURL = `/api/chat/title?client=web&conversation_id=${incomingConversationId}&title=${newTitle}`;
|
||||
fetch(editURL , { method: "PATCH" })
|
||||
.then(response => response.ok ? response.json() : Promise.reject(response))
|
||||
.then(data => {
|
||||
conversationButton.innerHTML = newTitle;
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
conversationTitleInputBox.remove();
|
||||
}});
|
||||
conversationTitleInputBox.appendChild(conversationTitleInputButton);
|
||||
conversationMenu.appendChild(conversationTitleInputBox);
|
||||
});
|
||||
conversationMenu.appendChild(editTitleButton);
|
||||
threeDotMenu.appendChild(conversationMenu);
|
||||
});
|
||||
threeDotMenu.appendChild(threeDotMenuButton);
|
||||
conversationButton.appendChild(threeDotMenu);
|
||||
conversationListBody.appendChild(conversationButton);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
return;
|
||||
});
|
||||
refreshChatSessionsPanel();
|
||||
|
||||
fetch('/api/chat/options')
|
||||
.then(response => response.json())
|
||||
|
@ -831,6 +695,147 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
renderMessage(welcome_message, "khoj");
|
||||
}
|
||||
|
||||
function refreshChatSessionsPanel() {
|
||||
fetch('/api/chat/sessions', { method: "GET" })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let conversationListBody = document.getElementById("conversation-list-body");
|
||||
conversationListBody.innerHTML = "";
|
||||
let conversationListBodyHeader = document.getElementById("conversation-list-header");
|
||||
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
conversationId = chatBody.dataset.conversationId;
|
||||
|
||||
if (data.length > 0) {
|
||||
conversationListBodyHeader.style.display = "block";
|
||||
for (let index in data) {
|
||||
let conversation = data[index];
|
||||
let conversationButton = document.createElement('div');
|
||||
let incomingConversationId = conversation["conversation_id"];
|
||||
const conversationTitle = conversation["slug"] || `New conversation 🌱`;
|
||||
conversationButton.innerHTML = conversationTitle;
|
||||
conversationButton.classList.add("conversation-button");
|
||||
if (incomingConversationId == conversationId) {
|
||||
conversationButton.classList.add("selected-conversation");
|
||||
}
|
||||
conversationButton.addEventListener('click', function() {
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
chatBody.dataset.conversationId = incomingConversationId;
|
||||
chatBody.dataset.conversationTitle = conversationTitle;
|
||||
loadChat();
|
||||
});
|
||||
let threeDotMenu = document.createElement('div');
|
||||
threeDotMenu.classList.add("three-dot-menu");
|
||||
let threeDotMenuButton = document.createElement('button');
|
||||
threeDotMenuButton.innerHTML = "⋮";
|
||||
threeDotMenuButton.classList.add("three-dot-menu-button");
|
||||
threeDotMenuButton.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
let existingChildren = threeDotMenu.children;
|
||||
|
||||
if (existingChildren.length > 1) {
|
||||
// Skip deleting the first, since that's the menu button.
|
||||
for (let i = 1; i < existingChildren.length; i++) {
|
||||
existingChildren[i].remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let conversationMenu = document.createElement('div');
|
||||
conversationMenu.classList.add("conversation-menu");
|
||||
|
||||
let editTitleButton = document.createElement('button');
|
||||
editTitleButton.innerHTML = "Rename";
|
||||
editTitleButton.classList.add("edit-title-button");
|
||||
editTitleButton.classList.add("three-dot-menu-button-item");
|
||||
editTitleButton.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
let conversationMenuChildren = conversationMenu.children;
|
||||
|
||||
let totalItems = conversationMenuChildren.length;
|
||||
|
||||
for (let i = totalItems - 1; i >= 0; i--) {
|
||||
conversationMenuChildren[i].remove();
|
||||
}
|
||||
|
||||
// Create a dialog box to get new title for conversation
|
||||
let conversationTitleInputBox = document.createElement('div');
|
||||
conversationTitleInputBox.classList.add("conversation-title-input-box");
|
||||
let conversationTitleInput = document.createElement('input');
|
||||
conversationTitleInput.classList.add("conversation-title-input");
|
||||
|
||||
conversationTitleInput.value = conversationTitle;
|
||||
|
||||
conversationTitleInput.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
conversationTitleInputButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
conversationTitleInputBox.appendChild(conversationTitleInput);
|
||||
let conversationTitleInputButton = document.createElement('button');
|
||||
conversationTitleInputButton.innerHTML = "Save";
|
||||
conversationTitleInputButton.classList.add("three-dot-menu-button-item");
|
||||
conversationTitleInputButton.addEventListener('click', function(event) {
|
||||
event.stopPropagation();
|
||||
let newTitle = conversationTitleInput.value;
|
||||
if (newTitle != null) {
|
||||
let editURL = `/api/chat/title?client=web&conversation_id=${incomingConversationId}&title=${newTitle}`;
|
||||
fetch(editURL , { method: "PATCH" })
|
||||
.then(response => response.ok ? response.json() : Promise.reject(response))
|
||||
.then(data => {
|
||||
conversationButton.innerHTML = newTitle;
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
conversationTitleInputBox.remove();
|
||||
}});
|
||||
conversationTitleInputBox.appendChild(conversationTitleInputButton);
|
||||
conversationMenu.appendChild(conversationTitleInputBox);
|
||||
});
|
||||
conversationMenu.appendChild(editTitleButton);
|
||||
threeDotMenu.appendChild(conversationMenu);
|
||||
|
||||
let deleteButton = document.createElement('button');
|
||||
deleteButton.innerHTML = "Delete";
|
||||
deleteButton.classList.add("delete-conversation-button");
|
||||
deleteButton.classList.add("three-dot-menu-button-item");
|
||||
deleteButton.addEventListener('click', function() {
|
||||
let deleteURL = `/api/chat/history?client=web&conversation_id=${incomingConversationId}`;
|
||||
fetch(deleteURL , { method: "DELETE" })
|
||||
.then(response => response.ok ? response.json() : Promise.reject(response))
|
||||
.then(data => {
|
||||
let chatBody = document.getElementById("chat-body");
|
||||
chatBody.innerHTML = "";
|
||||
chatBody.dataset.conversationId = "";
|
||||
chatBody.dataset.conversationTitle = "";
|
||||
loadChat();
|
||||
})
|
||||
.catch(err => {
|
||||
return;
|
||||
});
|
||||
});
|
||||
conversationMenu.appendChild(deleteButton);
|
||||
threeDotMenu.appendChild(conversationMenu);
|
||||
});
|
||||
threeDotMenu.appendChild(threeDotMenuButton);
|
||||
conversationButton.appendChild(threeDotMenu);
|
||||
conversationListBody.appendChild(conversationButton);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
function clearConversationHistory() {
|
||||
let chatInput = document.getElementById("chat-input");
|
||||
let originalPlaceholder = chatInput.placeholder;
|
||||
|
@ -1013,16 +1018,6 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
<div id="chat-footer">
|
||||
<div id="chat-tooltip" style="display: none;"></div>
|
||||
<div id="input-row">
|
||||
<button id="clear-chat-button" class="input-row-button" onclick="clearConversationHistory()">
|
||||
<svg class="input-row-button-img" alt="Clear Chat History" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
|
||||
<rect width="128" height="128" fill="none"/>
|
||||
<line x1="216" y1="56" x2="40" y2="56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<line x1="104" y1="104" x2="104" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<line x1="152" y1="104" x2="152" y2="168" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<path d="M200,56V208a8,8,0,0,1-8,8H64a8,8,0,0,1-8-8V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
<path d="M168,56V40a16,16,0,0,0-16-16H104A16,16,0,0,0,88,40V56" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<textarea id="chat-input" class="option" oninput="onChatInput()" onkeydown=incrementalChat(event) autofocus="autofocus" placeholder="Type / to see a list of commands"></textarea>
|
||||
<button id="speak-button" class="input-row-button"
|
||||
ontouchstart="speechToText(event)" ontouchend="speechToText(event)" ontouchcancel="speechToText(event)" onmousedown="speechToText(event)">
|
||||
|
@ -1361,7 +1356,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
|||
}
|
||||
#input-row {
|
||||
display: grid;
|
||||
grid-template-columns: 32px auto 32px 40px;
|
||||
grid-template-columns: auto 32px 40px;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 10px;
|
||||
background: var(--background-color);
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</h3>
|
||||
</div>
|
||||
<div class="card-description-row">
|
||||
<input type="text" id="profile_given_name" class="form-control" placeholder="Enter your name here" value="{{ given_name }}">
|
||||
<input type="text" id="profile_given_name" class="form-control" placeholder="Enter your name here" value="{% if given_name %}{{given_name}}{% endif %}">
|
||||
</div>
|
||||
<div class="card-action-row">
|
||||
<button id="save-model" class="card-button happy" onclick="saveProfileGivenName()">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from typing import Any, Iterator, List, Union
|
||||
|
@ -180,7 +181,7 @@ def converse_offline(
|
|||
simplified_online_results[result] = online_results[result]["extracted_content"]
|
||||
|
||||
conversation_primer = f"{prompts.online_search_conversation.format(online_results=str(simplified_online_results))}\n{conversation_primer}"
|
||||
if ConversationCommand.Notes in conversation_commands:
|
||||
if not is_none_or_empty(compiled_references_message):
|
||||
conversation_primer = f"{prompts.notes_conversation_gpt4all.format(references=compiled_references_message)}\n{conversation_primer}"
|
||||
|
||||
# Setup Prompt with Primer or Conversation History
|
||||
|
@ -212,21 +213,34 @@ def llm_thread(g, messages: List[ChatMessage], model: Any):
|
|||
for message in conversation_history
|
||||
]
|
||||
|
||||
stop_words = ["<s>", "INST]", "Notes:"]
|
||||
stop_phrases = ["<s>", "INST]", "Notes:"]
|
||||
chat_history = "".join(formatted_messages)
|
||||
templated_system_message = prompts.system_prompt_gpt4all.format(message=system_message.content)
|
||||
templated_user_message = prompts.user_message_gpt4all.format(message=user_message.content)
|
||||
prompted_message = templated_system_message + chat_history + templated_user_message
|
||||
response_queue: deque[str] = deque(maxlen=3) # Create a response queue with a maximum length of 3
|
||||
hit_stop_phrase = False
|
||||
|
||||
state.chat_lock.acquire()
|
||||
response_iterator = send_message_to_model_offline(prompted_message, loaded_model=model, streaming=True)
|
||||
try:
|
||||
for response in response_iterator:
|
||||
if any(stop_word in response.strip() for stop_word in stop_words):
|
||||
logger.debug(f"Stop response as hit stop word in {response}")
|
||||
response_queue.append(response)
|
||||
hit_stop_phrase = any(stop_phrase in "".join(response_queue) for stop_phrase in stop_phrases)
|
||||
if hit_stop_phrase:
|
||||
logger.debug(f"Stop response as hit stop phrase: {''.join(response_queue)}")
|
||||
break
|
||||
g.send(response)
|
||||
# Start streaming the response at a lag once the queue is full
|
||||
# This allows stop word testing before sending the response
|
||||
if len(response_queue) == response_queue.maxlen:
|
||||
g.send(response_queue[0])
|
||||
finally:
|
||||
if not hit_stop_phrase:
|
||||
if len(response_queue) == response_queue.maxlen:
|
||||
# remove already sent reponse chunk
|
||||
response_queue.popleft()
|
||||
# send the remaining response
|
||||
g.send("".join(response_queue))
|
||||
state.chat_lock.release()
|
||||
g.close()
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
@ -29,10 +30,6 @@ def extract_questions(
|
|||
"""
|
||||
Infer search queries to retrieve relevant notes to answer user query
|
||||
"""
|
||||
|
||||
def _valid_question(question: str):
|
||||
return not is_none_or_empty(question) and question != "[]"
|
||||
|
||||
location = f"{location_data.city}, {location_data.region}, {location_data.country}" if location_data else "Unknown"
|
||||
|
||||
# Extract Past User Message and Inferred Questions from Conversation Log
|
||||
|
@ -75,23 +72,13 @@ def extract_questions(
|
|||
|
||||
# Extract, Clean Message from GPT's Response
|
||||
try:
|
||||
split_questions = (
|
||||
response.content.strip(empty_escape_sequences)
|
||||
.replace("['", '["')
|
||||
.replace("']", '"]')
|
||||
.replace("', '", '", "')
|
||||
.replace('["', "")
|
||||
.replace('"]', "")
|
||||
.split('", "')
|
||||
)
|
||||
questions = []
|
||||
|
||||
for question in split_questions:
|
||||
if question not in questions and _valid_question(question):
|
||||
questions.append(question)
|
||||
|
||||
if is_none_or_empty(questions):
|
||||
raise ValueError("GPT returned empty JSON")
|
||||
response = response.strip()
|
||||
response = json.loads(response)
|
||||
response = [q.strip() for q in response if q.strip()]
|
||||
if not isinstance(response, list) or not response or len(response) == 0:
|
||||
logger.error(f"Invalid response for constructing subqueries: {response}")
|
||||
return [text]
|
||||
return response
|
||||
except:
|
||||
logger.warning(f"GPT returned invalid JSON. Falling back to using user message as search query.\n{response}")
|
||||
questions = [text]
|
||||
|
@ -165,7 +152,7 @@ def converse(
|
|||
simplified_online_results[result] = online_results[result]["extracted_content"]
|
||||
|
||||
conversation_primer = f"{prompts.online_search_conversation.format(online_results=str(simplified_online_results))}\n{conversation_primer}"
|
||||
if ConversationCommand.Notes in conversation_commands:
|
||||
if not is_none_or_empty(compiled_references):
|
||||
conversation_primer = f"{prompts.notes_conversation.format(query=user_query, references=compiled_references)}\n{conversation_primer}"
|
||||
|
||||
# Setup Prompt with Primer or Conversation History
|
||||
|
|
|
@ -316,7 +316,7 @@ User: I've been having a hard time at work. I'm thinking of quitting.
|
|||
AI: I'm sorry to hear that. It's important to take care of your mental health. Have you considered talking to your manager about your concerns?
|
||||
|
||||
Q: What are the best ways to quit a job?
|
||||
Khoj: ["general"]
|
||||
Khoj: ["default"]
|
||||
|
||||
Example 3:
|
||||
Chat History:
|
||||
|
@ -329,14 +329,14 @@ Khoj: ["notes"]
|
|||
Example 4:
|
||||
Chat History:
|
||||
|
||||
Q: I want to make chocolate cake. What was my recipe?
|
||||
Khoj: ["notes"]
|
||||
Q: What's the latest news with the first company I worked for?
|
||||
Khoj: ["notes", "online"]
|
||||
|
||||
Example 5:
|
||||
Chat History:
|
||||
|
||||
Q: What's the latest news with the first company I worked for?
|
||||
Khoj: ["notes", "online"]
|
||||
Q: Who is Sandra?
|
||||
Khoj: ["default"]
|
||||
|
||||
Now it's your turn to pick the tools you would like to use to answer the user's question. Provide your response as a list of strings.
|
||||
|
||||
|
|
|
@ -60,7 +60,8 @@ async def search_with_google(query: str, conversation_history: dict, location: L
|
|||
return sub_response_dict
|
||||
|
||||
if SERPER_DEV_API_KEY is None:
|
||||
raise ValueError("SERPER_DEV_API_KEY is not set")
|
||||
logger.warn("SERPER_DEV_API_KEY is not set")
|
||||
return {}
|
||||
|
||||
# Breakdown the query into subqueries to get the correct answer
|
||||
subqueries = await generate_online_subqueries(query, conversation_history, location)
|
||||
|
|
|
@ -283,7 +283,10 @@ async def extract_references_and_questions(
|
|||
compiled_references: List[Any] = []
|
||||
inferred_queries: List[str] = []
|
||||
|
||||
if not ConversationCommand.Notes in conversation_commands:
|
||||
if (
|
||||
not ConversationCommand.Notes in conversation_commands
|
||||
and not ConversationCommand.Default in conversation_commands
|
||||
):
|
||||
return compiled_references, inferred_queries, q
|
||||
|
||||
if not await sync_to_async(EntryAdapters.user_has_entries)(user=user):
|
||||
|
|
|
@ -331,7 +331,8 @@ async def chat(
|
|||
user_name,
|
||||
)
|
||||
|
||||
chat_metadata.update({"conversation_command": ",".join([cmd.value for cmd in conversation_commands])})
|
||||
cmd_set = set([cmd.value for cmd in conversation_commands])
|
||||
chat_metadata["conversation_command"] = cmd_set
|
||||
|
||||
update_telemetry_state(
|
||||
request=request,
|
||||
|
|
|
@ -36,6 +36,7 @@ from khoj.utils import state
|
|||
from khoj.utils.config import GPT4AllProcessorModel
|
||||
from khoj.utils.helpers import (
|
||||
ConversationCommand,
|
||||
is_none_or_empty,
|
||||
log_telemetry,
|
||||
tool_descriptions_for_llm,
|
||||
)
|
||||
|
@ -175,6 +176,9 @@ async def aget_relevant_information_sources(query: str, conversation_history: di
|
|||
if llm_suggested_tool in tool_options.keys():
|
||||
# Check whether the tool exists as a valid ConversationCommand
|
||||
final_response.append(ConversationCommand(llm_suggested_tool))
|
||||
|
||||
if is_none_or_empty(final_response):
|
||||
final_response = [ConversationCommand.Default]
|
||||
return final_response
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid response for determining relevant tools: {response}")
|
||||
|
|
|
@ -283,9 +283,9 @@ command_descriptions = {
|
|||
}
|
||||
|
||||
tool_descriptions_for_llm = {
|
||||
ConversationCommand.Default: "Use this if there might be a mix of general and personal knowledge in the question, or if you can't make sense of the query",
|
||||
ConversationCommand.Default: "Use this if there might be a mix of general and personal knowledge in the question, or if you don't entirely understand the query.",
|
||||
ConversationCommand.General: "Use this when you can answer the question without any outside information or personal knowledge",
|
||||
ConversationCommand.Notes: "Use this when you would like to use the user's personal knowledge base to answer the question",
|
||||
ConversationCommand.Notes: "Use this when you would like to use the user's personal knowledge base to answer the question. This is especially helpful if the query seems to be missing context.",
|
||||
ConversationCommand.Online: "Use this when you would like to look up information on the internet",
|
||||
}
|
||||
|
||||
|
|
|
@ -22,15 +22,21 @@ fake = Faker()
|
|||
|
||||
# Helpers
|
||||
# ----------------------------------------------------------------------------------------------------
|
||||
def populate_chat_history(message_list, user):
|
||||
def generate_history(message_list):
|
||||
# Generate conversation logs
|
||||
conversation_log = {"chat": []}
|
||||
for user_message, llm_message, context in message_list:
|
||||
for user_message, gpt_message, context in message_list:
|
||||
conversation_log["chat"] += message_to_log(
|
||||
user_message,
|
||||
llm_message,
|
||||
gpt_message,
|
||||
{"context": context, "intent": {"query": user_message, "inferred-queries": f'["{user_message}"]'}},
|
||||
)
|
||||
return conversation_log
|
||||
|
||||
|
||||
def populate_chat_history(message_list, user):
|
||||
# Generate conversation logs
|
||||
conversation_log = generate_history(message_list)
|
||||
|
||||
# Update Conversation Metadata Logs in Database
|
||||
ConversationFactory(user=user, conversation_log=conversation_log)
|
||||
|
@ -515,8 +521,9 @@ async def test_get_correct_tools_with_chat_history(client_offline_chat):
|
|||
(
|
||||
"Let's talk about the current events around the world.",
|
||||
"Sure, let's discuss the current events. What would you like to know?",
|
||||
[],
|
||||
),
|
||||
("What's up in New York City?", "A Pride parade has recently been held in New York City, on July 31st."),
|
||||
("What's up in New York City?", "A Pride parade has recently been held in New York City, on July 31st.", []),
|
||||
]
|
||||
chat_history = populate_chat_history(chat_log)
|
||||
|
||||
|
@ -526,15 +533,3 @@ async def test_get_correct_tools_with_chat_history(client_offline_chat):
|
|||
# Assert
|
||||
tools = [tool.value for tool in tools]
|
||||
assert tools == ["online"]
|
||||
|
||||
|
||||
def populate_chat_history(message_list):
|
||||
# Generate conversation logs
|
||||
conversation_log = {"chat": []}
|
||||
for user_message, gpt_message in message_list:
|
||||
conversation_log["chat"] += message_to_log(
|
||||
user_message,
|
||||
gpt_message,
|
||||
{"context": [], "intent": {"query": user_message, "inferred-queries": f'["{user_message}"]'}},
|
||||
)
|
||||
return conversation_log
|
||||
|
|
|
@ -22,7 +22,7 @@ if api_key is None:
|
|||
|
||||
# Helpers
|
||||
# ----------------------------------------------------------------------------------------------------
|
||||
def populate_chat_history(message_list, user=None):
|
||||
def generate_history(message_list):
|
||||
# Generate conversation logs
|
||||
conversation_log = {"chat": []}
|
||||
for user_message, gpt_message, context in message_list:
|
||||
|
@ -31,6 +31,12 @@ def populate_chat_history(message_list, user=None):
|
|||
gpt_message,
|
||||
{"context": context, "intent": {"query": user_message, "inferred-queries": f'["{user_message}"]'}},
|
||||
)
|
||||
return conversation_log
|
||||
|
||||
|
||||
def populate_chat_history(message_list, user):
|
||||
# Generate conversation logs
|
||||
conversation_log = generate_history(message_list)
|
||||
|
||||
# Update Conversation Metadata Logs in Database
|
||||
ConversationFactory(user=user, conversation_log=conversation_log)
|
||||
|
@ -491,10 +497,11 @@ async def test_get_correct_tools_with_chat_history(chat_client):
|
|||
(
|
||||
"Let's talk about the current events around the world.",
|
||||
"Sure, let's discuss the current events. What would you like to know?",
|
||||
[],
|
||||
),
|
||||
("What's up in New York City?", "A Pride parade has recently been held in New York City, on July 31st."),
|
||||
("What's up in New York City?", "A Pride parade has recently been held in New York City, on July 31st.", []),
|
||||
]
|
||||
chat_history = populate_chat_history(chat_log)
|
||||
chat_history = generate_history(chat_log)
|
||||
|
||||
# Act
|
||||
tools = await aget_relevant_information_sources(user_query, chat_history)
|
||||
|
@ -502,15 +509,3 @@ async def test_get_correct_tools_with_chat_history(chat_client):
|
|||
# Assert
|
||||
tools = [tool.value for tool in tools]
|
||||
assert tools == ["online"]
|
||||
|
||||
|
||||
def populate_chat_history(message_list):
|
||||
# Generate conversation logs
|
||||
conversation_log = {"chat": []}
|
||||
for user_message, gpt_message in message_list:
|
||||
conversation_log["chat"] += message_to_log(
|
||||
user_message,
|
||||
gpt_message,
|
||||
{"context": [], "intent": {"query": user_message, "inferred-queries": f'["{user_message}"]'}},
|
||||
)
|
||||
return conversation_log
|
||||
|
|
Loading…
Reference in a new issue