Convert the default conversation id to a uuid, plus other fixes (#918)

* Update the conversation_id primary key field to be a uuid

- update associated API endpoints
- this is to improve the overall application health, by obfuscating some information about the internal database
- conversation_id type is now implicitly a string, rather than an int
- ensure automations are also migrated in place, such that the conversation_ids they're pointing to are now mapped to the new IDs

* Update client-side API calls to correctly query with a string field

* Allow modifying of conversation properties from the chat title

* Improve drag and drop file experience for chat input area

* Use a phosphor icon for the copy to clipboard experience for code snippets

* Update conversation_id parameter to be a str type

* If django_apscheduler is not in the environment, skip the migration script

* Fix create automation flow by storing conversation id as string

The new UUID used for conversation id can't be directly serialized.
Convert to string for serializing it for later execution

---------

Co-authored-by: Debanjum Singh Solanky <debanjum@gmail.com>
This commit is contained in:
sabaimran
2024-09-24 14:12:50 -07:00
committed by GitHub
parent 0c936cecc0
commit 06777e1660
19 changed files with 213 additions and 66 deletions

View File

@@ -152,7 +152,7 @@
const chatApi = `${hostURL}/api/chat?client=desktop`; const chatApi = `${hostURL}/api/chat?client=desktop`;
const chatApiBody = { const chatApiBody = {
q: query, q: query,
conversation_id: parseInt(conversationID), conversation_id: conversationID,
stream: true, stream: true,
...(!!city && { city: city }), ...(!!city && { city: city }),
...(!!region && { region: region }), ...(!!region && { region: region }),

View File

@@ -405,7 +405,7 @@
const chatApi = `${hostURL}/api/chat?client=desktop`; const chatApi = `${hostURL}/api/chat?client=desktop`;
const chatApiBody = { const chatApiBody = {
q: query, q: query,
conversation_id: parseInt(conversationID), conversation_id: conversationID,
stream: true, stream: true,
...(!!city && { city: city }), ...(!!city && { city: city }),
...(!!region && { region: region }), ...(!!region && { region: region }),

View File

@@ -1055,7 +1055,7 @@ export class KhojChatView extends KhojPaneView {
q: query, q: query,
n: this.setting.resultsCount, n: this.setting.resultsCount,
stream: true, stream: true,
...(!!conversationId && { conversation_id: parseInt(conversationId) }), ...(!!conversationId && { conversation_id: conversationId }),
...(!!this.location && { ...(!!this.location && {
city: this.location.city, city: this.location.city,
region: this.location.region, region: this.location.region,

View File

@@ -31,7 +31,7 @@ input.inputBox:focus {
} }
div.inputBox:focus { div.inputBox:focus {
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
} }
div.chatBodyFull { div.chatBodyFull {
@@ -94,6 +94,9 @@ div.agentIndicator {
padding: 10px; padding: 10px;
} }
div.chatTitleWrapper {
grid-template-columns: auto 1fr;
}
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
div.inputBox { div.inputBox {

View File

@@ -3,7 +3,7 @@
import styles from "./chat.module.css"; import styles from "./chat.module.css";
import React, { Suspense, useEffect, useState } from "react"; import React, { Suspense, useEffect, useState } from "react";
import SidePanel from "../components/sidePanel/chatHistorySidePanel"; import SidePanel, { ChatSessionActionMenu } from "../components/sidePanel/chatHistorySidePanel";
import ChatHistory from "../components/chatHistory/chatHistory"; import ChatHistory from "../components/chatHistory/chatHistory";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
import Loading from "../components/loading/loading"; import Loading from "../components/loading/loading";
@@ -17,6 +17,8 @@ import { useIPLocationData, useIsMobileWidth, welcomeConsole } from "../common/u
import ChatInputArea, { ChatOptions } from "../components/chatInputArea/chatInputArea"; import ChatInputArea, { ChatOptions } from "../components/chatInputArea/chatInputArea";
import { useAuthenticatedData } from "../common/auth"; import { useAuthenticatedData } from "../common/auth";
import { AgentData } from "../agents/page"; import { AgentData } from "../agents/page";
import { DotsThreeVertical } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button";
interface ChatBodyDataProps { interface ChatBodyDataProps {
chatOptionsData: ChatOptions | null; chatOptionsData: ChatOptions | null;
@@ -104,7 +106,7 @@ function ChatBodyData(props: ChatBodyDataProps) {
/> />
</div> </div>
<div <div
className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-t-2xl rounded-b-none md:rounded-xl`} className={`${styles.inputBox} p-1 md:px-2 shadow-md bg-background align-middle items-center justify-center dark:bg-neutral-700 dark:border-0 dark:shadow-sm rounded-t-2xl rounded-b-none md:rounded-xl h-fit`}
> >
<ChatInputArea <ChatInputArea
agentColor={agentMetadata?.color} agentColor={agentMetadata?.color}
@@ -133,6 +135,7 @@ export default function Chat() {
const [processQuerySignal, setProcessQuerySignal] = useState(false); const [processQuerySignal, setProcessQuerySignal] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]); const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
const [image64, setImage64] = useState<string>(""); const [image64, setImage64] = useState<string>("");
const locationData = useIPLocationData(); const locationData = useIPLocationData();
const authenticatedData = useAuthenticatedData(); const authenticatedData = useAuthenticatedData();
const isMobileWidth = useIsMobileWidth(); const isMobileWidth = useIsMobileWidth();
@@ -235,7 +238,7 @@ export default function Chat() {
const chatAPI = "/api/chat?client=web"; const chatAPI = "/api/chat?client=web";
const chatAPIBody = { const chatAPIBody = {
q: queryToProcess, q: queryToProcess,
conversation_id: parseInt(conversationId), conversation_id: conversationId,
stream: true, stream: true,
...(locationData && { ...(locationData && {
region: locationData.region, region: locationData.region,
@@ -297,17 +300,22 @@ export default function Chat() {
</div> </div>
<div className={styles.chatBox}> <div className={styles.chatBox}>
<div className={styles.chatBoxBody}> <div className={styles.chatBoxBody}>
{!isMobileWidth && ( {!isMobileWidth && conversationId && (
<div <div
className={`text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8`} className={`${styles.chatTitleWrapper} text-nowrap text-ellipsis overflow-hidden max-w-screen-md grid items-top font-bold mr-8 pt-6 col-auto h-fit`}
> >
{title && ( {title && (
<h2 <h2
className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden pt-6`} className={`text-lg text-ellipsis whitespace-nowrap overflow-x-hidden`}
> >
{title} {title}
</h2> </h2>
)} )}
<ChatSessionActionMenu
conversationId={conversationId}
setTitle={setTitle}
sizing="md"
/>
</div> </div>
)} )}
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>

View File

@@ -177,7 +177,6 @@ export function modifyFileFilterForConversation(
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
console.log("ADDEDFILES DATA: ", data);
setAddedFiles(data); setAddedFiles(data);
}) })
.catch((err) => { .catch((err) => {

View File

@@ -144,7 +144,7 @@ export default function ChatHistory(props: ChatHistoryProps) {
let conversationFetchURL = ""; let conversationFetchURL = "";
if (props.conversationId) { if (props.conversationId) {
conversationFetchURL = `/api/chat/history?client=web&conversation_id=${props.conversationId}&n=${10 * nextPage}`; conversationFetchURL = `/api/chat/history?client=web&conversation_id=${encodeURIComponent(props.conversationId)}&n=${10 * nextPage}`;
} else if (props.publicConversationSlug) { } else if (props.publicConversationSlug) {
conversationFetchURL = `/api/chat/share/history?client=web&public_conversation_slug=${props.publicConversationSlug}&n=${10 * nextPage}`; conversationFetchURL = `/api/chat/share/history?client=web&public_conversation_slug=${props.publicConversationSlug}&n=${10 * nextPage}`;
} else { } else {

View File

@@ -442,7 +442,7 @@ export default function ChatInputArea(props: ChatInputProps) {
</div> </div>
)} )}
<div <div
className={`${styles.actualInputArea} items-center justify-between dark:bg-neutral-700 relative`} className={`${styles.actualInputArea} items-center justify-between dark:bg-neutral-700 relative ${isDragAndDropping && "animate-pulse"}`}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDragAndDropFiles} onDrop={handleDragAndDropFiles}
@@ -547,7 +547,6 @@ export default function ChatInputArea(props: ChatInputProps) {
<ArrowUp className="w-6 h-6" weight="bold" /> <ArrowUp className="w-6 h-6" weight="bold" />
</Button> </Button>
</div> </div>
{isDragAndDropping && <div className="text-muted-foreground">Drop file to upload</div>}
</> </>
); );
} }

View File

@@ -5,6 +5,7 @@ import styles from "./chatMessage.module.css";
import markdownIt from "markdown-it"; import markdownIt from "markdown-it";
import mditHljs from "markdown-it-highlightjs"; import mditHljs from "markdown-it-highlightjs";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
@@ -23,6 +24,7 @@ import {
MagnifyingGlass, MagnifyingGlass,
Pause, Pause,
Palette, Palette,
ClipboardText,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
@@ -377,12 +379,9 @@ export default function ChatMessage(props: ChatMessageProps) {
const preElements = messageRef.current.querySelectorAll("pre > .hljs"); const preElements = messageRef.current.querySelectorAll("pre > .hljs");
preElements.forEach((preElement) => { preElements.forEach((preElement) => {
const copyButton = document.createElement("button"); const copyButton = document.createElement("button");
const copyImage = document.createElement("img"); const copyIcon = <ClipboardText size={24} weight="bold" />;
copyImage.src = "/static/copy-button.svg"; createRoot(copyButton).render(copyIcon);
copyImage.alt = "Copy";
copyImage.width = 24;
copyImage.height = 24;
copyButton.appendChild(copyImage);
copyButton.className = `hljs ${styles.codeCopyButton}`; copyButton.className = `hljs ${styles.codeCopyButton}`;
copyButton.addEventListener("click", () => { copyButton.addEventListener("click", () => {
let textContent = preElement.textContent || ""; let textContent = preElement.textContent || "";
@@ -392,7 +391,6 @@ export default function ChatMessage(props: ChatMessageProps) {
textContent = textContent.replace(/^Copy/, ""); textContent = textContent.replace(/^Copy/, "");
textContent = textContent.trim(); textContent = textContent.trim();
navigator.clipboard.writeText(textContent); navigator.clipboard.writeText(textContent);
copyImage.src = "/static/copy-button-success.svg";
}); });
preElement.prepend(copyButton); preElement.prepend(copyButton);
}); });

View File

@@ -182,9 +182,12 @@ function FilesMenu(props: FilesMenuProps) {
useEffect(() => { useEffect(() => {
if (!files) return; if (!files) return;
const uniqueFiles = Array.from(new Set(files));
// First, sort lexically // First, sort lexically
files.sort(); uniqueFiles.sort();
let sortedFiles = files;
let sortedFiles = uniqueFiles;
if (addedFiles) { if (addedFiles) {
sortedFiles = addedFiles.concat( sortedFiles = addedFiles.concat(
@@ -458,12 +461,13 @@ function SessionsAndFiles(props: SessionsAndFilesProps) {
); );
} }
interface ChatSessionActionMenuProps { export interface ChatSessionActionMenuProps {
conversationId: string; conversationId: string;
setTitle: (title: string) => void; setTitle: (title: string) => void;
sizing?: "sm" | "md" | "lg";
} }
function ChatSessionActionMenu(props: ChatSessionActionMenuProps) { export function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
const [renamedTitle, setRenamedTitle] = useState(""); const [renamedTitle, setRenamedTitle] = useState("");
const [isRenaming, setIsRenaming] = useState(false); const [isRenaming, setIsRenaming] = useState(false);
const [isSharing, setIsSharing] = useState(false); const [isSharing, setIsSharing] = useState(false);
@@ -596,10 +600,25 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
); );
} }
function sizeClass() {
switch (props.sizing) {
case "sm":
return "h-4 w-4";
case "md":
return "h-6 w-6";
case "lg":
return "h-8 w-8";
default:
return "h-4 w-4";
}
}
const size = sizeClass();
return ( return (
<DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}> <DropdownMenu onOpenChange={(open) => setIsOpen(open)} open={isOpen}>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<DotsThreeVertical className="h-4 w-4" /> <DotsThreeVertical className={`${size}`} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem> <DropdownMenuItem>
@@ -608,7 +627,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
variant={"ghost"} variant={"ghost"}
onClick={() => setIsRenaming(true)} onClick={() => setIsRenaming(true)}
> >
<Pencil className="mr-2 h-4 w-4" /> <Pencil className={`mr-2 ${size}`} />
Rename Rename
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
@@ -618,7 +637,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
variant={"ghost"} variant={"ghost"}
onClick={() => setIsSharing(true)} onClick={() => setIsSharing(true)}
> >
<Share className="mr-2 h-4 w-4" /> <Share className={`mr-2 ${size}`} />
Share Share
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
@@ -628,7 +647,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
variant={"ghost"} variant={"ghost"}
onClick={() => setIsDeleting(true)} onClick={() => setIsDeleting(true)}
> >
<Trash className="mr-2 h-4 w-4" /> <Trash className={`mr-2 ${size}`} />
Delete Delete
</Button> </Button>
</DropdownMenuItem> </DropdownMenuItem>
@@ -640,15 +659,14 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) {
function ChatSession(props: ChatHistory) { function ChatSession(props: ChatHistory) {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [title, setTitle] = useState(props.slug || "New Conversation 🌱"); const [title, setTitle] = useState(props.slug || "New Conversation 🌱");
var currConversationId = parseInt( var currConversationId =
new URLSearchParams(window.location.search).get("conversationId") || "-1", new URLSearchParams(window.location.search).get("conversationId") || "-1";
);
return ( return (
<div <div
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
key={props.conversation_id} key={props.conversation_id}
className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === parseInt(props.conversation_id) && currConversationId != -1 ? "dark:bg-neutral-800 bg-white" : ""}`} className={`${styles.session} ${props.compressed ? styles.compressed : "!max-w-full"} ${isHovered ? `${styles.sessionHover}` : ""} ${currConversationId === props.conversation_id && currConversationId != "-1" ? "dark:bg-neutral-800 bg-white" : ""}`}
> >
<Link <Link
href={`/chat?conversationId=${props.conversation_id}`} href={`/chat?conversationId=${props.conversation_id}`}

View File

@@ -225,7 +225,7 @@ export default function SharedChat() {
const chatAPI = "/api/chat?client=web"; const chatAPI = "/api/chat?client=web";
const chatAPIBody = { const chatAPIBody = {
q: queryToProcess, q: queryToProcess,
conversation_id: parseInt(conversationId), conversation_id: conversationId,
stream: true, stream: true,
...(locationData && { ...(locationData && {
region: locationData.region, region: locationData.region,

View File

@@ -663,7 +663,7 @@ class ConversationAdapters:
@staticmethod @staticmethod
def get_conversation_by_user( def get_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None
) -> Optional[Conversation]: ) -> Optional[Conversation]:
if conversation_id: if conversation_id:
conversation = ( conversation = (
@@ -689,7 +689,7 @@ class ConversationAdapters:
@staticmethod @staticmethod
async def aset_conversation_title( async def aset_conversation_title(
user: KhojUser, client_application: ClientApplication, conversation_id: int, title: str user: KhojUser, client_application: ClientApplication, conversation_id: str, title: str
): ):
conversation = await Conversation.objects.filter( conversation = await Conversation.objects.filter(
user=user, client=client_application, id=conversation_id user=user, client=client_application, id=conversation_id
@@ -701,7 +701,7 @@ class ConversationAdapters:
return None return None
@staticmethod @staticmethod
def get_conversation_by_id(conversation_id: int): def get_conversation_by_id(conversation_id: str):
return Conversation.objects.filter(id=conversation_id).first() return Conversation.objects.filter(id=conversation_id).first()
@staticmethod @staticmethod
@@ -730,7 +730,7 @@ class ConversationAdapters:
@staticmethod @staticmethod
async def aget_conversation_by_user( async def aget_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None, title: str = None user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None, title: str = None
) -> Optional[Conversation]: ) -> Optional[Conversation]:
query = Conversation.objects.filter(user=user, client=client_application).prefetch_related("agent") query = Conversation.objects.filter(user=user, client=client_application).prefetch_related("agent")
@@ -747,7 +747,7 @@ class ConversationAdapters:
@staticmethod @staticmethod
async def adelete_conversation_by_user( async def adelete_conversation_by_user(
user: KhojUser, client_application: ClientApplication = None, conversation_id: int = None user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None
): ):
if conversation_id: if conversation_id:
return await Conversation.objects.filter(user=user, client=client_application, id=conversation_id).adelete() return await Conversation.objects.filter(user=user, client=client_application, id=conversation_id).adelete()
@@ -900,7 +900,7 @@ class ConversationAdapters:
user: KhojUser, user: KhojUser,
conversation_log: dict, conversation_log: dict,
client_application: ClientApplication = None, client_application: ClientApplication = None,
conversation_id: int = None, conversation_id: str = None,
user_message: str = None, user_message: str = None,
): ):
slug = user_message.strip()[:200] if user_message else None slug = user_message.strip()[:200] if user_message else None
@@ -1042,7 +1042,7 @@ class ConversationAdapters:
return new_config return new_config
@staticmethod @staticmethod
def add_files_to_filter(user: KhojUser, conversation_id: int, files: List[str]): def add_files_to_filter(user: KhojUser, conversation_id: str, files: List[str]):
conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id) conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
file_list = EntryAdapters.get_all_filenames_by_source(user, "computer") file_list = EntryAdapters.get_all_filenames_by_source(user, "computer")
for filename in files: for filename in files:
@@ -1056,7 +1056,7 @@ class ConversationAdapters:
return conversation.file_filters return conversation.file_filters
@staticmethod @staticmethod
def remove_files_from_filter(user: KhojUser, conversation_id: int, files: List[str]): def remove_files_from_filter(user: KhojUser, conversation_id: str, files: List[str]):
conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id) conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
for filename in files: for filename in files:
if filename in conversation.file_filters: if filename in conversation.file_filters:

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.0.8 on 2024-09-19 15:53
import uuid
from django.db import migrations, models
def create_uuid(apps, schema_editor):
Conversation = apps.get_model("database", "Conversation")
for conversation in Conversation.objects.all():
conversation.temp_id = uuid.uuid4()
conversation.save()
def remove_uuid(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("database", "0062_merge_20240913_0222"),
]
operations = [
migrations.AddField(
model_name="conversation",
name="temp_id",
field=models.UUIDField(default=uuid.uuid4, editable=False),
),
migrations.RunPython(create_uuid, reverse_code=remove_uuid),
migrations.AlterField(
model_name="conversation",
name="temp_id",
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
]

View File

@@ -0,0 +1,86 @@
# Generated by Django 5.0.8 on 2024-09-19 15:59
import json
import pickle
import uuid
from django.db import migrations, models
def reverse_remove_bigint_id(apps, schema_editor):
Conversation = apps.get_model("database", "Conversation")
index = 1
for conversation in Conversation.objects.all():
conversation.id = index
conversation.save()
index += 1
def update_conversation_id_in_job_state(apps, schema_editor):
try:
DjangoJob = apps.get_model("django_apscheduler", "DjangoJob")
Conversation = apps.get_model("database", "Conversation")
for job in DjangoJob.objects.all():
job_state = pickle.loads(job.job_state)
kwargs = job_state.get("kwargs")
conversation_id = kwargs.get("conversation_id") if kwargs else None
automation_metadata = json.loads(job_state.get("name", "{}"))
if not conversation_id:
job.delete()
if conversation_id:
try:
conversation = Conversation.objects.get(id=conversation_id)
automation_metadata["conversation_id"] = str(conversation.temp_id)
name = json.dumps(automation_metadata)
job_state["name"] = name
job_state["kwargs"]["conversation_id"] = str(conversation.temp_id)
job.job_state = pickle.dumps(job_state)
job.save()
except Conversation.DoesNotExist:
pass
except LookupError as e:
pass
def no_op(apps, schema_editor):
pass
def disable_triggers(apps, schema_editor):
schema_editor.execute('ALTER TABLE "database_conversation" DISABLE TRIGGER ALL;')
def enable_triggers(apps, schema_editor):
schema_editor.execute('ALTER TABLE "database_conversation" ENABLE TRIGGER ALL;')
class Migration(migrations.Migration):
dependencies = [
("database", "0063_conversation_temp_id"),
]
operations = [
migrations.RunPython(no_op, reverse_code=enable_triggers),
migrations.RunPython(update_conversation_id_in_job_state, reverse_code=no_op),
migrations.RemoveField(
model_name="conversation",
name="id",
),
migrations.RenameField(
model_name="conversation",
old_name="temp_id",
new_name="id",
),
migrations.AlterField(
model_name="conversation",
name="id",
field=models.UUIDField(
db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
),
),
migrations.RunPython(no_op, reverse_code=reverse_remove_bigint_id),
migrations.RunPython(no_op, reverse_code=disable_triggers),
]

View File

@@ -350,6 +350,7 @@ class Conversation(BaseModel):
title = models.CharField(max_length=200, default=None, null=True, blank=True) title = models.CharField(max_length=200, default=None, null=True, blank=True)
agent = models.ForeignKey(Agent, on_delete=models.SET_NULL, default=None, null=True, blank=True) agent = models.ForeignKey(Agent, on_delete=models.SET_NULL, default=None, null=True, blank=True)
file_filters = models.JSONField(default=list) file_filters = models.JSONField(default=list)
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True, db_index=True)
class PublicConversation(BaseModel): class PublicConversation(BaseModel):

View File

@@ -107,7 +107,7 @@ def save_to_conversation_log(
inferred_queries: List[str] = [], inferred_queries: List[str] = [],
intent_type: str = "remember", intent_type: str = "remember",
client_application: ClientApplication = None, client_application: ClientApplication = None,
conversation_id: int = None, conversation_id: str = None,
automation_id: str = None, automation_id: str = None,
uploaded_image_url: str = None, uploaded_image_url: str = None,
): ):

View File

@@ -328,7 +328,7 @@ async def extract_references_and_questions(
q: str, q: str,
n: int, n: int,
d: float, d: float,
conversation_id: int, conversation_id: str,
conversation_commands: List[ConversationCommand] = [ConversationCommand.Default], conversation_commands: List[ConversationCommand] = [ConversationCommand.Default],
location_data: LocationData = None, location_data: LocationData = None,
send_status_func: Optional[Callable] = None, send_status_func: Optional[Callable] = None,

View File

@@ -77,9 +77,7 @@ from khoj.routers.email import send_query_feedback
@api_chat.get("/conversation/file-filters/{conversation_id}", response_class=Response) @api_chat.get("/conversation/file-filters/{conversation_id}", response_class=Response)
@requires(["authenticated"]) @requires(["authenticated"])
def get_file_filter(request: Request, conversation_id: str) -> Response: def get_file_filter(request: Request, conversation_id: str) -> Response:
conversation = ConversationAdapters.get_conversation_by_user( conversation = ConversationAdapters.get_conversation_by_user(request.user.object, conversation_id=conversation_id)
request.user.object, conversation_id=int(conversation_id)
)
if not conversation: if not conversation:
return Response(content=json.dumps({"status": "error", "message": "Conversation not found"}), status_code=404) return Response(content=json.dumps({"status": "error", "message": "Conversation not found"}), status_code=404)
@@ -95,7 +93,7 @@ def get_file_filter(request: Request, conversation_id: str) -> Response:
@api_chat.delete("/conversation/file-filters/bulk", response_class=Response) @api_chat.delete("/conversation/file-filters/bulk", response_class=Response)
@requires(["authenticated"]) @requires(["authenticated"])
def remove_files_filter(request: Request, filter: FilesFilterRequest) -> Response: def remove_files_filter(request: Request, filter: FilesFilterRequest) -> Response:
conversation_id = int(filter.conversation_id) conversation_id = filter.conversation_id
files_filter = filter.filenames files_filter = filter.filenames
file_filters = ConversationAdapters.remove_files_from_filter(request.user.object, conversation_id, files_filter) file_filters = ConversationAdapters.remove_files_from_filter(request.user.object, conversation_id, files_filter)
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200) return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
@@ -105,7 +103,7 @@ def remove_files_filter(request: Request, filter: FilesFilterRequest) -> Respons
@requires(["authenticated"]) @requires(["authenticated"])
def add_files_filter(request: Request, filter: FilesFilterRequest): def add_files_filter(request: Request, filter: FilesFilterRequest):
try: try:
conversation_id = int(filter.conversation_id) conversation_id = filter.conversation_id
files_filter = filter.filenames files_filter = filter.filenames
file_filters = ConversationAdapters.add_files_to_filter(request.user.object, conversation_id, files_filter) file_filters = ConversationAdapters.add_files_to_filter(request.user.object, conversation_id, files_filter)
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200) return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
@@ -118,7 +116,7 @@ def add_files_filter(request: Request, filter: FilesFilterRequest):
@requires(["authenticated"]) @requires(["authenticated"])
def add_file_filter(request: Request, filter: FileFilterRequest): def add_file_filter(request: Request, filter: FileFilterRequest):
try: try:
conversation_id = int(filter.conversation_id) conversation_id = filter.conversation_id
files_filter = [filter.filename] files_filter = [filter.filename]
file_filters = ConversationAdapters.add_files_to_filter(request.user.object, conversation_id, files_filter) file_filters = ConversationAdapters.add_files_to_filter(request.user.object, conversation_id, files_filter)
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200) return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
@@ -130,7 +128,7 @@ def add_file_filter(request: Request, filter: FileFilterRequest):
@api_chat.delete("/conversation/file-filters", response_class=Response) @api_chat.delete("/conversation/file-filters", response_class=Response)
@requires(["authenticated"]) @requires(["authenticated"])
def remove_file_filter(request: Request, filter: FileFilterRequest) -> Response: def remove_file_filter(request: Request, filter: FileFilterRequest) -> Response:
conversation_id = int(filter.conversation_id) conversation_id = filter.conversation_id
files_filter = [filter.filename] files_filter = [filter.filename]
file_filters = ConversationAdapters.remove_files_from_filter(request.user.object, conversation_id, files_filter) file_filters = ConversationAdapters.remove_files_from_filter(request.user.object, conversation_id, files_filter)
return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200) return Response(content=json.dumps(file_filters), media_type="application/json", status_code=200)
@@ -189,7 +187,7 @@ async def chat_starters(
def chat_history( def chat_history(
request: Request, request: Request,
common: CommonQueryParams, common: CommonQueryParams,
conversation_id: Optional[int] = None, conversation_id: Optional[str] = None,
n: Optional[int] = None, n: Optional[int] = None,
): ):
user = request.user.object user = request.user.object
@@ -312,7 +310,7 @@ def get_shared_chat(
async def clear_chat_history( async def clear_chat_history(
request: Request, request: Request,
common: CommonQueryParams, common: CommonQueryParams,
conversation_id: Optional[int] = None, conversation_id: Optional[str] = None,
): ):
user = request.user.object user = request.user.object
@@ -375,7 +373,7 @@ def fork_public_conversation(
def duplicate_chat_history_public_conversation( def duplicate_chat_history_public_conversation(
request: Request, request: Request,
common: CommonQueryParams, common: CommonQueryParams,
conversation_id: int, conversation_id: str,
): ):
user = request.user.object user = request.user.object
domain = request.headers.get("host") domain = request.headers.get("host")
@@ -423,7 +421,7 @@ def chat_sessions(
session_values = [ session_values = [
{ {
"conversation_id": session[0], "conversation_id": str(session[0]),
"slug": session[2] or session[1], "slug": session[2] or session[1],
"agent_name": session[4], "agent_name": session[4],
"agent_avatar": session[5], "agent_avatar": session[5],
@@ -455,7 +453,7 @@ async def create_chat_session(
# Create new Conversation Session # Create new Conversation Session
conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app, agent_slug) conversation = await ConversationAdapters.acreate_conversation_session(user, request.user.client_app, agent_slug)
response = {"conversation_id": conversation.id} response = {"conversation_id": str(conversation.id)}
conversation_metadata = { conversation_metadata = {
"agent": agent_slug, "agent": agent_slug,
@@ -497,7 +495,7 @@ async def set_conversation_title(
request: Request, request: Request,
common: CommonQueryParams, common: CommonQueryParams,
title: str, title: str,
conversation_id: Optional[int] = None, conversation_id: Optional[str] = None,
) -> Response: ) -> Response:
user = request.user.object user = request.user.object
title = title.strip()[:200] title = title.strip()[:200]
@@ -527,7 +525,7 @@ class ChatRequestBody(BaseModel):
d: Optional[float] = None d: Optional[float] = None
stream: Optional[bool] = False stream: Optional[bool] = False
title: Optional[str] = None title: Optional[str] = None
conversation_id: Optional[int] = None conversation_id: Optional[str] = None
city: Optional[str] = None city: Optional[str] = None
region: Optional[str] = None region: Optional[str] = None
country: Optional[str] = None country: Optional[str] = None
@@ -1016,7 +1014,7 @@ async def get_chat(
d: float = None, d: float = None,
stream: Optional[bool] = False, stream: Optional[bool] = False,
title: Optional[str] = None, title: Optional[str] = None,
conversation_id: Optional[int] = None, conversation_id: Optional[str] = None,
city: Optional[str] = None, city: Optional[str] = None,
region: Optional[str] = None, region: Optional[str] = None,
country: Optional[str] = None, country: Optional[str] = None,

View File

@@ -21,7 +21,7 @@ from typing import (
Tuple, Tuple,
Union, Union,
) )
from urllib.parse import parse_qs, urljoin, urlparse from urllib.parse import parse_qs, quote, urljoin, urlparse
import cron_descriptor import cron_descriptor
import pytz import pytz
@@ -799,7 +799,7 @@ def generate_chat_response(
conversation_commands: List[ConversationCommand] = [ConversationCommand.Default], conversation_commands: List[ConversationCommand] = [ConversationCommand.Default],
user: KhojUser = None, user: KhojUser = None,
client_application: ClientApplication = None, client_application: ClientApplication = None,
conversation_id: int = None, conversation_id: str = None,
location_data: LocationData = None, location_data: LocationData = None,
user_name: Optional[str] = None, user_name: Optional[str] = None,
uploaded_image_url: Optional[str] = None, uploaded_image_url: Optional[str] = None,
@@ -1102,7 +1102,7 @@ def scheduled_chat(
user: KhojUser, user: KhojUser,
calling_url: URL, calling_url: URL,
job_id: str = None, job_id: str = None,
conversation_id: int = None, conversation_id: str = None,
): ):
logger.info(f"Processing scheduled_chat: {query_to_run}") logger.info(f"Processing scheduled_chat: {query_to_run}")
if job_id: if job_id:
@@ -1131,7 +1131,8 @@ def scheduled_chat(
# Replace the original conversation_id with the conversation_id # Replace the original conversation_id with the conversation_id
if conversation_id: if conversation_id:
query_dict["conversation_id"] = [conversation_id] # encode the conversation_id to avoid any issues with special characters
query_dict["conversation_id"] = [quote(conversation_id)]
# Restructure the original query_dict into a valid JSON payload for the chat API # Restructure the original query_dict into a valid JSON payload for the chat API
json_payload = {key: values[0] for key, values in query_dict.items()} json_payload = {key: values[0] for key, values in query_dict.items()}
@@ -1185,7 +1186,7 @@ def scheduled_chat(
async def create_automation( async def create_automation(
q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}, conversation_id: int = None q: str, timezone: str, user: KhojUser, calling_url: URL, meta_log: dict = {}, conversation_id: str = None
): ):
crontime, query_to_run, subject = await schedule_query(q, meta_log) crontime, query_to_run, subject = await schedule_query(q, meta_log)
job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url, conversation_id) job = await schedule_automation(query_to_run, subject, crontime, timezone, q, user, calling_url, conversation_id)
@@ -1200,7 +1201,7 @@ async def schedule_automation(
scheduling_request: str, scheduling_request: str,
user: KhojUser, user: KhojUser,
calling_url: URL, calling_url: URL,
conversation_id: int, conversation_id: str,
): ):
# Disable minute level automation recurrence # Disable minute level automation recurrence
minute_value = crontime.split(" ")[0] minute_value = crontime.split(" ")[0]
@@ -1218,7 +1219,7 @@ async def schedule_automation(
"scheduling_request": scheduling_request, "scheduling_request": scheduling_request,
"subject": subject, "subject": subject,
"crontime": crontime, "crontime": crontime,
"conversation_id": conversation_id, "conversation_id": str(conversation_id),
} }
) )
query_id = hashlib.md5(f"{query_to_run}_{crontime}".encode("utf-8")).hexdigest() query_id = hashlib.md5(f"{query_to_run}_{crontime}".encode("utf-8")).hexdigest()