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