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.
- prettyplay/.usages/lifecycle.md +117 -0
- prettyplay/.usages/steps.md +169 -0
- prettyplay/__init__.py +18 -0
- prettyplay/cache/.usages/addressing.md +44 -0
- prettyplay/cache/.usages/budgets.md +51 -0
- prettyplay/cache/.usages/storage.md +32 -0
- prettyplay/cache/__init__.py +8 -0
- prettyplay/cache/budgets.py +93 -0
- prettyplay/cache/models.py +55 -0
- prettyplay/cache/store.py +276 -0
- prettyplay/cache/text.py +36 -0
- prettyplay/config/.usages/configuration.md +246 -0
- prettyplay/config/__init__.py +6 -0
- prettyplay/config/loader.py +336 -0
- prettyplay/config/models.py +288 -0
- prettyplay/driver/.usages/error_kinds.md +26 -0
- prettyplay/driver/.usages/plumbing.md +52 -0
- prettyplay/driver/__init__.py +11 -0
- prettyplay/driver/errors.py +76 -0
- prettyplay/driver/page.py +175 -0
- prettyplay/driver/session.py +330 -0
- prettyplay/engine/.usages/generation.md +157 -0
- prettyplay/engine/.usages/healing.md +61 -0
- prettyplay/engine/__init__.py +19 -0
- prettyplay/engine/attempts.py +80 -0
- prettyplay/engine/classification.py +70 -0
- prettyplay/engine/compliance.py +125 -0
- prettyplay/engine/execution.py +48 -0
- prettyplay/engine/generator.py +1110 -0
- prettyplay/engine/groups/.usages/recovery.md +71 -0
- prettyplay/engine/groups/__init__.py +7 -0
- prettyplay/engine/groups/diagnosis.py +128 -0
- prettyplay/engine/groups/outcome.py +55 -0
- prettyplay/engine/groups/recovery.py +491 -0
- prettyplay/engine/healer.py +149 -0
- prettyplay/engine/polling/.usages/settle.md +55 -0
- prettyplay/engine/polling/__init__.py +6 -0
- prettyplay/engine/polling/settle.py +98 -0
- prettyplay/engine/polling/window.py +72 -0
- prettyplay/engine/renderer/.usages/memory.md +24 -0
- prettyplay/engine/renderer/.usages/rendering.md +44 -0
- prettyplay/engine/renderer/.usages/validation.md +20 -0
- prettyplay/engine/renderer/__init__.py +7 -0
- prettyplay/engine/renderer/memory.py +37 -0
- prettyplay/engine/renderer/render.py +184 -0
- prettyplay/engine/renderer/validation.py +76 -0
- prettyplay/engine/steering/.usages/steering.md +95 -0
- prettyplay/engine/steering/__init__.py +5 -0
- prettyplay/engine/steering/steering.py +765 -0
- prettyplay/engine/text.py +41 -0
- prettyplay/executor.py +654 -0
- prettyplay/failures/.usages/taxonomy.md +131 -0
- prettyplay/failures/__init__.py +25 -0
- prettyplay/failures/errors.py +359 -0
- prettyplay/groups.py +276 -0
- prettyplay/llm/.usages/classification.md +31 -0
- prettyplay/llm/.usages/providers.md +127 -0
- prettyplay/llm/.usages/transport.md +81 -0
- prettyplay/llm/__init__.py +38 -0
- prettyplay/llm/_request.py +823 -0
- prettyplay/llm/anthropic_provider.py +468 -0
- prettyplay/llm/models.py +273 -0
- prettyplay/llm/openai_provider.py +444 -0
- prettyplay/llm/provider.py +279 -0
- prettyplay/reporting/.usages/hooks.md +61 -0
- prettyplay/reporting/__init__.py +6 -0
- prettyplay/reporting/hooks.py +47 -0
- prettyplay/reporting/reporter.py +70 -0
- prettyplay/runtime.py +81 -0
- prettyplay/scenario.py +469 -0
- prettyplay-0.0.2.dist-info/METADATA +515 -0
- prettyplay-0.0.2.dist-info/RECORD +75 -0
- prettyplay-0.0.2.dist-info/WHEEL +5 -0
- prettyplay-0.0.2.dist-info/licenses/LICENSE +28 -0
- prettyplay-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""The unified LLM port of the library and the provider factory."""
|
|
2
|
+
|
|
3
|
+
from ..config import Config
|
|
4
|
+
from .models import ComplianceFinding, FailureClassification, GroupFailureClassification, ScenarioStep
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LLMProvider:
|
|
8
|
+
"""The single LLM port: step code generation, failure classification, the group diagnosis and the verdict."""
|
|
9
|
+
|
|
10
|
+
def generate_step_code( # noqa: PLR0913, PLR0917 — the signature is fixed by the port contract
|
|
11
|
+
self,
|
|
12
|
+
prompt: str,
|
|
13
|
+
user_instructions: str,
|
|
14
|
+
instruction: str,
|
|
15
|
+
step_type: str,
|
|
16
|
+
previous_steps: list[ScenarioStep],
|
|
17
|
+
group_prompt: str | None,
|
|
18
|
+
inputs: dict[str, str],
|
|
19
|
+
declarations: list[str],
|
|
20
|
+
snapshot: str,
|
|
21
|
+
page_url: str | None,
|
|
22
|
+
screenshot: bytes | None,
|
|
23
|
+
cheat_sheet: str,
|
|
24
|
+
attempt_history: list[str],
|
|
25
|
+
recommendation: str | None,
|
|
26
|
+
guidance: str | None,
|
|
27
|
+
) -> str:
|
|
28
|
+
"""Generate step code of the fixed form working through the standard Playwright sync API.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
prompt: the system prompt text supplied by the calling engine;
|
|
32
|
+
applied verbatim as the system message.
|
|
33
|
+
user_instructions: the project's code style instructions supplied
|
|
34
|
+
by the calling engine from the generation_prompt setting;
|
|
35
|
+
empty — the request carries no instructions block, non-empty —
|
|
36
|
+
rendered verbatim as a separate USER INSTRUCTIONS block of the
|
|
37
|
+
user content, identically in both implementations.
|
|
38
|
+
instruction: the prepared instruction of the step — the rendered
|
|
39
|
+
plain-text sentence with actual values embedded; rendered as
|
|
40
|
+
the STEP block; the raw template sentence never reaches the
|
|
41
|
+
request.
|
|
42
|
+
step_type: the type of the step; rendered as a STEP TYPE line
|
|
43
|
+
immediately before the STEP line, identically in both
|
|
44
|
+
implementations; takes no part in step addressing.
|
|
45
|
+
previous_steps: the typed scenario records of the previous steps
|
|
46
|
+
of the test, in execution order — each the prepared
|
|
47
|
+
instruction recorded at the step's execution plus its
|
|
48
|
+
permanent group membership; rendered by the provider
|
|
49
|
+
implementations as the PREVIOUS STEPS block with the group
|
|
50
|
+
entries marked, identically in both.
|
|
51
|
+
group_prompt: the group prompt of the current step's group;
|
|
52
|
+
None — an ordinary step, no GROUP PROMPT block; non-empty —
|
|
53
|
+
rendered by the provider implementations verbatim as a
|
|
54
|
+
separate GROUP PROMPT block immediately before the PREVIOUS
|
|
55
|
+
STEPS block, identically in both; takes no part in step
|
|
56
|
+
addressing.
|
|
57
|
+
inputs: the call-local input bindings of the step; non-empty —
|
|
58
|
+
rendered by the provider implementations as the INPUTS block
|
|
59
|
+
immediately after the STEP line, one ``name = value`` line
|
|
60
|
+
per binding, identically in both; empty — no block.
|
|
61
|
+
declarations: the declared result names of the step; non-empty —
|
|
62
|
+
rendered by the provider implementations as the RESULTS
|
|
63
|
+
block immediately after the INPUTS block stating the result
|
|
64
|
+
contract — the code returns a dictionary of exactly the
|
|
65
|
+
declared names to non-blank strings observed on the page;
|
|
66
|
+
empty — no block, the success-without-result code form.
|
|
67
|
+
snapshot: the accessibility snapshot of the current page.
|
|
68
|
+
page_url: the current URL of the page; non-empty — rendered by
|
|
69
|
+
the provider implementations as its own PAGE URL line
|
|
70
|
+
immediately after the PAGE SNAPSHOT block of the user
|
|
71
|
+
content, identically in both; None — no line; supplied by
|
|
72
|
+
the engine and steering generation paths.
|
|
73
|
+
screenshot: an optional PNG image of the page; passed only when
|
|
74
|
+
the project enables screenshots.
|
|
75
|
+
cheat_sheet: the compact standard Playwright sync API reference
|
|
76
|
+
supplied by the calling engine — rendered as the CHEAT SHEET
|
|
77
|
+
block after the scenario inputs of the user content,
|
|
78
|
+
identically in both implementations; guidance, not an
|
|
79
|
+
allowlist — everything standard stays allowed.
|
|
80
|
+
attempt_history: the rendered per-step attempt records — every
|
|
81
|
+
record a complete multi-line verbatim record composed by the
|
|
82
|
+
calling engine, the original cached code anchored as record 0
|
|
83
|
+
when it exists, the last record the code being fixed;
|
|
84
|
+
non-empty — rendered as a separate HISTORY block after the
|
|
85
|
+
USER INSTRUCTIONS block, every record verbatim, no
|
|
86
|
+
collapsing, no size limits; empty — no block; the list takes
|
|
87
|
+
the place of the former existing_code, error and
|
|
88
|
+
guidance_history inputs.
|
|
89
|
+
recommendation: the diagnosis of the classification that preceded
|
|
90
|
+
the regeneration; non-empty — rendered as a separate
|
|
91
|
+
RECOMMENDATION block after the HISTORY block, None — no
|
|
92
|
+
block.
|
|
93
|
+
guidance: the engineer guidance message of the interactive
|
|
94
|
+
steering; non-empty — rendered as a separate USER GUIDANCE
|
|
95
|
+
block, None — no block.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The generated step code of the fixed form, working through the
|
|
99
|
+
standard Playwright sync API — imports from playwright.sync_api
|
|
100
|
+
and the Python standard library only, global at the top level of
|
|
101
|
+
the code block.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
NotImplementedError: the port itself carries no implementation.
|
|
105
|
+
"""
|
|
106
|
+
raise NotImplementedError("LLMProvider is a port; use create_provider() to select an implementation")
|
|
107
|
+
|
|
108
|
+
def classify_step_failure( # noqa: PLR0913, PLR0917 — the signature is fixed by the port contract
|
|
109
|
+
self,
|
|
110
|
+
prompt: str,
|
|
111
|
+
user_instructions: str,
|
|
112
|
+
step_text: str,
|
|
113
|
+
code: str,
|
|
114
|
+
error: str,
|
|
115
|
+
snapshot: str,
|
|
116
|
+
screenshot: bytes | None,
|
|
117
|
+
) -> FailureClassification:
|
|
118
|
+
"""Classify a failed cached step — the former classify_failure, renamed nominally.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
prompt: the system prompt text supplied by the calling engine;
|
|
122
|
+
applied verbatim as the system message.
|
|
123
|
+
user_instructions: the project's classification guidance supplied
|
|
124
|
+
by the calling engine from the classification_prompt setting;
|
|
125
|
+
empty — the request carries no instructions block, non-empty —
|
|
126
|
+
rendered verbatim as a separate USER INSTRUCTIONS block placed
|
|
127
|
+
last of the user content, identically in both implementations.
|
|
128
|
+
step_text: the sentence of the failed step — the prepared
|
|
129
|
+
instruction, never the raw template sentence; a non-template
|
|
130
|
+
step passes its sentence unchanged.
|
|
131
|
+
code: the existing step code that failed.
|
|
132
|
+
error: the human-readable description of the failure.
|
|
133
|
+
snapshot: the accessibility snapshot of the current page.
|
|
134
|
+
screenshot: an optional PNG image of the page; passed only when
|
|
135
|
+
the project enables screenshots.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
The classification verdict.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
NotImplementedError: the port itself carries no implementation.
|
|
142
|
+
"""
|
|
143
|
+
raise NotImplementedError("LLMProvider is a port; use create_provider() to select an implementation")
|
|
144
|
+
|
|
145
|
+
def classify_group_failure( # noqa: PLR0913, PLR0917 — the signature is fixed by the port contract
|
|
146
|
+
self,
|
|
147
|
+
prompt: str,
|
|
148
|
+
user_instructions: str,
|
|
149
|
+
group_prompt: str,
|
|
150
|
+
group_steps: list[str],
|
|
151
|
+
previous_steps: list[ScenarioStep],
|
|
152
|
+
step_text: str,
|
|
153
|
+
attempt_history: list[str],
|
|
154
|
+
snapshot: str,
|
|
155
|
+
screenshot: bytes | None,
|
|
156
|
+
) -> GroupFailureClassification:
|
|
157
|
+
"""Diagnose a failed group step with the whole interaction in view.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
prompt: the group diagnosis system prompt supplied by the calling
|
|
161
|
+
engine; applied verbatim as the system message.
|
|
162
|
+
user_instructions: the project's classification guidance supplied
|
|
163
|
+
by the calling engine from the classification_prompt setting;
|
|
164
|
+
the diagnosis request carries them exactly as a classification
|
|
165
|
+
request does — empty, no instructions block; non-empty,
|
|
166
|
+
rendered verbatim as a separate USER INSTRUCTIONS block placed
|
|
167
|
+
last of the user content, identically in both implementations.
|
|
168
|
+
group_prompt: the group prompt of the diagnosed group, verbatim.
|
|
169
|
+
group_steps: the composed verbatim traces of the group's steps in
|
|
170
|
+
execution order — each the prepared instruction, the outcome
|
|
171
|
+
and the URL before -> after transition, supplied by the
|
|
172
|
+
calling engine.
|
|
173
|
+
previous_steps: the typed scenario records of the previous steps
|
|
174
|
+
of the test, in execution order — each the prepared
|
|
175
|
+
instruction recorded at the step's execution plus its
|
|
176
|
+
permanent group membership; rendered by the provider
|
|
177
|
+
implementations as the PREVIOUS STEPS block immediately before
|
|
178
|
+
the GROUP STEPS block with the group entries marked,
|
|
179
|
+
identically in both; empty — no block.
|
|
180
|
+
step_text: the prepared instruction of the failed step — never
|
|
181
|
+
the raw template sentence; a non-template step passes its
|
|
182
|
+
sentence unchanged.
|
|
183
|
+
attempt_history: the rendered verbatim records of the failed
|
|
184
|
+
step's attempt history; non-empty — rendered as a separate
|
|
185
|
+
HISTORY block, every record verbatim, no collapsing, no size
|
|
186
|
+
limits; empty — no block.
|
|
187
|
+
snapshot: the accessibility snapshot of the current page.
|
|
188
|
+
screenshot: an optional PNG image of the page; passed only when
|
|
189
|
+
the project enables screenshots.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
The diagnosis verdict — parsed strictly through
|
|
193
|
+
:func:`~prettyplay.llm.parse_group_failure_classification`; a
|
|
194
|
+
degraded answer is the conservative incurable, never a granted
|
|
195
|
+
regeneration.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
NotImplementedError: the port itself carries no implementation.
|
|
199
|
+
"""
|
|
200
|
+
raise NotImplementedError("LLMProvider is a port; use create_provider() to select an implementation")
|
|
201
|
+
|
|
202
|
+
def check_instruction_compliance( # noqa: PLR0913, PLR0917 — the signature is fixed by the port contract
|
|
203
|
+
self,
|
|
204
|
+
prompt: str,
|
|
205
|
+
user_instructions: str,
|
|
206
|
+
instruction: str,
|
|
207
|
+
step_type: str,
|
|
208
|
+
inputs: dict[str, str],
|
|
209
|
+
declarations: list[str],
|
|
210
|
+
code: str,
|
|
211
|
+
attempt_history: list[str],
|
|
212
|
+
) -> list[ComplianceFinding]:
|
|
213
|
+
"""Check the successfully executed candidate code in two dimensions.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
prompt: the gate system prompt text supplied by the calling
|
|
217
|
+
engine; applied verbatim as the system message.
|
|
218
|
+
user_instructions: the project's generation instructions
|
|
219
|
+
supplied by the calling engine from the generation_prompt
|
|
220
|
+
setting; the calling engine guarantees non-empty — the gate
|
|
221
|
+
never runs on empty instructions.
|
|
222
|
+
instruction: the prepared plain-text instruction of the generated
|
|
223
|
+
step — rendered as the STEP block; the raw template sentence
|
|
224
|
+
never reaches the request.
|
|
225
|
+
step_type: the type of the step; rendered as a STEP TYPE line
|
|
226
|
+
immediately before the STEP line, identically in both
|
|
227
|
+
implementations.
|
|
228
|
+
inputs: the call-local input bindings of the step; non-empty —
|
|
229
|
+
rendered by the provider implementations as the INPUTS block
|
|
230
|
+
immediately after the STEP block, one ``name = value`` line
|
|
231
|
+
per binding, identically in both; empty — no block.
|
|
232
|
+
declarations: the reached capture names of the step; non-empty —
|
|
233
|
+
rendered by the provider implementations as the RESULTS block
|
|
234
|
+
immediately after the INPUTS block stating the exact-key,
|
|
235
|
+
observed non-blank string return contract, identically in
|
|
236
|
+
both; empty — no block, the success-without-result form.
|
|
237
|
+
code: the successfully executed candidate code.
|
|
238
|
+
attempt_history: the rendered per-step attempt records — the
|
|
239
|
+
ground truth of what was already tried; non-empty — rendered
|
|
240
|
+
as a separate ATTEMPT HISTORY block, every record verbatim,
|
|
241
|
+
no collapsing, no size limits; empty — no block.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The parsed findings; an empty list means compliant — one verdict
|
|
245
|
+
request per executed candidate; the block order INSTRUCTIONS,
|
|
246
|
+
STEP, the optional INPUTS and RESULTS sections, ATTEMPT HISTORY,
|
|
247
|
+
CODE.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
NotImplementedError: the port itself carries no implementation.
|
|
251
|
+
"""
|
|
252
|
+
raise NotImplementedError("LLMProvider is a port; use create_provider() to select an implementation")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def create_provider(config: Config) -> LLMProvider:
|
|
256
|
+
"""Select and construct the LLM provider from configuration — the implementations are interchangeable.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
config: project settings; the provider setting selects the SDK
|
|
260
|
+
implementation.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The selected provider implementation.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
ValueError: the provider setting names no supported provider.
|
|
267
|
+
"""
|
|
268
|
+
# deferred: the implementations subclass the port defined in this module,
|
|
269
|
+
# so a top-level import here would be circular
|
|
270
|
+
from .anthropic_provider import AnthropicProvider # noqa: PLC0415
|
|
271
|
+
from .openai_provider import OpenAIProvider # noqa: PLC0415
|
|
272
|
+
|
|
273
|
+
if config.provider == "openai":
|
|
274
|
+
return OpenAIProvider(config)
|
|
275
|
+
|
|
276
|
+
if config.provider == "anthropic":
|
|
277
|
+
return AnthropicProvider(config)
|
|
278
|
+
|
|
279
|
+
raise ValueError(f"unsupported provider {config.provider!r}: expected one of openai, anthropic")
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Step and healing hooks
|
|
2
|
+
|
|
3
|
+
Domain: event callbacks of prettyplay. Audience: integrators building custom reporting, metrics or CI reactions on top of step execution.
|
|
4
|
+
|
|
5
|
+
StepHooks is a thin callback contract. The library calls the matching method synchronously while a step executes. The base implementation of every method is a no-op — override only the events you need. Hook implementations are registered on the main library object at the start of a test.
|
|
6
|
+
|
|
7
|
+
## Events
|
|
8
|
+
|
|
9
|
+
| Method | When | Payload |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| on_step_started | a step started executing | step_text, step_type (action or assertion) |
|
|
12
|
+
| on_step_passed | the step finished successfully | step_text, step_type |
|
|
13
|
+
| on_step_failed | the step failed | step_text, step_type, error — the **full rendered failure message** (see below) |
|
|
14
|
+
| on_step_verdict | the terminal failure carried a verdict (fires after on_step_failed) | step_text, category (rot, product_defect, fixable, incurable), explanation, recommendation |
|
|
15
|
+
| on_step_finished | the step ended — always the last step event, regardless of outcome | step_text, step_type, outcome (passed or failed) |
|
|
16
|
+
| on_group_started | a group block started (once per entered group, zero-step groups included) | group_prompt |
|
|
17
|
+
| on_group_passed | the group block completed without an exception — a recovered group reports passed; a zero-step group reports passed | group_prompt |
|
|
18
|
+
| on_group_failed | the group block exited through an exception — a failed step, a refused recovery or an author exception alike; the exception still propagates | group_prompt |
|
|
19
|
+
| on_group_finished | the group block ended — always the last group event, regardless of outcome, after on_group_passed/on_group_failed | group_prompt |
|
|
20
|
+
| on_generation_started | a generation attempt started | step_text, attempt (1-based, one per LLM request) |
|
|
21
|
+
| on_healing_started | healing of a failed cached step started | step_text, category (rot, product_defect, fixable, incurable — a group recovery row reports the diagnosis label recoverable) |
|
|
22
|
+
| on_healed | the step healed, cache updated | step_text, explanation (why it failed, what changed — interactive healings report here too) |
|
|
23
|
+
| on_cache_saved | step code written to the cache | step_text, filename |
|
|
24
|
+
| on_cache_skipped | cache write skipped | step_text, reason (e.g. read-only cache) |
|
|
25
|
+
|
|
26
|
+
## The error payload of on_step_failed
|
|
27
|
+
|
|
28
|
+
`error` carries the full structured render of the terminal failure — the same multi-line text the raised exception carries and the log record writes: the first line with the class name of the terminal failure and the authored reason, the `---` separated step/error section, the conditional received/cause/Call log details section and the unpadded verdict block. Display it verbatim in reports; do not parse it — structured data arrives through on_step_verdict fields. In strict mode on_generation_started and on_healing_started never fire: classification is the only LLM call.
|
|
29
|
+
|
|
30
|
+
## Attempt semantics
|
|
31
|
+
|
|
32
|
+
Hook events fire per step or per LLM attempt — never per execution retry. Settle re-executions of the same step code (transient failure absorption) emit no hook events: they are visible only as settle_retry log records. on_step_finished closes every step exactly once, passed or failed; on_group_finished closes every entered group exactly once the same way — the group verdict is the block outcome, never the traces: a step healed by the group recovery leaves its verbatim failed trace record behind while the group reports passed.
|
|
33
|
+
|
|
34
|
+
## Example
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from prettyplay.reporting import StepHooks
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FailureMonitor(StepHooks):
|
|
41
|
+
def on_step_failed(self, step_text: str, step_type: str, error: str) -> None:
|
|
42
|
+
print(f"failed [{step_type}]: {step_text}\n{error}") # the full render, verbatim
|
|
43
|
+
|
|
44
|
+
def on_step_verdict(self, step_text: str, category: str, explanation: str, recommendation: str) -> None:
|
|
45
|
+
print(f"verdict [{category}]: {step_text} — {explanation} → {recommendation}")
|
|
46
|
+
|
|
47
|
+
def on_step_finished(self, step_text: str, step_type: str, outcome: str) -> None:
|
|
48
|
+
print(f"finished [{outcome}]: {step_text}")
|
|
49
|
+
|
|
50
|
+
def on_healed(self, step_text: str, explanation: str) -> None:
|
|
51
|
+
print(f"healed: {step_text} — {explanation}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
|
|
56
|
+
- Handlers run synchronously inside step execution: keep them fast
|
|
57
|
+
- A raising handler is logged as a warning and skipped — the test run never fails because of a hook
|
|
58
|
+
- on_step_verdict fires only when the verdict exists: LLM unavailability skips the verdict quietly (WARNING in the log)
|
|
59
|
+
- The verdict payload is built from the verdict object of the failure — never parsed from the rendered `error` text
|
|
60
|
+
- Payload values are plain strings; the attempt counter of on_generation_started is an int
|
|
61
|
+
- Step texts land in logs and hooks: never put secrets or personal data into a step sentence
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Thin callback contract for integrator-side reactions to step execution events."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class StepHooks:
|
|
5
|
+
"""Callback contract of the fourteen prettyplay events; every base method is a no-op."""
|
|
6
|
+
|
|
7
|
+
def on_step_started(self, step_text: str, step_type: str) -> None:
|
|
8
|
+
"""A step started executing; ``step_type`` is action or assertion."""
|
|
9
|
+
|
|
10
|
+
def on_step_passed(self, step_text: str, step_type: str) -> None:
|
|
11
|
+
"""The step finished successfully."""
|
|
12
|
+
|
|
13
|
+
def on_step_failed(self, step_text: str, step_type: str, error: str) -> None:
|
|
14
|
+
"""The step failed; ``error`` is the rendered failure message, same as the exception text."""
|
|
15
|
+
|
|
16
|
+
def on_step_verdict(self, step_text: str, category: str, explanation: str, recommendation: str) -> None:
|
|
17
|
+
"""The terminal failure of the step carried a verdict; fires after on_step_failed."""
|
|
18
|
+
|
|
19
|
+
def on_step_finished(self, step_text: str, step_type: str, outcome: str) -> None:
|
|
20
|
+
"""The step ended — the closing event of every step, fired exactly once regardless of outcome."""
|
|
21
|
+
|
|
22
|
+
def on_group_started(self, group_prompt: str) -> None:
|
|
23
|
+
"""A group block started; ``group_prompt`` is the group prompt, verbatim."""
|
|
24
|
+
|
|
25
|
+
def on_group_passed(self, group_prompt: str) -> None:
|
|
26
|
+
"""The group block completed without an exception — a recovered group reports passed here."""
|
|
27
|
+
|
|
28
|
+
def on_group_failed(self, group_prompt: str) -> None:
|
|
29
|
+
"""The group block exited through an exception — a failed step, a refused recovery or an author error."""
|
|
30
|
+
|
|
31
|
+
def on_group_finished(self, group_prompt: str) -> None:
|
|
32
|
+
"""The group block ended — the closing event of every group, fired exactly once regardless of outcome."""
|
|
33
|
+
|
|
34
|
+
def on_generation_started(self, step_text: str, attempt: int) -> None:
|
|
35
|
+
"""A code generation attempt started; ``attempt`` is the 1-based attempt number."""
|
|
36
|
+
|
|
37
|
+
def on_healing_started(self, step_text: str, category: str) -> None:
|
|
38
|
+
"""Healing of a failed cached step started; ``category`` is rot, product_defect, fixable or incurable."""
|
|
39
|
+
|
|
40
|
+
def on_healed(self, step_text: str, explanation: str) -> None:
|
|
41
|
+
"""The step was healed and the cache updated; ``explanation`` says why and what changed."""
|
|
42
|
+
|
|
43
|
+
def on_cache_saved(self, step_text: str, filename: str) -> None:
|
|
44
|
+
"""Step code was written to the cache file ``filename``."""
|
|
45
|
+
|
|
46
|
+
def on_cache_skipped(self, step_text: str, reason: str) -> None:
|
|
47
|
+
"""The cache write was skipped; ``reason`` names the cause, e.g. a read-only cache."""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""The single visibility point of prettyplay: the ``prettyplay`` logger plus hook fan-out."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from .hooks import StepHooks
|
|
6
|
+
|
|
7
|
+
#: Events written at WARNING; every other event is step/generation/healing lifecycle INFO.
|
|
8
|
+
_WARNING_EVENTS = frozenset({"on_cache_skipped"})
|
|
9
|
+
|
|
10
|
+
#: Attributes Logger.makeRecord forbids overwriting: collision keys get a ``ctx_`` prefix
|
|
11
|
+
#: in the log record only — hooks always receive the original payload kwargs.
|
|
12
|
+
_LOG_RECORD_RESERVED = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"name",
|
|
15
|
+
"msg",
|
|
16
|
+
"message",
|
|
17
|
+
"args",
|
|
18
|
+
"levelname",
|
|
19
|
+
"levelno",
|
|
20
|
+
"pathname",
|
|
21
|
+
"filename",
|
|
22
|
+
"module",
|
|
23
|
+
"exc_info",
|
|
24
|
+
"funcName",
|
|
25
|
+
"lineno",
|
|
26
|
+
"created",
|
|
27
|
+
"msecs",
|
|
28
|
+
"relativeCreated",
|
|
29
|
+
"thread",
|
|
30
|
+
"threadName",
|
|
31
|
+
"process",
|
|
32
|
+
"processName",
|
|
33
|
+
"stack_info",
|
|
34
|
+
"asctime",
|
|
35
|
+
"taskName",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StepReporter:
|
|
41
|
+
"""Dispatches every step, generation, healing and cache event to the logger and hooks."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, hooks: list[StepHooks]) -> None:
|
|
44
|
+
"""Keep the hooks list by reference in the public ``hooks`` attribute.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
hooks: callback implementations registered by the integrator; may be empty.
|
|
48
|
+
``add_hooks`` of the main object appends into this same list.
|
|
49
|
+
"""
|
|
50
|
+
self.hooks = hooks
|
|
51
|
+
self._logger = logging.getLogger("prettyplay")
|
|
52
|
+
|
|
53
|
+
def emit(self, event: str, payload: dict[str, str | int]) -> None:
|
|
54
|
+
"""Dispatch one event to the logger and to every hook, in registration order.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
event: the event name — equals a ``StepHooks`` method name exactly.
|
|
58
|
+
payload: the payload fields of the event; values are strings, the
|
|
59
|
+
attempt counter is an int.
|
|
60
|
+
"""
|
|
61
|
+
level = logging.WARNING if event in _WARNING_EVENTS else logging.INFO
|
|
62
|
+
extra = {f"ctx_{key}" if key in _LOG_RECORD_RESERVED else key: value for key, value in payload.items()}
|
|
63
|
+
|
|
64
|
+
self._logger.log(level, event, extra=extra)
|
|
65
|
+
|
|
66
|
+
for hook in self.hooks:
|
|
67
|
+
try:
|
|
68
|
+
getattr(hook, event)(**payload)
|
|
69
|
+
except Exception:
|
|
70
|
+
self._logger.warning("hook call failed", extra={"event": event, "hook": type(hook).__name__})
|
prettyplay/runtime.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Per-test composition root of the library: the objects one test owns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
|
|
7
|
+
from .cache import RunBudgets
|
|
8
|
+
from .config import Config
|
|
9
|
+
from .driver import DriverSession, PageFacade
|
|
10
|
+
from .llm import LLMProvider, create_provider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PrettyplayRuntime:
|
|
14
|
+
"""The composition root: the per-test objects one test composes — nothing shared between tests.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
_config: validated project settings of the test.
|
|
18
|
+
_budgets: the per-test attempt registry owned by this runtime.
|
|
19
|
+
_driver: the browser driver of the test; ``None`` until first access.
|
|
20
|
+
_provider: the LLM provider of the test; ``None`` until first access.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, config: Config) -> None:
|
|
24
|
+
"""Compose the eager per-test objects from the config.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
config: validated project settings of the test.
|
|
28
|
+
"""
|
|
29
|
+
self._config = config
|
|
30
|
+
self._budgets = RunBudgets(config.generation_attempts, config.healing_attempts)
|
|
31
|
+
self._driver: DriverSession | None = None
|
|
32
|
+
self._provider: LLMProvider | None = None
|
|
33
|
+
atexit.register(self.close)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def config(self) -> Config:
|
|
37
|
+
"""The validated settings of the test."""
|
|
38
|
+
return self._config
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def budgets(self) -> RunBudgets:
|
|
42
|
+
"""The per-test attempt registry owned by this one runtime."""
|
|
43
|
+
return self._budgets
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def driver(self) -> DriverSession:
|
|
47
|
+
"""The browser driver of the test, constructed lazily exactly once.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The browser driver — a construction after a ``close`` re-arms
|
|
51
|
+
the ``atexit`` hook the close dropped.
|
|
52
|
+
"""
|
|
53
|
+
if self._driver is None:
|
|
54
|
+
self._driver = DriverSession(self._config)
|
|
55
|
+
atexit.register(self.close)
|
|
56
|
+
|
|
57
|
+
return self._driver
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def provider(self) -> LLMProvider:
|
|
61
|
+
"""The LLM provider of the test, constructed lazily exactly once."""
|
|
62
|
+
if self._provider is None:
|
|
63
|
+
self._provider = create_provider(self._config)
|
|
64
|
+
|
|
65
|
+
return self._provider
|
|
66
|
+
|
|
67
|
+
def open_page(self) -> PageFacade:
|
|
68
|
+
"""Open a fresh isolated page of the test driver.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The facade of the new page of a fresh isolated context.
|
|
72
|
+
"""
|
|
73
|
+
return self.driver.open_context()
|
|
74
|
+
|
|
75
|
+
def close(self) -> None:
|
|
76
|
+
"""Stop the browser driver of the test; idempotent; drops the ``atexit`` hook."""
|
|
77
|
+
atexit.unregister(self.close)
|
|
78
|
+
|
|
79
|
+
if self._driver is not None:
|
|
80
|
+
self._driver.close()
|
|
81
|
+
self._driver = None
|