-
Notifications
You must be signed in to change notification settings - Fork 862
Expand file tree
/
Copy pathask_question.py
More file actions
executable file
·324 lines (262 loc) · 9.83 KB
/
Copy pathask_question.py
File metadata and controls
executable file
·324 lines (262 loc) · 9.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#!/usr/bin/env python3
"""
Simple NotebookLM Question Interface
Based on MCP server implementation - simplified without sessions
Implements hybrid auth approach:
- Persistent browser profile (user_data_dir) for fingerprint consistency
- Manual cookie injection from state.json for session cookies (Playwright bug workaround)
See: https://github.com/microsoft/playwright/issues/36139
"""
import argparse
import sys
import time
import re
from pathlib import Path
from patchright.sync_api import sync_playwright
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent))
from auth_manager import AuthManager
from notebook_manager import NotebookLibrary
from config import QUERY_INPUT_SELECTORS, RESPONSE_SELECTORS, QUERY_TIMEOUT_SECONDS
from browser_utils import BrowserFactory, StealthUtils
# Follow-up reminder (adapted from MCP server for stateless operation)
# Since we don't have persistent sessions, we encourage comprehensive questions
FOLLOW_UP_REMINDER = (
"\n\nEXTREMELY IMPORTANT: Is that ALL you need to know? "
"You can always ask another question! Think about it carefully: "
"before you reply to the user, review their original request and this answer. "
"If anything is still unclear or missing, ask me another comprehensive question "
"that includes all necessary context (since each question opens a new browser session)."
)
TRANSIENT_RESPONSE_TEXTS = {
"Reading through pages...",
"Finding key words...",
}
def snapshot_responses(page):
"""Get the current assistant response texts."""
for selector in RESPONSE_SELECTORS:
try:
elements = page.query_selector_all(selector)
texts = []
for element in elements:
text = element.inner_text().strip()
if text:
texts.append(text)
if texts:
return texts
except Exception:
continue
return []
def wait_for_response_hydration(page, timeout_seconds: int = 10):
"""Wait briefly for existing conversation responses to finish hydrating."""
deadline = time.time() + timeout_seconds
last_count = None
stable_count = 0
while time.time() < deadline:
responses = snapshot_responses(page)
count = len(responses)
if count == last_count:
stable_count += 1
if stable_count >= 3:
return responses
else:
stable_count = 0
last_count = count
time.sleep(0.5)
return snapshot_responses(page)
def find_new_response_candidate(page, previous_responses, question):
"""Find the newest non-transient assistant response that was not present before asking."""
for selector in RESPONSE_SELECTORS:
try:
elements = page.query_selector_all(selector)
if not elements:
continue
texts = []
for element in elements:
text = element.inner_text().strip()
if text:
texts.append(text)
for text in reversed(texts):
if text == question:
continue
if text in TRANSIENT_RESPONSE_TEXTS:
continue
if text in previous_responses:
continue
return text
except Exception:
continue
return None
def ask_notebooklm(question: str, notebook_url: str, headless: bool = True) -> str:
"""
Ask a question to NotebookLM
Args:
question: Question to ask
notebook_url: NotebookLM notebook URL
headless: Run browser in headless mode
Returns:
Answer text from NotebookLM
"""
auth = AuthManager()
if not auth.is_authenticated():
print("⚠️ Not authenticated. Run: python auth_manager.py setup")
return None
print(f"💬 Asking: {question}")
print(f"📚 Notebook: {notebook_url}")
playwright = None
context = None
try:
# Start playwright
playwright = sync_playwright().start()
# Launch persistent browser context using factory
context = BrowserFactory.launch_persistent_context(
playwright,
headless=headless
)
# Navigate to notebook
page = context.new_page()
print(" 🌐 Opening notebook...")
page.goto(notebook_url, wait_until="domcontentloaded")
# Wait for NotebookLM
page.wait_for_url(re.compile(r"^https://notebooklm\.google\.com/"), timeout=10000)
# Wait for query input (MCP approach)
print(" ⏳ Waiting for query input...")
query_element = None
for selector in QUERY_INPUT_SELECTORS:
try:
query_element = page.wait_for_selector(
selector,
timeout=10000,
state="visible" # Only check visibility, not disabled!
)
if query_element:
print(f" ✓ Found input: {selector}")
break
except:
continue
if not query_element:
print(" ❌ Could not find query input")
return None
previous_responses = wait_for_response_hydration(page)
previous_count = len(previous_responses)
# Type question (human-like, fast)
print(" ⏳ Typing question...")
# Use primary selector for typing
input_selector = QUERY_INPUT_SELECTORS[0]
StealthUtils.human_type(page, input_selector, question)
# Submit
print(" 📤 Submitting...")
page.keyboard.press("Enter")
# Small pause
StealthUtils.random_delay(500, 1500)
# Wait for response (MCP approach: poll for stable text)
print(" ⏳ Waiting for answer...")
answer = None
stable_count = 0
last_text = None
saw_thinking = False
deadline = time.time() + QUERY_TIMEOUT_SECONDS
while time.time() < deadline:
# Check if NotebookLM is still thinking (most reliable indicator)
try:
thinking_element = page.query_selector('div.thinking-message')
if thinking_element and thinking_element.is_visible():
saw_thinking = True
time.sleep(1)
continue
except:
pass
text = find_new_response_candidate(page, previous_responses, question)
if text and (saw_thinking or len(snapshot_responses(page)) > previous_count):
if text == last_text:
stable_count += 1
if stable_count >= 3: # Stable for 3 polls
answer = text
break
else:
stable_count = 0
last_text = text
if answer:
break
time.sleep(1)
if not answer:
print(" ❌ Timeout waiting for answer")
return None
print(" ✅ Got answer!")
# Add follow-up reminder to encourage Claude to ask more questions
return answer + FOLLOW_UP_REMINDER
except Exception as e:
print(f" ❌ Error: {e}")
import traceback
traceback.print_exc()
return None
finally:
# Always clean up
if context:
try:
context.close()
except:
pass
if playwright:
try:
playwright.stop()
except:
pass
def main():
parser = argparse.ArgumentParser(description='Ask NotebookLM a question')
parser.add_argument('--question', required=True, help='Question to ask')
parser.add_argument('--notebook-url', help='NotebookLM notebook URL')
parser.add_argument('--notebook-id', help='Notebook ID from library')
parser.add_argument('--show-browser', action='store_true', help='Show browser')
args = parser.parse_args()
# Resolve notebook URL
notebook_url = args.notebook_url
if not notebook_url and args.notebook_id:
library = NotebookLibrary()
notebook = library.get_notebook(args.notebook_id)
if notebook:
notebook_url = notebook['url']
else:
print(f"❌ Notebook '{args.notebook_id}' not found")
return 1
if not notebook_url:
# Check for active notebook first
library = NotebookLibrary()
active = library.get_active_notebook()
if active:
notebook_url = active['url']
print(f"📚 Using active notebook: {active['name']}")
else:
# Show available notebooks
notebooks = library.list_notebooks()
if notebooks:
print("\n📚 Available notebooks:")
for nb in notebooks:
mark = " [ACTIVE]" if nb.get('id') == library.active_notebook_id else ""
print(f" {nb['id']}: {nb['name']}{mark}")
print("\nSpecify with --notebook-id or set active:")
print("python scripts/run.py notebook_manager.py activate --id ID")
else:
print("❌ No notebooks in library. Add one first:")
print("python scripts/run.py notebook_manager.py add --url URL --name NAME --description DESC --topics TOPICS")
return 1
# Ask the question
answer = ask_notebooklm(
question=args.question,
notebook_url=notebook_url,
headless=not args.show_browser
)
if answer:
print("\n" + "=" * 60)
print(f"Question: {args.question}")
print("=" * 60)
print()
print(answer)
print()
print("=" * 60)
return 0
else:
print("\n❌ Failed to get answer")
return 1
if __name__ == "__main__":
sys.exit(main())