suzmen commited on
Commit
0347658
Β·
verified Β·
1 Parent(s): dcf3155

Upload 64 files

Browse files
Files changed (2) hide show
  1. app/services/browser.py +74 -51
  2. requirements.txt +1 -0
app/services/browser.py CHANGED
@@ -6,7 +6,10 @@ and interacts with chatgpt.com to generate responses.
6
 
7
  import asyncio
8
  import threading
 
 
9
  from app.config import settings
 
10
 
11
 
12
  class BrowserEngine(threading.Thread):
@@ -116,7 +119,10 @@ class BrowserEngine(threading.Thread):
116
 
117
  # Navigate to ChatGPT
118
  print(f"[PhantomAPI] 🌐 Navigating to ChatGPT...")
119
- await page.goto("https://chatgpt.com/", wait_until="networkidle")
 
 
 
120
 
121
  # --- Diagnostic Logging ---
122
  title = await page.title()
@@ -159,10 +165,34 @@ class BrowserEngine(threading.Thread):
159
  '[data-message-author-role="assistant"]',
160
  timeout=settings.BROWSER_TIMEOUT,
161
  )
162
- print("[PhantomAPI] βœ… Assistant began responding.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  except Exception as e:
164
  # Diagnostics: What is actually on the page?
165
  print(f"[PhantomAPI] ❌ Response timeout/error: {e}")
 
166
 
167
  # Check for common error messages
168
  page_text = await page.evaluate("document.body.innerText")
@@ -177,69 +207,44 @@ class BrowserEngine(threading.Thread):
177
 
178
  raise
179
 
180
- # --- Response Polling (Two-Phase) ---
 
181
  last_text = ""
182
  unchanged_count = 0
183
  start_polling = asyncio.get_event_loop().time()
184
- found_first_char = False
185
-
186
- print("[PhantomAPI] ⏳ Phase 1: Waiting for first character...")
187
 
188
  while True:
189
- # hard break: 120 seconds total
190
  if asyncio.get_event_loop().time() - start_polling > 120:
191
  print("[PhantomAPI] ⚠️ Hard timeout reached.")
192
  break
193
 
194
- # Target the actual content div if possible
195
- elements = await page.query_selector_all(
196
- '[data-message-author-role="assistant"]'
197
- )
198
 
199
- if elements:
200
- # Look for markdown/prose inside the assistant bubble
201
- container = elements[-1]
202
- content_el = await container.query_selector(".markdown, .prose")
203
- target = content_el if content_el else container
204
-
205
- current_text = await target.inner_text()
206
- current_text = current_text.strip()
207
-
208
- # Phase 1: Wait for text to appear
209
- if not found_first_char:
210
- if len(current_text) > 0:
211
- found_first_char = True
212
- print(f"[PhantomAPI] πŸ“’ First character detected! Phase 2: Monitoring stream...")
213
- else:
214
- # If we've waited > 60s for the first char, something is wrong
215
- if asyncio.get_event_loop().time() - start_polling > 60:
216
- print("[PhantomAPI] ❌ Giving up: No text appeared after 60s.")
217
- break
218
 
219
- # Phase 2: Monitor stability
220
- if found_first_char:
221
- if current_text == last_text:
222
- unchanged_count += 1
223
- else:
224
- if len(current_text) > len(last_text):
225
- print(f"[PhantomAPI] ⏳ Generating... ({len(current_text)} chars)")
226
- last_text = current_text
227
- unchanged_count = 0
228
-
229
- # Once text starts, 2 seconds (4 iterations) of silence means done
230
- if unchanged_count >= 4:
231
- break
232
 
233
  await asyncio.sleep(0.5)
234
 
235
- if not found_first_char:
236
- # Fail diagnostics
237
- print("[PhantomAPI] ❌ Final check: Assistant bubble was empty.")
238
- bubble_html = await page.evaluate(
239
- "() => document.querySelector('[data-message-author-role=\"assistant\"]')?.outerHTML"
240
- )
241
- print(f"[PhantomAPI] πŸ“ Bubble HTML: {bubble_html[:500]}")
242
-
243
  print(f"[PhantomAPI] ✨ Response complete ({len(last_text)} chars).")
244
  return last_text.strip()
245
 
@@ -251,6 +256,24 @@ class BrowserEngine(threading.Thread):
251
  await context.close()
252
 
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  # ---------------------------------------------------------------------------
255
  # Singleton β€” created once at import time, started in app lifespan
256
  # ---------------------------------------------------------------------------
 
6
 
7
  import asyncio
8
  import threading
9
+ import os
10
+ import random
11
  from app.config import settings
12
+ from playwright_stealth import stealth_async
13
 
14
 
15
  class BrowserEngine(threading.Thread):
 
119
 
120
  # Navigate to ChatGPT
121
  print(f"[PhantomAPI] 🌐 Navigating to ChatGPT...")
122
+ await page.goto("https://chatgpt.com/", wait_until="load")
123
+
124
+ # Diagnostic Screenshot (See what the browser sees)
125
+ await self._save_debug_screenshot(page)
126
 
127
  # --- Diagnostic Logging ---
128
  title = await page.title()
 
165
  '[data-message-author-role="assistant"]',
166
  timeout=settings.BROWSER_TIMEOUT,
167
  )
