From 06777e16606fd1b6be79671f517be9361a84cd51 Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:12:50 -0700 Subject: [PATCH] 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 --- src/interface/desktop/chat.html | 2 +- src/interface/desktop/shortcut.html | 2 +- src/interface/obsidian/src/chat_view.ts | 2 +- src/interface/web/app/chat/chat.module.css | 5 +- src/interface/web/app/chat/page.tsx | 20 +++-- src/interface/web/app/common/chatFunctions.ts | 1 - .../components/chatHistory/chatHistory.tsx | 2 +- .../chatInputArea/chatInputArea.tsx | 3 +- .../components/chatMessage/chatMessage.tsx | 12 ++- .../sidePanel/chatHistorySidePanel.tsx | 42 ++++++--- src/interface/web/app/share/chat/page.tsx | 2 +- src/khoj/database/adapters/__init__.py | 16 ++-- .../migrations/0063_conversation_temp_id.py | 36 ++++++++ ...versation_temp_id_alter_conversation_id.py | 86 +++++++++++++++++++ src/khoj/database/models/__init__.py | 1 + src/khoj/processor/conversation/utils.py | 2 +- src/khoj/routers/api.py | 2 +- src/khoj/routers/api_chat.py | 28 +++--- src/khoj/routers/helpers.py | 15 ++-- 19 files changed, 213 insertions(+), 66 deletions(-) create mode 100644 src/khoj/database/migrations/0063_conversation_temp_id.py create mode 100644 src/khoj/database/migrations/0064_remove_conversation_temp_id_alter_conversation_id.py diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index ac7cc42bf..3df00efc7 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -152,7 +152,7 @@ const chatApi = `${hostURL}/api/chat?client=desktop`; const chatApiBody = { q: query, - conversation_id: parseInt(conversationID), + conversation_id: conversationID, stream: true, ...(!!city && { city: city }), ...(!!region && { region: region }), diff --git a/src/interface/desktop/shortcut.html b/src/interface/desktop/shortcut.html index 97b46b0c4..86e5d9065 100644 --- a/src/interface/desktop/shortcut.html +++ b/src/interface/desktop/shortcut.html @@ -405,7 +405,7 @@ const chatApi = `${hostURL}/api/chat?client=desktop`; const chatApiBody = { q: query, - conversation_id: parseInt(conversationID), + conversation_id: conversationID, stream: true, ...(!!city && { city: city }), ...(!!region && { region: region }), diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index 365548f52..237fccf97 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -1055,7 +1055,7 @@ export class KhojChatView extends KhojPaneView { q: query, n: this.setting.resultsCount, stream: true, - ...(!!conversationId && { conversation_id: parseInt(conversationId) }), + ...(!!conversationId && { conversation_id: conversationId }), ...(!!this.location && { city: this.location.city, region: this.location.region, diff --git a/src/interface/web/app/chat/chat.module.css b/src/interface/web/app/chat/chat.module.css index 97f08a291..d0cc9e117 100644 --- a/src/interface/web/app/chat/chat.module.css +++ b/src/interface/web/app/chat/chat.module.css @@ -31,7 +31,7 @@ input.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 { @@ -94,6 +94,9 @@ div.agentIndicator { padding: 10px; } +div.chatTitleWrapper { + grid-template-columns: auto 1fr; +} @media screen and (max-width: 768px) { div.inputBox { diff --git a/src/interface/web/app/chat/page.tsx b/src/interface/web/app/chat/page.tsx index 8a22dc5a1..0879956a1 100644 --- a/src/interface/web/app/chat/page.tsx +++ b/src/interface/web/app/chat/page.tsx @@ -3,7 +3,7 @@ import styles from "./chat.module.css"; 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 { useSearchParams } from "next/navigation"; 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 { useAuthenticatedData } from "../common/auth"; import { AgentData } from "../agents/page"; +import { DotsThreeVertical } from "@phosphor-icons/react"; +import { Button } from "@/components/ui/button"; interface ChatBodyDataProps { chatOptionsData: ChatOptions | null; @@ -104,7 +106,7 @@ function ChatBodyData(props: ChatBodyDataProps) { />
([]); const [image64, setImage64] = useState(""); + const locationData = useIPLocationData(); const authenticatedData = useAuthenticatedData(); const isMobileWidth = useIsMobileWidth(); @@ -235,7 +238,7 @@ export default function Chat() { const chatAPI = "/api/chat?client=web"; const chatAPIBody = { q: queryToProcess, - conversation_id: parseInt(conversationId), + conversation_id: conversationId, stream: true, ...(locationData && { region: locationData.region, @@ -297,17 +300,22 @@ export default function Chat() {
- {!isMobileWidth && ( + {!isMobileWidth && conversationId && (
{title && (

{title}

)} +
)} }> diff --git a/src/interface/web/app/common/chatFunctions.ts b/src/interface/web/app/common/chatFunctions.ts index 8a6f5d1e5..14c6fb500 100644 --- a/src/interface/web/app/common/chatFunctions.ts +++ b/src/interface/web/app/common/chatFunctions.ts @@ -177,7 +177,6 @@ export function modifyFileFilterForConversation( }) .then((response) => response.json()) .then((data) => { - console.log("ADDEDFILES DATA: ", data); setAddedFiles(data); }) .catch((err) => { diff --git a/src/interface/web/app/components/chatHistory/chatHistory.tsx b/src/interface/web/app/components/chatHistory/chatHistory.tsx index 85d31f4a7..df41a426b 100644 --- a/src/interface/web/app/components/chatHistory/chatHistory.tsx +++ b/src/interface/web/app/components/chatHistory/chatHistory.tsx @@ -144,7 +144,7 @@ export default function ChatHistory(props: ChatHistoryProps) { let conversationFetchURL = ""; 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) { conversationFetchURL = `/api/chat/share/history?client=web&public_conversation_slug=${props.publicConversationSlug}&n=${10 * nextPage}`; } else { diff --git a/src/interface/web/app/components/chatInputArea/chatInputArea.tsx b/src/interface/web/app/components/chatInputArea/chatInputArea.tsx index 0e9ec8c7b..bec7623ed 100644 --- a/src/interface/web/app/components/chatInputArea/chatInputArea.tsx +++ b/src/interface/web/app/components/chatInputArea/chatInputArea.tsx @@ -442,7 +442,7 @@ export default function ChatInputArea(props: ChatInputProps) {
)}
- {isDragAndDropping &&
Drop file to upload
} ); } diff --git a/src/interface/web/app/components/chatMessage/chatMessage.tsx b/src/interface/web/app/components/chatMessage/chatMessage.tsx index 5fa20c2bb..ea3f0f7cc 100644 --- a/src/interface/web/app/components/chatMessage/chatMessage.tsx +++ b/src/interface/web/app/components/chatMessage/chatMessage.tsx @@ -5,6 +5,7 @@ import styles from "./chatMessage.module.css"; import markdownIt from "markdown-it"; import mditHljs from "markdown-it-highlightjs"; import React, { useEffect, useRef, useState } from "react"; +import { createRoot } from "react-dom/client"; import "katex/dist/katex.min.css"; @@ -23,6 +24,7 @@ import { MagnifyingGlass, Pause, Palette, + ClipboardText, } from "@phosphor-icons/react"; import DOMPurify from "dompurify"; @@ -377,12 +379,9 @@ export default function ChatMessage(props: ChatMessageProps) { const preElements = messageRef.current.querySelectorAll("pre > .hljs"); preElements.forEach((preElement) => { const copyButton = document.createElement("button"); - const copyImage = document.createElement("img"); - copyImage.src = "/static/copy-button.svg"; - copyImage.alt = "Copy"; - copyImage.width = 24; - copyImage.height = 24; - copyButton.appendChild(copyImage); + const copyIcon = ; + createRoot(copyButton).render(copyIcon); + copyButton.className = `hljs ${styles.codeCopyButton}`; copyButton.addEventListener("click", () => { let textContent = preElement.textContent || ""; @@ -392,7 +391,6 @@ export default function ChatMessage(props: ChatMessageProps) { textContent = textContent.replace(/^Copy/, ""); textContent = textContent.trim(); navigator.clipboard.writeText(textContent); - copyImage.src = "/static/copy-button-success.svg"; }); preElement.prepend(copyButton); }); diff --git a/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx index a0bee3569..3074fac33 100644 --- a/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx +++ b/src/interface/web/app/components/sidePanel/chatHistorySidePanel.tsx @@ -182,9 +182,12 @@ function FilesMenu(props: FilesMenuProps) { useEffect(() => { if (!files) return; + const uniqueFiles = Array.from(new Set(files)); + // First, sort lexically - files.sort(); - let sortedFiles = files; + uniqueFiles.sort(); + + let sortedFiles = uniqueFiles; if (addedFiles) { sortedFiles = addedFiles.concat( @@ -458,12 +461,13 @@ function SessionsAndFiles(props: SessionsAndFilesProps) { ); } -interface ChatSessionActionMenuProps { +export interface ChatSessionActionMenuProps { conversationId: string; setTitle: (title: string) => void; + sizing?: "sm" | "md" | "lg"; } -function ChatSessionActionMenu(props: ChatSessionActionMenuProps) { +export function ChatSessionActionMenu(props: ChatSessionActionMenuProps) { const [renamedTitle, setRenamedTitle] = useState(""); const [isRenaming, setIsRenaming] = 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 ( setIsOpen(open)} open={isOpen}> - + @@ -608,7 +627,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) { variant={"ghost"} onClick={() => setIsRenaming(true)} > - + Rename @@ -618,7 +637,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) { variant={"ghost"} onClick={() => setIsSharing(true)} > - + Share @@ -628,7 +647,7 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) { variant={"ghost"} onClick={() => setIsDeleting(true)} > - + Delete @@ -640,15 +659,14 @@ function ChatSessionActionMenu(props: ChatSessionActionMenuProps) { function ChatSession(props: ChatHistory) { const [isHovered, setIsHovered] = useState(false); const [title, setTitle] = useState(props.slug || "New Conversation 🌱"); - var currConversationId = parseInt( - new URLSearchParams(window.location.search).get("conversationId") || "-1", - ); + var currConversationId = + new URLSearchParams(window.location.search).get("conversationId") || "-1"; return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} 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" : ""}`} > Optional[Conversation]: if conversation_id: conversation = ( @@ -689,7 +689,7 @@ def get_conversation_sessions(user: KhojUser, client_application: ClientApplicat @staticmethod 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( user=user, client=client_application, id=conversation_id @@ -701,7 +701,7 @@ async def aset_conversation_title( return None @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() @staticmethod @@ -730,7 +730,7 @@ def create_conversation_session( @staticmethod 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]: query = Conversation.objects.filter(user=user, client=client_application).prefetch_related("agent") @@ -747,7 +747,7 @@ async def aget_conversation_by_user( @staticmethod 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: return await Conversation.objects.filter(user=user, client=client_application, id=conversation_id).adelete() @@ -900,7 +900,7 @@ def save_conversation( user: KhojUser, conversation_log: dict, client_application: ClientApplication = None, - conversation_id: int = None, + conversation_id: str = None, user_message: str = None, ): slug = user_message.strip()[:200] if user_message else None @@ -1042,7 +1042,7 @@ async def aset_user_text_to_image_model(user: KhojUser, text_to_image_model_conf return new_config @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) file_list = EntryAdapters.get_all_filenames_by_source(user, "computer") for filename in files: @@ -1056,7 +1056,7 @@ def add_files_to_filter(user: KhojUser, conversation_id: int, files: List[str]): return conversation.file_filters @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) for filename in files: if filename in conversation.file_filters: diff --git a/src/khoj/database/migrations/0063_conversation_temp_id.py b/src/khoj/database/migrations/0063_conversation_temp_id.py new file mode 100644 index 000000000..6696add69 --- /dev/null +++ b/src/khoj/database/migrations/0063_conversation_temp_id.py @@ -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), + ), + ] diff --git a/src/khoj/database/migrations/0064_remove_conversation_temp_id_alter_conversation_id.py b/src/khoj/database/migrations/0064_remove_conversation_temp_id_alter_conversation_id.py new file mode 100644 index 000000000..16d76d109 --- /dev/null +++ b/src/khoj/database/migrations/0064_remove_conversation_temp_id_alter_conversation_id.py @@ -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), + ] diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index 4029cf3c9..ed91a027a 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -350,6 +350,7 @@ class Conversation(BaseModel): 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) 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): diff --git a/src/khoj/processor/conversation/utils.py b/src/khoj/processor/conversation/utils.py index 3f3977986..ff3451f55 100644 --- a/src/khoj/processor/conversation/utils.py +++ b/src/khoj/processor/conversation/utils.py @@ -107,7 +107,7 @@ def save_to_conversation_log( inferred_queries: List[str] = [], intent_type: str = "remember", client_application: ClientApplication = None, - conversation_id: int = None, + conversation_id: str = None, automation_id: str = None, uploaded_image_url: str = None, ): diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 31947c41e..73e7fee30 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -328,7 +328,7 @@ async def extract_references_and_questions( q: str, n: int, d: float, - conversation_id: int, + conversation_id: str, conversation_commands: List[ConversationCommand] = [ConversationCommand.Default], location_data: LocationData = None, send_status_func: Optional[Callable] = None, diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 181593e82..a11ae54ee 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -77,9 +77,7 @@ @api_chat.get("/conversation/file-filters/{conversation_id}", response_class=Response) @requires(["authenticated"]) def get_file_filter(request: Request, conversation_id: str) -> Response: - conversation = ConversationAdapters.get_conversation_by_user( - request.user.object, conversation_id=int(conversation_id) - ) + conversation = ConversationAdapters.get_conversation_by_user(request.user.object, conversation_id=conversation_id) if not conversation: 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) @requires(["authenticated"]) def remove_files_filter(request: Request, filter: FilesFilterRequest) -> Response: - conversation_id = int(filter.conversation_id) + conversation_id = filter.conversation_id files_filter = filter.filenames 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) @@ -105,7 +103,7 @@ def remove_files_filter(request: Request, filter: FilesFilterRequest) -> Respons @requires(["authenticated"]) def add_files_filter(request: Request, filter: FilesFilterRequest): try: - conversation_id = int(filter.conversation_id) + conversation_id = filter.conversation_id files_filter = filter.filenames 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) @@ -118,7 +116,7 @@ def add_files_filter(request: Request, filter: FilesFilterRequest): @requires(["authenticated"]) def add_file_filter(request: Request, filter: FileFilterRequest): try: - conversation_id = int(filter.conversation_id) + conversation_id = filter.conversation_id files_filter = [filter.filename] 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) @@ -130,7 +128,7 @@ def add_file_filter(request: Request, filter: FileFilterRequest): @api_chat.delete("/conversation/file-filters", response_class=Response) @requires(["authenticated"]) def remove_file_filter(request: Request, filter: FileFilterRequest) -> Response: - conversation_id = int(filter.conversation_id) + conversation_id = filter.conversation_id files_filter = [filter.filename] 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) @@ -189,7 +187,7 @@ async def chat_starters( def chat_history( request: Request, common: CommonQueryParams, - conversation_id: Optional[int] = None, + conversation_id: Optional[str] = None, n: Optional[int] = None, ): user = request.user.object @@ -312,7 +310,7 @@ def get_shared_chat( async def clear_chat_history( request: Request, common: CommonQueryParams, - conversation_id: Optional[int] = None, + conversation_id: Optional[str] = None, ): user = request.user.object @@ -375,7 +373,7 @@ def fork_public_conversation( def duplicate_chat_history_public_conversation( request: Request, common: CommonQueryParams, - conversation_id: int, + conversation_id: str, ): user = request.user.object domain = request.headers.get("host") @@ -423,7 +421,7 @@ def chat_sessions( session_values = [ { - "conversation_id": session[0], + "conversation_id": str(session[0]), "slug": session[2] or session[1], "agent_name": session[4], "agent_avatar": session[5], @@ -455,7 +453,7 @@ async def create_chat_session( # Create new Conversation Session 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 = { "agent": agent_slug, @@ -497,7 +495,7 @@ async def set_conversation_title( request: Request, common: CommonQueryParams, title: str, - conversation_id: Optional[int] = None, + conversation_id: Optional[str] = None, ) -> Response: user = request.user.object title = title.strip()[:200] @@ -527,7 +525,7 @@ class ChatRequestBody(BaseModel): d: Optional[float] = None stream: Optional[bool] = False title: Optional[str] = None - conversation_id: Optional[int] = None + conversation_id: Optional[str] = None city: Optional[str] = None region: Optional[str] = None country: Optional[str] = None @@ -1016,7 +1014,7 @@ async def get_chat( d: float = None, stream: Optional[bool] = False, title: Optional[str] = None, - conversation_id: Optional[int] = None, + conversation_id: Optional[str] = None, city: Optional[str] = None, region: Optional[str] = None, country: Optional[str] = None, diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index 0098526de..0f60b1003 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -21,7 +21,7 @@ Tuple, Union, ) -from urllib.parse import parse_qs, urljoin, urlparse +from urllib.parse import parse_qs, quote, urljoin, urlparse import cron_descriptor import pytz @@ -799,7 +799,7 @@ def generate_chat_response( conversation_commands: List[ConversationCommand] = [ConversationCommand.Default], user: KhojUser = None, client_application: ClientApplication = None, - conversation_id: int = None, + conversation_id: str = None, location_data: LocationData = None, user_name: Optional[str] = None, uploaded_image_url: Optional[str] = None, @@ -1102,7 +1102,7 @@ def scheduled_chat( user: KhojUser, calling_url: URL, job_id: str = None, - conversation_id: int = None, + conversation_id: str = None, ): logger.info(f"Processing scheduled_chat: {query_to_run}") if job_id: @@ -1131,7 +1131,8 @@ def scheduled_chat( # Replace the original conversation_id with the 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 json_payload = {key: values[0] for key, values in query_dict.items()} @@ -1185,7 +1186,7 @@ def scheduled_chat( 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) 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, user: KhojUser, calling_url: URL, - conversation_id: int, + conversation_id: str, ): # Disable minute level automation recurrence minute_value = crontime.split(" ")[0] @@ -1218,7 +1219,7 @@ async def schedule_automation( "scheduling_request": scheduling_request, "subject": subject, "crontime": crontime, - "conversation_id": conversation_id, + "conversation_id": str(conversation_id), } ) query_id = hashlib.md5(f"{query_to_run}_{crontime}".encode("utf-8")).hexdigest()