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:
sabaimran 2024-02-20 13:55:35 -08:00 committed by GitHub
parent 138f5223bd
commit 44f8f20ea7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 348 additions and 331 deletions

View file

@ -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;
}

View file

@ -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;

View file

@ -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,

View file

@ -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)

View file

@ -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);
});

View file

@ -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

View file

@ -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),
]

View file

@ -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);

View file

@ -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()">

View file

@ -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()

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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):

View file

@ -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,

View file

@ -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}")

View file

@ -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",
}

View file

@ -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

View file

@ -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