168
+ print("[PhantomAPI] βœ… Assistant bubble appeared.")
169
+
170
+ # Wait for FIRST CHARACTER (Phase 1)
171
+ phase1_start = asyncio.get_event_loop().time()
172
+ while True:
173
+ if asyncio.get_event_loop().time() - phase1_start > 60:
174
+ print("[PhantomAPI] ❌ Timeout waiting for first character.")
175
+ break
176
+
177
+ bubble = await page.query_selector('[data-message-author-role="assistant"]')
178
+ content = await (await bubble.query_selector(".markdown, .prose, pre") or bubble).inner_text()
179
+
180
+ if content.strip():
181
+ print("[PhantomAPI] πŸ“’ Detected typing start...")
182
+ break
183
+
184
+ # Log if it's still busy but empty
185
+ busy = await bubble.get_attribute("aria-busy")
186
+ if busy == "true":
187
+ # Still generating, just wait
188
+ pass
189
+
190
+ await asyncio.sleep(1.0)
191
+
192
  except Exception as e:
193
  # Diagnostics: What is actually on the page?
194
  print(f"[PhantomAPI] ❌ Response timeout/error: {e}")
195
+ await self._save_debug_screenshot(page)
196
 
197
  # Check for common error messages
198
  page_text = await page.evaluate("document.body.innerText")
 
207
 
208
  raise
209
 
210
+ # --- Phase 2: Wait for stability (aria-busy=false) ---
211
+ print("[PhantomAPI] ⏳ Phase 2: Monitoring completion...")
212
  last_text = ""
213
  unchanged_count = 0
214
  start_polling = asyncio.get_event_loop().time()
 
 
 
215
 
216
  while True:
 
217
  if asyncio.get_event_loop().time() - start_polling > 120:
218
  print("[PhantomAPI] ⚠️ Hard timeout reached.")
219
  break
220
 
221
+ bubble = await page.query_selector('[data-message-author-role="assistant"]')
222
+ if not bubble: break
 
 
223
 
224
+ # Check busy status
225
+ is_busy = await bubble.get_attribute("aria-busy")
226
+
227
+ target = await bubble.query_selector(".markdown, .prose, pre") or bubble
228
+ current_text = await target.inner_text()
229
+ current_text = current_text.strip()
230
+
231
+ if current_text != last_text:
232
+ if len(current_text) > len(last_text):
233
+ print(f"[PhantomAPI] ⏳ Generating... ({len(current_text)} chars)")
234
+ last_text = current_text
235
+ unchanged_count = 0
236
+ else:
237
+ # If text is stable AND not busy, it's done
238
+ if is_busy != "true":
239
+ unchanged_count += 1
 
 
 
240
 
241
+ if unchanged_count >= 4:
242
+ print("[PhantomAPI] βœ… Generation finished (stability reached).")
243
+ break
 
 
 
 
 
 
 
 
 
 
244
 
245
  await asyncio.sleep(0.5)
246
 
247
+ await self._save_debug_screenshot(page)
 
 
 
 
 
 
 
248
  print(f"[PhantomAPI] ✨ Response complete ({len(last_text)} chars).")
249
  return last_text.strip()
250
 
 
256
  await context.close()
257
 
258
 
259
+ # ------------------------------------------------------------------
260
+ # Debugging
261
+ # ------------------------------------------------------------------
262
+
263
+ async def _save_debug_screenshot(self, page) -> None:
264
+ """Save a screenshot to the static folder for visual debugging."""
265
+ try:
266
+ static_dir = os.path.join(os.getcwd(), "static")
267
+ if not os.path.exists(static_dir):
268
+ os.makedirs(static_dir)
269
+
270
+ path = os.path.join(static_dir, "debug.png")
271
+ await page.screenshot(path=path, full_page=False)
272
+ print(f"[PhantomAPI] πŸ“Έ Debug screenshot saved to static/debug.png")
273
+ except Exception as e:
274
+ print(f"[PhantomAPI] ⚠️ Failed to save screenshot: {e}")
275
+
276
+
277
  # ---------------------------------------------------------------------------
278
  # Singleton β€” created once at import time, started in app lifespan
279
  # ---------------------------------------------------------------------------
requirements.txt CHANGED
@@ -7,3 +7,4 @@ python-dotenv>=1.0.0,<2.0.0
7
  python-multipart>=0.0.9,<1.0.0
8
  httpx>=0.27.0,<1.0.0
9
  pytest>=8.0.0,<9.0.0
 
 
7
  python-multipart>=0.0.9,<1.0.0
8
  httpx>=0.27.0,<1.0.0
9
  pytest>=8.0.0,<9.0.0
10
+ playwright-stealth>=1.0.6