Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
162 lines
5.7 KiB
Python
162 lines
5.7 KiB
Python
"""Offline state-machine demo for validated browser workflow learning.
|
|
|
|
It uses the same Workflow/StatePredicate schema as the Playwright replayer,
|
|
but replaces a real site with a deterministic page state. This makes the
|
|
candidate -> reset -> replay -> validated -> invalid lifecycle reproducible.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Dict, Iterable
|
|
|
|
from learning_agent.workflow import (
|
|
ActionType,
|
|
PredicateType,
|
|
StatePredicate,
|
|
Workflow,
|
|
WorkflowStep,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class SimulatedPage:
|
|
url: str
|
|
visible_elements: set[str] = field(default_factory=set)
|
|
element_text: Dict[str, str] = field(default_factory=dict)
|
|
state: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
def _check(page: SimulatedPage, predicate: StatePredicate) -> tuple[bool, Any]:
|
|
if predicate.predicate_type == PredicateType.URL_CONTAINS:
|
|
return str(predicate.expected) in page.url, page.url
|
|
if predicate.predicate_type == PredicateType.ELEMENT_VISIBLE:
|
|
actual = predicate.selector in page.visible_elements
|
|
return actual == bool(predicate.expected), actual
|
|
if predicate.predicate_type == PredicateType.ELEMENT_TEXT_CONTAINS:
|
|
actual = page.element_text.get(predicate.selector or "", "")
|
|
return str(predicate.expected) in actual, actual
|
|
if predicate.predicate_type == PredicateType.PAGE_STATE_EQUALS:
|
|
actual = page.state.get(predicate.state_key or "")
|
|
return actual == predicate.expected, actual
|
|
return False, "unsupported predicate"
|
|
|
|
|
|
def _assert_all(page: SimulatedPage, predicates: Iterable[StatePredicate], location: str) -> None:
|
|
for predicate in predicates:
|
|
passed, actual = _check(page, predicate)
|
|
if not passed:
|
|
label = predicate.description or predicate.predicate_type.value
|
|
raise RuntimeError(
|
|
f"{location}: {label}; expected={predicate.expected!r}, actual={actual!r}"
|
|
)
|
|
|
|
|
|
def replay(workflow: Workflow, page: SimulatedPage) -> Dict[str, Any]:
|
|
completed = 0
|
|
try:
|
|
for index, step in enumerate(workflow.steps, 1):
|
|
_assert_all(page, step.preconditions, f"step {index} precondition")
|
|
effect = step.parameters.get("simulated_effect", {})
|
|
if "url" in effect:
|
|
page.url = effect["url"]
|
|
page.state.update(effect.get("state", {}))
|
|
page.element_text.update(effect.get("element_text", {}))
|
|
page.visible_elements.difference_update(effect.get("hide", []))
|
|
page.visible_elements.update(effect.get("show", []))
|
|
_assert_all(page, step.postconditions, f"step {index} postcondition")
|
|
completed += 1
|
|
_assert_all(page, workflow.final_predicates, "workflow final predicate")
|
|
return {"success": True, "steps_completed": completed, "fallback_required": False, "error": None}
|
|
except RuntimeError as error:
|
|
return {
|
|
"success": False,
|
|
"steps_completed": completed,
|
|
"fallback_required": True,
|
|
"error": str(error),
|
|
}
|
|
|
|
|
|
def candidate_workflow() -> Workflow:
|
|
step = WorkflowStep(
|
|
action_type=ActionType.CLICK,
|
|
css_selector="#send",
|
|
description="send message",
|
|
parameters={
|
|
"simulated_effect": {
|
|
"url": "https://mail.example/sent/42",
|
|
"state": {"sent": True},
|
|
"element_text": {"#status": "Message sent"},
|
|
}
|
|
},
|
|
preconditions=[StatePredicate(
|
|
PredicateType.ELEMENT_VISIBLE, True, selector="#send",
|
|
description="send button is visible",
|
|
)],
|
|
postconditions=[
|
|
StatePredicate(
|
|
PredicateType.PAGE_STATE_EQUALS, True, state_key="sent",
|
|
description="message state changed to sent",
|
|
),
|
|
StatePredicate(
|
|
PredicateType.URL_CONTAINS, "/sent/",
|
|
description="browser reached the sent-message page",
|
|
),
|
|
],
|
|
)
|
|
return Workflow(
|
|
workflow_id="send-message-v1",
|
|
intent="send a message",
|
|
initial_url="https://mail.example/compose",
|
|
steps=[step],
|
|
final_predicates=[StatePredicate(
|
|
PredicateType.ELEMENT_TEXT_CONTAINS,
|
|
"Message sent",
|
|
selector="#status",
|
|
description="success confirmation is visible",
|
|
)],
|
|
)
|
|
|
|
|
|
def reset_environment() -> SimulatedPage:
|
|
return SimulatedPage(
|
|
url="https://mail.example/compose",
|
|
visible_elements={"#send"},
|
|
element_text={"#status": "Draft"},
|
|
state={"sent": False},
|
|
)
|
|
|
|
|
|
def changed_environment() -> SimulatedPage:
|
|
return SimulatedPage(
|
|
url="https://mail.example/compose-v2",
|
|
visible_elements={"#send-v2"},
|
|
element_text={"#status": "Draft"},
|
|
state={"sent": False},
|
|
)
|
|
|
|
|
|
def validate_candidate(workflow: Workflow) -> Dict[str, Any]:
|
|
result = replay(workflow, reset_environment())
|
|
if result["success"]:
|
|
workflow.mark_validated()
|
|
return result
|
|
|
|
|
|
def main() -> None:
|
|
workflow = candidate_workflow()
|
|
print(f"first-run artifact: {workflow.validation_status.value}")
|
|
validation = validate_candidate(workflow)
|
|
print(f"reset-and-replay: success={validation['success']}, status={workflow.validation_status.value}")
|
|
|
|
changed = replay(workflow, changed_environment())
|
|
if not changed["success"]:
|
|
workflow.mark_invalid(changed["error"])
|
|
print(f"after page change: success={changed['success']}, status={workflow.validation_status.value}")
|
|
print(f"fallback to full Agent: {changed['fallback_required']}")
|
|
print(f"reason: {changed['error']}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|