agent-vx 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_vx-0.1.0.dist-info/LICENSE +21 -0
- agent_vx-0.1.0.dist-info/METADATA +87 -0
- agent_vx-0.1.0.dist-info/RECORD +10 -0
- agent_vx-0.1.0.dist-info/WHEEL +5 -0
- agent_vx-0.1.0.dist-info/entry_points.txt +2 -0
- agent_vx-0.1.0.dist-info/top_level.txt +1 -0
- agentaix/__init__.py +111 -0
- agentaix/gui.py +1321 -0
- agentaix/py.typed +0 -0
- agentaix/readme.txt +275 -0
agentaix/gui.py
ADDED
|
@@ -0,0 +1,1321 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import queue
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import tkinter as tk
|
|
14
|
+
from tkinter import scrolledtext
|
|
15
|
+
|
|
16
|
+
from playwright.async_api import (
|
|
17
|
+
Page,
|
|
18
|
+
TimeoutError as PlaywrightTimeoutError,
|
|
19
|
+
async_playwright,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ----------------------------------------------------------------------
|
|
24
|
+
# Core constants (unchanged)
|
|
25
|
+
# ----------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
CHATGPT_URL = "https://chatgpt.com/"
|
|
28
|
+
PROFILE_DIR = Path.home() / ".chatgpt_terminal_agent"
|
|
29
|
+
OUTPUT_DIR = Path.cwd() / "agent_output"
|
|
30
|
+
|
|
31
|
+
DEFAULT_TIMEOUT = 30_000
|
|
32
|
+
RESPONSE_TIMEOUT = 600_000
|
|
33
|
+
NAVIGATION_TIMEOUT = 120_000
|
|
34
|
+
MAX_RETRIES = 3
|
|
35
|
+
|
|
36
|
+
# Non-markdown markers (*** would be eaten by bold+italic rendering).
|
|
37
|
+
START_MARKER = "@@@START@@@"
|
|
38
|
+
END_MARKER = "@@@END@@@"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
SYSTEM_PROMPT = r"""
|
|
42
|
+
You are a production-grade Python coding agent.
|
|
43
|
+
|
|
44
|
+
The user will give you a programming request.
|
|
45
|
+
|
|
46
|
+
You MUST return exactly ONE complete Python file that fulfils the request.
|
|
47
|
+
|
|
48
|
+
STRICT RULES:
|
|
49
|
+
|
|
50
|
+
1. No introduction.
|
|
51
|
+
2. No explanation.
|
|
52
|
+
3. No conclusion.
|
|
53
|
+
4. No conversational text.
|
|
54
|
+
5. No "Sure", "Here is", or similar phrases.
|
|
55
|
+
6. Return COMPLETE working code.
|
|
56
|
+
7. Never return pseudocode.
|
|
57
|
+
8. Never use TODO placeholders.
|
|
58
|
+
9. Include every required import.
|
|
59
|
+
10. NEVER return multiple files, markdown tables, or prose.
|
|
60
|
+
11. The output must be a SINGLE self-contained Python file.
|
|
61
|
+
12. If third-party packages are needed, the Python file MUST
|
|
62
|
+
install them itself at runtime using pip (via subprocess with
|
|
63
|
+
sys.executable -m pip) and only install what is missing.
|
|
64
|
+
13. Do NOT emit a separate INSTALL block - the Python file handles
|
|
65
|
+
installation on its own.
|
|
66
|
+
14. If the task requires OS-level setup on Windows (registry
|
|
67
|
+
changes, service install, scheduled task, persistent
|
|
68
|
+
environment variable, firewall rule, etc.), the Python file
|
|
69
|
+
MUST contain the PowerShell script as a Python string, WRITE
|
|
70
|
+
it to a temporary .ps1 file, run it via subprocess, then
|
|
71
|
+
delete the .ps1 file afterwards.
|
|
72
|
+
15. Do not hard-code secrets.
|
|
73
|
+
16. Use secure and practical defaults.
|
|
74
|
+
17. Make the Python file runnable as `python the_file.py` with no
|
|
75
|
+
arguments required.
|
|
76
|
+
18. If arguments are useful, provide sensible defaults via
|
|
77
|
+
argparse.
|
|
78
|
+
19. Never wrap the code in anything other than a single python
|
|
79
|
+
fenced block.
|
|
80
|
+
|
|
81
|
+
RESPONSE FORMAT (very important).
|
|
82
|
+
|
|
83
|
+
Your reply must have exactly this shape, in this order, and nothing
|
|
84
|
+
else. Do NOT write out the words in square brackets literally -
|
|
85
|
+
produce the actual characters they describe.
|
|
86
|
+
|
|
87
|
+
- Line 1: three at-sign characters, then the word START, then three
|
|
88
|
+
at-sign characters. (In other words: the character @ appears three
|
|
89
|
+
times, immediately followed by START, immediately followed by @
|
|
90
|
+
three more times. No spaces. Nothing before it.)
|
|
91
|
+
|
|
92
|
+
- Line 2: three backticks, then the word python (this is the opening
|
|
93
|
+
of a fenced code block).
|
|
94
|
+
|
|
95
|
+
- Then: the complete, real, working Python source code of the single
|
|
96
|
+
file the user asked for. Write ACTUAL code. Do NOT write any
|
|
97
|
+
placeholder text such as "full python file here" or similar.
|
|
98
|
+
|
|
99
|
+
- Then: three backticks on their own line (this closes the fenced
|
|
100
|
+
code block).
|
|
101
|
+
|
|
102
|
+
- Final line: three at-sign characters, then the word END, then three
|
|
103
|
+
at-sign characters. (Same pattern as the first line but with END.)
|
|
104
|
+
|
|
105
|
+
Your response is INVALID if it contains anything before the first
|
|
106
|
+
line or after the final line, or if it contains any explanation,
|
|
107
|
+
heading, greeting, or prose.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ----------------------------------------------------------------------
|
|
112
|
+
# UI palette (red-on-black techy)
|
|
113
|
+
# ----------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
UI_BG = "#0a0000" # window base
|
|
116
|
+
UI_BG_DEEP = "#050000" # log / entry background
|
|
117
|
+
UI_FG = "#ff1a1a" # primary red text
|
|
118
|
+
UI_FG_DIM = "#8a1010" # dim red
|
|
119
|
+
UI_FG_BRIGHT = "#ff4444" # bright red
|
|
120
|
+
UI_ACCENT = "#ff0033" # accent / cursor
|
|
121
|
+
UI_BORDER = "#3a0000" # divider / outline
|
|
122
|
+
UI_OK = "#00ff66" # success
|
|
123
|
+
UI_WARN = "#ff8800" # status / progress
|
|
124
|
+
UI_ERR = "#ff0033" # error
|
|
125
|
+
UI_OUT = "#cc4444" # program output
|
|
126
|
+
UI_USER = "#ffaaaa" # user input echo
|
|
127
|
+
|
|
128
|
+
UI_FONT = ("Consolas", 10)
|
|
129
|
+
UI_FONT_BOLD = ("Consolas", 10, "bold")
|
|
130
|
+
UI_FONT_SMALL = ("Consolas", 9)
|
|
131
|
+
UI_FONT_TITLE = ("Consolas", 22, "bold")
|
|
132
|
+
UI_FONT_GLYPH = ("Consolas", 12, "bold")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ----------------------------------------------------------------------
|
|
136
|
+
# CLI helpers (unchanged)
|
|
137
|
+
# ----------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def parse_args():
|
|
140
|
+
parser = argparse.ArgumentParser(
|
|
141
|
+
description="Terminal ChatGPT coding agent"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
"prompt",
|
|
146
|
+
nargs="*",
|
|
147
|
+
help="Programming request (one-shot mode)"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"-f",
|
|
152
|
+
"--file",
|
|
153
|
+
help="Read request from a text file (one-shot mode)"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"-o",
|
|
158
|
+
"--output",
|
|
159
|
+
default=str(OUTPUT_DIR / "response.txt"),
|
|
160
|
+
help="Save full response here (one-shot mode)"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--code-output",
|
|
165
|
+
default=str(OUTPUT_DIR / "code.txt"),
|
|
166
|
+
help="Save extracted code here (one-shot mode)"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--browser",
|
|
171
|
+
choices=("chromium", "chrome", "msedge"),
|
|
172
|
+
default="chromium"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--new-session",
|
|
177
|
+
action="store_true"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--timeout",
|
|
182
|
+
type=int,
|
|
183
|
+
default=DEFAULT_TIMEOUT
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--response-timeout",
|
|
188
|
+
type=int,
|
|
189
|
+
default=RESPONSE_TIMEOUT
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return parser.parse_args()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_one_shot_request(args) -> str:
|
|
196
|
+
if args.file:
|
|
197
|
+
path = Path(args.file)
|
|
198
|
+
|
|
199
|
+
if not path.exists():
|
|
200
|
+
raise FileNotFoundError(
|
|
201
|
+
f"Request file not found: {path}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
request = path.read_text(
|
|
205
|
+
encoding="utf-8"
|
|
206
|
+
).strip()
|
|
207
|
+
|
|
208
|
+
if not request:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
"Request file is empty."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return request
|
|
214
|
+
|
|
215
|
+
request = " ".join(args.prompt).strip()
|
|
216
|
+
|
|
217
|
+
if not request:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
"No request supplied."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return request
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_interactive_request() -> str:
|
|
226
|
+
print()
|
|
227
|
+
print("Enter your request (single line).")
|
|
228
|
+
print("Type 'exit' or 'quit' to stop.")
|
|
229
|
+
print()
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
line = input("request> ")
|
|
233
|
+
except EOFError:
|
|
234
|
+
return ""
|
|
235
|
+
|
|
236
|
+
line = line.strip()
|
|
237
|
+
|
|
238
|
+
if line.lower() in ("exit", "quit", ""):
|
|
239
|
+
return ""
|
|
240
|
+
|
|
241
|
+
return line
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def build_prompt(request: str) -> str:
|
|
245
|
+
return (
|
|
246
|
+
SYSTEM_PROMPT
|
|
247
|
+
+ "\n\n"
|
|
248
|
+
+ "USER REQUEST:\n"
|
|
249
|
+
+ request
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ----------------------------------------------------------------------
|
|
254
|
+
# Input-box discovery (used by both navigation and sending)
|
|
255
|
+
# ----------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
MESSAGE_BOX_SELECTORS = [
|
|
258
|
+
'textarea[data-testid="textbox"]',
|
|
259
|
+
'textarea[placeholder*="Message"]',
|
|
260
|
+
'textarea[placeholder*="message"]',
|
|
261
|
+
'textarea',
|
|
262
|
+
'[contenteditable="true"][role="textbox"]',
|
|
263
|
+
'[contenteditable="true"]',
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def visible_locator(
|
|
268
|
+
page: Page,
|
|
269
|
+
selectors: list[str],
|
|
270
|
+
timeout: int = 1500
|
|
271
|
+
):
|
|
272
|
+
for selector in selectors:
|
|
273
|
+
try:
|
|
274
|
+
locator = page.locator(selector).first
|
|
275
|
+
|
|
276
|
+
if await locator.is_visible(timeout=timeout):
|
|
277
|
+
return locator
|
|
278
|
+
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
async def find_message_box(
|
|
286
|
+
page: Page,
|
|
287
|
+
timeout: int
|
|
288
|
+
):
|
|
289
|
+
deadline = time.monotonic() + timeout / 1000
|
|
290
|
+
|
|
291
|
+
while time.monotonic() < deadline:
|
|
292
|
+
box = await visible_locator(page, MESSAGE_BOX_SELECTORS)
|
|
293
|
+
|
|
294
|
+
if box:
|
|
295
|
+
return box
|
|
296
|
+
|
|
297
|
+
await page.wait_for_timeout(500)
|
|
298
|
+
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def find_send_button(page: Page):
|
|
303
|
+
selectors = [
|
|
304
|
+
'button[data-testid="send-button"]',
|
|
305
|
+
'button[aria-label="Send prompt"]',
|
|
306
|
+
'button[aria-label*="Send"]',
|
|
307
|
+
'button:has-text("Send")',
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
return await visible_locator(page, selectors)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def detect_blocked_page(page: Page):
|
|
314
|
+
try:
|
|
315
|
+
body = (
|
|
316
|
+
await page.locator("body").inner_text(timeout=3000)
|
|
317
|
+
).lower()
|
|
318
|
+
except Exception:
|
|
319
|
+
body = ""
|
|
320
|
+
|
|
321
|
+
captcha = (
|
|
322
|
+
"captcha",
|
|
323
|
+
"verify you are human",
|
|
324
|
+
"verify you're human",
|
|
325
|
+
"security check",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
blocked = (
|
|
329
|
+
"access denied",
|
|
330
|
+
"request blocked",
|
|
331
|
+
"unusual activity",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
login = (
|
|
335
|
+
"log in to chatgpt",
|
|
336
|
+
"sign in to chatgpt",
|
|
337
|
+
"create an account",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if any(x in body for x in captcha):
|
|
341
|
+
return "CAPTCHA/security verification detected."
|
|
342
|
+
|
|
343
|
+
if any(x in body for x in blocked):
|
|
344
|
+
return "Access/security block detected."
|
|
345
|
+
|
|
346
|
+
if any(x in body for x in login):
|
|
347
|
+
box = await find_message_box(page, 1000)
|
|
348
|
+
|
|
349
|
+
if box is None:
|
|
350
|
+
return (
|
|
351
|
+
"ChatGPT requires authentication "
|
|
352
|
+
"or logged-out chat is unavailable."
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
async def launch_context(playwright, args):
|
|
359
|
+
# Not headless - but launched minimized so it stays out of the way.
|
|
360
|
+
launch_options = {
|
|
361
|
+
"headless": False,
|
|
362
|
+
"args": [
|
|
363
|
+
"--start-minimized",
|
|
364
|
+
"--window-position=-32000,-32000",
|
|
365
|
+
"--window-size=200,200",
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if args.browser == "chrome":
|
|
370
|
+
launch_options["channel"] = "chrome"
|
|
371
|
+
|
|
372
|
+
elif args.browser == "msedge":
|
|
373
|
+
launch_options["channel"] = "msedge"
|
|
374
|
+
|
|
375
|
+
if args.new_session:
|
|
376
|
+
browser = await playwright.chromium.launch(**launch_options)
|
|
377
|
+
|
|
378
|
+
context = await browser.new_context(
|
|
379
|
+
viewport={
|
|
380
|
+
"width": 1440,
|
|
381
|
+
"height": 900,
|
|
382
|
+
},
|
|
383
|
+
locale="en-US",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return browser, context
|
|
387
|
+
|
|
388
|
+
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
|
389
|
+
|
|
390
|
+
context = (
|
|
391
|
+
await playwright.chromium.launch_persistent_context(
|
|
392
|
+
user_data_dir=str(PROFILE_DIR),
|
|
393
|
+
viewport={
|
|
394
|
+
"width": 1440,
|
|
395
|
+
"height": 900,
|
|
396
|
+
},
|
|
397
|
+
locale="en-US",
|
|
398
|
+
**launch_options,
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
return None, context
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# ----------------------------------------------------------------------
|
|
406
|
+
# Screen text helpers
|
|
407
|
+
# ----------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
async def get_screen_text(page: Page) -> str:
|
|
410
|
+
"""Return all visible text on the page as a single string."""
|
|
411
|
+
try:
|
|
412
|
+
return await page.locator("body").inner_text(timeout=5000)
|
|
413
|
+
except Exception:
|
|
414
|
+
return ""
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def count_marker(text: str, marker: str) -> int:
|
|
418
|
+
return text.count(marker)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def extract_between_markers(text: str) -> str:
|
|
422
|
+
"""
|
|
423
|
+
Return the text between the LAST @@@START@@@ and the LAST @@@END@@@.
|
|
424
|
+
"""
|
|
425
|
+
if not text:
|
|
426
|
+
return ""
|
|
427
|
+
|
|
428
|
+
start = text.rfind(START_MARKER)
|
|
429
|
+
end = text.rfind(END_MARKER)
|
|
430
|
+
|
|
431
|
+
if start == -1 or end == -1 or end <= start:
|
|
432
|
+
return ""
|
|
433
|
+
|
|
434
|
+
return text[start + len(START_MARKER):end].strip()
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ----------------------------------------------------------------------
|
|
438
|
+
# Send
|
|
439
|
+
# ----------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
async def send_prompt(page: Page, prompt: str, timeout: int):
|
|
442
|
+
box = await find_message_box(page, timeout)
|
|
443
|
+
|
|
444
|
+
if box is None:
|
|
445
|
+
try:
|
|
446
|
+
await page.screenshot(path="debug_no_input.png", full_page=True)
|
|
447
|
+
print("[!] Screenshot saved: debug_no_input.png")
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
raise RuntimeError(
|
|
451
|
+
f"Could not find ChatGPT input. URL={page.url}"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
await box.click()
|
|
455
|
+
await page.wait_for_timeout(150)
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
await page.keyboard.insert_text(prompt)
|
|
459
|
+
except Exception:
|
|
460
|
+
try:
|
|
461
|
+
await box.fill(prompt)
|
|
462
|
+
except Exception:
|
|
463
|
+
await page.keyboard.type(prompt, delay=1)
|
|
464
|
+
|
|
465
|
+
await page.wait_for_timeout(400)
|
|
466
|
+
|
|
467
|
+
await box.press("Enter")
|
|
468
|
+
|
|
469
|
+
await page.wait_for_timeout(600)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ----------------------------------------------------------------------
|
|
473
|
+
# Navigate to a fresh chat (new per iteration)
|
|
474
|
+
# ----------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
async def open_new_chat(page: Page, timeout_ms: int):
|
|
477
|
+
"""
|
|
478
|
+
Navigate to the ChatGPT root and wait until the input box is
|
|
479
|
+
actually present. Returns the input box locator.
|
|
480
|
+
"""
|
|
481
|
+
try:
|
|
482
|
+
await page.goto(
|
|
483
|
+
CHATGPT_URL,
|
|
484
|
+
wait_until="domcontentloaded",
|
|
485
|
+
timeout=NAVIGATION_TIMEOUT,
|
|
486
|
+
)
|
|
487
|
+
except PlaywrightTimeoutError:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
# Wait for the React app to hydrate and render the input box.
|
|
491
|
+
box = await find_message_box(page, timeout_ms)
|
|
492
|
+
|
|
493
|
+
if box is None:
|
|
494
|
+
try:
|
|
495
|
+
await page.screenshot(
|
|
496
|
+
path="debug_no_input.png", full_page=True
|
|
497
|
+
)
|
|
498
|
+
print("[!] Screenshot saved: debug_no_input.png")
|
|
499
|
+
except Exception:
|
|
500
|
+
pass
|
|
501
|
+
raise RuntimeError(
|
|
502
|
+
f"Input box never appeared after navigation. URL={page.url}"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
return box
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# ----------------------------------------------------------------------
|
|
509
|
+
# Wait for response
|
|
510
|
+
# ----------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
async def wait_for_response(
|
|
513
|
+
page: Page,
|
|
514
|
+
timeout_ms: int,
|
|
515
|
+
baseline_end_count: int,
|
|
516
|
+
):
|
|
517
|
+
"""
|
|
518
|
+
Wait until at least ONE new @@@END@@@ marker appears on screen
|
|
519
|
+
(from the AI reply), then extract the text between the last
|
|
520
|
+
@@@START@@@ and the last @@@END@@@.
|
|
521
|
+
"""
|
|
522
|
+
start = time.monotonic()
|
|
523
|
+
last_debug = -1
|
|
524
|
+
target = baseline_end_count + 1
|
|
525
|
+
|
|
526
|
+
print(
|
|
527
|
+
f"[*] Waiting for reply (baseline ENDs={baseline_end_count}, "
|
|
528
|
+
f"target={target})..."
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
while True:
|
|
532
|
+
elapsed = (time.monotonic() - start) * 1000
|
|
533
|
+
|
|
534
|
+
if elapsed > timeout_ms:
|
|
535
|
+
try:
|
|
536
|
+
await page.screenshot(
|
|
537
|
+
path="debug_timeout.png", full_page=True
|
|
538
|
+
)
|
|
539
|
+
print("[!] Screenshot saved: debug_timeout.png")
|
|
540
|
+
except Exception:
|
|
541
|
+
pass
|
|
542
|
+
raise TimeoutError(
|
|
543
|
+
"Timed out waiting for the reply. "
|
|
544
|
+
"See debug_timeout.png."
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
text = await get_screen_text(page)
|
|
548
|
+
end_count = count_marker(text, END_MARKER)
|
|
549
|
+
|
|
550
|
+
if end_count >= target:
|
|
551
|
+
extracted = extract_between_markers(text)
|
|
552
|
+
if extracted:
|
|
553
|
+
print(
|
|
554
|
+
f"[✓] Reply captured "
|
|
555
|
+
f"(ENDs={end_count}, chars={len(extracted)})."
|
|
556
|
+
)
|
|
557
|
+
return extracted
|
|
558
|
+
|
|
559
|
+
sec = int(elapsed / 1000)
|
|
560
|
+
if sec != last_debug and sec % 5 == 0:
|
|
561
|
+
last_debug = sec
|
|
562
|
+
print(
|
|
563
|
+
f"[debug] elapsed={sec}s "
|
|
564
|
+
f"ends={end_count}/{target} "
|
|
565
|
+
f"screen_chars={len(text)}"
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
await page.wait_for_timeout(500)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def extract_code(response: str) -> str:
|
|
572
|
+
blocks = re.findall(
|
|
573
|
+
r"```(?:[a-zA-Z0-9_+#.-]+)?\s*\n(.*?)```",
|
|
574
|
+
response,
|
|
575
|
+
re.DOTALL,
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if blocks:
|
|
579
|
+
return "\n\n".join(
|
|
580
|
+
block.strip()
|
|
581
|
+
for block in blocks
|
|
582
|
+
if block.strip()
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
return response.strip()
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def save_output(response: str, output_path: str, code_path: str):
|
|
589
|
+
output = Path(output_path)
|
|
590
|
+
code = Path(code_path)
|
|
591
|
+
|
|
592
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
593
|
+
code.parent.mkdir(parents=True, exist_ok=True)
|
|
594
|
+
|
|
595
|
+
output.write_text(response, encoding="utf-8")
|
|
596
|
+
code.write_text(extract_code(response), encoding="utf-8")
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def build_unique_paths(prefix: str) -> tuple[Path, Path, Path]:
|
|
600
|
+
"""
|
|
601
|
+
Build a unique set of output paths for one iteration.
|
|
602
|
+
Returns (response_txt, code_txt, run_py).
|
|
603
|
+
"""
|
|
604
|
+
stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
605
|
+
|
|
606
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
607
|
+
|
|
608
|
+
response_path = OUTPUT_DIR / f"{prefix}_response_{stamp}.txt"
|
|
609
|
+
code_path = OUTPUT_DIR / f"{prefix}_code_{stamp}.txt"
|
|
610
|
+
py_path = OUTPUT_DIR / f"{prefix}_generated_{stamp}.py"
|
|
611
|
+
|
|
612
|
+
return response_path, code_path, py_path
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def run_generated_code(py_path: Path) -> int:
|
|
616
|
+
"""
|
|
617
|
+
Execute the generated .py with the same interpreter, in a
|
|
618
|
+
subprocess, and stream its output live.
|
|
619
|
+
Returns the exit code.
|
|
620
|
+
"""
|
|
621
|
+
print()
|
|
622
|
+
print("=" * 70)
|
|
623
|
+
print(f"[*] Running: {py_path}")
|
|
624
|
+
print("=" * 70)
|
|
625
|
+
|
|
626
|
+
try:
|
|
627
|
+
result = subprocess.run(
|
|
628
|
+
[sys.executable, str(py_path)],
|
|
629
|
+
check=False,
|
|
630
|
+
)
|
|
631
|
+
print("=" * 70)
|
|
632
|
+
print(f"[*] Exit code: {result.returncode}")
|
|
633
|
+
print("=" * 70)
|
|
634
|
+
return result.returncode
|
|
635
|
+
|
|
636
|
+
except KeyboardInterrupt:
|
|
637
|
+
print("\n[!] Interrupted.")
|
|
638
|
+
return -1
|
|
639
|
+
|
|
640
|
+
except Exception as exc:
|
|
641
|
+
print(f"[!] Failed to run: {exc}")
|
|
642
|
+
return -1
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
async def do_one_iteration(
|
|
646
|
+
page: Page,
|
|
647
|
+
request: str,
|
|
648
|
+
args,
|
|
649
|
+
label_prefix: str,
|
|
650
|
+
):
|
|
651
|
+
"""
|
|
652
|
+
One full round: new chat -> send -> capture -> save -> run.
|
|
653
|
+
"""
|
|
654
|
+
print()
|
|
655
|
+
print("#" * 70)
|
|
656
|
+
print(
|
|
657
|
+
f"[*] New chat. Request: {request[:80]}"
|
|
658
|
+
+ ("..." if len(request) > 80 else "")
|
|
659
|
+
)
|
|
660
|
+
print("#" * 70)
|
|
661
|
+
|
|
662
|
+
# ------------------------------------------------------------
|
|
663
|
+
# Open a fresh chat each iteration and wait for input box.
|
|
664
|
+
# ------------------------------------------------------------
|
|
665
|
+
await open_new_chat(page, args.timeout)
|
|
666
|
+
|
|
667
|
+
blocked = await detect_blocked_page(page)
|
|
668
|
+
if blocked:
|
|
669
|
+
raise RuntimeError(blocked)
|
|
670
|
+
|
|
671
|
+
baseline_text = await get_screen_text(page)
|
|
672
|
+
baseline_end_count = count_marker(baseline_text, END_MARKER)
|
|
673
|
+
|
|
674
|
+
print(f"[*] Baseline END markers on screen: {baseline_end_count}")
|
|
675
|
+
print("[*] Sending request...")
|
|
676
|
+
|
|
677
|
+
await send_prompt(page, build_prompt(request), args.timeout)
|
|
678
|
+
|
|
679
|
+
print("[*] Waiting for response...")
|
|
680
|
+
|
|
681
|
+
response = await wait_for_response(
|
|
682
|
+
page,
|
|
683
|
+
args.response_timeout,
|
|
684
|
+
baseline_end_count,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
if not response.strip():
|
|
688
|
+
raise RuntimeError("Empty ChatGPT response.")
|
|
689
|
+
|
|
690
|
+
# ------------------------------------------------------------
|
|
691
|
+
# Save outputs.
|
|
692
|
+
# ------------------------------------------------------------
|
|
693
|
+
response_path, code_path, py_path = build_unique_paths(label_prefix)
|
|
694
|
+
|
|
695
|
+
# NOTE: argument order must match the signature
|
|
696
|
+
# save_output(response, output_path, code_path).
|
|
697
|
+
save_output(response, str(response_path), str(code_path))
|
|
698
|
+
|
|
699
|
+
# Save the extracted python code as a runnable .py file.
|
|
700
|
+
code_body = extract_code(response)
|
|
701
|
+
py_path.write_text(code_body, encoding="utf-8")
|
|
702
|
+
|
|
703
|
+
print()
|
|
704
|
+
print("=" * 70)
|
|
705
|
+
print(response)
|
|
706
|
+
print("=" * 70)
|
|
707
|
+
print()
|
|
708
|
+
print(f"Response saved to: {response_path.resolve()}")
|
|
709
|
+
print(f"Code saved to: {code_path.resolve()}")
|
|
710
|
+
print(f"Python file: {py_path.resolve()}")
|
|
711
|
+
|
|
712
|
+
# ------------------------------------------------------------
|
|
713
|
+
# Run the generated code.
|
|
714
|
+
# ------------------------------------------------------------
|
|
715
|
+
run_generated_code(py_path)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
async def main_async(args):
|
|
719
|
+
one_shot = bool(args.prompt) or bool(args.file)
|
|
720
|
+
|
|
721
|
+
if one_shot:
|
|
722
|
+
request = get_one_shot_request(args)
|
|
723
|
+
label_prefix = "oneshot"
|
|
724
|
+
else:
|
|
725
|
+
label_prefix = "run"
|
|
726
|
+
|
|
727
|
+
browser = None
|
|
728
|
+
context = None
|
|
729
|
+
|
|
730
|
+
async with async_playwright() as playwright:
|
|
731
|
+
try:
|
|
732
|
+
browser, context = await launch_context(playwright, args)
|
|
733
|
+
|
|
734
|
+
context.set_default_timeout(args.timeout)
|
|
735
|
+
|
|
736
|
+
pages = context.pages
|
|
737
|
+
|
|
738
|
+
if pages:
|
|
739
|
+
page = pages[0]
|
|
740
|
+
else:
|
|
741
|
+
page = await context.new_page()
|
|
742
|
+
|
|
743
|
+
page.set_default_navigation_timeout(NAVIGATION_TIMEOUT)
|
|
744
|
+
|
|
745
|
+
print("[*] Opening ChatGPT...")
|
|
746
|
+
|
|
747
|
+
# Initial load; wait for input box to be sure we're logged in.
|
|
748
|
+
try:
|
|
749
|
+
await open_new_chat(page, args.timeout)
|
|
750
|
+
except Exception as exc:
|
|
751
|
+
print(f"[!] Initial load failed: {exc}")
|
|
752
|
+
raise
|
|
753
|
+
|
|
754
|
+
# ----------------------------------------------------
|
|
755
|
+
# One-shot mode.
|
|
756
|
+
# ----------------------------------------------------
|
|
757
|
+
if one_shot:
|
|
758
|
+
for attempt in range(1, MAX_RETRIES + 1):
|
|
759
|
+
try:
|
|
760
|
+
print(
|
|
761
|
+
f"[*] Attempt {attempt}/{MAX_RETRIES}"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
await do_one_iteration(
|
|
765
|
+
page, request, args, label_prefix
|
|
766
|
+
)
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
except KeyboardInterrupt:
|
|
770
|
+
raise
|
|
771
|
+
|
|
772
|
+
except Exception as exc:
|
|
773
|
+
print(
|
|
774
|
+
f"[!] Attempt failed: {exc}",
|
|
775
|
+
file=sys.stderr,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
if attempt < MAX_RETRIES:
|
|
779
|
+
delay = attempt * 3
|
|
780
|
+
print(
|
|
781
|
+
f"[*] Retrying in {delay}s..."
|
|
782
|
+
)
|
|
783
|
+
await asyncio.sleep(delay)
|
|
784
|
+
|
|
785
|
+
raise RuntimeError(
|
|
786
|
+
f"Agent failed after {MAX_RETRIES} attempts."
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
# ----------------------------------------------------
|
|
790
|
+
# Interactive loop mode.
|
|
791
|
+
# ----------------------------------------------------
|
|
792
|
+
iteration = 0
|
|
793
|
+
|
|
794
|
+
while True:
|
|
795
|
+
request = get_interactive_request()
|
|
796
|
+
|
|
797
|
+
if not request:
|
|
798
|
+
print("[*] Exiting.")
|
|
799
|
+
break
|
|
800
|
+
|
|
801
|
+
iteration += 1
|
|
802
|
+
prefix = f"run{iteration:03d}"
|
|
803
|
+
|
|
804
|
+
try:
|
|
805
|
+
await do_one_iteration(
|
|
806
|
+
page, request, args, prefix
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
except KeyboardInterrupt:
|
|
810
|
+
print("\n[*] Interrupted, exiting.")
|
|
811
|
+
break
|
|
812
|
+
|
|
813
|
+
except Exception as exc:
|
|
814
|
+
print(
|
|
815
|
+
f"[!] Iteration failed: {exc}",
|
|
816
|
+
file=sys.stderr,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
finally:
|
|
822
|
+
if context:
|
|
823
|
+
try:
|
|
824
|
+
await context.close()
|
|
825
|
+
except Exception:
|
|
826
|
+
pass
|
|
827
|
+
|
|
828
|
+
if browser:
|
|
829
|
+
try:
|
|
830
|
+
await browser.close()
|
|
831
|
+
except Exception:
|
|
832
|
+
pass
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
# ======================================================================
|
|
836
|
+
# GUI AGENT - AgentAnon
|
|
837
|
+
# ======================================================================
|
|
838
|
+
|
|
839
|
+
def _build_gui_args():
|
|
840
|
+
"""Lightweight namespace that mirrors the CLI args used by the
|
|
841
|
+
existing core logic (browser / new_session / timeout ...)."""
|
|
842
|
+
|
|
843
|
+
class _Args:
|
|
844
|
+
browser = "chromium"
|
|
845
|
+
new_session = False
|
|
846
|
+
timeout = DEFAULT_TIMEOUT
|
|
847
|
+
response_timeout = RESPONSE_TIMEOUT
|
|
848
|
+
|
|
849
|
+
return _Args()
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
class AgentAnonApp:
|
|
853
|
+
"""
|
|
854
|
+
Red-on-black techy GUI wrapper around the existing async core.
|
|
855
|
+
All heavy lifting still happens through the same helpers
|
|
856
|
+
(launch_context, open_new_chat, send_prompt, wait_for_response,
|
|
857
|
+
extract_code, save_output, ...).
|
|
858
|
+
"""
|
|
859
|
+
|
|
860
|
+
def __init__(self, root: tk.Tk):
|
|
861
|
+
self.root = root
|
|
862
|
+
|
|
863
|
+
# Thread-safe channels between GUI thread and worker thread.
|
|
864
|
+
self.out_queue: queue.Queue = queue.Queue()
|
|
865
|
+
self.request_queue: queue.Queue = queue.Queue()
|
|
866
|
+
|
|
867
|
+
self.running = True
|
|
868
|
+
self.busy = False
|
|
869
|
+
self.ready = False
|
|
870
|
+
|
|
871
|
+
self._setup_ui()
|
|
872
|
+
self._start_worker()
|
|
873
|
+
|
|
874
|
+
self.root.after(50, self._poll)
|
|
875
|
+
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
876
|
+
|
|
877
|
+
# ------------------------------------------------------------------
|
|
878
|
+
# UI construction
|
|
879
|
+
# ------------------------------------------------------------------
|
|
880
|
+
|
|
881
|
+
def _setup_ui(self):
|
|
882
|
+
self.root.title("AgentAnon")
|
|
883
|
+
self.root.configure(bg=UI_BG)
|
|
884
|
+
self.root.geometry("960x680")
|
|
885
|
+
self.root.minsize(720, 520)
|
|
886
|
+
|
|
887
|
+
# ---- Header ------------------------------------------------
|
|
888
|
+
header = tk.Frame(self.root, bg=UI_BG)
|
|
889
|
+
header.pack(fill="x", padx=14, pady=(12, 0))
|
|
890
|
+
|
|
891
|
+
title = tk.Label(
|
|
892
|
+
header,
|
|
893
|
+
text="\u25e2 AGENTANON \u25e3",
|
|
894
|
+
bg=UI_BG,
|
|
895
|
+
fg=UI_FG,
|
|
896
|
+
font=UI_FONT_TITLE,
|
|
897
|
+
)
|
|
898
|
+
title.pack(side="left")
|
|
899
|
+
|
|
900
|
+
subtitle = tk.Label(
|
|
901
|
+
header,
|
|
902
|
+
text="// autonomous code synthesis unit",
|
|
903
|
+
bg=UI_BG,
|
|
904
|
+
fg=UI_FG_DIM,
|
|
905
|
+
font=UI_FONT_SMALL,
|
|
906
|
+
)
|
|
907
|
+
subtitle.pack(side="left", padx=(12, 0), pady=(10, 0))
|
|
908
|
+
|
|
909
|
+
self._divider(pady=(8, 8))
|
|
910
|
+
|
|
911
|
+
# ---- Log / chat area --------------------------------------
|
|
912
|
+
log_frame = tk.Frame(self.root, bg=UI_BG)
|
|
913
|
+
log_frame.pack(fill="both", expand=True, padx=14)
|
|
914
|
+
|
|
915
|
+
self.log_area = scrolledtext.ScrolledText(
|
|
916
|
+
log_frame,
|
|
917
|
+
bg=UI_BG_DEEP,
|
|
918
|
+
fg=UI_FG,
|
|
919
|
+
insertbackground=UI_ACCENT,
|
|
920
|
+
font=UI_FONT,
|
|
921
|
+
relief="flat",
|
|
922
|
+
borderwidth=0,
|
|
923
|
+
highlightthickness=1,
|
|
924
|
+
highlightbackground=UI_BORDER,
|
|
925
|
+
highlightcolor=UI_BORDER,
|
|
926
|
+
wrap="word",
|
|
927
|
+
state="disabled",
|
|
928
|
+
padx=10,
|
|
929
|
+
pady=8,
|
|
930
|
+
)
|
|
931
|
+
self.log_area.pack(fill="both", expand=True)
|
|
932
|
+
|
|
933
|
+
self.log_area.tag_configure("info", foreground=UI_FG)
|
|
934
|
+
self.log_area.tag_configure("status", foreground=UI_WARN)
|
|
935
|
+
self.log_area.tag_configure("success", foreground=UI_OK)
|
|
936
|
+
self.log_area.tag_configure("error", foreground=UI_ERR)
|
|
937
|
+
self.log_area.tag_configure("output", foreground=UI_OUT)
|
|
938
|
+
self.log_area.tag_configure("user", foreground=UI_USER,
|
|
939
|
+
font=UI_FONT_BOLD)
|
|
940
|
+
self.log_area.tag_configure("heading", foreground=UI_FG_BRIGHT,
|
|
941
|
+
font=UI_FONT_BOLD)
|
|
942
|
+
self.log_area.tag_configure("system", foreground=UI_FG_DIM,
|
|
943
|
+
font=UI_FONT_SMALL)
|
|
944
|
+
|
|
945
|
+
self._divider(pady=(8, 8))
|
|
946
|
+
|
|
947
|
+
# ---- Input row --------------------------------------------
|
|
948
|
+
input_frame = tk.Frame(self.root, bg=UI_BG)
|
|
949
|
+
input_frame.pack(fill="x", padx=14, pady=(0, 6))
|
|
950
|
+
|
|
951
|
+
prompt_glyph = tk.Label(
|
|
952
|
+
input_frame,
|
|
953
|
+
text="\u25b8",
|
|
954
|
+
bg=UI_BG,
|
|
955
|
+
fg=UI_FG,
|
|
956
|
+
font=UI_FONT_GLYPH,
|
|
957
|
+
)
|
|
958
|
+
prompt_glyph.pack(side="left", padx=(0, 6))
|
|
959
|
+
|
|
960
|
+
self.entry = tk.Entry(
|
|
961
|
+
input_frame,
|
|
962
|
+
bg=UI_BG_DEEP,
|
|
963
|
+
fg=UI_FG,
|
|
964
|
+
insertbackground=UI_ACCENT,
|
|
965
|
+
font=UI_FONT,
|
|
966
|
+
relief="flat",
|
|
967
|
+
highlightthickness=1,
|
|
968
|
+
highlightbackground=UI_BORDER,
|
|
969
|
+
highlightcolor=UI_ACCENT,
|
|
970
|
+
disabledbackground=UI_BG_DEEP,
|
|
971
|
+
disabledforeground=UI_FG_DIM,
|
|
972
|
+
)
|
|
973
|
+
self.entry.pack(
|
|
974
|
+
side="left", fill="x", expand=True, padx=(0, 10), ipady=6
|
|
975
|
+
)
|
|
976
|
+
self.entry.bind("<Return>", self._submit_event)
|
|
977
|
+
|
|
978
|
+
self.send_btn = tk.Button(
|
|
979
|
+
input_frame,
|
|
980
|
+
text="[ TRANSMIT ]",
|
|
981
|
+
bg=UI_BG,
|
|
982
|
+
fg=UI_FG,
|
|
983
|
+
activebackground=UI_ACCENT,
|
|
984
|
+
activeforeground="#000000",
|
|
985
|
+
font=UI_FONT_BOLD,
|
|
986
|
+
relief="flat",
|
|
987
|
+
borderwidth=0,
|
|
988
|
+
padx=14,
|
|
989
|
+
pady=4,
|
|
990
|
+
cursor="hand2",
|
|
991
|
+
command=self._submit_event,
|
|
992
|
+
)
|
|
993
|
+
self.send_btn.pack(side="right")
|
|
994
|
+
|
|
995
|
+
# ---- Status bar -------------------------------------------
|
|
996
|
+
status_frame = tk.Frame(self.root, bg=UI_BG)
|
|
997
|
+
status_frame.pack(fill="x", padx=14, pady=(0, 12))
|
|
998
|
+
|
|
999
|
+
self.status_label = tk.Label(
|
|
1000
|
+
status_frame,
|
|
1001
|
+
text="\u25cf initializing",
|
|
1002
|
+
bg=UI_BG,
|
|
1003
|
+
fg=UI_WARN,
|
|
1004
|
+
font=UI_FONT_SMALL,
|
|
1005
|
+
anchor="w",
|
|
1006
|
+
)
|
|
1007
|
+
self.status_label.pack(side="left")
|
|
1008
|
+
|
|
1009
|
+
self.hint_label = tk.Label(
|
|
1010
|
+
status_frame,
|
|
1011
|
+
text="enter \u2192 transmit | esc \u2192 clear",
|
|
1012
|
+
bg=UI_BG,
|
|
1013
|
+
fg=UI_FG_DIM,
|
|
1014
|
+
font=UI_FONT_SMALL,
|
|
1015
|
+
anchor="e",
|
|
1016
|
+
)
|
|
1017
|
+
self.hint_label.pack(side="right")
|
|
1018
|
+
|
|
1019
|
+
self.root.bind("<Escape>", lambda e: self.entry.delete(0, "end"))
|
|
1020
|
+
|
|
1021
|
+
# Intro banner
|
|
1022
|
+
self._append("\u2500" * 68, "system")
|
|
1023
|
+
self._append(" AGENTANON :: autonomous code synthesis unit", "heading")
|
|
1024
|
+
self._append(" channel: secure | mode: interactive | status: warming up", "system")
|
|
1025
|
+
self._append("\u2500" * 68, "system")
|
|
1026
|
+
self.entry.focus_set()
|
|
1027
|
+
|
|
1028
|
+
def _divider(self, pady=(0, 0)):
|
|
1029
|
+
sep = tk.Frame(self.root, bg=UI_BORDER, height=1)
|
|
1030
|
+
sep.pack(fill="x", padx=14, pady=pady)
|
|
1031
|
+
|
|
1032
|
+
# ------------------------------------------------------------------
|
|
1033
|
+
# Threading / worker
|
|
1034
|
+
# ------------------------------------------------------------------
|
|
1035
|
+
|
|
1036
|
+
def _start_worker(self):
|
|
1037
|
+
t = threading.Thread(
|
|
1038
|
+
target=self._worker_thread,
|
|
1039
|
+
name="AgentAnonWorker",
|
|
1040
|
+
daemon=True,
|
|
1041
|
+
)
|
|
1042
|
+
t.start()
|
|
1043
|
+
|
|
1044
|
+
def _worker_thread(self):
|
|
1045
|
+
try:
|
|
1046
|
+
asyncio.run(self._worker_main())
|
|
1047
|
+
except Exception as exc:
|
|
1048
|
+
self._log(f"worker crashed: {exc}", "error")
|
|
1049
|
+
self._status("halted", UI_ERR)
|
|
1050
|
+
|
|
1051
|
+
async def _worker_main(self):
|
|
1052
|
+
args = _build_gui_args()
|
|
1053
|
+
|
|
1054
|
+
async with async_playwright() as playwright:
|
|
1055
|
+
browser = None
|
|
1056
|
+
context = None
|
|
1057
|
+
try:
|
|
1058
|
+
self._log("booting headless runtime...", "system")
|
|
1059
|
+
self._status("booting", UI_WARN)
|
|
1060
|
+
|
|
1061
|
+
browser, context = await launch_context(playwright, args)
|
|
1062
|
+
context.set_default_timeout(args.timeout)
|
|
1063
|
+
|
|
1064
|
+
pages = context.pages
|
|
1065
|
+
page = pages[0] if pages else await context.new_page()
|
|
1066
|
+
page.set_default_navigation_timeout(NAVIGATION_TIMEOUT)
|
|
1067
|
+
|
|
1068
|
+
self._log("opening secure channel...", "system")
|
|
1069
|
+
await open_new_chat(page, args.timeout)
|
|
1070
|
+
|
|
1071
|
+
self.ready = True
|
|
1072
|
+
self._log("channel established.", "success")
|
|
1073
|
+
self._status("ready", UI_OK)
|
|
1074
|
+
self._log("awaiting instruction.", "system")
|
|
1075
|
+
|
|
1076
|
+
loop = asyncio.get_event_loop()
|
|
1077
|
+
|
|
1078
|
+
while self.running:
|
|
1079
|
+
try:
|
|
1080
|
+
request = await loop.run_in_executor(
|
|
1081
|
+
None, self.request_queue.get
|
|
1082
|
+
)
|
|
1083
|
+
except RuntimeError:
|
|
1084
|
+
break
|
|
1085
|
+
|
|
1086
|
+
if request is None:
|
|
1087
|
+
break
|
|
1088
|
+
|
|
1089
|
+
self._status("working", UI_WARN)
|
|
1090
|
+
self._log("", "info")
|
|
1091
|
+
self._log("\u25b8 working on:", "heading")
|
|
1092
|
+
self._log(f" {request}", "user")
|
|
1093
|
+
|
|
1094
|
+
try:
|
|
1095
|
+
py_path = await self._run_iteration(
|
|
1096
|
+
page, request, args
|
|
1097
|
+
)
|
|
1098
|
+
await self._run_generated_async(py_path)
|
|
1099
|
+
except Exception as exc:
|
|
1100
|
+
self._log(f" \u2717 {exc}", "error")
|
|
1101
|
+
|
|
1102
|
+
self._status("ready", UI_OK)
|
|
1103
|
+
self._done()
|
|
1104
|
+
|
|
1105
|
+
finally:
|
|
1106
|
+
if context:
|
|
1107
|
+
try:
|
|
1108
|
+
await context.close()
|
|
1109
|
+
except Exception:
|
|
1110
|
+
pass
|
|
1111
|
+
if browser:
|
|
1112
|
+
try:
|
|
1113
|
+
await browser.close()
|
|
1114
|
+
except Exception:
|
|
1115
|
+
pass
|
|
1116
|
+
|
|
1117
|
+
# ------------------------------------------------------------------
|
|
1118
|
+
# GUI-flavoured version of do_one_iteration (same core logic,
|
|
1119
|
+
# only prints replaced with queue messages).
|
|
1120
|
+
# ------------------------------------------------------------------
|
|
1121
|
+
|
|
1122
|
+
async def _run_iteration(self, page: Page, request: str, args):
|
|
1123
|
+
await open_new_chat(page, args.timeout)
|
|
1124
|
+
|
|
1125
|
+
blocked = await detect_blocked_page(page)
|
|
1126
|
+
if blocked:
|
|
1127
|
+
raise RuntimeError(blocked)
|
|
1128
|
+
|
|
1129
|
+
baseline_text = await get_screen_text(page)
|
|
1130
|
+
baseline_end_count = count_marker(baseline_text, END_MARKER)
|
|
1131
|
+
|
|
1132
|
+
self._log(" \u00b7 transmitting payload...", "status")
|
|
1133
|
+
await send_prompt(page, build_prompt(request), args.timeout)
|
|
1134
|
+
|
|
1135
|
+
self._log(" \u00b7 synthesizing response...", "status")
|
|
1136
|
+
response = await self._wait_for_response(
|
|
1137
|
+
page, args.response_timeout, baseline_end_count
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
if not response.strip():
|
|
1141
|
+
raise RuntimeError("empty response payload.")
|
|
1142
|
+
|
|
1143
|
+
response_path, code_path, py_path = build_unique_paths("run")
|
|
1144
|
+
save_output(response, str(response_path), str(code_path))
|
|
1145
|
+
|
|
1146
|
+
code_body = extract_code(response)
|
|
1147
|
+
py_path.write_text(code_body, encoding="utf-8")
|
|
1148
|
+
|
|
1149
|
+
self._log(f" \u2713 artifact: {py_path.name}", "success")
|
|
1150
|
+
self._log(f" \u2192 {py_path.resolve()}", "system")
|
|
1151
|
+
|
|
1152
|
+
return py_path
|
|
1153
|
+
|
|
1154
|
+
async def _wait_for_response(
|
|
1155
|
+
self,
|
|
1156
|
+
page: Page,
|
|
1157
|
+
timeout_ms: int,
|
|
1158
|
+
baseline_end_count: int,
|
|
1159
|
+
):
|
|
1160
|
+
start = time.monotonic()
|
|
1161
|
+
target = baseline_end_count + 1
|
|
1162
|
+
last_tick = -1
|
|
1163
|
+
|
|
1164
|
+
while True:
|
|
1165
|
+
elapsed_ms = (time.monotonic() - start) * 1000
|
|
1166
|
+
|
|
1167
|
+
if elapsed_ms > timeout_ms:
|
|
1168
|
+
raise TimeoutError("response window expired.")
|
|
1169
|
+
|
|
1170
|
+
text = await get_screen_text(page)
|
|
1171
|
+
end_count = count_marker(text, END_MARKER)
|
|
1172
|
+
|
|
1173
|
+
if end_count >= target:
|
|
1174
|
+
extracted = extract_between_markers(text)
|
|
1175
|
+
if extracted:
|
|
1176
|
+
return extracted
|
|
1177
|
+
|
|
1178
|
+
sec = int(elapsed_ms / 1000)
|
|
1179
|
+
if sec != last_tick and sec % 5 == 0 and sec > 0:
|
|
1180
|
+
last_tick = sec
|
|
1181
|
+
self._log(f" \u00b7 still synthesizing ({sec}s)", "status")
|
|
1182
|
+
|
|
1183
|
+
await page.wait_for_timeout(500)
|
|
1184
|
+
|
|
1185
|
+
async def _run_generated_async(self, py_path: Path):
|
|
1186
|
+
self._log(" \u00b7 executing artifact...", "status")
|
|
1187
|
+
|
|
1188
|
+
try:
|
|
1189
|
+
proc = await asyncio.create_subprocess_exec(
|
|
1190
|
+
sys.executable, str(py_path),
|
|
1191
|
+
stdout=asyncio.subprocess.PIPE,
|
|
1192
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
if proc.stdout is not None:
|
|
1196
|
+
async for raw in proc.stdout:
|
|
1197
|
+
text = raw.decode(errors="replace").rstrip()
|
|
1198
|
+
if text:
|
|
1199
|
+
self._log(f" | {text}", "output")
|
|
1200
|
+
|
|
1201
|
+
await proc.wait()
|
|
1202
|
+
|
|
1203
|
+
if proc.returncode == 0:
|
|
1204
|
+
self._log(" \u2713 execution complete (exit 0)", "success")
|
|
1205
|
+
else:
|
|
1206
|
+
self._log(
|
|
1207
|
+
f" \u2717 execution finished (exit {proc.returncode})",
|
|
1208
|
+
"error",
|
|
1209
|
+
)
|
|
1210
|
+
except Exception as exc:
|
|
1211
|
+
self._log(f" \u2717 execution failed: {exc}", "error")
|
|
1212
|
+
|
|
1213
|
+
# ------------------------------------------------------------------
|
|
1214
|
+
# Cross-thread messaging
|
|
1215
|
+
# ------------------------------------------------------------------
|
|
1216
|
+
|
|
1217
|
+
def _log(self, text: str, tag: str = "info"):
|
|
1218
|
+
self.out_queue.put(("log", text, tag))
|
|
1219
|
+
|
|
1220
|
+
def _status(self, text: str, color: str = UI_FG):
|
|
1221
|
+
self.out_queue.put(("status", text, color))
|
|
1222
|
+
|
|
1223
|
+
def _done(self):
|
|
1224
|
+
self.out_queue.put(("done",))
|
|
1225
|
+
|
|
1226
|
+
def _poll(self):
|
|
1227
|
+
try:
|
|
1228
|
+
while True:
|
|
1229
|
+
item = self.out_queue.get_nowait()
|
|
1230
|
+
kind = item[0]
|
|
1231
|
+
|
|
1232
|
+
if kind == "log":
|
|
1233
|
+
self._append(item[1], item[2])
|
|
1234
|
+
elif kind == "status":
|
|
1235
|
+
self.status_label.configure(
|
|
1236
|
+
text=f"\u25cf {item[1]}", fg=item[2]
|
|
1237
|
+
)
|
|
1238
|
+
elif kind == "done":
|
|
1239
|
+
self.busy = False
|
|
1240
|
+
self.send_btn.configure(state="normal")
|
|
1241
|
+
self.entry.focus_set()
|
|
1242
|
+
except queue.Empty:
|
|
1243
|
+
pass
|
|
1244
|
+
|
|
1245
|
+
if self.running:
|
|
1246
|
+
self.root.after(50, self._poll)
|
|
1247
|
+
|
|
1248
|
+
def _append(self, text: str, tag: str = "info"):
|
|
1249
|
+
self.log_area.configure(state="normal")
|
|
1250
|
+
self.log_area.insert("end", text + "\n", tag)
|
|
1251
|
+
self.log_area.see("end")
|
|
1252
|
+
self.log_area.configure(state="disabled")
|
|
1253
|
+
|
|
1254
|
+
# ------------------------------------------------------------------
|
|
1255
|
+
# User actions
|
|
1256
|
+
# ------------------------------------------------------------------
|
|
1257
|
+
|
|
1258
|
+
def _submit_event(self, event=None):
|
|
1259
|
+
text = self.entry.get().strip()
|
|
1260
|
+
if not text:
|
|
1261
|
+
return
|
|
1262
|
+
|
|
1263
|
+
if not self.ready:
|
|
1264
|
+
self._append("[ waiting ] channel not ready yet", "status")
|
|
1265
|
+
return
|
|
1266
|
+
|
|
1267
|
+
if self.busy:
|
|
1268
|
+
self._append("[ busy ] previous task still running", "status")
|
|
1269
|
+
return
|
|
1270
|
+
|
|
1271
|
+
self.entry.delete(0, "end")
|
|
1272
|
+
self._append("", "info")
|
|
1273
|
+
self._append(f"\u25b8 {text}", "user")
|
|
1274
|
+
|
|
1275
|
+
self.busy = True
|
|
1276
|
+
self.send_btn.configure(state="disabled")
|
|
1277
|
+
self.request_queue.put(text)
|
|
1278
|
+
|
|
1279
|
+
def _on_close(self):
|
|
1280
|
+
self.running = False
|
|
1281
|
+
try:
|
|
1282
|
+
self.request_queue.put(None)
|
|
1283
|
+
except Exception:
|
|
1284
|
+
pass
|
|
1285
|
+
try:
|
|
1286
|
+
self.root.destroy()
|
|
1287
|
+
except Exception:
|
|
1288
|
+
pass
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def launch_gui():
|
|
1292
|
+
root = tk.Tk()
|
|
1293
|
+
AgentAnonApp(root)
|
|
1294
|
+
root.mainloop()
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
# ======================================================================
|
|
1298
|
+
# Entry point
|
|
1299
|
+
# ======================================================================
|
|
1300
|
+
|
|
1301
|
+
def main():
|
|
1302
|
+
# If CLI arguments are given, keep the original terminal behaviour.
|
|
1303
|
+
# Otherwise, launch the AgentAnon GUI.
|
|
1304
|
+
if len(sys.argv) > 1:
|
|
1305
|
+
args = parse_args()
|
|
1306
|
+
|
|
1307
|
+
try:
|
|
1308
|
+
asyncio.run(main_async(args))
|
|
1309
|
+
|
|
1310
|
+
except KeyboardInterrupt:
|
|
1311
|
+
print("\nStopped.")
|
|
1312
|
+
|
|
1313
|
+
except Exception as exc:
|
|
1314
|
+
print(f"\nERROR: {exc}", file=sys.stderr)
|
|
1315
|
+
sys.exit(1)
|
|
1316
|
+
else:
|
|
1317
|
+
launch_gui()
|
|
1318
|
+
|
|
1319
|
+
|
|
1320
|
+
if __name__ == "__main__":
|
|
1321
|
+
main()
|