Files
antigravity-skills-reference/web-app/public/skills/notebooklm/scripts/browser_session.py

255 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""
Browser Session Management for NotebookLM
Individual browser session for persistent NotebookLM conversations
Based on the original NotebookLM API implementation
"""
import time
import sys
from typing import Any, Dict, Optional
from pathlib import Path
from patchright.sync_api import BrowserContext, Page
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent))
from browser_utils import StealthUtils
class BrowserSession:
"""
Represents a single persistent browser session for NotebookLM
Each session gets its own Page (tab) within a shared BrowserContext,
allowing for contextual conversations where NotebookLM remembers
previous messages.
"""
def __init__(self, session_id: str, context: BrowserContext, notebook_url: str):
"""
Initialize a new browser session
Args:
session_id: Unique identifier for this session
context: Browser context (shared or dedicated)
notebook_url: Target NotebookLM URL for this session
"""
self.id = session_id
self.created_at = time.time()
self.last_activity = time.time()
self.message_count = 0
self.notebook_url = notebook_url
self.context = context
self.page = None
self.stealth = StealthUtils()
# Initialize the session
self._initialize()
def _initialize(self):
"""Initialize the browser session and navigate to NotebookLM"""
print(f"🚀 Creating session {self.id}...")
# Create new page (tab) in context
self.page = self.context.new_page()
print(f" 🌐 Navigating to NotebookLM...")
try:
# Navigate to notebook
self.page.goto(self.notebook_url, wait_until="domcontentloaded", timeout=30000)
# Check if login is needed
if "accounts.google.com" in self.page.url:
raise RuntimeError("Authentication required. Please run auth_manager.py setup first.")
# Wait for page to be ready
self._wait_for_ready()
# Simulate human inspection
self.stealth.random_mouse_movement(self.page)
self.stealth.random_delay(300, 600)
print(f"✅ Session {self.id} ready!")
except Exception as e:
print(f"❌ Failed to initialize session: {e}")
if self.page:
self.page.close()
raise
def _wait_for_ready(self):
"""Wait for NotebookLM page to be ready"""
try:
# Wait for chat input
self.page.wait_for_selector("textarea.query-box-input", timeout=10000, state="visible")
except Exception:
# Try alternative selector
self.page.wait_for_selector('textarea[aria-label="Feld für Anfragen"]', timeout=5000, state="visible")
def ask(self, question: str) -> Dict[str, Any]:
"""
Ask a question in this session
Args:
question: The question to ask
Returns:
Dict with status, question, answer, session_id
"""
try:
self.last_activity = time.time()
self.message_count += 1
print(f"💬 [{self.id}] Asking: {question}")
# Snapshot current answer to detect new response
previous_answer = self._snapshot_latest_response()
# Find chat input
chat_input_selector = "textarea.query-box-input"
try:
self.page.wait_for_selector(chat_input_selector, timeout=5000, state="visible")
except Exception:
chat_input_selector = 'textarea[aria-label="Feld für Anfragen"]'
self.page.wait_for_selector(chat_input_selector, timeout=5000, state="visible")
# Click and type with human-like behavior
self.stealth.realistic_click(self.page, chat_input_selector)
self.stealth.human_type(self.page, chat_input_selector, question)
# Small pause before submit
self.stealth.random_delay(300, 800)
# Submit
self.page.keyboard.press("Enter")
# Wait for response
print(" ⏳ Waiting for response...")
self.stealth.random_delay(1500, 3000)
# Get new answer
answer = self._wait_for_latest_answer(previous_answer)
if not answer:
raise Exception("Empty response from NotebookLM")
print(f" ✅ Got response ({len(answer)} chars)")
return {
"status": "success",
"question": question,
"answer": answer,
"session_id": self.id,
"notebook_url": self.notebook_url
}
except Exception as e:
print(f" ❌ Error: {e}")
return {
"status": "error",
"question": question,
"error": str(e),
"session_id": self.id
}
def _snapshot_latest_response(self) -> Optional[str]:
"""Get the current latest response text"""
try:
# Use correct NotebookLM selector
responses = self.page.query_selector_all(".to-user-container .message-text-content")
if responses:
return responses[-1].inner_text()
except Exception:
pass
return None
def _wait_for_latest_answer(self, previous_answer: Optional[str], timeout: int = 120) -> str:
"""Wait for and extract the new answer"""
start_time = time.time()
last_candidate = None
stable_count = 0
while time.time() - start_time < timeout:
# Check if NotebookLM is still thinking (most reliable indicator)
try:
thinking_element = self.page.query_selector('div.thinking-message')
if thinking_element and thinking_element.is_visible():
time.sleep(0.5)
continue
except Exception:
pass
try:
# Use correct NotebookLM selector
responses = self.page.query_selector_all(".to-user-container .message-text-content")
if responses:
latest_text = responses[-1].inner_text().strip()
# Check if it's a new response
if latest_text and latest_text != previous_answer:
# Check if text is stable (3 consecutive polls)
if latest_text == last_candidate:
stable_count += 1
if stable_count >= 3:
return latest_text
else:
stable_count = 1
last_candidate = latest_text
except Exception:
pass
time.sleep(0.5)
raise TimeoutError(f"No response received within {timeout} seconds")
def reset(self):
"""Reset the chat by reloading the page"""
print(f"🔄 Resetting session {self.id}...")
self.page.reload(wait_until="domcontentloaded")
self._wait_for_ready()
previous_count = self.message_count
self.message_count = 0
self.last_activity = time.time()
print(f"✅ Session reset (cleared {previous_count} messages)")
return previous_count
def close(self):
"""Close this session and clean up resources"""
print(f"🛑 Closing session {self.id}...")
if self.page:
try:
self.page.close()
except Exception as e:
print(f" ⚠️ Error closing page: {e}")
print(f"✅ Session {self.id} closed")
def get_info(self) -> Dict[str, Any]:
"""Get information about this session"""
return {
"id": self.id,
"created_at": self.created_at,
"last_activity": self.last_activity,
"age_seconds": time.time() - self.created_at,
"inactive_seconds": time.time() - self.last_activity,
"message_count": self.message_count,
"notebook_url": self.notebook_url
}
def is_expired(self, timeout_seconds: int = 900) -> bool:
"""Check if session has expired (default: 15 minutes)"""
return (time.time() - self.last_activity) > timeout_seconds
if __name__ == "__main__":
# Example usage
print("Browser Session Module - Use ask_question.py for main interface")
print("This module provides low-level browser session management.")