From 41eaba59bdb785a16a0f9dc04152687d03dc62a7 Mon Sep 17 00:00:00 2001 From: Om Gate Date: Tue, 23 Sep 2025 16:12:23 +0530 Subject: [PATCH 1/5] feat: rename and share session handling --- backend/director/core/session.py | 4 + backend/director/db/base.py | 20 +++ backend/director/db/postgres/db.py | 84 +++++++++- backend/director/db/postgres/initialize.py | 5 + backend/director/db/sqlite/db.py | 91 +++++++++- backend/director/db/sqlite/initialize.py | 12 ++ backend/director/entrypoint/api/routes.py | 83 ++++++++++ backend/director/handler.py | 10 ++ frontend/src/hooks/shareViewHandler.js | 183 +++++++++++++++++++++ frontend/src/router/index.js | 6 + frontend/src/views/ShareView.vue | 39 +++++ 11 files changed, 530 insertions(+), 7 deletions(-) create mode 100644 frontend/src/hooks/shareViewHandler.js create mode 100644 frontend/src/views/ShareView.vue diff --git a/backend/director/core/session.py b/backend/director/core/session.py index cbcdaf24..a012e271 100644 --- a/backend/director/core/session.py +++ b/backend/director/core/session.py @@ -393,6 +393,10 @@ def delete(self): """Delete the session from the database.""" return self.db.delete_session(self.session_id) + def update(self, **kwargs): + """Update the session in the database.""" + return self.db.update_session(self.session_id, **kwargs) + def emit_event(self, event: BaseEvent, namespace="/chat"): """Emits a structured WebSocket event to notify all clients about updates.""" diff --git a/backend/director/db/base.py b/backend/director/db/base.py index b166bab5..c402eff2 100644 --- a/backend/director/db/base.py +++ b/backend/director/db/base.py @@ -43,6 +43,26 @@ def add_or_update_context_msg( """Update context messages for a session.""" pass + @abstractmethod + def update_session(self, session_id: str, **kwargs) -> bool: + """Update a session in the database.""" + pass + + @abstractmethod + def delete_session(self, session_id: str) -> bool: + """Delete a session from the database.""" + pass + + @abstractmethod + def make_session_public(self, session_id: str, is_public: bool) -> bool: + """Make a session public or private.""" + pass + + @abstractmethod + def get_public_session(self, session_id: str) -> dict: + """Get a public session by session_id.""" + pass + @abstractmethod def health_check(self) -> bool: """Check if the database is healthy.""" diff --git a/backend/director/db/postgres/db.py b/backend/director/db/postgres/db.py index 4a2d5b8e..987d52d4 100644 --- a/backend/director/db/postgres/db.py +++ b/backend/director/db/postgres/db.py @@ -31,12 +31,13 @@ def __init__(self): port=os.getenv("POSTGRES_PORT", "5432"), ) self.cursor = self.conn.cursor(cursor_factory=RealDictCursor) - + initialize_postgres() def create_session( self, session_id: str, video_id: str, collection_id: str, + name: str = None, created_at: int = None, updated_at: int = None, metadata: dict = {}, @@ -47,14 +48,15 @@ def create_session( self.cursor.execute( """ - INSERT INTO sessions (session_id, video_id, collection_id, created_at, updated_at, metadata) - VALUES (%s, %s, %s, %s, %s, %s) + INSERT INTO sessions (session_id, video_id, collection_id, name, created_at, updated_at, metadata) + VALUES (%s, %s, %s, %s, %s, %s, %s) ON CONFLICT (session_id) DO NOTHING """, ( session_id, video_id, collection_id, + name, created_at, updated_at, json.dumps(metadata), @@ -210,6 +212,82 @@ def delete_session(self, session_id: str) -> bool: success = len(failed_components) < 3 return success, failed_components + def update_session(self, session_id: str, **kwargs) -> bool: + """Update a session in the database.""" + try: + if not kwargs: + return False + + update_fields = [] + update_values = [] + + for key, value in kwargs.items(): + update_fields.append(f"{key} = %s") + update_values.append(value) + + update_fields.append("updated_at = %s") + update_values.append(int(time.time())) + + update_values.extend([session_id]) + + query = f""" + UPDATE sessions + SET {', '.join(update_fields)} + WHERE session_id = %s + """ + + self.cursor.execute(query, update_values) + self.conn.commit() + return True + + except Exception as e: + logger.error(f"Error updating session {session_id}: {e}") + return False + + def make_session_public(self, session_id: str, is_public: bool) -> bool: + """Make a session public or private.""" + try: + query = """ + UPDATE sessions + SET is_public = %s, updated_at = %s + WHERE session_id = %s + """ + import time + current_time = int(time.time() * 1000) + self.cursor.execute(query, (is_public, current_time, session_id)) + self.conn.commit() + return self.cursor.rowcount > 0 + except Exception as e: + logger.exception(f"Error making session public/private: {e}") + return False + + def get_public_session(self, session_id: str) -> dict: + """Get a public session by session_id.""" + try: + query = """ + SELECT session_id, video_id, collection_id, name, created_at, updated_at, metadata, is_public + FROM sessions + WHERE session_id = %s AND is_public = TRUE + """ + self.cursor.execute(query, (session_id,)) + row = self.cursor.fetchone() + if row: + session = { + "session_id": row["session_id"], + "video_id": row["video_id"], + "collection_id": row["collection_id"], + "name": row["name"], + "created_at": row["created_at"], + "updated_at": row["updated_at"], + "metadata": row["metadata"] if row["metadata"] else {}, + "is_public": row["is_public"] + } + return session + return {} + except Exception as e: + logger.exception(f"Error getting public session: {e}") + return {} + def health_check(self) -> bool: try: query = """ diff --git a/backend/director/db/postgres/initialize.py b/backend/director/db/postgres/initialize.py index 18da07cc..007301ee 100644 --- a/backend/director/db/postgres/initialize.py +++ b/backend/director/db/postgres/initialize.py @@ -11,6 +11,8 @@ session_id TEXT PRIMARY KEY, video_id TEXT, collection_id TEXT, + name TEXT, + is_public BOOLEAN DEFAULT FALSE, created_at BIGINT, updated_at BIGINT, metadata JSONB @@ -69,6 +71,9 @@ def initialize_postgres(): cursor.execute(CREATE_CONVERSATIONS_TABLE) cursor.execute(CREATE_CONTEXT_MESSAGES_TABLE) conn.commit() + + cursor.execute("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS name TEXT") + cursor.execute("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS is_public BOOLEAN DEFAULT FALSE") logger.info("PostgreSQL tables created successfully") except Exception as e: logger.exception(f"Error creating PostgreSQL tables: {e}") diff --git a/backend/director/db/sqlite/db.py b/backend/director/db/sqlite/db.py index f47781a9..5ab218aa 100644 --- a/backend/director/db/sqlite/db.py +++ b/backend/director/db/sqlite/db.py @@ -23,6 +23,7 @@ def __init__(self, db_path: str = None): self.db_path = os.getenv("SQLITE_DB_PATH", "director.db") else: self.db_path = db_path + initialize_sqlite(self.db_path) self.conn = sqlite3.connect(self.db_path, check_same_thread=True) self.conn.row_factory = sqlite3.Row self.cursor = self.conn.cursor() @@ -33,6 +34,7 @@ def create_session( session_id: str, video_id: str, collection_id: str, + name: str = None, created_at: int = None, updated_at: int = None, metadata: dict = {}, @@ -52,13 +54,14 @@ def create_session( self.cursor.execute( """ - INSERT OR IGNORE INTO sessions (session_id, video_id, collection_id, created_at, updated_at, metadata) - VALUES (?, ?, ?, ?, ?, ?) + INSERT OR IGNORE INTO sessions (session_id, video_id, collection_id, name, created_at, updated_at, metadata) + VALUES (?, ?, ?, ?, ?, ?, ?) """, ( session_id, video_id, collection_id, + name, created_at, updated_at, json.dumps(metadata), @@ -153,8 +156,7 @@ def add_or_update_msg_to_conv( def get_conversations(self, session_id: str) -> list: self.cursor.execute( - "SELECT * FROM conversations WHERE session_id = ? ORDER BY created_at ASC", - (session_id,), + "SELECT * FROM conversations WHERE session_id = ?", (session_id,) ) rows = self.cursor.fetchall() conversations = [] @@ -259,6 +261,87 @@ def delete_session(self, session_id: str) -> bool: success = len(failed_components) < 3 return success, failed_components + def update_session(self, session_id: str, **kwargs) -> bool: + """Update a session in the database. + + :param session_id: Unique session ID. + :param kwargs: Fields to update. + :return: True if update was successful, False otherwise. + """ + try: + if not kwargs: + return False + + update_fields = [] + update_values = [] + + for key, value in kwargs.items(): + update_fields.append(f"{key} = ?") + update_values.append(value) + + update_fields.append("updated_at = ?") + update_values.append(int(time.time())) + + update_values.extend([session_id]) + + query = f""" + UPDATE sessions + SET {', '.join(update_fields)} + WHERE session_id = ? + """ + + self.cursor.execute(query, update_values) + self.conn.commit() + return True + + except Exception as e: + logger.error(f"Error updating session {session_id}: {e}") + return False + + def make_session_public(self, session_id: str, is_public: bool) -> bool: + """Make a session public or private.""" + try: + query = """ + UPDATE sessions + SET is_public = ?, updated_at = ? + WHERE session_id = ? + """ + import time + current_time = int(time.time() * 1000) + self.cursor.execute(query, (is_public, current_time, session_id)) + self.conn.commit() + return self.cursor.rowcount > 0 + except Exception as e: + logger.exception(f"Error making session public/private: {e}") + return False + + def get_public_session(self, session_id: str) -> dict: + """Get a public session by session_id.""" + try: + query = """ + SELECT session_id, video_id, collection_id, name, created_at, updated_at, metadata, is_public + FROM sessions + WHERE session_id = ? AND is_public = TRUE + """ + self.cursor.execute(query, (session_id,)) + row = self.cursor.fetchone() + if row: + session = { + "session_id": row[0], + "video_id": row[1], + "collection_id": row[2], + "name": row[3], + "created_at": row[4], + "updated_at": row[5], + "metadata": json.loads(row[6]) if row[6] else {}, + "is_public": row[7] + } + return session + return {} + except Exception as e: + logger.exception(f"Error getting public session: {e}") + return {} + def health_check(self) -> bool: """Check if the SQLite database is healthy and the necessary tables exist. If not, create them.""" try: diff --git a/backend/director/db/sqlite/initialize.py b/backend/director/db/sqlite/initialize.py index 76a06e16..79f3aaa4 100644 --- a/backend/director/db/sqlite/initialize.py +++ b/backend/director/db/sqlite/initialize.py @@ -7,6 +7,8 @@ session_id TEXT PRIMARY KEY, video_id TEXT, collection_id TEXT, + name TEXT, + is_public BOOLEAN DEFAULT FALSE, created_at INTEGER, updated_at INTEGER, metadata JSON @@ -49,10 +51,20 @@ def initialize_sqlite(db_name="director.db"): conn = sqlite3.connect(db_name) cursor = conn.cursor() + # Create base tables cursor.execute(CREATE_SESSIONS_TABLE) cursor.execute(CREATE_CONVERSATIONS_TABLE) cursor.execute(CREATE_CONTEXT_MESSAGES_TABLE) + cursor.execute("PRAGMA table_info(sessions)") + columns = [col[1] for col in cursor.fetchall()] + + if "name" not in columns: + cursor.execute("ALTER TABLE sessions ADD COLUMN name TEXT") + + if "is_public" not in columns: + cursor.execute("ALTER TABLE sessions ADD COLUMN is_public BOOLEAN DEFAULT FALSE") + conn.commit() conn.close() diff --git a/backend/director/entrypoint/api/routes.py b/backend/director/entrypoint/api/routes.py index f7c2ae48..ee2b43c0 100644 --- a/backend/director/entrypoint/api/routes.py +++ b/backend/director/entrypoint/api/routes.py @@ -62,6 +62,89 @@ def get_session(session_id): }, 500 +@session_bp.route("//rename", methods=["PUT"]) +def rename_session(session_id): + """ + Rename a session by updating its metadata + """ + if not session_id: + return {"message": "Please provide session_id."}, 400 + + data = request.get_json() + if not data or not data.get("name"): + return {"message": "Session name is required."}, 400 + + new_name = data["name"] + if not new_name.strip(): + return {"message": "Session name cannot be empty."}, 400 + + session_handler = SessionHandler( + db=load_db(os.getenv("SERVER_DB_TYPE", app.config["DB_TYPE"])) + ) + + session = session_handler.get_session(session_id) + if not session: + return {"message": "Session not found."}, 404 + + result = session_handler.rename_session(session_id, new_name) + return result + +@session_bp.route("//public", methods=["PUT"]) +def make_session_public(session_id): + """ + Make a session public or private + """ + if not session_id: + return {"message": "Please provide session_id."}, 400 + + data = request.get_json() + if not data or "is_public" not in data: + return {"message": "is_public field is required."}, 400 + + is_public = data["is_public"] + if not isinstance(is_public, bool): + return {"message": "is_public must be a boolean value."}, 400 + + db = load_db(os.getenv("SERVER_DB_TYPE", app.config["DB_TYPE"])) + + # Check if session exists and belongs to user + session_handler = SessionHandler(db=db) + session = session_handler.get_session(session_id) + if not session: + return {"message": "Session not found."}, 404 + + # Update the session's public status + success = db.make_session_public(session_id, is_public) + + if success: + status = "public" if is_public else "private" + return {"message": f"Session successfully made {status}."}, 200 + else: + return {"message": "Failed to update session visibility."}, 500 + + +@session_bp.route("/public/", methods=["GET"]) +def get_public_session(session_id): + """ + Get a public session by session_id (no authentication required) + """ + if not session_id: + return {"message": "Please provide session_id."}, 400 + + db = load_db(os.getenv("SERVER_DB_TYPE", app.config["DB_TYPE"])) + + # Get the public session + session = db.get_public_session(session_id) + if not session: + return {"message": "Public session not found."}, 404 + + # Get the conversation for this session + conversation = db.get_conversations(session_id) + session["conversation"] = conversation + + return session, 200 + + @videodb_bp.route("/collection", defaults={"collection_id": None}, methods=["GET"]) @videodb_bp.route("/collection/", methods=["GET"]) def get_collection_or_all(collection_id): diff --git a/backend/director/handler.py b/backend/director/handler.py index e42e20da..7881e95c 100644 --- a/backend/director/handler.py +++ b/backend/director/handler.py @@ -140,6 +140,16 @@ def delete_session(self, session_id): session = Session(db=self.db, session_id=session_id) return session.delete() + def rename_session(self, session_id, new_name): + """Rename a session by updating its name field.""" + session = Session(db=self.db, session_id=session_id) + success = session.update(name=new_name) + if success: + return {"message": "Session renamed successfully", "session_id": session_id, "name": new_name} + else: + return {"message": "Failed to rename session"}, 500 + + class VideoDBHandler: def __init__(self, collection_id="default"): diff --git a/frontend/src/hooks/shareViewHandler.js b/frontend/src/hooks/shareViewHandler.js new file mode 100644 index 00000000..9115edf0 --- /dev/null +++ b/frontend/src/hooks/shareViewHandler.js @@ -0,0 +1,183 @@ +import { computed, onBeforeMount, reactive, ref, toRefs, watch } from "vue"; + +const fetchData = async (rootUrl, endpoint) => { + const res = {}; + res.status = "success"; + res.data = {}; + return res; +}; + +export function publicUseVideoDBAgent(config, sessionId) { + const { debug = false, socketUrl, httpUrl } = config; + if (debug) console.log("debug :videodb-chat config", config); + + const session = reactive({ + isConnected: true, + sessionId, + videoId: null, + collectionId: "default", + }); + + const configStatus = ref(null); + + const collections = ref([]); + const sessions = ref([]); + const sessionsSorted = computed(() => { + return [...sessions.value].sort((a, b) => b.created_at - a.created_at); + }); + const agents = ref([]); + + const conversations = reactive({}); + const activeCollectionData = ref(null); + + const activeCollectionVideos = ref(null); + const activeVideoData = ref(null); + + const activeCollectionAudios = ref(null); + const activeAudioData = ref(null); + + const activeCollectionImages = ref(null); + const activeImageData = ref(null); + + fetch(`${httpUrl}/session/public/${sessionId}`) + .then((res) => res.json()) + .then((res) => { + console.log("debug :videodb-chat res", res); + if (res) { + session.videoId = res.video_id || null; + session.collectionId = + res.collection_id || session.collectionId || null; + + // Clear existing conversations + Object.keys(conversations).forEach((key) => delete conversations[key]); + + // Populate conversations with fetched data + if (res.conversation) { + res.conversation.forEach((message) => { + const { conv_id, msg_id } = message; + if (!conversations[conv_id]) { + conversations[conv_id] = {}; + } + conversations[String(conv_id)][String(msg_id)] = { + sender: message.msg_type === "input" ? "user" : "assistant", + ...message, + }; + }); + } + } + }) + .catch((error) => { + if (debug) console.error("Error fetching public session:", error); + }); + const fetchConfigStatus = async () => { + try { + const res = await fetchData(httpUrl, "/config/check"); + return res; + } catch (error) { + if (debug) console.error("Error fetching config status:", error); + return { data: { backend: false } }; + } + }; + + const uploadMedia = async (uploadData) => {}; + + const generateAudioUrl = async (collectionId, audioId) => { + const res = {}; + res.status = "success"; + res.url = ""; + return res; + }; + + const generateImageUrl = async (collectionId, imageId) => { + const res = {}; + res.status = "success"; + res.url = ""; + return res; + }; + + const saveMeetingContext = async (msgId, context) => { + const res = {}; + res.status = "success"; + res.data = {}; + return res; + }; + + const makeSessionPublic = async (sessionId, isPublic = true) => { + const res = {}; + res.status = "success"; + res.data = {}; + return res; + }; + + const fetchMeetingContext = async (uiId) => { + const res = {}; + res.status = "success"; + res.data = {}; + return res; + }; + + const refetchCollectionVideos = async () => {}; + + const refetchCollectionAudios = async () => {}; + + const refetchCollectionImages = async () => {}; + + onBeforeMount(() => { + fetchConfigStatus().then((res) => { + if (debug) console.log("debug :videodb-chat config status", res); + configStatus.value = res.data; + }); + }); + + const loadSession = (sessionId) => {}; + + const deleteSession = (sessionId) => {}; + + const updateCollection = async () => {}; + + const createCollection = async (name, description) => {}; + + const deleteCollection = async (collectionId) => {}; + + const deleteVideo = async (collectionId, videoId) => {}; + + const deleteAudio = async (collectionId, audioId) => {}; + + const deleteImage = async (collectionId, imageId) => {}; + + const addMessage = (message) => {}; + + return { + ...toRefs(session), + configStatus, + collections, + sessions: sessionsSorted, + agents, + activeCollectionData, + activeCollectionVideos, + activeVideoData, + refetchCollectionVideos, + activeCollectionAudios, + activeAudioData, + refetchCollectionAudios, + activeCollectionImages, + activeImageData, + refetchCollectionImages, + conversations, + addMessage, + loadSession, + deleteSession, + updateCollection, + createCollection, + deleteCollection, + deleteVideo, + deleteAudio, + deleteImage, + uploadMedia, + generateImageUrl, + generateAudioUrl, + saveMeetingContext, + fetchMeetingContext, + makeSessionPublic, + }; +} diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 78bac065..a220e90e 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from "vue-router"; import DefaultView from "../views/DefaultView.vue"; +import ShareView from "../views/ShareView.vue"; const routes = [ { @@ -7,6 +8,11 @@ const routes = [ name: "Default", component: DefaultView, }, + { + path: "/share/:sessionId", + name: "Share", + component: ShareView, + }, ]; const router = createRouter({ diff --git a/frontend/src/views/ShareView.vue b/frontend/src/views/ShareView.vue new file mode 100644 index 00000000..931c2e78 --- /dev/null +++ b/frontend/src/views/ShareView.vue @@ -0,0 +1,39 @@ + + + + + From 594a89bc542ea7223f98af1522864de7a01aff3a Mon Sep 17 00:00:00 2001 From: Om Gate Date: Tue, 23 Sep 2025 17:18:45 +0530 Subject: [PATCH 2/5] handle code review --- backend/director/core/session.py | 2 +- backend/director/db/base.py | 6 ++++-- backend/director/db/postgres/db.py | 19 ++++++++++++------ backend/director/db/postgres/initialize.py | 3 +-- backend/director/db/sqlite/db.py | 23 ++++++++++++++-------- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/backend/director/core/session.py b/backend/director/core/session.py index a012e271..ad10a2c6 100644 --- a/backend/director/core/session.py +++ b/backend/director/core/session.py @@ -393,7 +393,7 @@ def delete(self): """Delete the session from the database.""" return self.db.delete_session(self.session_id) - def update(self, **kwargs): + def update(self, **kwargs) -> bool: """Update the session in the database.""" return self.db.update_session(self.session_id, **kwargs) diff --git a/backend/director/db/base.py b/backend/director/db/base.py index c402eff2..5501bcf8 100644 --- a/backend/director/db/base.py +++ b/backend/director/db/base.py @@ -49,8 +49,10 @@ def update_session(self, session_id: str, **kwargs) -> bool: pass @abstractmethod - def delete_session(self, session_id: str) -> bool: - """Delete a session from the database.""" + def delete_session(self, session_id: str) -> tuple[bool, list]: + """Delete a session from the database. + :return: (success, failed_components) + """ pass @abstractmethod diff --git a/backend/director/db/postgres/db.py b/backend/director/db/postgres/db.py index 987d52d4..b8a2043b 100644 --- a/backend/director/db/postgres/db.py +++ b/backend/director/db/postgres/db.py @@ -197,7 +197,7 @@ def delete_context(self, session_id: str) -> bool: self.conn.commit() return self.cursor.rowcount > 0 - def delete_session(self, session_id: str) -> bool: + def delete_session(self, session_id: str) -> tuple[bool, list]: failed_components = [] if not self.delete_conversation(session_id): failed_components.append("conversation") @@ -218,13 +218,21 @@ def update_session(self, session_id: str, **kwargs) -> bool: if not kwargs: return False + allowed_fields = {"name", "video_id", "collection_id", "metadata"} update_fields = [] update_values = [] for key, value in kwargs.items(): + if key not in allowed_fields: + continue + if key == "metadata" and not isinstance(value, str): + value = json.dumps(value) update_fields.append(f"{key} = %s") update_values.append(value) + if not update_fields: + return False + update_fields.append("updated_at = %s") update_values.append(int(time.time())) @@ -238,10 +246,10 @@ def update_session(self, session_id: str, **kwargs) -> bool: self.cursor.execute(query, update_values) self.conn.commit() - return True + return self.cursor.rowcount > 0 - except Exception as e: - logger.error(f"Error updating session {session_id}: {e}") + except Exception: + logger.exception(f"Error updating session {session_id}") return False def make_session_public(self, session_id: str, is_public: bool) -> bool: @@ -252,8 +260,7 @@ def make_session_public(self, session_id: str, is_public: bool) -> bool: SET is_public = %s, updated_at = %s WHERE session_id = %s """ - import time - current_time = int(time.time() * 1000) + current_time = int(time.time()) self.cursor.execute(query, (is_public, current_time, session_id)) self.conn.commit() return self.cursor.rowcount > 0 diff --git a/backend/director/db/postgres/initialize.py b/backend/director/db/postgres/initialize.py index 007301ee..6bce50a7 100644 --- a/backend/director/db/postgres/initialize.py +++ b/backend/director/db/postgres/initialize.py @@ -70,10 +70,9 @@ def initialize_postgres(): cursor.execute(CREATE_SESSIONS_TABLE) cursor.execute(CREATE_CONVERSATIONS_TABLE) cursor.execute(CREATE_CONTEXT_MESSAGES_TABLE) - conn.commit() - cursor.execute("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS name TEXT") cursor.execute("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS is_public BOOLEAN DEFAULT FALSE") + conn.commit() logger.info("PostgreSQL tables created successfully") except Exception as e: logger.exception(f"Error creating PostgreSQL tables: {e}") diff --git a/backend/director/db/sqlite/db.py b/backend/director/db/sqlite/db.py index 5ab218aa..835e8fd5 100644 --- a/backend/director/db/sqlite/db.py +++ b/backend/director/db/sqlite/db.py @@ -243,7 +243,7 @@ def delete_context(self, session_id: str) -> bool: self.conn.commit() return self.cursor.rowcount > 0 - def delete_session(self, session_id: str) -> bool: + def delete_session(self, session_id: str) -> tuple[bool, list]: """Delete a session and all its associated data. :param str session_id: Unique session ID. @@ -272,13 +272,21 @@ def update_session(self, session_id: str, **kwargs) -> bool: if not kwargs: return False + allowed_fields = {"name", "video_id", "collection_id", "metadata"} update_fields = [] update_values = [] for key, value in kwargs.items(): + if key not in allowed_fields: + continue + if key == "metadata" and not isinstance(value, str): + value = json.dumps(value) update_fields.append(f"{key} = ?") update_values.append(value) + if not update_fields: + return False + update_fields.append("updated_at = ?") update_values.append(int(time.time())) @@ -292,10 +300,10 @@ def update_session(self, session_id: str, **kwargs) -> bool: self.cursor.execute(query, update_values) self.conn.commit() - return True + return self.cursor.rowcount > 0 - except Exception as e: - logger.error(f"Error updating session {session_id}: {e}") + except Exception: + logger.exception(f"Error updating session {session_id}") return False def make_session_public(self, session_id: str, is_public: bool) -> bool: @@ -306,13 +314,12 @@ def make_session_public(self, session_id: str, is_public: bool) -> bool: SET is_public = ?, updated_at = ? WHERE session_id = ? """ - import time - current_time = int(time.time() * 1000) + current_time = int(time.time()) self.cursor.execute(query, (is_public, current_time, session_id)) self.conn.commit() return self.cursor.rowcount > 0 - except Exception as e: - logger.exception(f"Error making session public/private: {e}") + except Exception: + logger.exception("Error making session public/private") return False def get_public_session(self, session_id: str) -> dict: From 3ddaf9834cfaac208fb1f3c348158a34f5c8ed3e Mon Sep 17 00:00:00 2001 From: Om Gate Date: Wed, 24 Sep 2025 01:28:09 +0530 Subject: [PATCH 3/5] feat: session rooms handling --- backend/director/core/session.py | 17 ++++++---- backend/director/entrypoint/api/socket_io.py | 34 ++++++++++++++++---- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/backend/director/core/session.py b/backend/director/core/session.py index ad10a2c6..6405caa6 100644 --- a/backend/director/core/session.py +++ b/backend/director/core/session.py @@ -220,28 +220,31 @@ class OutputMessage(BaseMessage): status: MsgStatus = MsgStatus.progress def update_status(self, status: MsgStatus): - """Update the status of the message and publish the message to the socket. for loading state.""" + """Update the status and broadcast to session room.""" self.status = status self._publish() def push_update(self): - """Publish the message to the socket.""" + """Push real-time update to session room.""" try: self._publish() except Exception as e: - print(f"Error in emitting message: {str(e)}") + print(f"Error in emitting update to session {self.session_id}: {str(e)}") def publish(self): - """Store the message in the database. for conversation history and publish the message to the socket.""" + """Store in database and broadcast final result to session room.""" self._publish() def _publish(self): try: - emit("chat", self.model_dump(), namespace="/chat") + emit("chat", self.model_dump(), + room=self.session_id, + namespace="/chat") + print(f"Emitted message to session room: {self.session_id}") except Exception as e: - print(f"Error in emitting message: {str(e)}") - self.db.add_or_update_msg_to_conv(**self.model_dump()) + print(f"Error emitting to session {self.session_id}: {str(e)}") + self.db.add_or_update_msg_to_conv(**self.model_dump()) def format_user_message(message: dict) -> dict: message_content = message.get("content") diff --git a/backend/director/entrypoint/api/socket_io.py b/backend/director/entrypoint/api/socket_io.py index 857e3392..a60ca4aa 100644 --- a/backend/director/entrypoint/api/socket_io.py +++ b/backend/director/entrypoint/api/socket_io.py @@ -1,18 +1,40 @@ import os from flask import current_app as app -from flask_socketio import Namespace - +from flask_socketio import Namespace, join_room, emit from director.db import load_db from director.handler import ChatHandler - class ChatNamespace(Namespace): - """Chat namespace for socket.io""" - + """Chat namespace for socket.io with session-based rooms""" + + def on_connect(self): + """Handle client connection""" + print(f"Client connected to chat namespace") + def on_chat(self, message): - """Handle chat messages""" + """Handle chat messages and auto-join session room""" + session_id = message.get('session_id') + if not session_id: + emit('error', {'message': 'session_id is required'}) + return + + join_room(session_id) + print(f"Client joined session room {session_id}") + chat_handler = ChatHandler( db=load_db(os.getenv("SERVER_DB_TYPE", app.config["DB_TYPE"])) ) chat_handler.chat(message) + + def on_join_session(self, data): + """Explicitly join a session room (for reconnection)""" + session_id = data.get('session_id') + if session_id: + join_room(session_id) + emit('session_joined', {'session_id': session_id}) + print(f"Client explicitly joined session room {session_id}") + + def on_disconnect(self): + """Handle client disconnection""" + print(f"Client disconnected from chat namespace") From d0702913763892a775806b6d2bb52c068c6a0f63 Mon Sep 17 00:00:00 2001 From: Om Gate Date: Wed, 8 Oct 2025 15:57:09 +0530 Subject: [PATCH 4/5] feat: ai name start --- backend/director/entrypoint/api/routes.py | 13 ++++++++++++- backend/director/handler.py | 4 ++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/director/entrypoint/api/routes.py b/backend/director/entrypoint/api/routes.py index ee2b43c0..2aa07d4b 100644 --- a/backend/director/entrypoint/api/routes.py +++ b/backend/director/entrypoint/api/routes.py @@ -35,7 +35,7 @@ def get_sessions(): return session_handler.get_sessions() -@session_bp.route("/", methods=["GET", "DELETE"]) +@session_bp.route("/", methods=["GET", "DELETE", "POST"]) def get_session(session_id): """ Get or delete the session details @@ -60,6 +60,17 @@ def get_session(session_id): return { "message": f"Failed to delete the entry for following components: {', '.join(failed_components)}" }, 500 + elif request.method == "POST": + data = request.get_json() + print(data) + session_handler.create_session( + data.get("message") + ) + return { + "session_id": data.get("session_id"), + "created_at": data.get("created_at"), + "name": "New Session!" + }, 200 @session_bp.route("//rename", methods=["PUT"]) diff --git a/backend/director/handler.py b/backend/director/handler.py index 7881e95c..74e0bc84 100644 --- a/backend/director/handler.py +++ b/backend/director/handler.py @@ -1,5 +1,6 @@ import os import logging +from typing import Optional from director.agents.frame import FrameAgent from director.agents.summarize_video import SummarizeVideoAgent @@ -149,6 +150,9 @@ def rename_session(self, session_id, new_name): else: return {"message": "Failed to rename session"}, 500 + def create_session(self, message): + session = Session(db=self.db, **message) + session.create() class VideoDBHandler: From 83c8cc64f863644991db94edc86e0d7dd8b66f29 Mon Sep 17 00:00:00 2001 From: Om Gate Date: Wed, 8 Oct 2025 17:03:39 +0530 Subject: [PATCH 5/5] feat: AI Driven Name --- backend/director/constants.py | 38 +++++++++++++++++++++++ backend/director/entrypoint/api/routes.py | 11 +++---- backend/director/handler.py | 34 +++++++++++++++++++- 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/backend/director/constants.py b/backend/director/constants.py index a398d134..ce32ad14 100644 --- a/backend/director/constants.py +++ b/backend/director/constants.py @@ -31,3 +31,41 @@ class EnvPrefix(str, Enum): GOOGLEAI_ = "GOOGLEAI_" DOWNLOADS_PATH="director/downloads" + +CHAT_NAMING_SYSTEM_PROMPT = """ +You are an assistant that generates short, descriptive titles for chat conversations. +The title should summarize the main intent or topic of the user's first message. + +Guidelines: + +**Output you give should strictly just be the title without any markdown content in plain text** + +Keep the title under 6 words. + +Use concise, professional phrasing. + +Don't include punctuation unless necessary. + +Capitalize like a headline (e.g., “Check SQL Query Logic”). + +Avoid emojis or filler words. + +Example input and outputs: + +“Can you give me download links for these videos?” → Generate Video Download Links + +“Summarize the lecture video for me” → Lecture Video Summary + +“Add a watermark to my demo” → Add Watermark to Video + +“Trim the video from 2:10 to 3:45” → Trim Video Segment + +“Combine these three clips into one” → Merge Video Clips + +“Extract subtitles from this recording” → Extract Video Subtitles + +“Translate this video into Spanish” → Translate Video to Spanish + +“Generate a short trailer from the full video” → Create Video Trailer + +""" \ No newline at end of file diff --git a/backend/director/entrypoint/api/routes.py b/backend/director/entrypoint/api/routes.py index 2aa07d4b..ab38badc 100644 --- a/backend/director/entrypoint/api/routes.py +++ b/backend/director/entrypoint/api/routes.py @@ -62,15 +62,12 @@ def get_session(session_id): }, 500 elif request.method == "POST": data = request.get_json() - print(data) - session_handler.create_session( + + session = session_handler.create_session( data.get("message") ) - return { - "session_id": data.get("session_id"), - "created_at": data.get("created_at"), - "name": "New Session!" - }, 200 + + return session, 200 @session_bp.route("//rename", methods=["PUT"]) diff --git a/backend/director/handler.py b/backend/director/handler.py index 74e0bc84..028d5123 100644 --- a/backend/director/handler.py +++ b/backend/director/handler.py @@ -1,7 +1,12 @@ +import json import os import logging from typing import Optional +from director.constants import CHAT_NAMING_SYSTEM_PROMPT +from director.llm import get_default_llm +from director.llm.base import LLMResponseStatus + from director.agents.frame import FrameAgent from director.agents.summarize_video import SummarizeVideoAgent from director.agents.download import DownloadAgent @@ -29,7 +34,7 @@ from director.agents.voice_replacement import VoiceReplacementAgent -from director.core.session import Session, InputMessage, MsgStatus +from director.core.session import ContextMessage, RoleTypes, Session, InputMessage, MsgStatus from director.core.reasoning import ReasoningEngine from director.db.base import BaseDB from director.db import load_db @@ -154,6 +159,33 @@ def create_session(self, message): session = Session(db=self.db, **message) session.create() + session_dict = session.get() + if not session_dict["name"]: + llm = get_default_llm() + context_messages = [ + ContextMessage( + role=RoleTypes.system, + content=CHAT_NAMING_SYSTEM_PROMPT + ), + ContextMessage( + role=RoleTypes.user, + content=json.dumps(session_dict["conversation"]) if session_dict["conversation"] is not None else message.get("content", json.dumps(message, indent=4)) + ) + ] + + response = llm.chat_completions( + messages=[msg.to_llm_msg() for msg in context_messages] + ) + if response.status == LLMResponseStatus.SUCCESS: + name = response.content + self.rename_session( + session_id=session.session_id, + new_name=name + ) + + return session.get() + + class VideoDBHandler: def __init__(self, collection_id="default"):