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