gemx 0.2.4__tar.gz → 0.2.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gemx
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Drive the Gemini web UI from Python via Playwright
5
5
  Project-URL: Homepage, https://github.com/ra0x3/gemx
6
6
  Project-URL: Repository, https://github.com/ra0x3/gemx
@@ -26,13 +26,12 @@ Description-Content-Type: text/markdown
26
26
  `gemini.google.com` as if it were an API, using Playwright to enter a prompt,
27
27
  submit it, and capture the structured reply.
28
28
 
29
- It exists because the obvious approaches don't work: Gemini's editor is
30
- [Quill](https://quilljs.com/), which keeps its own document model and ignores
31
- DOM surgery, Playwright `fill()`, and synthetic input events — those leave the
32
- model empty and the turn errors with *"I encountered an error."* Gemx injects
33
- text via `execCommand('insertText')` (the same trusted input pipeline a manual
34
- paste uses), reads the reply from `message-content .markdown`, and retains the
35
- *peak* streamed text because Gemini re-mounts the response node mid-stream.
29
+ Gemx enters prompts using the input routine verified by the browser-console
30
+ recovery script: it replaces the editor contents with a paragraph and dispatches
31
+ a bubbling `input` event before clicking Send. It reads replies from
32
+ `message-content .markdown` and retains the *peak* streamed text because Gemini
33
+ can re-mount the response node mid-stream. Responses must settle for six seconds
34
+ by default; JSON and XML must also parse before Gemx returns them.
36
35
 
37
36
  ## Install
38
37
 
@@ -4,13 +4,12 @@
4
4
  `gemini.google.com` as if it were an API, using Playwright to enter a prompt,
5
5
  submit it, and capture the structured reply.
6
6
 
7
- It exists because the obvious approaches don't work: Gemini's editor is
8
- [Quill](https://quilljs.com/), which keeps its own document model and ignores
9
- DOM surgery, Playwright `fill()`, and synthetic input events — those leave the
10
- model empty and the turn errors with *"I encountered an error."* Gemx injects
11
- text via `execCommand('insertText')` (the same trusted input pipeline a manual
12
- paste uses), reads the reply from `message-content .markdown`, and retains the
13
- *peak* streamed text because Gemini re-mounts the response node mid-stream.
7
+ Gemx enters prompts using the input routine verified by the browser-console
8
+ recovery script: it replaces the editor contents with a paragraph and dispatches
9
+ a bubbling `input` event before clicking Send. It reads replies from
10
+ `message-content .markdown` and retains the *peak* streamed text because Gemini
11
+ can re-mount the response node mid-stream. Responses must settle for six seconds
12
+ by default; JSON and XML must also parse before Gemx returns them.
14
13
 
15
14
  ## Install
16
15
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gemx"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "Drive the Gemini web UI from Python via Playwright"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -28,4 +28,4 @@ __all__ = [
28
28
  "parse_output",
29
29
  ]
30
30
 
31
- __version__ = "0.2.4"
31
+ __version__ = "0.2.5"
@@ -3,11 +3,9 @@
3
3
  This is a formalized version of a hack that drives ``gemini.google.com/app`` as
4
4
  if it were an API. The non-obvious parts (and why this exists):
5
5
 
6
- * **Input** must go through ``document.execCommand('insertText')``. Gemini's
7
- editor is Quill, which keeps its own document model and ignores DOM surgery,
8
- Playwright ``fill()``, and synthetic ``InputEvent``s — those leave the model
9
- empty and the turn errors with "I encountered an error". ``execCommand`` fires
10
- the trusted ``beforeinput``/``input`` pair Quill honors, exactly like a paste.
6
+ * **Input** follows the browser-console recovery script: replace the editor's
7
+ contents with a paragraph and dispatch a bubbling ``input`` event before
8
+ clicking Send.
11
9
  * **The response** lives in ``message-content .markdown`` (not a bare
12
10
  ``.markdown``), and Gemini re-mounts / empties that node after streaming, so we
13
11
  retain the *peak* text seen rather than reading the DOM once at the end.
@@ -109,13 +107,13 @@ _INSERT_TEXT_JS = """(text) => {
109
107
  const editor = document.querySelector('.ql-editor[contenteditable="true"]');
110
108
  if (!editor) return false;
111
109
  editor.focus();
112
- const sel = window.getSelection();
113
- sel.removeAllRanges();
114
- const range = document.createRange();
115
- range.selectNodeContents(editor);
116
- range.collapse(false);
117
- sel.addRange(range);
118
- return document.execCommand('insertText', false, text);
110
+ editor.classList.remove('ql-blank');
111
+ editor.textContent = '';
112
+ const p = document.createElement('p');
113
+ p.textContent = text;
114
+ editor.appendChild(p);
115
+ editor.dispatchEvent(new Event('input', { bubbles: true }));
116
+ return true;
119
117
  }"""
120
118
 
121
119
  _INSTALL_RESPONSE_OBSERVER_JS = """(selectors) => {
@@ -297,9 +295,8 @@ class GemxConfig: # pylint: disable=too-many-instance-attributes
297
295
  scroll_input_into_view: bool = True
298
296
  # The peak response must exceed this length and stay non-growing (or be
299
297
  # emptied) for this many poll ticks before it is accepted as complete.
300
- # Matches the original bot/gemini.py driver (stable_count >= 2).
301
298
  min_response_chars: int = 50
302
- stable_ticks: int = 2
299
+ stable_ticks: int = 3
303
300
  # How often (seconds) to emit page diagnostics while waiting for the response
304
301
  # node to appear.
305
302
  diagnostics_interval_s: int = 10
@@ -40,13 +40,13 @@ def format_instruction(fmt: OutputFormat) -> str:
40
40
  """Return the prompt instruction that asks Gemini for ``fmt`` output."""
41
41
  if fmt is OutputFormat.JSON:
42
42
  return (
43
- "Respond with a single valid JSON object only. Do not include prose, "
44
- "markdown fences, or commentary outside the JSON."
43
+ "Respond with exactly one ```json fenced code block holding a single "
44
+ "valid JSON object, and nothing outside the block."
45
45
  )
46
46
  if fmt is OutputFormat.XML:
47
47
  return (
48
- "Respond with a single well-formed XML document only. Do not include "
49
- "prose, markdown fences, or commentary outside the XML."
48
+ "Respond with exactly one ```xml fenced code block holding a single "
49
+ "well-formed XML document, and nothing outside the block."
50
50
  )
51
51
  return (
52
52
  "Respond with plain text only. Do not include markdown fences, JSON, or "
@@ -140,8 +140,17 @@ def parse_output(text: str, fmt: OutputFormat) -> Any:
140
140
  end = repaired.rfind("}") + 1
141
141
  candidate = repaired[start:end] if start != -1 and end != 0 else repaired
142
142
  try:
143
- return json.loads(candidate)
143
+ return json.loads(candidate, strict=False)
144
144
  except json.JSONDecodeError as exc:
145
+ restarts = list(
146
+ re.finditer(r"```json[ \t]*\r?\n", text, flags=re.IGNORECASE)
147
+ )
148
+ if restarts:
149
+ candidate = text[restarts[-1].end() :].strip().removesuffix("```").rstrip()
150
+ try:
151
+ return json.loads(_clean_json_text(candidate), strict=False)
152
+ except json.JSONDecodeError:
153
+ pass
145
154
  raise ValueError(f"Could not parse JSON from response: {exc}") from exc
146
155
 
147
156
  start = cleaned.find("<")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes