screenforge 0.6.1__tar.gz → 0.10.0__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.
- {screenforge-0.6.1 → screenforge-0.10.0}/PKG-INFO +4 -1
- {screenforge-0.6.1 → screenforge-0.10.0}/README.md +3 -0
- screenforge-0.10.0/cli/_version.py +1 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/doctor.py +0 -4
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/modes/action.py +11 -6
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/session.py +1 -5
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/shared.py +0 -9
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/tool_protocol_handlers.py +44 -17
- {screenforge-0.6.1 → screenforge-0.10.0}/common/adapters/base_adapter.py +5 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/ai.py +13 -8
- {screenforge-0.6.1 → screenforge-0.10.0}/common/ai_autonomous.py +7 -27
- {screenforge-0.6.1 → screenforge-0.10.0}/common/cache/cache_stats.py +0 -5
- {screenforge-0.6.1 → screenforge-0.10.0}/common/case_memory.py +14 -2
- {screenforge-0.6.1 → screenforge-0.10.0}/common/executor.py +134 -125
- {screenforge-0.6.1 → screenforge-0.10.0}/common/failure_diagnosis.py +40 -4
- {screenforge-0.6.1 → screenforge-0.10.0}/common/mcp_server.py +25 -7
- screenforge-0.10.0/common/observation.py +48 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/tool_protocol.py +6 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/config/config.py +2 -4
- screenforge-0.10.0/config/env_loader.py +23 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/pyproject.toml +10 -1
- {screenforge-0.6.1 → screenforge-0.10.0}/screenforge.egg-info/PKG-INFO +4 -1
- {screenforge-0.6.1 → screenforge-0.10.0}/screenforge.egg-info/SOURCES.txt +22 -0
- screenforge-0.10.0/tests/test_cache_hash.py +52 -0
- screenforge-0.10.0/tests/test_case_memory_identity.py +93 -0
- screenforge-0.10.0/tests/test_failure_diagnosis_ios.py +62 -0
- screenforge-0.10.0/tests/test_failure_diagnosis_mobile.py +62 -0
- screenforge-0.10.0/tests/test_forward_candidates.py +61 -0
- screenforge-0.10.0/tests/test_inspect_ui_candidates.py +67 -0
- screenforge-0.10.0/tests/test_inspect_ui_envelope.py +92 -0
- screenforge-0.10.0/tests/test_inspect_ui_memory.py +116 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_mcp_ref_cache.py +1 -1
- screenforge-0.10.0/tests/test_mcp_tool_result.py +68 -0
- screenforge-0.10.0/tests/test_observation.py +76 -0
- screenforge-0.10.0/tests/test_observation_action_parity.py +59 -0
- screenforge-0.10.0/tests/test_review_cases.py +98 -0
- screenforge-0.10.0/tests/test_review_conftest_gating.py +24 -0
- screenforge-0.10.0/tests/test_review_filmstrip.py +46 -0
- screenforge-0.10.0/tests/test_review_html.py +73 -0
- screenforge-0.10.0/tests/test_review_json.py +26 -0
- screenforge-0.10.0/tests/test_review_locate_frame.py +32 -0
- screenforge-0.10.0/tests/test_review_patching.py +87 -0
- screenforge-0.10.0/tests/test_review_recorder.py +66 -0
- screenforge-0.10.0/tests/test_review_screenshot.py +41 -0
- screenforge-0.10.0/tests/test_review_smoke_live.py +89 -0
- screenforge-0.6.1/cli/_version.py +0 -1
- screenforge-0.6.1/config/env_loader.py +0 -76
- {screenforge-0.6.1 → screenforge-0.10.0}/LICENSE +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/__init__.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/dispatch.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/modes/__init__.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/modes/default.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/modes/demo.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/modes/dry_run.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/modes/init.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/modes/plan.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/modes/workflow.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/parser.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/playground_sink.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/reporter.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/cli/shorthand.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/__init__.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/adapters/__init__.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/adapters/android_adapter.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/adapters/ios_adapter.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/adapters/web_adapter.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/ai_heal.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/cache/__init__.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/cache/cache_hash.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/cache/cache_manager.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/cache/cache_storage.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/cache/embedding_loader.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/capabilities.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/error_codes.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/exceptions.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/history_manager.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/logs.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/preflight.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/progress.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/run_reporter.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/run_resume.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/runtime_modes.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/visual_fallback.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/common/workflow_schema.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/config/__init__.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/screenforge.egg-info/dependency_links.txt +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/screenforge.egg-info/entry_points.txt +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/screenforge.egg-info/requires.txt +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/screenforge.egg-info/top_level.txt +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/setup.cfg +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_action_mode_observation_stash.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_ai_autonomous.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_ai_brain.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_ai_heal.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_android_smoke_live.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_cache_manager.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_capabilities.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_cli_action_json.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_codegen_quality.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_dispatch.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_doctor_orphan_browser.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_dom_capture.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_error_codes.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_executor.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_failure_diagnosis.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_interaction_actions.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_ios_smoke_live.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_mcp_execute_observation.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_mcp_execute_parity_live.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_ml_optional.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_mode_error_code_threading.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_parser.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_playground_app.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_playground_sink.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_playground_sink_integration.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_run_reporter.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_run_resume.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_runtime_modes.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_screenshot_annotator.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_shared_observation_stash.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_shorthand.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_tool_protocol_diagnosis.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_utils_ios.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_utils_web.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_utils_xml.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_visual_fallback.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_web_adapter.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_web_dom_complex_live.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_web_smoke_live.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/tests/test_workflow_mode_observation_stash.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/utils/__init__.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/utils/screenshot_annotator.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/utils/utils_ios.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/utils/utils_web.py +0 -0
- {screenforge-0.6.1 → screenforge-0.10.0}/utils/utils_xml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: screenforge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: AI-driven cross-platform UI automation engine with test script generation
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/jhinzzz/ScreenForge
|
|
@@ -119,6 +119,9 @@ Each step: **inspect → decide → act → verify**. The AI decides, ScreenForg
|
|
|
119
119
|
- **MCP server**: Any MCP-compatible Agent can drive ScreenForge natively
|
|
120
120
|
- **Structured output**: JSON Lines events + `report/runs/<id>/` artifacts for CI integration
|
|
121
121
|
- **Live Mirror playground**: Watch the generated pytest code grow line-by-line beside a live screenshot as the test runs — `screenforge --playground`. See the [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md)
|
|
122
|
+
- **Execution-replay review report**: Run any test with `REVIEW_RECORD=1` and get a self-contained, shareable offline `report.html` — a drag-timeline of each step's screenshot + the test source line that triggered it + the DOM tree, plus a `video.gif` filmstrip. Default off, zero overhead. (Web verified; mobile registry/recording seam in place but unverified.)
|
|
123
|
+
- **Case memory hints on `inspect_ui`**: pass an optional `task` label and the payload's `memory` field carries what the last run of that task did — locator, counts, and outcome — a hint the Agent weighs against the live tree, seen before it acts, never auto-replayed. It records failures too, so check `last_status`/`success_count` before trusting a hint. The label must match the one the earlier run recorded (`--action-name`, else `"{action}:{locator_value}"`); `load_case_memory` lists them. See the [Agent Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md).
|
|
124
|
+
- **Plain-language element lookup on `inspect_ui`**: pass an optional `intent` phrase (e.g. `"登录按钮"`) and the payload's `candidates` field returns up to 3 ranked elements with ready-to-use locators — additive to the full `ui_tree`, never a replacement. Matching is literal string similarity, not semantic, so phrase `intent` in the same language as the UI. See the [Agent Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md).
|
|
122
125
|
|
|
123
126
|
## Agent Integration (Claude Code / Cursor / Codex)
|
|
124
127
|
|
|
@@ -74,6 +74,9 @@ Each step: **inspect → decide → act → verify**. The AI decides, ScreenForg
|
|
|
74
74
|
- **MCP server**: Any MCP-compatible Agent can drive ScreenForge natively
|
|
75
75
|
- **Structured output**: JSON Lines events + `report/runs/<id>/` artifacts for CI integration
|
|
76
76
|
- **Live Mirror playground**: Watch the generated pytest code grow line-by-line beside a live screenshot as the test runs — `screenforge --playground`. See the [Playground Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/playground-guide.md)
|
|
77
|
+
- **Execution-replay review report**: Run any test with `REVIEW_RECORD=1` and get a self-contained, shareable offline `report.html` — a drag-timeline of each step's screenshot + the test source line that triggered it + the DOM tree, plus a `video.gif` filmstrip. Default off, zero overhead. (Web verified; mobile registry/recording seam in place but unverified.)
|
|
78
|
+
- **Case memory hints on `inspect_ui`**: pass an optional `task` label and the payload's `memory` field carries what the last run of that task did — locator, counts, and outcome — a hint the Agent weighs against the live tree, seen before it acts, never auto-replayed. It records failures too, so check `last_status`/`success_count` before trusting a hint. The label must match the one the earlier run recorded (`--action-name`, else `"{action}:{locator_value}"`); `load_case_memory` lists them. See the [Agent Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md).
|
|
79
|
+
- **Plain-language element lookup on `inspect_ui`**: pass an optional `intent` phrase (e.g. `"登录按钮"`) and the payload's `candidates` field returns up to 3 ranked elements with ready-to-use locators — additive to the full `ui_tree`, never a replacement. Matching is literal string similarity, not semantic, so phrase `intent` in the same language as the UI. See the [Agent Guide](https://github.com/jhinzzz/ScreenForge/blob/main/docs/agent_guide.md).
|
|
77
80
|
|
|
78
81
|
## Agent Integration (Claude Code / Cursor / Codex)
|
|
79
82
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.10.0"
|
|
@@ -373,10 +373,6 @@ def _build_doctor_summary(checks: list[dict]) -> dict:
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
|
|
376
|
-
def _build_doctor_remediation_items(checks: list[dict]) -> list[str]:
|
|
377
|
-
return _build_doctor_summary(checks).get("top_items", [])
|
|
378
|
-
|
|
379
|
-
|
|
380
376
|
def _build_doctor_check_failure_message(check: dict) -> str:
|
|
381
377
|
details = []
|
|
382
378
|
|
|
@@ -26,6 +26,7 @@ from cli.shared import (
|
|
|
26
26
|
save_to_disk,
|
|
27
27
|
)
|
|
28
28
|
from common.failure_diagnosis import diagnose
|
|
29
|
+
from common.observation import build_observation
|
|
29
30
|
from common.runtime_modes import MODE_RUN
|
|
30
31
|
|
|
31
32
|
|
|
@@ -69,9 +70,11 @@ def build_failure_payload(
|
|
|
69
70
|
ui_elements=ui_tree.get("ui_elements", []) or [],
|
|
70
71
|
)
|
|
71
72
|
payload.update(diag.to_dict())
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
observation = build_observation(ui_tree=ui_tree, current_url=current_url)
|
|
74
|
+
observation.pop("screenshot_base64") # engine_error has never carried one
|
|
75
|
+
observation.pop("memory") # decision-time hint; action is post-decision
|
|
76
|
+
observation.pop("candidates") # engine_error already has its own diagnosis candidates
|
|
77
|
+
payload.update(observation)
|
|
75
78
|
return payload
|
|
76
79
|
|
|
77
80
|
|
|
@@ -88,14 +91,16 @@ def build_success_payload(
|
|
|
88
91
|
Single source of truth shared by the shell --json stdout write and the MCP
|
|
89
92
|
observation stash, so the two can never drift. Pure: no I/O, no device.
|
|
90
93
|
"""
|
|
94
|
+
observation = build_observation(ui_tree=ui_tree, current_url=current_url)
|
|
95
|
+
observation.pop("screenshot_base64") # action payloads have never carried one
|
|
96
|
+
observation.pop("memory") # decision-time hint; action is post-decision
|
|
97
|
+
observation.pop("candidates") # engine_error already has its own diagnosis candidates
|
|
91
98
|
return {
|
|
92
99
|
"ok": True,
|
|
93
100
|
"action": action_name,
|
|
94
101
|
"platform": platform,
|
|
95
|
-
|
|
96
|
-
"element_count": len(ui_tree.get("ui_elements", []) or []),
|
|
102
|
+
**observation,
|
|
97
103
|
"output_script": output_script,
|
|
98
|
-
"current_url": current_url,
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
|
|
@@ -18,10 +18,6 @@ def _session_file(session_id: str) -> str:
|
|
|
18
18
|
return os.path.join(_SESSION_DIR, f"{session_id}.json")
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def session_exists(session_id: str) -> bool:
|
|
22
|
-
return os.path.exists(_session_file(session_id))
|
|
23
|
-
|
|
24
|
-
|
|
25
21
|
def load_session(session_id: str) -> dict | None:
|
|
26
22
|
path = _session_file(session_id)
|
|
27
23
|
if not os.path.exists(path):
|
|
@@ -125,7 +121,7 @@ def stop_session_recording(session_id: str) -> str:
|
|
|
125
121
|
pgid = os.getpgid(pid)
|
|
126
122
|
os.killpg(pgid, signal.SIGINT)
|
|
127
123
|
os.waitpid(pid, 0)
|
|
128
|
-
except
|
|
124
|
+
except OSError:
|
|
129
125
|
try:
|
|
130
126
|
os.kill(pid, signal.SIGINT)
|
|
131
127
|
time.sleep(2)
|
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
import base64
|
|
4
4
|
import time
|
|
5
5
|
|
|
6
|
-
from common.capabilities import (
|
|
7
|
-
ACTIONS_REQUIRING_EXTRA_VALUE,
|
|
8
|
-
GLOBAL_ACTIONS,
|
|
9
|
-
SUPPORTED_ACTIONS,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
6
|
|
|
13
7
|
class _LazyProxy:
|
|
14
8
|
def __init__(self, loader):
|
|
@@ -62,9 +56,6 @@ load_workflow_file = None
|
|
|
62
56
|
WorkflowLoadError = None
|
|
63
57
|
parse_workflow_var_overrides = None
|
|
64
58
|
resolve_workflow_definition = None
|
|
65
|
-
SUPPORTED_INLINE_ACTIONS = SUPPORTED_ACTIONS
|
|
66
|
-
GLOBAL_INLINE_ACTIONS = GLOBAL_ACTIONS
|
|
67
|
-
INLINE_ACTIONS_REQUIRING_EXTRA_VALUE = ACTIONS_REQUIRING_EXTRA_VALUE
|
|
68
59
|
|
|
69
60
|
|
|
70
61
|
def _slug_to_test_name(label: str) -> str:
|
|
@@ -20,6 +20,8 @@ from cli.shared import (
|
|
|
20
20
|
current_url,
|
|
21
21
|
log,
|
|
22
22
|
)
|
|
23
|
+
from common.failure_diagnosis import rank_candidates
|
|
24
|
+
from common.observation import build_observation
|
|
23
25
|
from common.run_resume import RunContextLoadError, load_run_bundle
|
|
24
26
|
from common.runtime_modes import MODE_DOCTOR, resolve_execution_mode
|
|
25
27
|
from common.tool_protocol import (
|
|
@@ -137,6 +139,30 @@ def _find_case_memory_hit(args, execution_mode: str) -> dict | None:
|
|
|
137
139
|
)
|
|
138
140
|
|
|
139
141
|
|
|
142
|
+
def _lookup_task_memory(platform: str, task: str) -> dict:
|
|
143
|
+
"""Case-memory hint for an inspect_ui request, or {} when there's nothing.
|
|
144
|
+
|
|
145
|
+
A HINT, not an instruction — see common/observation.py. Matching is fuzzy by
|
|
146
|
+
design (find_entry ORs control_label with source_ref), so a wrong hit costs
|
|
147
|
+
one useless hint, never a wrong click. control_kind is "action" because an
|
|
148
|
+
inspect precedes a single action, and that's the kind those runs record under.
|
|
149
|
+
Any read failure degrades to {}: a missing hint must never cost the agent
|
|
150
|
+
its UI tree.
|
|
151
|
+
"""
|
|
152
|
+
if not str(task).strip():
|
|
153
|
+
return {}
|
|
154
|
+
try:
|
|
155
|
+
entry = _load_case_memory_store().find_entry(
|
|
156
|
+
platform=platform,
|
|
157
|
+
control_kind="action",
|
|
158
|
+
control_label=task,
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
log.warning(f"⚠️ [Warning] Case-memory lookup failed, continuing without hint: {e}")
|
|
162
|
+
return {}
|
|
163
|
+
return entry or {}
|
|
164
|
+
|
|
165
|
+
|
|
140
166
|
def build_load_case_memory_payload(
|
|
141
167
|
platform: str = "",
|
|
142
168
|
control_kind: str = "",
|
|
@@ -182,13 +208,6 @@ def build_inspect_ui_payload(request, shared_adapter_manager: _SharedAdapterMana
|
|
|
182
208
|
1,
|
|
183
209
|
)
|
|
184
210
|
|
|
185
|
-
if not screenshot_base64:
|
|
186
|
-
try:
|
|
187
|
-
img_bytes = adapter.take_screenshot()
|
|
188
|
-
screenshot_base64 = base64.b64encode(img_bytes).decode("utf-8")
|
|
189
|
-
except Exception as e:
|
|
190
|
-
log.warning(f"⚠️ [Warning] inspect_ui screenshot capture failed: {e}")
|
|
191
|
-
|
|
192
211
|
try:
|
|
193
212
|
ui_tree = json.loads(ui_json)
|
|
194
213
|
except json.JSONDecodeError:
|
|
@@ -207,30 +226,36 @@ def build_inspect_ui_payload(request, shared_adapter_manager: _SharedAdapterMana
|
|
|
207
226
|
except Exception as e:
|
|
208
227
|
log.warning(f"⚠️ [Warning] Failed to sync ref cache from inspect_ui: {e}")
|
|
209
228
|
|
|
210
|
-
|
|
229
|
+
# Annotate in place: ref labels are only meaningful where the compressor
|
|
230
|
+
# emits ref + bbox + clickable (web). Mobile trees carry none of those, so
|
|
231
|
+
# annotate_screenshot draws nothing and just re-encodes the image — same
|
|
232
|
+
# picture, no labels. Either way exactly ONE image ships.
|
|
211
233
|
if screenshot_base64 and ui_tree.get("ui_elements"):
|
|
212
234
|
try:
|
|
213
235
|
from utils.screenshot_annotator import annotate_screenshot
|
|
214
236
|
raw_bytes = base64.b64decode(screenshot_base64)
|
|
215
237
|
annotated_bytes = annotate_screenshot(raw_bytes, ui_tree["ui_elements"])
|
|
216
|
-
|
|
238
|
+
screenshot_base64 = base64.b64encode(annotated_bytes).decode("utf-8")
|
|
217
239
|
except Exception as e:
|
|
218
240
|
log.warning(f"⚠️ [Warning] Annotated screenshot generation failed: {e}")
|
|
219
241
|
|
|
220
|
-
page_url = current_url(adapter, request.platform)
|
|
221
|
-
|
|
222
242
|
return {
|
|
223
243
|
"ok": True,
|
|
224
244
|
"operation": "inspect_ui",
|
|
225
245
|
"exit_code": 0,
|
|
226
246
|
"platform": request.platform,
|
|
227
247
|
"env": request.env,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
248
|
+
**build_observation(
|
|
249
|
+
ui_tree=ui_tree,
|
|
250
|
+
current_url=current_url(adapter, request.platform),
|
|
251
|
+
screenshot_base64=screenshot_base64 or "",
|
|
252
|
+
memory=_lookup_task_memory(request.platform, request.task),
|
|
253
|
+
candidates=(
|
|
254
|
+
rank_candidates(request.intent, ui_tree.get("ui_elements") or [])
|
|
255
|
+
if str(request.intent).strip()
|
|
256
|
+
else []
|
|
257
|
+
),
|
|
258
|
+
),
|
|
234
259
|
}
|
|
235
260
|
except Exception as e:
|
|
236
261
|
return {
|
|
@@ -241,6 +266,8 @@ def build_inspect_ui_payload(request, shared_adapter_manager: _SharedAdapterMana
|
|
|
241
266
|
"env": request.env,
|
|
242
267
|
"error": str(e),
|
|
243
268
|
"current_url": "",
|
|
269
|
+
"memory": {},
|
|
270
|
+
"candidates": [],
|
|
244
271
|
}
|
|
245
272
|
finally:
|
|
246
273
|
if owns_adapter and adapter:
|
|
@@ -17,6 +17,11 @@ class BasePlatformAdapter(ABC):
|
|
|
17
17
|
def take_screenshot(self) -> bytes:
|
|
18
18
|
pass
|
|
19
19
|
|
|
20
|
+
# 录像 seam(诚实边界):
|
|
21
|
+
# - android/ios:start_record/stop_record_and_get_path 落真视频文件(scrcpy / simctl)。
|
|
22
|
+
# - web:start_record 是 no-op(CDP attach 无法用 Playwright 录像);web 的"录像"
|
|
23
|
+
# 由 review 层用逐操作截图 ffmpeg 拼胶片(review/render.py make_filmstrip)实现,
|
|
24
|
+
# 不走本 seam。调用方须把返回 "" / None 当作"无视频",非错误。
|
|
20
25
|
def start_record(self, video_name: str):
|
|
21
26
|
pass
|
|
22
27
|
|
|
@@ -9,6 +9,15 @@ from common.logs import log
|
|
|
9
9
|
from common.progress import ai_status
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def _strip_json_fences(text: str) -> str:
|
|
13
|
+
"""Unwrap markdown code fences the model may add despite being told not to."""
|
|
14
|
+
if "```json" in text:
|
|
15
|
+
return text.split("```json")[1].split("```")[0].strip()
|
|
16
|
+
if "```" in text:
|
|
17
|
+
return text.replace("```", "").strip()
|
|
18
|
+
return text
|
|
19
|
+
|
|
20
|
+
|
|
12
21
|
class AIBrain:
|
|
13
22
|
def __init__(self):
|
|
14
23
|
# 实例化文本专属客户端
|
|
@@ -251,6 +260,7 @@ class AIBrain:
|
|
|
251
260
|
"""
|
|
252
261
|
封装底层的 LLM 网络调用
|
|
253
262
|
"""
|
|
263
|
+
result_text = ""
|
|
254
264
|
try:
|
|
255
265
|
with ai_status(f"Thinking ({model_name})..."):
|
|
256
266
|
response = client.chat.completions.create(
|
|
@@ -262,16 +272,11 @@ class AIBrain:
|
|
|
262
272
|
temperature=0.1,
|
|
263
273
|
)
|
|
264
274
|
|
|
265
|
-
result_text = response.choices[0].message.content.strip()
|
|
266
|
-
|
|
267
|
-
if "```json" in result_text:
|
|
268
|
-
result_text = result_text.split("```json")[1].split("```")[0].strip()
|
|
269
|
-
elif "```" in result_text:
|
|
270
|
-
result_text = result_text.replace("```", "").strip()
|
|
271
|
-
|
|
275
|
+
result_text = _strip_json_fences(response.choices[0].message.content.strip())
|
|
272
276
|
parsed_json = json.loads(result_text)
|
|
273
277
|
return parsed_json.get("result", {})
|
|
274
278
|
|
|
275
279
|
except Exception as e:
|
|
276
|
-
|
|
280
|
+
# {} = "retry without cache" per caller contract; log raw text so parse-fail ≠ network-fail.
|
|
281
|
+
log.error(f"[Error] Model ({model_name}) request or parse failed: {e}\nRaw response: {result_text}")
|
|
277
282
|
return {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
3
|
import config.config as config
|
|
4
|
-
from common.ai import AIBrain
|
|
4
|
+
from common.ai import AIBrain, _strip_json_fences
|
|
5
5
|
from common.logs import log
|
|
6
6
|
from common.progress import ai_status
|
|
7
7
|
|
|
@@ -62,14 +62,10 @@ class AutonomousBrain(AIBrain):
|
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
if screenshot_base64:
|
|
65
|
-
active_client =
|
|
66
|
-
self, "text_client", None
|
|
67
|
-
)
|
|
65
|
+
active_client = self.vision_client or self.text_client
|
|
68
66
|
active_model = config.VISION_MODEL_NAME
|
|
69
67
|
else:
|
|
70
|
-
active_client =
|
|
71
|
-
self, "client", None
|
|
72
|
-
)
|
|
68
|
+
active_client = self.text_client
|
|
73
69
|
active_model = config.MODEL_NAME
|
|
74
70
|
|
|
75
71
|
if not active_client:
|
|
@@ -92,13 +88,7 @@ class AutonomousBrain(AIBrain):
|
|
|
92
88
|
],
|
|
93
89
|
temperature=0.1,
|
|
94
90
|
)
|
|
95
|
-
result_text = response.choices[0].message.content.strip()
|
|
96
|
-
|
|
97
|
-
if "```json" in result_text:
|
|
98
|
-
result_text = result_text.split("```json")[1].split("```")[0].strip()
|
|
99
|
-
elif "```" in result_text:
|
|
100
|
-
result_text = result_text.replace("```", "").strip()
|
|
101
|
-
|
|
91
|
+
result_text = _strip_json_fences(response.choices[0].message.content.strip())
|
|
102
92
|
parsed_json = json.loads(result_text)
|
|
103
93
|
parsed_json.setdefault("current_state_summary", "")
|
|
104
94
|
parsed_json.setdefault("planned_steps", [])
|
|
@@ -216,14 +206,10 @@ class AutonomousBrain(AIBrain):
|
|
|
216
206
|
)
|
|
217
207
|
|
|
218
208
|
if screenshot_base64:
|
|
219
|
-
active_client =
|
|
220
|
-
self, "text_client", None
|
|
221
|
-
) # 兼容配置
|
|
209
|
+
active_client = self.vision_client or self.text_client # 兼容配置
|
|
222
210
|
active_model = config.VISION_MODEL_NAME
|
|
223
211
|
else:
|
|
224
|
-
active_client =
|
|
225
|
-
self, "client", None
|
|
226
|
-
)
|
|
212
|
+
active_client = self.text_client
|
|
227
213
|
active_model = config.MODEL_NAME
|
|
228
214
|
|
|
229
215
|
if not active_client:
|
|
@@ -248,13 +234,7 @@ class AutonomousBrain(AIBrain):
|
|
|
248
234
|
temperature=0.1,
|
|
249
235
|
)
|
|
250
236
|
|
|
251
|
-
result_text = response.choices[0].message.content.strip()
|
|
252
|
-
|
|
253
|
-
if "```json" in result_text:
|
|
254
|
-
result_text = result_text.split("```json")[1].split("```")[0].strip()
|
|
255
|
-
elif "```" in result_text:
|
|
256
|
-
result_text = result_text.replace("```", "").strip()
|
|
257
|
-
|
|
237
|
+
result_text = _strip_json_fences(response.choices[0].message.content.strip())
|
|
258
238
|
parsed_json = json.loads(result_text)
|
|
259
239
|
|
|
260
240
|
thought = parsed_json.get("thought", "无")
|
|
@@ -48,11 +48,6 @@ class CacheStats:
|
|
|
48
48
|
def last_cache_date(self) -> Optional[str]:
|
|
49
49
|
return self._last_cache_date
|
|
50
50
|
|
|
51
|
-
def increment_query(self) -> None:
|
|
52
|
-
self._total_queries += 1
|
|
53
|
-
self._update_last_cache_date()
|
|
54
|
-
self._save_stats()
|
|
55
|
-
|
|
56
51
|
def increment_hit(self) -> None:
|
|
57
52
|
self._total_queries += 1
|
|
58
53
|
self._cache_hits += 1
|
|
@@ -24,6 +24,17 @@ def _slugify(value: str) -> str:
|
|
|
24
24
|
return text.lower() or "memory"
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
# Inline `--action` runs all record this same source_ref: it says "typed on the
|
|
28
|
+
# command line", not "which control". Treating it as an identity made the first
|
|
29
|
+
# inline action on a platform absorb every later one, so a named action could
|
|
30
|
+
# never get its own entry — or be found by its name.
|
|
31
|
+
_NON_IDENTIFYING_SOURCE_REFS = frozenset({"inline://action"})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _identifying_source_ref(source_ref: str) -> str:
|
|
35
|
+
return "" if source_ref in _NON_IDENTIFYING_SOURCE_REFS else source_ref
|
|
36
|
+
|
|
37
|
+
|
|
27
38
|
def _build_memory_id(
|
|
28
39
|
platform: str,
|
|
29
40
|
control_kind: str,
|
|
@@ -240,7 +251,7 @@ class CaseMemoryStore:
|
|
|
240
251
|
normalized_platform = _normalize_text(platform).lower()
|
|
241
252
|
normalized_kind = _normalize_text(control_kind).lower()
|
|
242
253
|
normalized_label = _normalize_text(control_label)
|
|
243
|
-
normalized_source_ref = _normalize_text(source_ref)
|
|
254
|
+
normalized_source_ref = _identifying_source_ref(_normalize_text(source_ref))
|
|
244
255
|
document = self.load_document()
|
|
245
256
|
|
|
246
257
|
for entry in document.entries:
|
|
@@ -277,10 +288,11 @@ class CaseMemoryStore:
|
|
|
277
288
|
|
|
278
289
|
document = self.load_document()
|
|
279
290
|
existing_entry = None
|
|
291
|
+
identifying_source_ref = _identifying_source_ref(source_ref)
|
|
280
292
|
for entry in document.entries:
|
|
281
293
|
if entry.platform != platform or entry.control_kind != control_kind:
|
|
282
294
|
continue
|
|
283
|
-
if
|
|
295
|
+
if identifying_source_ref and entry.source_ref == identifying_source_ref:
|
|
284
296
|
existing_entry = entry
|
|
285
297
|
break
|
|
286
298
|
if entry.control_label == control_label:
|