prettyplay 0.0.2__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.
Files changed (75) hide show
  1. prettyplay/.usages/lifecycle.md +117 -0
  2. prettyplay/.usages/steps.md +169 -0
  3. prettyplay/__init__.py +18 -0
  4. prettyplay/cache/.usages/addressing.md +44 -0
  5. prettyplay/cache/.usages/budgets.md +51 -0
  6. prettyplay/cache/.usages/storage.md +32 -0
  7. prettyplay/cache/__init__.py +8 -0
  8. prettyplay/cache/budgets.py +93 -0
  9. prettyplay/cache/models.py +55 -0
  10. prettyplay/cache/store.py +276 -0
  11. prettyplay/cache/text.py +36 -0
  12. prettyplay/config/.usages/configuration.md +246 -0
  13. prettyplay/config/__init__.py +6 -0
  14. prettyplay/config/loader.py +336 -0
  15. prettyplay/config/models.py +288 -0
  16. prettyplay/driver/.usages/error_kinds.md +26 -0
  17. prettyplay/driver/.usages/plumbing.md +52 -0
  18. prettyplay/driver/__init__.py +11 -0
  19. prettyplay/driver/errors.py +76 -0
  20. prettyplay/driver/page.py +175 -0
  21. prettyplay/driver/session.py +330 -0
  22. prettyplay/engine/.usages/generation.md +157 -0
  23. prettyplay/engine/.usages/healing.md +61 -0
  24. prettyplay/engine/__init__.py +19 -0
  25. prettyplay/engine/attempts.py +80 -0
  26. prettyplay/engine/classification.py +70 -0
  27. prettyplay/engine/compliance.py +125 -0
  28. prettyplay/engine/execution.py +48 -0
  29. prettyplay/engine/generator.py +1110 -0
  30. prettyplay/engine/groups/.usages/recovery.md +71 -0
  31. prettyplay/engine/groups/__init__.py +7 -0
  32. prettyplay/engine/groups/diagnosis.py +128 -0
  33. prettyplay/engine/groups/outcome.py +55 -0
  34. prettyplay/engine/groups/recovery.py +491 -0
  35. prettyplay/engine/healer.py +149 -0
  36. prettyplay/engine/polling/.usages/settle.md +55 -0
  37. prettyplay/engine/polling/__init__.py +6 -0
  38. prettyplay/engine/polling/settle.py +98 -0
  39. prettyplay/engine/polling/window.py +72 -0
  40. prettyplay/engine/renderer/.usages/memory.md +24 -0
  41. prettyplay/engine/renderer/.usages/rendering.md +44 -0
  42. prettyplay/engine/renderer/.usages/validation.md +20 -0
  43. prettyplay/engine/renderer/__init__.py +7 -0
  44. prettyplay/engine/renderer/memory.py +37 -0
  45. prettyplay/engine/renderer/render.py +184 -0
  46. prettyplay/engine/renderer/validation.py +76 -0
  47. prettyplay/engine/steering/.usages/steering.md +95 -0
  48. prettyplay/engine/steering/__init__.py +5 -0
  49. prettyplay/engine/steering/steering.py +765 -0
  50. prettyplay/engine/text.py +41 -0
  51. prettyplay/executor.py +654 -0
  52. prettyplay/failures/.usages/taxonomy.md +131 -0
  53. prettyplay/failures/__init__.py +25 -0
  54. prettyplay/failures/errors.py +359 -0
  55. prettyplay/groups.py +276 -0
  56. prettyplay/llm/.usages/classification.md +31 -0
  57. prettyplay/llm/.usages/providers.md +127 -0
  58. prettyplay/llm/.usages/transport.md +81 -0
  59. prettyplay/llm/__init__.py +38 -0
  60. prettyplay/llm/_request.py +823 -0
  61. prettyplay/llm/anthropic_provider.py +468 -0
  62. prettyplay/llm/models.py +273 -0
  63. prettyplay/llm/openai_provider.py +444 -0
  64. prettyplay/llm/provider.py +279 -0
  65. prettyplay/reporting/.usages/hooks.md +61 -0
  66. prettyplay/reporting/__init__.py +6 -0
  67. prettyplay/reporting/hooks.py +47 -0
  68. prettyplay/reporting/reporter.py +70 -0
  69. prettyplay/runtime.py +81 -0
  70. prettyplay/scenario.py +469 -0
  71. prettyplay-0.0.2.dist-info/METADATA +515 -0
  72. prettyplay-0.0.2.dist-info/RECORD +75 -0
  73. prettyplay-0.0.2.dist-info/WHEEL +5 -0
  74. prettyplay-0.0.2.dist-info/licenses/LICENSE +28 -0
  75. prettyplay-0.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,444 @@
1
+ """The openai SDK implementation of the LLMProvider port."""
2
+
3
+ import os
4
+
5
+ from openai import OpenAI
6
+
7
+ from ..config import Config
8
+ from ..failures import LLMUnavailableError
9
+ from ._request import (
10
+ build_classification_fields,
11
+ build_compliance_fields,
12
+ build_fields_text,
13
+ build_group_diagnosis_fields,
14
+ classify_openai_failure,
15
+ extract_code_block,
16
+ openai_user_content,
17
+ parse_classification_line,
18
+ require_completion_text,
19
+ send_with_retries,
20
+ unparsable_classification,
21
+ )
22
+ from .models import (
23
+ ComplianceFinding,
24
+ FailureClassification,
25
+ GroupFailureClassification,
26
+ ScenarioStep,
27
+ parse_compliance_verdict,
28
+ parse_group_failure_classification,
29
+ )
30
+ from .provider import LLMProvider
31
+
32
+
33
+ def _first_choice_text(response: object) -> str | None:
34
+ """Extract the message text of the first choice of an openai response.
35
+
36
+ Args:
37
+ response: the SDK response of a ``chat.completions.create`` call.
38
+
39
+ Returns:
40
+ The text of the first choice message, or ``None`` when the response
41
+ carries no choice at all (empty or missing ``choices``/``message``).
42
+ """
43
+ choices = getattr(response, "choices", None) or []
44
+ message = getattr(choices[0], "message", None) if choices else None
45
+
46
+ return getattr(message, "content", None)
47
+
48
+
49
+ class OpenAIProvider(LLMProvider):
50
+ """The LLMProvider implementation served by the openai SDK — full parity with :class:`AnthropicProvider`."""
51
+
52
+ def __init__(self, config: Config) -> None:
53
+ """Keep the config; the SDK client stays unconstructed until the first request.
54
+
55
+ Args:
56
+ config: project settings; the effective models and the optional
57
+ base_url endpoint override come from it. The API key is
58
+ read from the environment only and never logged.
59
+ """
60
+ self._config = config
61
+ self._client: OpenAI | None = None
62
+
63
+ def _get_client(self) -> OpenAI:
64
+ """Construct the SDK client on the first request.
65
+
66
+ Returns:
67
+ The lazily constructed openai SDK client.
68
+
69
+ Raises:
70
+ LLMUnavailableError: the OPENAI_API_KEY environment variable is
71
+ missing or empty — all LLM operations are blocked.
72
+ """
73
+ if self._client is None:
74
+ api_key = os.environ.get("OPENAI_API_KEY")
75
+
76
+ if not api_key:
77
+ raise LLMUnavailableError("llm unavailable: openai: OPENAI_API_KEY is not set")
78
+
79
+ self._client = OpenAI(api_key=api_key, base_url=self._config.base_url or None, max_retries=0)
80
+ return self._client
81
+
82
+ def generate_step_code( # noqa: PLR0913, PLR0917 — the signature is fixed by the port contract
83
+ self,
84
+ prompt: str,
85
+ user_instructions: str,
86
+ instruction: str,
87
+ step_type: str,
88
+ previous_steps: list[ScenarioStep],
89
+ group_prompt: str | None,
90
+ inputs: dict[str, str],
91
+ declarations: list[str],
92
+ snapshot: str,
93
+ page_url: str | None,
94
+ screenshot: bytes | None,
95
+ cheat_sheet: str,
96
+ attempt_history: list[str],
97
+ recommendation: str | None,
98
+ guidance: str | None,
99
+ ) -> str:
100
+ """Generate step code of the fixed form working through the standard Playwright sync API.
101
+
102
+ Args:
103
+ prompt: the system prompt text supplied by the calling engine;
104
+ applied verbatim as the system message.
105
+ user_instructions: the project's code style instructions from the
106
+ generation_prompt setting; empty — the request carries no
107
+ instructions block, non-empty — rendered verbatim as a
108
+ separate USER INSTRUCTIONS block of the user content,
109
+ identically to the anthropic implementation.
110
+ instruction: the prepared instruction of the step — the rendered
111
+ plain-text sentence with actual values embedded; rendered as
112
+ the STEP block; the raw template sentence never reaches the
113
+ request.
114
+ step_type: the type of the step; rendered as a STEP TYPE line
115
+ immediately before the STEP line, identically to the
116
+ anthropic implementation; takes no part in step addressing.
117
+ previous_steps: the typed scenario records of the previous steps
118
+ of the test, in execution order — each the prepared
119
+ instruction recorded at the step's execution plus its
120
+ permanent group membership; rendered as the PREVIOUS STEPS
121
+ block with the group entries marked, identically to the
122
+ anthropic implementation.
123
+ group_prompt: the group prompt of the current step's group;
124
+ None — an ordinary step, no GROUP PROMPT block; non-empty —
125
+ rendered verbatim as a separate GROUP PROMPT block
126
+ immediately before the PREVIOUS STEPS block, identically to
127
+ the anthropic implementation; takes no part in step
128
+ addressing.
129
+ inputs: the call-local input bindings of the step; non-empty —
130
+ rendered as the INPUTS block immediately after the STEP
131
+ line, one ``name = value`` line per binding, identically to
132
+ the anthropic implementation; empty — no block.
133
+ declarations: the declared result names of the step; non-empty —
134
+ rendered as the RESULTS block immediately after the INPUTS
135
+ block stating the result contract — the code returns a
136
+ dictionary of exactly the declared names to non-blank
137
+ strings observed on the page — identically to the anthropic
138
+ implementation; empty — no block, the
139
+ success-without-result code form.
140
+ snapshot: the accessibility snapshot of the current page.
141
+ page_url: the current URL of the page; non-empty — rendered as
142
+ its own PAGE URL line immediately after the PAGE SNAPSHOT
143
+ block of the user content, identically to the anthropic
144
+ implementation; None — no line; supplied by the engine and
145
+ steering generation paths.
146
+ screenshot: an optional PNG image of the page; passed only when
147
+ the project enables screenshots.
148
+ cheat_sheet: the compact standard Playwright sync API reference
149
+ supplied by the calling engine — rendered as the CHEAT SHEET
150
+ block after the scenario inputs of the user content,
151
+ identically to the anthropic implementation; guidance, not
152
+ an allowlist — everything standard stays allowed.
153
+ attempt_history: the rendered per-step attempt records — every
154
+ record a complete multi-line verbatim record composed by the
155
+ calling engine, the original cached code anchored as record 0
156
+ when it exists, the last record the code being fixed;
157
+ non-empty — rendered as a separate HISTORY block after the
158
+ USER INSTRUCTIONS block, every record verbatim, no
159
+ collapsing, no size limits; empty — no block; the list takes
160
+ the place of the former existing_code, error and
161
+ guidance_history inputs; rendered identically to the
162
+ anthropic implementation.
163
+ recommendation: the diagnosis of the classification that preceded
164
+ the regeneration; non-empty — rendered as a separate
165
+ RECOMMENDATION block after the HISTORY block, None — no
166
+ block.
167
+ guidance: the engineer guidance message of the interactive
168
+ steering; non-empty — rendered as a separate USER GUIDANCE
169
+ block, None — no block.
170
+
171
+ Returns:
172
+ The generated step code of the fixed form, working through the
173
+ standard Playwright sync API — imports from playwright.sync_api
174
+ and the Python standard library only, global at the top level of
175
+ the code block.
176
+
177
+ Raises:
178
+ LLMUnavailableError: the SDK client is unavailable or the
179
+ service request failed.
180
+ """
181
+ text = build_fields_text(
182
+ user_instructions=user_instructions,
183
+ instruction=instruction,
184
+ step_type=step_type,
185
+ previous_steps=previous_steps,
186
+ group_prompt=group_prompt,
187
+ inputs=inputs,
188
+ declarations=declarations,
189
+ snapshot=snapshot,
190
+ page_url=page_url,
191
+ cheat_sheet=cheat_sheet,
192
+ attempt_history=attempt_history,
193
+ recommendation=recommendation,
194
+ guidance=guidance,
195
+ )
196
+ messages = [
197
+ {"role": "system", "content": prompt},
198
+ {"role": "user", "content": openai_user_content(text, screenshot)},
199
+ ]
200
+
201
+ client = self._get_client()
202
+ request = {
203
+ "model": self._config.effective_generation_model,
204
+ "messages": messages,
205
+ }
206
+
207
+ response = send_with_retries(
208
+ "openai",
209
+ "generation",
210
+ self._config.llm_request_attempts,
211
+ classify_openai_failure,
212
+ lambda: client.chat.completions.create(**request),
213
+ )
214
+
215
+ return extract_code_block(require_completion_text(_first_choice_text(response), "openai"))
216
+
217
+ def classify_step_failure( # noqa: PLR0913, PLR0917 — the signature is fixed by the port contract
218
+ self,
219
+ prompt: str,
220
+ user_instructions: str,
221
+ step_text: str,
222
+ code: str,
223
+ error: str,
224
+ snapshot: str,
225
+ screenshot: bytes | None,
226
+ ) -> FailureClassification:
227
+ """Classify a failed cached step — the former classify_failure, renamed nominally.
228
+
229
+ Args:
230
+ prompt: the system prompt text supplied by the calling engine;
231
+ applied verbatim as the system message.
232
+ user_instructions: the project's classification guidance from the
233
+ classification_prompt setting; empty — the request carries no
234
+ instructions block, non-empty — rendered verbatim as a
235
+ separate USER INSTRUCTIONS block placed last of the user
236
+ content, identically to the anthropic implementation.
237
+ step_text: the sentence of the failed step — the prepared
238
+ instruction, never the raw template sentence; a non-template
239
+ step passes its sentence unchanged.
240
+ code: the existing step code that failed.
241
+ error: the human-readable description of the failure.
242
+ snapshot: the accessibility snapshot of the current page.
243
+ screenshot: an optional PNG image of the page; passed only when
244
+ the project enables screenshots.
245
+
246
+ Returns:
247
+ The classification verdict; an unparsable or unknown answer maps
248
+ protectively to the incurable category.
249
+
250
+ Raises:
251
+ LLMUnavailableError: the SDK client is unavailable or the
252
+ service request failed.
253
+ """
254
+ text = build_classification_fields(user_instructions, step_text, code, error, snapshot)
255
+ messages = [
256
+ {"role": "system", "content": prompt},
257
+ {"role": "user", "content": openai_user_content(text, screenshot)},
258
+ ]
259
+
260
+ client = self._get_client()
261
+ request = {
262
+ "model": self._config.effective_classification_model,
263
+ "messages": messages,
264
+ }
265
+
266
+ response = send_with_retries(
267
+ "openai",
268
+ "classification",
269
+ self._config.llm_request_attempts,
270
+ classify_openai_failure,
271
+ lambda: client.chat.completions.create(**request),
272
+ )
273
+
274
+ answer = require_completion_text(_first_choice_text(response), "openai")
275
+ parsed = parse_classification_line(answer)
276
+
277
+ if parsed is None:
278
+ return FailureClassification(**unparsable_classification())
279
+
280
+ category, explanation, recommendation = parsed
281
+ return FailureClassification(category=category, explanation=explanation, recommendation=recommendation)
282
+
283
+ def classify_group_failure( # noqa: PLR0913, PLR0917 — the signature is fixed by the port contract
284
+ self,
285
+ prompt: str,
286
+ user_instructions: str,
287
+ group_prompt: str,
288
+ group_steps: list[str],
289
+ previous_steps: list[ScenarioStep],
290
+ step_text: str,
291
+ attempt_history: list[str],
292
+ snapshot: str,
293
+ screenshot: bytes | None,
294
+ ) -> GroupFailureClassification:
295
+ """Diagnose a failed group step with the whole interaction in view.
296
+
297
+ Args:
298
+ prompt: the group diagnosis system prompt supplied by the calling
299
+ engine; applied verbatim as the system message.
300
+ user_instructions: the project's classification guidance from
301
+ the classification_prompt setting; empty — the request
302
+ carries no instructions block, non-empty — rendered verbatim
303
+ as a separate USER INSTRUCTIONS block placed last of the
304
+ user content, identically to the anthropic implementation.
305
+ group_prompt: the group prompt of the diagnosed group, verbatim.
306
+ group_steps: the composed verbatim traces of the group's steps
307
+ in execution order — each the prepared instruction, the
308
+ outcome and the URL before -> after transition, supplied by
309
+ the calling engine.
310
+ previous_steps: the typed scenario records of the previous steps
311
+ of the test, in execution order — each the prepared
312
+ instruction recorded at the step's execution plus its
313
+ permanent group membership; rendered as the PREVIOUS STEPS
314
+ block immediately before the GROUP STEPS block with the
315
+ group entries marked, identically to the anthropic
316
+ implementation; empty — no block.
317
+ step_text: the prepared instruction of the failed step — never
318
+ the raw template sentence; a non-template step passes its
319
+ sentence unchanged.
320
+ attempt_history: the rendered verbatim records of the failed
321
+ step's attempt history; non-empty — rendered as a separate
322
+ HISTORY block, every record verbatim, no collapsing, no size
323
+ limits; empty — no block.
324
+ snapshot: the accessibility snapshot of the current page.
325
+ screenshot: an optional PNG image of the page; passed only when
326
+ the project enables screenshots.
327
+
328
+ Returns:
329
+ The diagnosis verdict; a degraded answer carries the
330
+ conservative incurable with the raw answer in root_cause — the
331
+ block order GROUP PROMPT, optional PREVIOUS STEPS, GROUP STEPS,
332
+ STEP, HISTORY, PAGE SNAPSHOT; an unusable answer degrades inside
333
+ the provider, never raising across the port.
334
+
335
+ Raises:
336
+ LLMUnavailableError: the SDK client is unavailable or the
337
+ service request failed.
338
+ """
339
+ text = build_group_diagnosis_fields(
340
+ user_instructions=user_instructions,
341
+ group_prompt=group_prompt,
342
+ group_steps=group_steps,
343
+ previous_steps=previous_steps,
344
+ step_text=step_text,
345
+ attempt_history=attempt_history,
346
+ snapshot=snapshot,
347
+ )
348
+ messages = [
349
+ {"role": "system", "content": prompt},
350
+ {"role": "user", "content": openai_user_content(text, screenshot)},
351
+ ]
352
+
353
+ client = self._get_client()
354
+ request = {
355
+ "model": self._config.effective_classification_model,
356
+ "messages": messages,
357
+ }
358
+
359
+ response = send_with_retries(
360
+ "openai",
361
+ "group diagnosis",
362
+ self._config.llm_request_attempts,
363
+ classify_openai_failure,
364
+ lambda: client.chat.completions.create(**request),
365
+ )
366
+
367
+ return parse_group_failure_classification(require_completion_text(_first_choice_text(response), "openai"))
368
+
369
+ def check_instruction_compliance( # noqa: PLR0913, PLR0917 — the signature is fixed by the port contract
370
+ self,
371
+ prompt: str,
372
+ user_instructions: str,
373
+ instruction: str,
374
+ step_type: str,
375
+ inputs: dict[str, str],
376
+ declarations: list[str],
377
+ code: str,
378
+ attempt_history: list[str],
379
+ ) -> list[ComplianceFinding]:
380
+ """Check the successfully executed candidate code in two dimensions.
381
+
382
+ Args:
383
+ prompt: the gate system prompt text supplied by the calling
384
+ engine; applied verbatim as the system message.
385
+ user_instructions: the project's generation instructions from
386
+ the generation_prompt setting; the calling engine
387
+ guarantees non-empty — the gate never runs on empty
388
+ instructions.
389
+ instruction: the prepared plain-text instruction of the generated
390
+ step — rendered as the STEP block; the raw template sentence
391
+ never reaches the request.
392
+ step_type: the type of the step; rendered as a STEP TYPE line
393
+ immediately before the STEP line, identically to the
394
+ anthropic implementation.
395
+ inputs: the call-local input bindings of the step; non-empty —
396
+ rendered as the INPUTS block immediately after the STEP
397
+ block, one ``name = value`` line per binding, identically to
398
+ the anthropic implementation; empty — no block.
399
+ declarations: the reached capture names of the step; non-empty —
400
+ rendered as the RESULTS block immediately after the INPUTS
401
+ block stating the exact-key, observed non-blank string
402
+ return contract, identically to the anthropic
403
+ implementation; empty — no block, the
404
+ success-without-result form.
405
+ code: the successfully executed candidate code.
406
+ attempt_history: the rendered per-step attempt records — the
407
+ ground truth of what was already tried; non-empty — rendered
408
+ as a separate ATTEMPT HISTORY block, every record verbatim,
409
+ no collapsing, no size limits; empty — no block; rendered
410
+ identically to the anthropic implementation.
411
+
412
+ Returns:
413
+ The parsed findings; an empty list means compliant — the block
414
+ order INSTRUCTIONS, STEP, the optional INPUTS and RESULTS
415
+ sections, ATTEMPT HISTORY, CODE; no fence unwrapping: a
416
+ malformed verdict raises, never a silent pass.
417
+
418
+ Raises:
419
+ LLMUnavailableError: the SDK client is unavailable or the
420
+ service request failed.
421
+ """
422
+ text = build_compliance_fields(
423
+ user_instructions, instruction, step_type, inputs, declarations, attempt_history, code
424
+ )
425
+ messages = [
426
+ {"role": "system", "content": prompt},
427
+ {"role": "user", "content": text},
428
+ ]
429
+
430
+ client = self._get_client()
431
+ request = {
432
+ "model": self._config.effective_classification_model,
433
+ "messages": messages,
434
+ }
435
+
436
+ response = send_with_retries(
437
+ "openai",
438
+ "compliance verdict",
439
+ self._config.llm_request_attempts,
440
+ classify_openai_failure,
441
+ lambda: client.chat.completions.create(**request),
442
+ )
443
+
444
+ return parse_compliance_verdict(require_completion_text(_first_choice_text(response), "openai"))