Convert to standard model repository layout
Browse files- .gitattributes +2 -2
- MODEL_REPO_MANIFEST.json +50 -0
- README.md +18 -14
- config.json +7 -6
- evidence/leo_ui7m_final_clean_release_v1.json +0 -7
- examples/run_agent.py +3 -0
- leo_ui7m/__init__.py +6 -0
- leo_ui7m/__pycache__/__init__.cpython-312.pyc +0 -0
- leo_ui7m/__pycache__/api.cpython-312.pyc +0 -0
- leo_ui7m/__pycache__/cli.cpython-312.pyc +0 -0
- leo_ui7m/__pycache__/schema.cpython-312.pyc +0 -0
- leo_ui7m/adapters/__init__.py +0 -0
- leo_ui7m/adapters/__pycache__/__init__.cpython-312.pyc +0 -0
- leo_ui7m/adapters/__pycache__/playwright_dom.cpython-312.pyc +0 -0
- leo_ui7m/adapters/__pycache__/real_web_v2.cpython-312.pyc +0 -0
- leo_ui7m/adapters/playwright_dom.py +41 -0
- leo_ui7m/adapters/real_web_v2.py +192 -0
- leo_ui7m/agent/__pycache__/chat_agent_v1.cpython-312.pyc +0 -0
- leo_ui7m/agent/chat_agent_v1.py +395 -0
- leo_ui7m/api.py +45 -0
- leo_ui7m/cli.py +38 -0
- leo_ui7m/config.json +7 -0
- leo_ui7m/executors/__init__.py +0 -0
- leo_ui7m/executors/__pycache__/__init__.cpython-312.pyc +0 -0
- leo_ui7m/executors/__pycache__/playwright_executor.cpython-312.pyc +0 -0
- leo_ui7m/executors/playwright_executor.py +63 -0
- leo_ui7m/outcomes/__pycache__/work_outcome_v1.cpython-312.pyc +0 -0
- leo_ui7m/outcomes/work_outcome_v1.py +305 -0
- leo_ui7m/runtime/__init__.py +0 -0
- leo_ui7m/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- leo_ui7m/runtime/__pycache__/model.cpython-312.pyc +0 -0
- leo_ui7m/runtime/__pycache__/navigation_reranker_v1.cpython-312.pyc +0 -0
- leo_ui7m/runtime/__pycache__/safe_executor_v2.cpython-312.pyc +0 -0
- leo_ui7m/runtime/model.py +198 -0
- leo_ui7m/runtime/navigation_reranker_v1.py +145 -0
- leo_ui7m/runtime/safe_executor_v2.py +75 -0
- leo_ui7m/schema.py +55 -0
- leo-ui7m-v0.1.0-private.tar.gz → model.safetensors +2 -2
- leo-ui7m-v0.1.0-private.tar.gz.sha256 → pytorch_model.bin +2 -2
- requirements.txt +3 -0
- tests/__pycache__/test_private_package_v0_1.cpython-312-pytest-9.0.3.pyc +0 -0
- tests/test_private_package_v0_1.py +47 -0
.gitattributes
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
*.
|
| 2 |
-
|
|
|
|
| 1 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
MODEL_REPO_MANIFEST.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"project": "LEO-UI7M",
|
| 3 |
+
"status": "STANDARD_MODEL_REPO_LAYOUT_READY",
|
| 4 |
+
"created_unix": 1780431992,
|
| 5 |
+
"parameter_count": 7458567,
|
| 6 |
+
"safetensors_status": "created",
|
| 7 |
+
"files": [
|
| 8 |
+
".gitattributes",
|
| 9 |
+
"README.md",
|
| 10 |
+
"config.json",
|
| 11 |
+
"examples/run_agent.py",
|
| 12 |
+
"leo_ui7m/__init__.py",
|
| 13 |
+
"leo_ui7m/__pycache__/__init__.cpython-312.pyc",
|
| 14 |
+
"leo_ui7m/__pycache__/api.cpython-312.pyc",
|
| 15 |
+
"leo_ui7m/__pycache__/cli.cpython-312.pyc",
|
| 16 |
+
"leo_ui7m/__pycache__/schema.cpython-312.pyc",
|
| 17 |
+
"leo_ui7m/adapters/__init__.py",
|
| 18 |
+
"leo_ui7m/adapters/__pycache__/__init__.cpython-312.pyc",
|
| 19 |
+
"leo_ui7m/adapters/__pycache__/playwright_dom.cpython-312.pyc",
|
| 20 |
+
"leo_ui7m/adapters/__pycache__/real_web_v2.cpython-312.pyc",
|
| 21 |
+
"leo_ui7m/adapters/playwright_dom.py",
|
| 22 |
+
"leo_ui7m/adapters/real_web_v2.py",
|
| 23 |
+
"leo_ui7m/agent/__pycache__/chat_agent_v1.cpython-312.pyc",
|
| 24 |
+
"leo_ui7m/agent/chat_agent_v1.py",
|
| 25 |
+
"leo_ui7m/api.py",
|
| 26 |
+
"leo_ui7m/cli.py",
|
| 27 |
+
"leo_ui7m/config.json",
|
| 28 |
+
"leo_ui7m/executors/__init__.py",
|
| 29 |
+
"leo_ui7m/executors/__pycache__/__init__.cpython-312.pyc",
|
| 30 |
+
"leo_ui7m/executors/__pycache__/playwright_executor.cpython-312.pyc",
|
| 31 |
+
"leo_ui7m/executors/playwright_executor.py",
|
| 32 |
+
"leo_ui7m/outcomes/__pycache__/work_outcome_v1.cpython-312.pyc",
|
| 33 |
+
"leo_ui7m/outcomes/work_outcome_v1.py",
|
| 34 |
+
"leo_ui7m/runtime/__init__.py",
|
| 35 |
+
"leo_ui7m/runtime/__pycache__/__init__.cpython-312.pyc",
|
| 36 |
+
"leo_ui7m/runtime/__pycache__/model.cpython-312.pyc",
|
| 37 |
+
"leo_ui7m/runtime/__pycache__/navigation_reranker_v1.cpython-312.pyc",
|
| 38 |
+
"leo_ui7m/runtime/__pycache__/safe_executor_v2.cpython-312.pyc",
|
| 39 |
+
"leo_ui7m/runtime/model.py",
|
| 40 |
+
"leo_ui7m/runtime/navigation_reranker_v1.py",
|
| 41 |
+
"leo_ui7m/runtime/safe_executor_v2.py",
|
| 42 |
+
"leo_ui7m/schema.py",
|
| 43 |
+
"model.safetensors",
|
| 44 |
+
"pytorch_model.bin",
|
| 45 |
+
"requirements.txt",
|
| 46 |
+
"tests/__pycache__/test_private_package_v0_1.cpython-312-pytest-9.0.3.pyc",
|
| 47 |
+
"tests/test_private_package_v0_1.py"
|
| 48 |
+
],
|
| 49 |
+
"pytorch_model_sha256": "dbbb598fffba5f19558264165c3c3636c76f1a530c12c80c7528d5addd6c870a"
|
| 50 |
+
}
|
README.md
CHANGED
|
@@ -13,26 +13,30 @@ license: other
|
|
| 13 |
|
| 14 |
# LEO-UI7M v0.1-private
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
Correct parameter count:
|
| 19 |
|
| 20 |
-
|
| 21 |
|
| 22 |
-
|
| 23 |
-
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
|
|
|
| 27 |
- Wikipedia command to research note
|
| 28 |
- Python docs command to structured docs results
|
| 29 |
- Hacker News jobs command to job digest
|
| 30 |
- Example or IANA command to page explanation
|
| 31 |
-
-
|
| 32 |
-
|
| 33 |
-
## Non-claims
|
| 34 |
|
| 35 |
-
-
|
| 36 |
-
-
|
| 37 |
-
-
|
| 38 |
-
-
|
|
|
|
|
|
| 13 |
|
| 14 |
# LEO-UI7M v0.1-private
|
| 15 |
|
| 16 |
+
Private bounded useful browser-agent release.
|
| 17 |
|
| 18 |
+
Correct parameter count: 7,458,567.
|
| 19 |
|
| 20 |
+
This repository uses a standard model-repo layout with direct weight files, config, source package, requirements, examples, and tests.
|
| 21 |
|
| 22 |
+
Files:
|
| 23 |
+
- pytorch_model.bin
|
| 24 |
+
- model.safetensors when available
|
| 25 |
+
- config.json
|
| 26 |
+
- leo_ui7m/
|
| 27 |
+
- requirements.txt
|
| 28 |
+
- examples/
|
| 29 |
+
- tests/
|
| 30 |
|
| 31 |
+
Validated capabilities:
|
| 32 |
- Wikipedia command to research note
|
| 33 |
- Python docs command to structured docs results
|
| 34 |
- Hacker News jobs command to job digest
|
| 35 |
- Example or IANA command to page explanation
|
| 36 |
+
- Refusal for unsafe tasks
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
Non-claims:
|
| 39 |
+
- not a general Fara replacement
|
| 40 |
+
- not screenshot-based visual computer-use
|
| 41 |
+
- not arbitrary-web automation
|
| 42 |
+
- not public-product-ready
|
config.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
| 1 |
{
|
|
|
|
| 2 |
"name": "LEO-UI7M v0.1-private",
|
| 3 |
-
"version": "0.1.0-private",
|
| 4 |
"parameter_count": 7458567,
|
| 5 |
-
"
|
| 6 |
-
"
|
| 7 |
-
"
|
|
|
|
| 8 |
"python_package": "leo_ui7m",
|
| 9 |
-
"
|
| 10 |
-
"
|
| 11 |
"not_claimed": [
|
| 12 |
"not a general Fara replacement",
|
| 13 |
"not screenshot-based visual computer-use",
|
|
|
|
| 1 |
{
|
| 2 |
+
"model_type": "leo_ui7m_browser_agent",
|
| 3 |
"name": "LEO-UI7M v0.1-private",
|
|
|
|
| 4 |
"parameter_count": 7458567,
|
| 5 |
+
"version": "0.1.0-private",
|
| 6 |
+
"library_name": "other",
|
| 7 |
+
"weights": "pytorch_model.bin",
|
| 8 |
+
"safetensors": "model.safetensors",
|
| 9 |
"python_package": "leo_ui7m",
|
| 10 |
+
"entrypoint": "leo_ui7m.agent.chat_agent_v1:run_chat_message",
|
| 11 |
+
"scope": "private safe read-only browser agent",
|
| 12 |
"not_claimed": [
|
| 13 |
"not a general Fara replacement",
|
| 14 |
"not screenshot-based visual computer-use",
|
evidence/leo_ui7m_final_clean_release_v1.json
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"project": "LEO-UI7M",
|
| 3 |
-
"parameter_count": 7458567,
|
| 4 |
-
"pytest_stdout": "..... [100%]\n5 passed in 24.86s\n",
|
| 5 |
-
"old_name_hits": [],
|
| 6 |
-
"artifact_sha256": "4cb33cb984cbfe273c27e0d7636e7be6d2d478c3bf86fe9a0b6d6ec9408f8648"
|
| 7 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/run_agent.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from leo_ui7m.agent.chat_agent_v1 import run_chat_message
|
| 2 |
+
r = run_chat_message('Search Wikipedia for Alan Turing')
|
| 3 |
+
print((r.get('work_outcome') or {}).get('answer', '')[:800])
|
leo_ui7m/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LEO-UI7M local UI-action inference package."""
|
| 2 |
+
|
| 3 |
+
__version__ = "0.1.0"
|
| 4 |
+
from .api import UIActionModel, UIAction, UIElement
|
| 5 |
+
|
| 6 |
+
__all__ = ["UIActionModel", "UIAction", "UIElement"]
|
leo_ui7m/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (364 Bytes). View file
|
|
|
leo_ui7m/__pycache__/api.cpython-312.pyc
ADDED
|
Binary file (2.34 kB). View file
|
|
|
leo_ui7m/__pycache__/cli.cpython-312.pyc
ADDED
|
Binary file (2.37 kB). View file
|
|
|
leo_ui7m/__pycache__/schema.cpython-312.pyc
ADDED
|
Binary file (3 kB). View file
|
|
|
leo_ui7m/adapters/__init__.py
ADDED
|
File without changes
|
leo_ui7m/adapters/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (175 Bytes). View file
|
|
|
leo_ui7m/adapters/__pycache__/playwright_dom.cpython-312.pyc
ADDED
|
Binary file (1.88 kB). View file
|
|
|
leo_ui7m/adapters/__pycache__/real_web_v2.cpython-312.pyc
ADDED
|
Binary file (7.92 kB). View file
|
|
|
leo_ui7m/adapters/playwright_dom.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def extract_ui_elements(page) -> list[dict[str, Any]]:
|
| 8 |
+
"""Extract browser DOM elements into LEO-UI7M UIElement-compatible dicts.
|
| 9 |
+
|
| 10 |
+
Supports Playwright sync Page.
|
| 11 |
+
"""
|
| 12 |
+
return page.evaluate(
|
| 13 |
+
"""() => Array.from(document.querySelectorAll('button,input,select,a,[role="row"],[role="tab"],[role="menuitem"],textarea')).map((el, i) => {
|
| 14 |
+
const tag = el.tagName.toLowerCase();
|
| 15 |
+
const typ = (el.getAttribute('type') || '').toLowerCase();
|
| 16 |
+
let role = el.getAttribute('role') || (
|
| 17 |
+
tag === 'button' ? 'button' :
|
| 18 |
+
tag === 'select' ? 'combobox' :
|
| 19 |
+
tag === 'a' ? 'link' :
|
| 20 |
+
tag === 'textarea' ? 'textbox' :
|
| 21 |
+
typ === 'checkbox' ? 'checkbox' :
|
| 22 |
+
tag === 'input' ? 'textbox' :
|
| 23 |
+
'button'
|
| 24 |
+
);
|
| 25 |
+
const r = el.getBoundingClientRect();
|
| 26 |
+
const text = (el.innerText || el.value || '').trim();
|
| 27 |
+
const name = el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('name') || text || el.id || el.dataset.eid || '';
|
| 28 |
+
return {
|
| 29 |
+
element_id: el.id || el.dataset.eid || `auto_${i}`,
|
| 30 |
+
role,
|
| 31 |
+
text,
|
| 32 |
+
name,
|
| 33 |
+
value: el.value || '',
|
| 34 |
+
enabled: !el.disabled,
|
| 35 |
+
visible: !!(r.width || r.height),
|
| 36 |
+
section: el.closest('[data-section]')?.dataset.section || 'main',
|
| 37 |
+
bbox: [r.x, r.y, r.width, r.height],
|
| 38 |
+
depth: 1
|
| 39 |
+
};
|
| 40 |
+
})"""
|
| 41 |
+
)
|
leo_ui7m/adapters/real_web_v2.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
EXTRACT_REAL_WEB_ELEMENTS_V2_JS = r"""
|
| 8 |
+
() => {
|
| 9 |
+
const isVisible = (el) => {
|
| 10 |
+
const r = el.getBoundingClientRect();
|
| 11 |
+
const st = window.getComputedStyle(el);
|
| 12 |
+
return !!(r.width || r.height) && st.visibility !== 'hidden' && st.display !== 'none' && st.opacity !== '0';
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
const cssPath = (el) => {
|
| 16 |
+
const parts = [];
|
| 17 |
+
let cur = el;
|
| 18 |
+
while (cur && cur.nodeType === 1 && cur !== document.body && parts.length < 6) {
|
| 19 |
+
let part = cur.tagName.toLowerCase();
|
| 20 |
+
if (cur.id) {
|
| 21 |
+
part += '#' + CSS.escape(cur.id);
|
| 22 |
+
parts.unshift(part);
|
| 23 |
+
break;
|
| 24 |
+
}
|
| 25 |
+
const parent = cur.parentElement;
|
| 26 |
+
if (parent) {
|
| 27 |
+
const siblings = Array.from(parent.children).filter(x => x.tagName === cur.tagName);
|
| 28 |
+
if (siblings.length > 1) part += `:nth-of-type(${siblings.indexOf(cur) + 1})`;
|
| 29 |
+
}
|
| 30 |
+
parts.unshift(part);
|
| 31 |
+
cur = parent;
|
| 32 |
+
}
|
| 33 |
+
return parts.join(' > ');
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
const inferRole = (el) => {
|
| 37 |
+
const tag = el.tagName.toLowerCase();
|
| 38 |
+
const typ = (el.getAttribute('type') || '').toLowerCase();
|
| 39 |
+
return el.getAttribute('role') || (
|
| 40 |
+
tag === 'button' ? 'button' :
|
| 41 |
+
tag === 'select' ? 'combobox' :
|
| 42 |
+
tag === 'a' ? 'link' :
|
| 43 |
+
tag === 'textarea' ? 'textbox' :
|
| 44 |
+
typ === 'checkbox' ? 'checkbox' :
|
| 45 |
+
tag === 'input' ? (typ === 'submit' || typ === 'button' ? 'button' : 'textbox') :
|
| 46 |
+
'button'
|
| 47 |
+
);
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const nodes = Array.from(document.querySelectorAll(
|
| 51 |
+
'button,input,select,a,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="combobox"],[contenteditable="true"]'
|
| 52 |
+
));
|
| 53 |
+
|
| 54 |
+
return nodes.map((el, i) => {
|
| 55 |
+
if (!el.dataset.leoStableUid) {
|
| 56 |
+
el.dataset.leoStableUid = `leo_stable_${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
const role = inferRole(el);
|
| 60 |
+
const r = el.getBoundingClientRect();
|
| 61 |
+
const text = (el.innerText || el.value || el.getAttribute('value') || '').trim();
|
| 62 |
+
const name = (
|
| 63 |
+
el.getAttribute('aria-label') ||
|
| 64 |
+
el.getAttribute('placeholder') ||
|
| 65 |
+
el.getAttribute('name') ||
|
| 66 |
+
el.getAttribute('title') ||
|
| 67 |
+
text ||
|
| 68 |
+
el.id ||
|
| 69 |
+
el.dataset.leoStableUid ||
|
| 70 |
+
''
|
| 71 |
+
).trim();
|
| 72 |
+
|
| 73 |
+
const stable_uid = el.dataset.leoStableUid;
|
| 74 |
+
const element_id = el.id || stable_uid;
|
| 75 |
+
|
| 76 |
+
const selectors = [];
|
| 77 |
+
selectors.push(`[data-leo-stable-uid="${stable_uid}"]`);
|
| 78 |
+
if (el.id) selectors.push(`#${CSS.escape(el.id)}`);
|
| 79 |
+
if (el.getAttribute('name')) selectors.push(`${el.tagName.toLowerCase()}[name="${CSS.escape(el.getAttribute('name'))}"]`);
|
| 80 |
+
if (el.getAttribute('aria-label')) selectors.push(`${el.tagName.toLowerCase()}[aria-label="${CSS.escape(el.getAttribute('aria-label'))}"]`);
|
| 81 |
+
const path = cssPath(el);
|
| 82 |
+
if (path) selectors.push(path);
|
| 83 |
+
|
| 84 |
+
return {
|
| 85 |
+
element_id,
|
| 86 |
+
stable_uid,
|
| 87 |
+
role,
|
| 88 |
+
text,
|
| 89 |
+
name,
|
| 90 |
+
value: el.value || el.getAttribute('value') || '',
|
| 91 |
+
href: el.href || '',
|
| 92 |
+
tag: el.tagName.toLowerCase(),
|
| 93 |
+
type: (el.getAttribute('type') || '').toLowerCase(),
|
| 94 |
+
enabled: !el.disabled,
|
| 95 |
+
visible: isVisible(el),
|
| 96 |
+
section: el.closest('[data-section]')?.dataset.section || 'main',
|
| 97 |
+
bbox: [r.x, r.y, r.width, r.height],
|
| 98 |
+
depth: 1,
|
| 99 |
+
selectors,
|
| 100 |
+
fingerprint: {
|
| 101 |
+
role,
|
| 102 |
+
name,
|
| 103 |
+
text,
|
| 104 |
+
tag: el.tagName.toLowerCase(),
|
| 105 |
+
type: (el.getAttribute('type') || '').toLowerCase(),
|
| 106 |
+
href: el.href || '',
|
| 107 |
+
id: el.id || '',
|
| 108 |
+
stable_uid,
|
| 109 |
+
css_path: path,
|
| 110 |
+
}
|
| 111 |
+
};
|
| 112 |
+
});
|
| 113 |
+
}
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def extract_real_web_elements_v2(page, *, include_hidden: bool = False) -> List[Dict[str, Any]]:
|
| 118 |
+
elements = page.evaluate(EXTRACT_REAL_WEB_ELEMENTS_V2_JS)
|
| 119 |
+
if not include_hidden:
|
| 120 |
+
elements = [e for e in elements if e.get("visible") and e.get("enabled")]
|
| 121 |
+
return elements
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _score_candidate(element: Dict[str, Any], cand: Dict[str, Any]) -> float:
|
| 125 |
+
fp = element.get("fingerprint") or {}
|
| 126 |
+
score = 0.0
|
| 127 |
+
|
| 128 |
+
if cand.get("role") == element.get("role"):
|
| 129 |
+
score += 3.0
|
| 130 |
+
if cand.get("name") and cand.get("name") == element.get("name"):
|
| 131 |
+
score += 3.0
|
| 132 |
+
if cand.get("text") and cand.get("text") == element.get("text"):
|
| 133 |
+
score += 2.0
|
| 134 |
+
if cand.get("tag") == fp.get("tag"):
|
| 135 |
+
score += 1.0
|
| 136 |
+
if cand.get("href") and cand.get("href") == fp.get("href"):
|
| 137 |
+
score += 2.0
|
| 138 |
+
if cand.get("element_id") == element.get("element_id"):
|
| 139 |
+
score += 4.0
|
| 140 |
+
if cand.get("stable_uid") == element.get("stable_uid"):
|
| 141 |
+
score += 8.0
|
| 142 |
+
|
| 143 |
+
return score
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def resolve_real_web_locator_v2(page, element: Dict[str, Any]):
|
| 147 |
+
"""Resolve a Playwright locator for a previously extracted element.
|
| 148 |
+
|
| 149 |
+
Priority:
|
| 150 |
+
1. Stable injected data uid.
|
| 151 |
+
2. Original id/name/aria selectors.
|
| 152 |
+
3. Best fingerprint match from fresh extraction.
|
| 153 |
+
"""
|
| 154 |
+
selectors = element.get("selectors") or []
|
| 155 |
+
for sel in selectors:
|
| 156 |
+
try:
|
| 157 |
+
loc = page.locator(sel).first
|
| 158 |
+
if loc.count() > 0:
|
| 159 |
+
return loc
|
| 160 |
+
except Exception:
|
| 161 |
+
continue
|
| 162 |
+
|
| 163 |
+
# Re-extract and find best current fingerprint match.
|
| 164 |
+
fresh = extract_real_web_elements_v2(page, include_hidden=False)
|
| 165 |
+
if not fresh:
|
| 166 |
+
return None
|
| 167 |
+
|
| 168 |
+
best = max(fresh, key=lambda c: _score_candidate(element, c))
|
| 169 |
+
if _score_candidate(element, best) < 4.0:
|
| 170 |
+
return None
|
| 171 |
+
|
| 172 |
+
for sel in best.get("selectors") or []:
|
| 173 |
+
try:
|
| 174 |
+
loc = page.locator(sel).first
|
| 175 |
+
if loc.count() > 0:
|
| 176 |
+
return loc
|
| 177 |
+
except Exception:
|
| 178 |
+
continue
|
| 179 |
+
return None
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def real_web_element_safe_text(element: Optional[Dict[str, Any]]) -> str:
|
| 183 |
+
if not element:
|
| 184 |
+
return ""
|
| 185 |
+
fields = [
|
| 186 |
+
element.get("element_id"),
|
| 187 |
+
element.get("role"),
|
| 188 |
+
element.get("name"),
|
| 189 |
+
element.get("text"),
|
| 190 |
+
element.get("href"),
|
| 191 |
+
]
|
| 192 |
+
return " ".join(str(x or "") for x in fields).lower()
|
leo_ui7m/agent/__pycache__/chat_agent_v1.cpython-312.pyc
ADDED
|
Binary file (17.8 kB). View file
|
|
|
leo_ui7m/agent/chat_agent_v1.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
import time
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, Dict, Optional
|
| 10 |
+
|
| 11 |
+
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
|
| 12 |
+
|
| 13 |
+
from leo_ui7m import UIActionModel
|
| 14 |
+
from leo_ui7m.adapters.real_web_v2 import extract_real_web_elements_v2, resolve_real_web_locator_v2
|
| 15 |
+
from leo_ui7m.runtime.navigation_reranker_v1 import rerank_navigation_action
|
| 16 |
+
from leo_ui7m.runtime.safe_executor_v2 import check_safe_action, find_element, strict_url_success
|
| 17 |
+
from leo_ui7m.outcomes.work_outcome_v1 import build_work_outcome_v1
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 21 |
+
RUN_LOG = ROOT / "reports/leo_ui7m_agent_chat_v1_runs.jsonl"
|
| 22 |
+
RUN_LOG.parent.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@dataclass
|
| 26 |
+
class RouteResult:
|
| 27 |
+
ok: bool
|
| 28 |
+
task: Optional[Dict[str, Any]] = None
|
| 29 |
+
refusal: Optional[str] = None
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _clean_query(q: str) -> str:
|
| 33 |
+
q = re.sub(r"\s+", " ", q or "").strip()
|
| 34 |
+
q = q.strip(" .!?\"'")
|
| 35 |
+
return q
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def route_message_to_task(message: str) -> RouteResult:
|
| 39 |
+
"""Safe bounded natural-language router.
|
| 40 |
+
|
| 41 |
+
This is intentionally not a general web agent router. It only maps messages into
|
| 42 |
+
accepted safe read-only task families.
|
| 43 |
+
"""
|
| 44 |
+
raw = message or ""
|
| 45 |
+
msg = raw.lower().strip()
|
| 46 |
+
|
| 47 |
+
forbidden = [
|
| 48 |
+
"login", "sign in", "password", "checkout", "buy ", "purchase", "payment",
|
| 49 |
+
"credit card", "bank", "send email", "post ", "comment ", "delete ",
|
| 50 |
+
"account", "captcha", "upload", "download private", "personal data",
|
| 51 |
+
]
|
| 52 |
+
if any(x in msg for x in forbidden):
|
| 53 |
+
return RouteResult(False, refusal="Blocked: this private agent only supports safe read-only/no-login/no-payment tasks.")
|
| 54 |
+
|
| 55 |
+
# Wikipedia search: "search wikipedia for X", "wikipedia X"
|
| 56 |
+
m = re.search(r"(?:search\s+)?wikipedia\s+(?:for\s+)?(.+)$", msg)
|
| 57 |
+
if m:
|
| 58 |
+
query = _clean_query(m.group(1))
|
| 59 |
+
if not query:
|
| 60 |
+
return RouteResult(False, refusal="Missing Wikipedia search query.")
|
| 61 |
+
return RouteResult(True, {
|
| 62 |
+
"family": "wikipedia_search",
|
| 63 |
+
"title": f"Wikipedia search: {query}",
|
| 64 |
+
"url": "https://www.wikipedia.org/",
|
| 65 |
+
"goal": f"Search Wikipedia for {query}.",
|
| 66 |
+
"values": {"searchInput": query},
|
| 67 |
+
"default_type_text": query,
|
| 68 |
+
"success_url_contains": ["wikipedia.org/wiki/"],
|
| 69 |
+
"safe_target_contains": {"type": ["search"], "click": ["search"], "press": ["search"]},
|
| 70 |
+
"max_steps": 5,
|
| 71 |
+
})
|
| 72 |
+
|
| 73 |
+
# Python docs search: "search python docs for X"
|
| 74 |
+
m = re.search(r"(?:search\s+)?(?:python\s+docs|python\s+documentation|docs\.python)\s+(?:for\s+)?(.+)$", msg)
|
| 75 |
+
if m:
|
| 76 |
+
query = _clean_query(m.group(1))
|
| 77 |
+
if not query:
|
| 78 |
+
return RouteResult(False, refusal="Missing Python docs search query.")
|
| 79 |
+
return RouteResult(True, {
|
| 80 |
+
"family": "python_docs_search",
|
| 81 |
+
"title": f"Python docs search: {query}",
|
| 82 |
+
"url": "https://docs.python.org/3/",
|
| 83 |
+
"goal": f"Search Python documentation for {query}.",
|
| 84 |
+
"values": {},
|
| 85 |
+
"default_type_text": query,
|
| 86 |
+
"success_url_contains": ["docs.python.org/3/search.html", query.lower().replace(" ", "+").replace("_", "_")],
|
| 87 |
+
"safe_target_contains": {"type": ["search", "quick search"], "click": ["go", "search"], "press": ["search", "quick search"]},
|
| 88 |
+
"max_steps": 5,
|
| 89 |
+
})
|
| 90 |
+
|
| 91 |
+
# Hacker News nav.
|
| 92 |
+
hn_targets = {
|
| 93 |
+
"new": ("new stories", "https://news.ycombinator.com/", ["news.ycombinator.com/newest"], ["new"]),
|
| 94 |
+
"newest": ("new stories", "https://news.ycombinator.com/", ["news.ycombinator.com/newest"], ["new"]),
|
| 95 |
+
"jobs": ("jobs", "https://news.ycombinator.com/", ["news.ycombinator.com/jobs"], ["jobs"]),
|
| 96 |
+
"ask": ("ask", "https://news.ycombinator.com/", ["news.ycombinator.com/ask"], ["ask"]),
|
| 97 |
+
"show": ("show", "https://news.ycombinator.com/", ["news.ycombinator.com/show"], ["show"]),
|
| 98 |
+
"comments": ("comments", "https://news.ycombinator.com/", ["news.ycombinator.com/newcomments"], ["comments"]),
|
| 99 |
+
}
|
| 100 |
+
if "hacker news" in msg or "hn " in msg or msg.startswith("hn"):
|
| 101 |
+
for key, (label, url, success, safe_words) in hn_targets.items():
|
| 102 |
+
if re.search(rf"\b{re.escape(key)}\b", msg):
|
| 103 |
+
return RouteResult(True, {
|
| 104 |
+
"family": "hn_navigation",
|
| 105 |
+
"title": f"Hacker News navigation: {label}",
|
| 106 |
+
"url": url,
|
| 107 |
+
"goal": f"Open the {label} navigation link.",
|
| 108 |
+
"values": {},
|
| 109 |
+
"default_type_text": "",
|
| 110 |
+
"success_url_contains": success,
|
| 111 |
+
"safe_target_contains": {"click": safe_words, "wait": [""]},
|
| 112 |
+
"max_steps": 4,
|
| 113 |
+
})
|
| 114 |
+
|
| 115 |
+
# Example.com
|
| 116 |
+
if "example.com" in msg and ("learn more" in msg or "more information" in msg or "main link" in msg):
|
| 117 |
+
return RouteResult(True, {
|
| 118 |
+
"family": "example_learn_more",
|
| 119 |
+
"title": "Example.com Learn more",
|
| 120 |
+
"url": "https://example.com/",
|
| 121 |
+
"goal": "Open the main Learn more link.",
|
| 122 |
+
"values": {},
|
| 123 |
+
"default_type_text": "",
|
| 124 |
+
"success_url_contains": ["iana.org/help/example-domains"],
|
| 125 |
+
"safe_target_contains": {"click": ["learn more"], "wait": [""]},
|
| 126 |
+
"max_steps": 4,
|
| 127 |
+
})
|
| 128 |
+
|
| 129 |
+
return RouteResult(False, refusal=(
|
| 130 |
+
"Unsupported safe task. Try: 'Search Wikipedia for Alan Turing', "
|
| 131 |
+
"'Search Python docs for asyncio', 'Open Hacker News jobs', or 'Open example.com learn more'."
|
| 132 |
+
))
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def best_value(task: Dict[str, Any], eid: Optional[str]) -> str:
|
| 136 |
+
if eid and eid in task.get("values", {}):
|
| 137 |
+
return task["values"][eid]
|
| 138 |
+
return task.get("default_type_text", "")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def apply_action(page, action: Dict[str, Any], elements, task: Dict[str, Any], last_text_target: Optional[str]):
|
| 142 |
+
act = action.get("action")
|
| 143 |
+
eid = action.get("target_element_id")
|
| 144 |
+
|
| 145 |
+
if act == "done":
|
| 146 |
+
return {"ok": True, "event": "done", "last_text_target": last_text_target}
|
| 147 |
+
if act == "wait":
|
| 148 |
+
page.wait_for_timeout(500)
|
| 149 |
+
return {"ok": True, "event": "wait", "last_text_target": last_text_target}
|
| 150 |
+
if act == "press":
|
| 151 |
+
if not last_text_target:
|
| 152 |
+
return {"ok": False, "error": "press_without_last_text_target"}
|
| 153 |
+
e = find_element(elements, last_text_target)
|
| 154 |
+
loc = resolve_real_web_locator_v2(page, e) if e else None
|
| 155 |
+
if loc is None:
|
| 156 |
+
return {"ok": False, "error": "press_target_not_resolved", "target": last_text_target}
|
| 157 |
+
loc.press("Enter", timeout=4000)
|
| 158 |
+
try:
|
| 159 |
+
page.wait_for_load_state("domcontentloaded", timeout=7000)
|
| 160 |
+
except Exception:
|
| 161 |
+
pass
|
| 162 |
+
return {"ok": True, "event": "press_enter", "target": last_text_target, "last_text_target": last_text_target}
|
| 163 |
+
|
| 164 |
+
if not eid:
|
| 165 |
+
return {"ok": False, "error": "missing_target"}
|
| 166 |
+
|
| 167 |
+
e = find_element(elements, eid)
|
| 168 |
+
if not e:
|
| 169 |
+
return {"ok": False, "error": "target_not_in_snapshot", "target": eid}
|
| 170 |
+
|
| 171 |
+
loc = resolve_real_web_locator_v2(page, e)
|
| 172 |
+
if loc is None or loc.count() < 1:
|
| 173 |
+
return {"ok": False, "error": "target_not_resolved", "target": eid}
|
| 174 |
+
|
| 175 |
+
if act == "type":
|
| 176 |
+
text = best_value(task, eid)
|
| 177 |
+
if not text:
|
| 178 |
+
return {"ok": False, "error": "missing_type_text", "target": eid}
|
| 179 |
+
loc.fill(text, timeout=4000)
|
| 180 |
+
return {"ok": True, "event": "type", "target": eid, "text": text, "last_text_target": eid}
|
| 181 |
+
|
| 182 |
+
if act == "select":
|
| 183 |
+
value = best_value(task, eid)
|
| 184 |
+
if value:
|
| 185 |
+
loc.select_option(label=value, timeout=4000)
|
| 186 |
+
return {"ok": True, "event": "select", "target": eid, "value": value, "last_text_target": last_text_target}
|
| 187 |
+
|
| 188 |
+
if act == "click":
|
| 189 |
+
before = page.url
|
| 190 |
+
loc.click(timeout=6000)
|
| 191 |
+
try:
|
| 192 |
+
page.wait_for_load_state("domcontentloaded", timeout=7000)
|
| 193 |
+
except Exception:
|
| 194 |
+
pass
|
| 195 |
+
return {"ok": True, "event": "click", "target": eid, "url_before": before, "url_after": page.url, "last_text_target": last_text_target}
|
| 196 |
+
|
| 197 |
+
return {"ok": False, "error": "unsupported_action", "action": act}
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def search_fallback(page, task: Dict[str, Any]):
|
| 201 |
+
elems = extract_real_web_elements_v2(page, include_hidden=False)
|
| 202 |
+
candidates = []
|
| 203 |
+
for e in elems:
|
| 204 |
+
txt = " ".join(str(e.get(k) or "") for k in ["element_id", "role", "name", "text"]).lower()
|
| 205 |
+
if e.get("role") == "textbox" and ("search" in txt or "quick search" in txt or str(e.get("name", "")).lower() == "q"):
|
| 206 |
+
candidates.append(e)
|
| 207 |
+
if not candidates:
|
| 208 |
+
return False, {"event": "fallback_no_search_box"}
|
| 209 |
+
target = candidates[0]
|
| 210 |
+
loc = resolve_real_web_locator_v2(page, target)
|
| 211 |
+
text = best_value(task, target.get("element_id"))
|
| 212 |
+
if loc is None or not text:
|
| 213 |
+
return False, {"event": "fallback_missing_locator_or_text"}
|
| 214 |
+
before = page.url
|
| 215 |
+
loc.fill(text, timeout=4000)
|
| 216 |
+
loc.press("Enter", timeout=4000)
|
| 217 |
+
try:
|
| 218 |
+
page.wait_for_load_state("domcontentloaded", timeout=7000)
|
| 219 |
+
except Exception:
|
| 220 |
+
pass
|
| 221 |
+
page.wait_for_timeout(900)
|
| 222 |
+
ok = strict_url_success(page.url, task["success_url_contains"])
|
| 223 |
+
return ok, {
|
| 224 |
+
"event": "search_fallback_fill_enter",
|
| 225 |
+
"target": target.get("element_id"),
|
| 226 |
+
"text": text,
|
| 227 |
+
"url_before": before,
|
| 228 |
+
"url_after": page.url,
|
| 229 |
+
"success": ok,
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def run_chat_message(message: str, *, headed: bool = False) -> Dict[str, Any]:
|
| 234 |
+
route = route_message_to_task(message)
|
| 235 |
+
if not route.ok:
|
| 236 |
+
result = {
|
| 237 |
+
"ok": False,
|
| 238 |
+
"accepted": False,
|
| 239 |
+
"message": message,
|
| 240 |
+
"refusal": route.refusal,
|
| 241 |
+
"hard_rules": {
|
| 242 |
+
"no_login": True,
|
| 243 |
+
"no_payment": True,
|
| 244 |
+
"no_sensitive_data": True,
|
| 245 |
+
"no_training": True,
|
| 246 |
+
"no_gpu": True,
|
| 247 |
+
},
|
| 248 |
+
}
|
| 249 |
+
return result
|
| 250 |
+
|
| 251 |
+
task = route.task
|
| 252 |
+
model = UIActionModel()
|
| 253 |
+
trace = []
|
| 254 |
+
unsafe_attempts = 0
|
| 255 |
+
executor_errors = 0
|
| 256 |
+
|
| 257 |
+
with sync_playwright() as p:
|
| 258 |
+
browser = p.chromium.launch(headless=not headed, slow_mo=500 if headed else 0)
|
| 259 |
+
page = browser.new_page(viewport={"width": 1200, "height": 820})
|
| 260 |
+
page.goto(task["url"], wait_until="domcontentloaded", timeout=30000)
|
| 261 |
+
page.wait_for_timeout(1000)
|
| 262 |
+
|
| 263 |
+
history = []
|
| 264 |
+
completed = set()
|
| 265 |
+
last_text_target = None
|
| 266 |
+
|
| 267 |
+
for step in range(task["max_steps"]):
|
| 268 |
+
if strict_url_success(page.url, task["success_url_contains"]):
|
| 269 |
+
trace.append({"step": step, "result": "SUCCESS_BEFORE_ACTION", "url": page.url})
|
| 270 |
+
break
|
| 271 |
+
|
| 272 |
+
elements = extract_real_web_elements_v2(page, include_hidden=False)
|
| 273 |
+
for e in elements:
|
| 274 |
+
if e["element_id"] in completed:
|
| 275 |
+
e["enabled"] = False
|
| 276 |
+
|
| 277 |
+
raw_obj = model.predict_action(task["goal"], elements, history, step)
|
| 278 |
+
raw = raw_obj.to_dict()
|
| 279 |
+
raw["raw"] = raw_obj.raw
|
| 280 |
+
|
| 281 |
+
rerank = rerank_navigation_action(goal=task["goal"], elements=elements, model_action=raw)
|
| 282 |
+
action = rerank["action"]
|
| 283 |
+
safe = check_safe_action(action=action, elements=elements, safe_target_contains=task["safe_target_contains"])
|
| 284 |
+
|
| 285 |
+
row = {
|
| 286 |
+
"step": step,
|
| 287 |
+
"url_before": page.url,
|
| 288 |
+
"raw_action": {"action": raw.get("action"), "target": raw.get("target_element_id"), "confidence": raw.get("confidence")},
|
| 289 |
+
"final_action": {"action": action.get("action"), "target": action.get("target_element_id")},
|
| 290 |
+
"reranker_changed": bool(rerank.get("changed")),
|
| 291 |
+
"safe": safe.safe,
|
| 292 |
+
"safe_reason": safe.reason,
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
if not safe.safe:
|
| 296 |
+
unsafe_attempts += 1
|
| 297 |
+
row["blocked"] = True
|
| 298 |
+
trace.append(row)
|
| 299 |
+
break
|
| 300 |
+
|
| 301 |
+
try:
|
| 302 |
+
result = apply_action(page, action, elements, task, last_text_target)
|
| 303 |
+
except PlaywrightTimeoutError as exc:
|
| 304 |
+
executor_errors += 1
|
| 305 |
+
result = {"ok": False, "error": "playwright_timeout", "message": str(exc).splitlines()[0]}
|
| 306 |
+
except Exception as exc:
|
| 307 |
+
executor_errors += 1
|
| 308 |
+
result = {"ok": False, "error": type(exc).__name__, "message": str(exc)[:300]}
|
| 309 |
+
|
| 310 |
+
row["executor_result"] = result
|
| 311 |
+
if "last_text_target" in result:
|
| 312 |
+
last_text_target = result["last_text_target"]
|
| 313 |
+
|
| 314 |
+
if result.get("ok") and action.get("target_element_id"):
|
| 315 |
+
completed.add(action["target_element_id"])
|
| 316 |
+
history.append({
|
| 317 |
+
"action": action["action"],
|
| 318 |
+
"target_element_id": action.get("target_element_id"),
|
| 319 |
+
"text": best_value(task, action.get("target_element_id")),
|
| 320 |
+
"value": best_value(task, action.get("target_element_id")),
|
| 321 |
+
})
|
| 322 |
+
|
| 323 |
+
page.wait_for_timeout(800)
|
| 324 |
+
row["url_after"] = page.url
|
| 325 |
+
row["success_after"] = strict_url_success(page.url, task["success_url_contains"])
|
| 326 |
+
trace.append(row)
|
| 327 |
+
|
| 328 |
+
if not result.get("ok"):
|
| 329 |
+
break
|
| 330 |
+
if row["success_after"]:
|
| 331 |
+
break
|
| 332 |
+
|
| 333 |
+
if action.get("action") in {"type", "done"} and "search" in task.get("family", ""):
|
| 334 |
+
ok, fb = search_fallback(page, task)
|
| 335 |
+
trace.append({"step": step + 0.5, "fallback": fb})
|
| 336 |
+
if ok:
|
| 337 |
+
break
|
| 338 |
+
if action.get("action") == "done":
|
| 339 |
+
break
|
| 340 |
+
|
| 341 |
+
if action.get("action") == "done":
|
| 342 |
+
break
|
| 343 |
+
|
| 344 |
+
success = strict_url_success(page.url, task["success_url_contains"])
|
| 345 |
+
work_outcome = None
|
| 346 |
+
if success:
|
| 347 |
+
work_outcome = build_work_outcome_v1(page=page, task=task, message=message, root=ROOT)
|
| 348 |
+
final = {
|
| 349 |
+
"ok": True,
|
| 350 |
+
"accepted": True,
|
| 351 |
+
"message": message,
|
| 352 |
+
"task_title": task["title"],
|
| 353 |
+
"goal": task["goal"],
|
| 354 |
+
"success": success,
|
| 355 |
+
"final_url": page.url,
|
| 356 |
+
"final_title": page.title(),
|
| 357 |
+
"unsafe_attempts": unsafe_attempts,
|
| 358 |
+
"executor_errors": executor_errors,
|
| 359 |
+
"work_outcome": work_outcome,
|
| 360 |
+
"trace": trace,
|
| 361 |
+
"runtime_stack": {
|
| 362 |
+
"checkpoint": "V4",
|
| 363 |
+
"adapter": "real_web_v2",
|
| 364 |
+
"safe_executor": "safe_executor_v2",
|
| 365 |
+
"navigation_reranker": "navigation_reranker_v1_repaired_explicit_target_groups",
|
| 366 |
+
},
|
| 367 |
+
"hard_rules": {
|
| 368 |
+
"no_login": True,
|
| 369 |
+
"no_payment": True,
|
| 370 |
+
"no_sensitive_data": True,
|
| 371 |
+
"no_training": True,
|
| 372 |
+
"no_gpu": True,
|
| 373 |
+
},
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
browser.close()
|
| 377 |
+
|
| 378 |
+
with RUN_LOG.open("a", encoding="utf-8") as f:
|
| 379 |
+
f.write(json.dumps({"created_unix": int(time.time()), **final}, ensure_ascii=False) + "\n")
|
| 380 |
+
|
| 381 |
+
return final
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def main():
|
| 385 |
+
ap = argparse.ArgumentParser(description="LEO-UI7M chat-style private agent interface v1")
|
| 386 |
+
ap.add_argument("message", nargs="+")
|
| 387 |
+
ap.add_argument("--headed", action="store_true", help="headed browser; only use on local desktop/XServer")
|
| 388 |
+
args = ap.parse_args()
|
| 389 |
+
msg = " ".join(args.message)
|
| 390 |
+
result = run_chat_message(msg, headed=args.headed)
|
| 391 |
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
if __name__ == "__main__":
|
| 395 |
+
main()
|
leo_ui7m/api.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
from .runtime.model import LoadedUIActionPolicy
|
| 8 |
+
from .schema import UIAction, UIElement
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
DEFAULT_CHECKPOINT = Path(os.environ.get("LEO_UI7M_CHECKPOINT", Path(__file__).resolve().parents[1] / "checkpoints" / "leo_ui7m_v4_best.pt"))
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class UIActionModel:
|
| 16 |
+
"""LEO-UI7M local inference API."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, checkpoint_path: str | Path | None = None, device: str = "cpu") -> None:
|
| 19 |
+
self.checkpoint_path = Path(checkpoint_path) if checkpoint_path else DEFAULT_CHECKPOINT
|
| 20 |
+
self.device = device
|
| 21 |
+
self.policy = LoadedUIActionPolicy(self.checkpoint_path, device=device)
|
| 22 |
+
|
| 23 |
+
def predict_action(
|
| 24 |
+
self,
|
| 25 |
+
goal: str,
|
| 26 |
+
elements: list[UIElement | dict[str, Any]],
|
| 27 |
+
history: list[dict[str, Any]] | None = None,
|
| 28 |
+
step_index: int = 0,
|
| 29 |
+
) -> UIAction:
|
| 30 |
+
normalized = [
|
| 31 |
+
e if isinstance(e, dict) else e.__dict__
|
| 32 |
+
for e in elements
|
| 33 |
+
]
|
| 34 |
+
raw = self.policy.predict_raw(
|
| 35 |
+
goal=goal,
|
| 36 |
+
elements=normalized,
|
| 37 |
+
history=history or [],
|
| 38 |
+
step_index=step_index,
|
| 39 |
+
)
|
| 40 |
+
return UIAction(
|
| 41 |
+
action=raw["action"],
|
| 42 |
+
target_element_id=raw.get("target_element_id"),
|
| 43 |
+
confidence=raw.get("confidence"),
|
| 44 |
+
raw=raw,
|
| 45 |
+
)
|
leo_ui7m/cli.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from leo_ui7m import UIActionModel
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def main() -> int:
|
| 12 |
+
parser = argparse.ArgumentParser(description="LEO-UI7M local UI-action prediction CLI")
|
| 13 |
+
parser.add_argument("--goal", required=True, help="Task goal")
|
| 14 |
+
parser.add_argument("--elements", required=True, help="Path to JSON file containing a list of UI elements")
|
| 15 |
+
parser.add_argument("--history", default=None, help="Optional path to JSON file containing action history")
|
| 16 |
+
parser.add_argument("--checkpoint", default=None, help="Optional checkpoint path")
|
| 17 |
+
parser.add_argument("--step-index", type=int, default=0)
|
| 18 |
+
parser.add_argument("--pretty", action="store_true")
|
| 19 |
+
args = parser.parse_args()
|
| 20 |
+
|
| 21 |
+
elements = json.loads(Path(args.elements).read_text(encoding="utf-8"))
|
| 22 |
+
history = json.loads(Path(args.history).read_text(encoding="utf-8")) if args.history else []
|
| 23 |
+
|
| 24 |
+
model = UIActionModel(checkpoint_path=args.checkpoint)
|
| 25 |
+
action = model.predict_action(
|
| 26 |
+
goal=args.goal,
|
| 27 |
+
elements=elements,
|
| 28 |
+
history=history,
|
| 29 |
+
step_index=args.step_index,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
out = action.to_dict()
|
| 33 |
+
print(json.dumps(out, indent=2 if args.pretty else None, ensure_ascii=False))
|
| 34 |
+
return 0
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
if __name__ == "__main__":
|
| 38 |
+
raise SystemExit(main())
|
leo_ui7m/config.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"project": "LEO-UI7M",
|
| 3 |
+
"default_model": "V4",
|
| 4 |
+
"default_checkpoint": "/root/leo-ui7m/reports/leo_ui7m_action_v4_elem_repair_probe_account_A_output/leo_ui7m_action_v4_elem_repair_probe_best.pt",
|
| 5 |
+
"public_product_ready": false,
|
| 6 |
+
"gpu_required": false
|
| 7 |
+
}
|
leo_ui7m/executors/__init__.py
ADDED
|
File without changes
|
leo_ui7m/executors/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (176 Bytes). View file
|
|
|
leo_ui7m/executors/__pycache__/playwright_executor.cpython-312.pyc
ADDED
|
Binary file (3.22 kB). View file
|
|
|
leo_ui7m/executors/playwright_executor.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from leo_ui7m.schema import UIAction
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class PlaywrightExecutor:
|
| 10 |
+
"""Execute LEO-UI7M UIAction objects on a Playwright sync Page."""
|
| 11 |
+
|
| 12 |
+
def __init__(self, page) -> None:
|
| 13 |
+
self.page = page
|
| 14 |
+
self.history: list[dict[str, Any]] = []
|
| 15 |
+
self.completed_ids: set[str] = set()
|
| 16 |
+
|
| 17 |
+
def apply(self, action: UIAction, values: dict[str, str] | None = None) -> dict[str, Any]:
|
| 18 |
+
values = values or {}
|
| 19 |
+
data = action.to_dict()
|
| 20 |
+
act = data["action"]
|
| 21 |
+
eid = data.get("target_element_id")
|
| 22 |
+
|
| 23 |
+
if act == "done":
|
| 24 |
+
event = {"type": "done", "action": "done", "target_element_id": None}
|
| 25 |
+
self.history.append(event)
|
| 26 |
+
return {"ok": True, "event": event}
|
| 27 |
+
|
| 28 |
+
if act == "wait":
|
| 29 |
+
self.page.wait_for_timeout(100)
|
| 30 |
+
event = {"type": "wait", "action": "wait", "target_element_id": None}
|
| 31 |
+
self.history.append(event)
|
| 32 |
+
return {"ok": True, "event": event}
|
| 33 |
+
|
| 34 |
+
if not eid:
|
| 35 |
+
return {"ok": False, "error": "missing_target_element_id", "action": data}
|
| 36 |
+
|
| 37 |
+
loc = self.page.locator(f"#{eid}")
|
| 38 |
+
if loc.count() < 1:
|
| 39 |
+
return {"ok": False, "error": "target_not_found", "target_element_id": eid, "action": data}
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
if act == "type":
|
| 43 |
+
text = data.get("text") or values.get(eid, "")
|
| 44 |
+
loc.fill(text)
|
| 45 |
+
event = {"type": "type", "action": "type", "target_element_id": eid, "text": text}
|
| 46 |
+
elif act == "select":
|
| 47 |
+
value = data.get("value") or values.get(eid, "")
|
| 48 |
+
loc.select_option(label=value)
|
| 49 |
+
event = {"type": "select", "action": "select", "target_element_id": eid, "value": value}
|
| 50 |
+
elif act == "click":
|
| 51 |
+
if loc.get_attribute("type") == "checkbox":
|
| 52 |
+
loc.check()
|
| 53 |
+
else:
|
| 54 |
+
loc.click()
|
| 55 |
+
event = {"type": "click", "action": "click", "target_element_id": eid}
|
| 56 |
+
else:
|
| 57 |
+
return {"ok": False, "error": "unsupported_action", "action": data}
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return {"ok": False, "error": type(e).__name__, "message": str(e), "action": data}
|
| 60 |
+
|
| 61 |
+
self.completed_ids.add(eid)
|
| 62 |
+
self.history.append(event)
|
| 63 |
+
return {"ok": True, "event": event}
|
leo_ui7m/outcomes/__pycache__/work_outcome_v1.cpython-312.pyc
ADDED
|
Binary file (14.6 kB). View file
|
|
|
leo_ui7m/outcomes/work_outcome_v1.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import re
|
| 5 |
+
import time
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any, Dict, List
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _slug(s: str) -> str:
|
| 11 |
+
s = (s or "").lower()
|
| 12 |
+
s = re.sub(r"[^a-z0-9]+", "_", s).strip("_")
|
| 13 |
+
return s[:80] or "outcome"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _clean(s: str) -> str:
|
| 17 |
+
return re.sub(r"\s+", " ", (s or "")).strip()
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _page_snapshot(page) -> Dict[str, Any]:
|
| 21 |
+
return page.evaluate("""
|
| 22 |
+
() => {
|
| 23 |
+
const clean = s => (s || '').replace(/\\s+/g, ' ').trim();
|
| 24 |
+
const paragraphs = Array.from(document.querySelectorAll('p'))
|
| 25 |
+
.map(p => clean(p.innerText))
|
| 26 |
+
.filter(t => t.length > 40)
|
| 27 |
+
.slice(0, 20);
|
| 28 |
+
const links = Array.from(document.querySelectorAll('a'))
|
| 29 |
+
.map(a => ({
|
| 30 |
+
text: clean(a.innerText || a.textContent || ''),
|
| 31 |
+
href: a.href || '',
|
| 32 |
+
title: clean(a.getAttribute('title') || '')
|
| 33 |
+
}))
|
| 34 |
+
.filter(x => x.text.length > 0 && x.href.length > 0)
|
| 35 |
+
.slice(0, 200);
|
| 36 |
+
const headings = Array.from(document.querySelectorAll('h1,h2,h3'))
|
| 37 |
+
.map(h => clean(h.innerText))
|
| 38 |
+
.filter(Boolean)
|
| 39 |
+
.slice(0, 20);
|
| 40 |
+
return {
|
| 41 |
+
url: location.href,
|
| 42 |
+
title: document.title || '',
|
| 43 |
+
h1: clean((document.querySelector('h1') || {}).innerText || ''),
|
| 44 |
+
headings,
|
| 45 |
+
paragraphs,
|
| 46 |
+
links,
|
| 47 |
+
body_text: clean(document.body ? document.body.innerText : '').slice(0, 8000)
|
| 48 |
+
};
|
| 49 |
+
}
|
| 50 |
+
""")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def _dedupe_items(items: List[Dict[str, Any]], limit: int = 10) -> List[Dict[str, Any]]:
|
| 54 |
+
seen = set()
|
| 55 |
+
out = []
|
| 56 |
+
for it in items:
|
| 57 |
+
key = (it.get("title") or it.get("text") or "", it.get("url") or it.get("href") or "")
|
| 58 |
+
if key in seen:
|
| 59 |
+
continue
|
| 60 |
+
seen.add(key)
|
| 61 |
+
out.append(it)
|
| 62 |
+
if len(out) >= limit:
|
| 63 |
+
break
|
| 64 |
+
return out
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _write_artifact(root: Path, slug: str, title: str, content: str) -> str:
|
| 68 |
+
out_dir = root / "reports" / "outcomes"
|
| 69 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 70 |
+
path = out_dir / f"{slug}.md"
|
| 71 |
+
path.write_text(content, encoding="utf-8")
|
| 72 |
+
return str(path)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def build_work_outcome_v1(*, page, task: Dict[str, Any], message: str, root: Path) -> Dict[str, Any]:
|
| 76 |
+
"""Convert a completed browser task into a useful user-facing outcome.
|
| 77 |
+
|
| 78 |
+
This layer intentionally does not train, browse extra pages, log in, or perform risky actions.
|
| 79 |
+
It only extracts from the page already reached by the safe browser action loop.
|
| 80 |
+
"""
|
| 81 |
+
snap = _page_snapshot(page)
|
| 82 |
+
family = task.get("family") or ""
|
| 83 |
+
title = _clean(task.get("title") or snap.get("title") or message)
|
| 84 |
+
url = snap.get("url") or ""
|
| 85 |
+
paragraphs = [_clean(x) for x in snap.get("paragraphs", []) if _clean(x)]
|
| 86 |
+
links = snap.get("links", [])
|
| 87 |
+
|
| 88 |
+
outcome: Dict[str, Any] = {
|
| 89 |
+
"version": "work_outcome_v1",
|
| 90 |
+
"created_unix": int(time.time()),
|
| 91 |
+
"message": message,
|
| 92 |
+
"task_family": family,
|
| 93 |
+
"page_title": snap.get("title"),
|
| 94 |
+
"final_url": url,
|
| 95 |
+
"useful_output": False,
|
| 96 |
+
"outcome_type": None,
|
| 97 |
+
"answer": None,
|
| 98 |
+
"structured_items": [],
|
| 99 |
+
"sources": [url] if url else [],
|
| 100 |
+
"artifact_path": None,
|
| 101 |
+
"notes": [],
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
if family == "wikipedia_search":
|
| 105 |
+
answer_parts = paragraphs[:2]
|
| 106 |
+
answer = "\\n\\n".join(answer_parts)
|
| 107 |
+
key_facts = []
|
| 108 |
+
for p in paragraphs[:4]:
|
| 109 |
+
if len(p) > 80:
|
| 110 |
+
key_facts.append(p[:280])
|
| 111 |
+
if len(key_facts) >= 4:
|
| 112 |
+
break
|
| 113 |
+
|
| 114 |
+
outcome.update({
|
| 115 |
+
"useful_output": bool(answer),
|
| 116 |
+
"outcome_type": "research_note",
|
| 117 |
+
"answer": answer,
|
| 118 |
+
"structured_items": [{"type": "key_fact", "text": x} for x in key_facts],
|
| 119 |
+
})
|
| 120 |
+
|
| 121 |
+
md = "# " + title + "\\n\\n"
|
| 122 |
+
md += "Source: " + url + "\\n\\n"
|
| 123 |
+
md += "## Summary\\n\\n" + (answer or "No summary extracted.") + "\\n\\n"
|
| 124 |
+
if key_facts:
|
| 125 |
+
md += "## Key facts\\n\\n" + "\\n".join("- " + x for x in key_facts) + "\\n"
|
| 126 |
+
outcome["artifact_path"] = _write_artifact(root, _slug("wikipedia_" + title), title, md)
|
| 127 |
+
|
| 128 |
+
elif family == "python_docs_search":
|
| 129 |
+
candidates = []
|
| 130 |
+
for a in links:
|
| 131 |
+
text = _clean(a.get("text"))
|
| 132 |
+
href = a.get("href") or ""
|
| 133 |
+
if not text or len(text) < 2:
|
| 134 |
+
continue
|
| 135 |
+
if "docs.python.org/3/" not in href:
|
| 136 |
+
continue
|
| 137 |
+
if any(skip in text.lower() for skip in ["previous topic", "next topic", "navigation", "index"]):
|
| 138 |
+
continue
|
| 139 |
+
candidates.append({"title": text[:160], "url": href})
|
| 140 |
+
|
| 141 |
+
items = _dedupe_items(candidates, limit=8)
|
| 142 |
+
answer = "Found " + str(len(items)) + " Python documentation result(s)." if items else "No Python documentation result was extracted."
|
| 143 |
+
|
| 144 |
+
outcome.update({
|
| 145 |
+
"useful_output": bool(items),
|
| 146 |
+
"outcome_type": "docs_search_results",
|
| 147 |
+
"answer": answer,
|
| 148 |
+
"structured_items": items,
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
md = "# " + title + "\\n\\n"
|
| 152 |
+
md += "Source: " + url + "\\n\\n"
|
| 153 |
+
md += "## Results\\n\\n"
|
| 154 |
+
md += "\\n".join("- " + it["title"] + " — " + it["url"] for it in items) if items else "No results extracted."
|
| 155 |
+
md += "\\n"
|
| 156 |
+
outcome["artifact_path"] = _write_artifact(root, _slug("python_docs_" + title), title, md)
|
| 157 |
+
|
| 158 |
+
elif family == "hn_navigation":
|
| 159 |
+
nav_words = {"new", "past", "comments", "ask", "show", "jobs", "submit", "login", "news", "guidelines", "faq", "lists"}
|
| 160 |
+
items = []
|
| 161 |
+
for a in links:
|
| 162 |
+
text = _clean(a.get("text"))
|
| 163 |
+
href = a.get("href") or ""
|
| 164 |
+
low = text.lower()
|
| 165 |
+
if not text or low in nav_words:
|
| 166 |
+
continue
|
| 167 |
+
if low in {"hacker news", "more", "hide", "discuss"}:
|
| 168 |
+
continue
|
| 169 |
+
if len(text) < 8:
|
| 170 |
+
continue
|
| 171 |
+
if "news.ycombinator.com" not in href and not href.startswith("http"):
|
| 172 |
+
continue
|
| 173 |
+
items.append({"title": text[:180], "url": href})
|
| 174 |
+
# For HN jobs, keep only job-like items; remove nav/domain/date/comment metadata.
|
| 175 |
+
if "jobs" in family or "jobs" in title.lower() or "jobs" in url.lower():
|
| 176 |
+
job_words = [
|
| 177 |
+
"hiring", "engineer", "developer", "designer", "design", "gtm",
|
| 178 |
+
"sales", "founder", "software", "backend", "frontend", "full-stack",
|
| 179 |
+
"full stack", "ai", "ml", "data", "platform", "infrastructure",
|
| 180 |
+
"security", "product", "remote", "onsite", "on-site", "yc"
|
| 181 |
+
]
|
| 182 |
+
bad_exact = {"hacker news", "ycombinator.com/jobs", "news", "jobs", "comments", "ask", "show"}
|
| 183 |
+
bad_patterns = ["from?site=", "item?id=", "news.ycombinator.com/item", " day ago", " hours ago", " minutes ago"]
|
| 184 |
+
|
| 185 |
+
filtered = []
|
| 186 |
+
for it in items:
|
| 187 |
+
t = (it.get("title") or "").strip()
|
| 188 |
+
u = (it.get("url") or "").strip()
|
| 189 |
+
low = t.lower()
|
| 190 |
+
ulow = u.lower()
|
| 191 |
+
|
| 192 |
+
if not t or low in bad_exact:
|
| 193 |
+
continue
|
| 194 |
+
if any(x in low for x in [" day ago", " hours ago", " minutes ago"]):
|
| 195 |
+
continue
|
| 196 |
+
if any(x in ulow for x in bad_patterns):
|
| 197 |
+
continue
|
| 198 |
+
|
| 199 |
+
jobish_title = any(w in low for w in job_words)
|
| 200 |
+
jobish_url = ("ycombinator.com/companies/" in ulow and "/jobs/" in ulow)
|
| 201 |
+
|
| 202 |
+
if jobish_title or jobish_url:
|
| 203 |
+
filtered.append(it)
|
| 204 |
+
|
| 205 |
+
items = filtered
|
| 206 |
+
|
| 207 |
+
items = _dedupe_items(items, limit=15)
|
| 208 |
+
|
| 209 |
+
# HN jobs can expose useful job text without clean external links.
|
| 210 |
+
# If link extraction is empty, fall back to visible body lines.
|
| 211 |
+
if not items:
|
| 212 |
+
body = snap.get("body_text") or ""
|
| 213 |
+
raw_lines = []
|
| 214 |
+
noise_exact = {
|
| 215 |
+
"hacker news", "new", "past", "comments", "ask", "show", "jobs", "submit",
|
| 216 |
+
"login", "news", "guidelines", "faq", "lists", "api", "security", "legal",
|
| 217 |
+
"apply to yc", "contact"
|
| 218 |
+
}
|
| 219 |
+
noise_contains = [
|
| 220 |
+
"applications are open", "y combinator", "hacker news guidelines",
|
| 221 |
+
"more", "hide", "discuss", "past", "submit", "login"
|
| 222 |
+
]
|
| 223 |
+
|
| 224 |
+
for line in body.split("\n"):
|
| 225 |
+
line = _clean(line)
|
| 226 |
+
if not line:
|
| 227 |
+
continue
|
| 228 |
+
low = line.lower().strip()
|
| 229 |
+
if low in noise_exact:
|
| 230 |
+
continue
|
| 231 |
+
if any(x in low for x in noise_contains):
|
| 232 |
+
continue
|
| 233 |
+
if len(line) < 28:
|
| 234 |
+
continue
|
| 235 |
+
|
| 236 |
+
# Prefer lines that look like job/work/company postings.
|
| 237 |
+
jobish = any(x in low for x in [
|
| 238 |
+
"hiring", "engineer", "developer", "remote", "onsite", "founder",
|
| 239 |
+
"startup", "full-time", "full time", "software", "ai", "ml",
|
| 240 |
+
"data", "backend", "frontend", "infrastructure", "product",
|
| 241 |
+
"design", "security", "platform"
|
| 242 |
+
])
|
| 243 |
+
if not jobish and "jobs" in family:
|
| 244 |
+
continue
|
| 245 |
+
|
| 246 |
+
raw_lines.append(line)
|
| 247 |
+
|
| 248 |
+
fallback_items = []
|
| 249 |
+
for line in raw_lines[:15]:
|
| 250 |
+
fallback_items.append({
|
| 251 |
+
"title": line[:220],
|
| 252 |
+
"url": url,
|
| 253 |
+
"source": "visible_page_text"
|
| 254 |
+
})
|
| 255 |
+
items = _dedupe_items(fallback_items, limit=10)
|
| 256 |
+
|
| 257 |
+
answer = "Extracted " + str(len(items)) + " visible Hacker News item(s)." if items else "No Hacker News items extracted."
|
| 258 |
+
|
| 259 |
+
outcome.update({
|
| 260 |
+
"useful_output": bool(items),
|
| 261 |
+
"outcome_type": "link_digest",
|
| 262 |
+
"answer": answer,
|
| 263 |
+
"structured_items": items,
|
| 264 |
+
})
|
| 265 |
+
|
| 266 |
+
md = "# " + title + "\\n\\n"
|
| 267 |
+
md += "Source: " + url + "\\n\\n"
|
| 268 |
+
md += "## Items\\n\\n"
|
| 269 |
+
md += "\\n".join("- " + it["title"] + " — " + it["url"] for it in items) if items else "No items extracted."
|
| 270 |
+
md += "\\n"
|
| 271 |
+
outcome["artifact_path"] = _write_artifact(root, _slug("hn_" + title), title, md)
|
| 272 |
+
|
| 273 |
+
elif family == "example_learn_more":
|
| 274 |
+
answer = "\\n\\n".join(paragraphs[:3])
|
| 275 |
+
if not answer:
|
| 276 |
+
answer = snap.get("body_text", "")[:700]
|
| 277 |
+
|
| 278 |
+
outcome.update({
|
| 279 |
+
"useful_output": bool(answer),
|
| 280 |
+
"outcome_type": "page_explanation",
|
| 281 |
+
"answer": answer,
|
| 282 |
+
"structured_items": [{"title": _clean(a.get("text")), "url": a.get("href")} for a in links[:5]],
|
| 283 |
+
})
|
| 284 |
+
|
| 285 |
+
md = "# " + title + "\\n\\n"
|
| 286 |
+
md += "Source: " + url + "\\n\\n"
|
| 287 |
+
md += "## Explanation\\n\\n" + (answer or "No explanation extracted.") + "\\n"
|
| 288 |
+
outcome["artifact_path"] = _write_artifact(root, _slug("example_" + title), title, md)
|
| 289 |
+
|
| 290 |
+
else:
|
| 291 |
+
answer = "\\n\\n".join(paragraphs[:2]) or snap.get("body_text", "")[:1000]
|
| 292 |
+
outcome.update({
|
| 293 |
+
"useful_output": bool(answer),
|
| 294 |
+
"outcome_type": "generic_page_summary",
|
| 295 |
+
"answer": answer,
|
| 296 |
+
"structured_items": [{"title": _clean(a.get("text")), "url": a.get("href")} for a in links[:10]],
|
| 297 |
+
})
|
| 298 |
+
md = "# " + title + "\\n\\nSource: " + url + "\\n\\n" + (answer or "No output extracted.") + "\\n"
|
| 299 |
+
outcome["artifact_path"] = _write_artifact(root, _slug("generic_" + title), title, md)
|
| 300 |
+
|
| 301 |
+
if outcome["useful_output"] and not outcome.get("answer") and not outcome.get("structured_items"):
|
| 302 |
+
outcome["useful_output"] = False
|
| 303 |
+
outcome["notes"].append("Marked not useful because answer and structured_items are empty.")
|
| 304 |
+
|
| 305 |
+
return outcome
|
leo_ui7m/runtime/__init__.py
ADDED
|
File without changes
|
leo_ui7m/runtime/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (174 Bytes). View file
|
|
|
leo_ui7m/runtime/__pycache__/model.cpython-312.pyc
ADDED
|
Binary file (13.1 kB). View file
|
|
|
leo_ui7m/runtime/__pycache__/navigation_reranker_v1.cpython-312.pyc
ADDED
|
Binary file (6.8 kB). View file
|
|
|
leo_ui7m/runtime/__pycache__/safe_executor_v2.cpython-312.pyc
ADDED
|
Binary file (4.07 kB). View file
|
|
|
leo_ui7m/runtime/model.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
MAX_CTX = 96
|
| 12 |
+
MAX_ELEMS = 72
|
| 13 |
+
MAX_ETXT = 28
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _tok(text: str) -> list[str]:
|
| 17 |
+
text = (text or "").lower()
|
| 18 |
+
for c in "\n\t\r.,:;!?()[]{}<>/\\|\"'`~@#$%^&*+=_-":
|
| 19 |
+
text = text.replace(c, " ")
|
| 20 |
+
return [x for x in text.split() if x]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _enc(text: str, n: int, vocab: dict[str, int]) -> list[int]:
|
| 24 |
+
ids = [vocab.get(t, 1) for t in _tok(text)[:n]]
|
| 25 |
+
return ids + [0] * (n - len(ids))
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _state_for(element_id: str, history: list[dict[str, Any]], states: dict[str, int]) -> int:
|
| 29 |
+
clicked = typed = selected = False
|
| 30 |
+
for h in history:
|
| 31 |
+
if h.get("target_element_id") == element_id:
|
| 32 |
+
clicked = clicked or h.get("action", h.get("type")) == "click"
|
| 33 |
+
typed = typed or h.get("action", h.get("type")) == "type"
|
| 34 |
+
selected = selected or h.get("action", h.get("type")) == "select"
|
| 35 |
+
key = "_".join(
|
| 36 |
+
name for name, flag in [
|
| 37 |
+
("clicked", clicked),
|
| 38 |
+
("typed", typed),
|
| 39 |
+
("selected", selected),
|
| 40 |
+
] if flag
|
| 41 |
+
) or "none"
|
| 42 |
+
return states.get(key, 0)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class _TorchPolicy(nn.Module):
|
| 46 |
+
def __init__(self, vocab_size: int, role_size: int, state_size: int, d: int, layers: int, heads: int, action_size: int) -> None:
|
| 47 |
+
super().__init__()
|
| 48 |
+
self.emb = nn.Embedding(vocab_size, d, padding_idx=0)
|
| 49 |
+
self.role = nn.Embedding(role_size, d)
|
| 50 |
+
self.state = nn.Embedding(state_size, d)
|
| 51 |
+
self.eproj = nn.Linear(d * 3, d)
|
| 52 |
+
layer = nn.TransformerEncoderLayer(d, heads, d * 4, 0.1, batch_first=True, activation="gelu")
|
| 53 |
+
self.tr = nn.TransformerEncoder(layer, layers)
|
| 54 |
+
self.action = nn.Linear(d, action_size)
|
| 55 |
+
self.elem = nn.Linear(d, 1)
|
| 56 |
+
|
| 57 |
+
def _mean(self, ids: torch.Tensor) -> torch.Tensor:
|
| 58 |
+
x = self.emb(ids)
|
| 59 |
+
m = (ids != 0).float().unsqueeze(-1)
|
| 60 |
+
return (x * m).sum(1) / m.sum(1).clamp_min(1)
|
| 61 |
+
|
| 62 |
+
def forward(self, ctx: torch.Tensor, et: torch.Tensor, er: torch.Tensor, es: torch.Tensor, em: torch.Tensor):
|
| 63 |
+
b, e, t = et.shape
|
| 64 |
+
cv = self._mean(ctx)
|
| 65 |
+
ev = self._mean(et.reshape(b * e, t)).reshape(b, e, -1)
|
| 66 |
+
ev = self.eproj(torch.cat([ev, self.role(er), self.state(es)], -1))
|
| 67 |
+
seq = torch.cat([cv[:, None, :], ev], 1)
|
| 68 |
+
pad = torch.cat([torch.zeros(b, 1, dtype=torch.bool, device=em.device), ~em], 1)
|
| 69 |
+
z = self.tr(seq, src_key_padding_mask=pad)
|
| 70 |
+
return self.action(z[:, 0]), self.elem(z[:, 1:]).squeeze(-1).masked_fill(~em, -1e9)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class LoadedUIActionPolicy:
|
| 74 |
+
def __init__(self, checkpoint_path: str | Path, device: str = "cpu") -> None:
|
| 75 |
+
self.checkpoint_path = Path(checkpoint_path)
|
| 76 |
+
self.device = torch.device(device)
|
| 77 |
+
self.ckpt = torch.load(self.checkpoint_path, map_location=self.device, weights_only=False)
|
| 78 |
+
self.vocab = self.ckpt["vocab"]
|
| 79 |
+
self.roles = self.ckpt["roles"]
|
| 80 |
+
self.states = self.ckpt["states"]
|
| 81 |
+
self.actions = self.ckpt["actions"]
|
| 82 |
+
cfg = self.ckpt["config"]
|
| 83 |
+
|
| 84 |
+
self.model = _TorchPolicy(
|
| 85 |
+
vocab_size=len(self.vocab),
|
| 86 |
+
role_size=len(self.roles),
|
| 87 |
+
state_size=len(self.states),
|
| 88 |
+
d=cfg["d"],
|
| 89 |
+
layers=cfg["layers"],
|
| 90 |
+
heads=cfg["heads"],
|
| 91 |
+
action_size=len(self.actions),
|
| 92 |
+
).to(self.device)
|
| 93 |
+
self.model.load_state_dict(self.ckpt["state_dict"])
|
| 94 |
+
self.model.eval()
|
| 95 |
+
|
| 96 |
+
def predict_raw(
|
| 97 |
+
self,
|
| 98 |
+
goal: str,
|
| 99 |
+
elements: list[dict[str, Any]],
|
| 100 |
+
history: list[dict[str, Any]] | None = None,
|
| 101 |
+
step_index: int = 0,
|
| 102 |
+
) -> dict[str, Any]:
|
| 103 |
+
history = history or []
|
| 104 |
+
elems = elements[:MAX_ELEMS]
|
| 105 |
+
|
| 106 |
+
ctx_text = " ".join(
|
| 107 |
+
[f"step {step_index}", goal]
|
| 108 |
+
+ [
|
| 109 |
+
f"{h.get('action', h.get('type', ''))} {h.get('target_element_id', '')} {h.get('text', h.get('value', ''))}"
|
| 110 |
+
for h in history
|
| 111 |
+
]
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
et, er, es, em = [], [], [], []
|
| 115 |
+
for i in range(MAX_ELEMS):
|
| 116 |
+
if i < len(elems):
|
| 117 |
+
e = elems[i]
|
| 118 |
+
et.append(_enc(" ".join([
|
| 119 |
+
str(e.get("role", "")),
|
| 120 |
+
str(e.get("name", "")),
|
| 121 |
+
str(e.get("text", "")),
|
| 122 |
+
str(e.get("value", "")),
|
| 123 |
+
str(e.get("section", "")),
|
| 124 |
+
]), MAX_ETXT, self.vocab))
|
| 125 |
+
er.append(self.roles.get(str(e.get("role", "")), 0))
|
| 126 |
+
es.append(_state_for(str(e.get("element_id", "")), history, self.states))
|
| 127 |
+
em.append(bool(e.get("visible", True) and e.get("enabled", True)))
|
| 128 |
+
else:
|
| 129 |
+
et.append([0] * MAX_ETXT)
|
| 130 |
+
er.append(0)
|
| 131 |
+
es.append(0)
|
| 132 |
+
em.append(False)
|
| 133 |
+
|
| 134 |
+
with torch.no_grad():
|
| 135 |
+
al, el = self.model(
|
| 136 |
+
torch.tensor([_enc(ctx_text, MAX_CTX, self.vocab)], device=self.device),
|
| 137 |
+
torch.tensor([et], device=self.device),
|
| 138 |
+
torch.tensor([er], device=self.device),
|
| 139 |
+
torch.tensor([es], device=self.device),
|
| 140 |
+
torch.tensor([em], device=self.device).bool(),
|
| 141 |
+
)
|
| 142 |
+
action_probs = torch.softmax(al, dim=-1)[0].detach().cpu()
|
| 143 |
+
elem_probs = torch.softmax(el, dim=-1)[0][:len(elems)].detach().cpu()
|
| 144 |
+
|
| 145 |
+
action_order = [self.actions[i] for i in torch.argsort(action_probs, descending=True).tolist()]
|
| 146 |
+
elem_order = torch.argsort(elem_probs, descending=True).tolist()
|
| 147 |
+
|
| 148 |
+
compatible = {
|
| 149 |
+
"select": {"combobox"},
|
| 150 |
+
"type": {"textbox"},
|
| 151 |
+
"click": {"button", "link", "checkbox", "row", "tab", "menuitem"},
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
selected_action = "done"
|
| 155 |
+
selected_idx = None
|
| 156 |
+
for action in action_order:
|
| 157 |
+
if action in {"done", "wait", "press"}:
|
| 158 |
+
selected_action = action
|
| 159 |
+
selected_idx = None
|
| 160 |
+
break
|
| 161 |
+
for idx in elem_order:
|
| 162 |
+
if idx < len(elems):
|
| 163 |
+
e = elems[idx]
|
| 164 |
+
if (
|
| 165 |
+
e.get("visible", True)
|
| 166 |
+
and e.get("enabled", True)
|
| 167 |
+
and str(e.get("role", "")) in compatible.get(action, set())
|
| 168 |
+
):
|
| 169 |
+
selected_action = action
|
| 170 |
+
selected_idx = idx
|
| 171 |
+
break
|
| 172 |
+
if selected_idx is not None:
|
| 173 |
+
break
|
| 174 |
+
|
| 175 |
+
target_id = elems[selected_idx]["element_id"] if selected_idx is not None else None
|
| 176 |
+
confidence = float(action_probs[self.actions.index(selected_action)])
|
| 177 |
+
if selected_idx is not None:
|
| 178 |
+
confidence *= float(elem_probs[selected_idx])
|
| 179 |
+
|
| 180 |
+
return {
|
| 181 |
+
"action": selected_action,
|
| 182 |
+
"target_element_id": target_id,
|
| 183 |
+
"confidence": confidence,
|
| 184 |
+
"top_actions": [
|
| 185 |
+
{"action": self.actions[i], "prob": float(action_probs[i])}
|
| 186 |
+
for i in torch.argsort(action_probs, descending=True).tolist()[:6]
|
| 187 |
+
],
|
| 188 |
+
"top_elements": [
|
| 189 |
+
{
|
| 190 |
+
"element_id": elems[i]["element_id"],
|
| 191 |
+
"role": elems[i].get("role", ""),
|
| 192 |
+
"name": elems[i].get("name", ""),
|
| 193 |
+
"text": elems[i].get("text", ""),
|
| 194 |
+
"prob": float(elem_probs[i]),
|
| 195 |
+
}
|
| 196 |
+
for i in elem_order[: min(8, len(elems))]
|
| 197 |
+
],
|
| 198 |
+
}
|
leo_ui7m/runtime/navigation_reranker_v1.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
from typing import Any, Dict, Iterable
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
TARGET_ORDER = [
|
| 8 |
+
("comments", {"comments", "comment", "newcomments"}),
|
| 9 |
+
("jobs", {"jobs", "job"}),
|
| 10 |
+
("ask", {"ask"}),
|
| 11 |
+
("show", {"show"}),
|
| 12 |
+
("new", {"new", "newest", "latest", "recent"}),
|
| 13 |
+
("past", {"past"}),
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _tokens(s: str) -> set[str]:
|
| 18 |
+
return set(re.findall(r"[a-z0-9]+", (s or "").lower()))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _label(e: Dict[str, Any]) -> str:
|
| 22 |
+
return " ".join(str(e.get(k) or "") for k in ["name", "text", "element_id", "href"]).strip().lower()
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _text(e: Dict[str, Any]) -> str:
|
| 26 |
+
return str(e.get("text") or e.get("name") or "").strip().lower()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _is_nav_intent(goal: str) -> bool:
|
| 30 |
+
g = goal.lower()
|
| 31 |
+
return any(x in g for x in ["navigation", "nav", "open the", "go to", "stories", "new stories", "latest", "comments", "jobs", "ask", "show"])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def infer_target_group(goal: str) -> str | None:
|
| 35 |
+
g = goal.lower()
|
| 36 |
+
toks = _tokens(g)
|
| 37 |
+
for group, words in TARGET_ORDER:
|
| 38 |
+
if toks & words:
|
| 39 |
+
return group
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def score_navigation_candidate(goal: str, e: Dict[str, Any]) -> float:
|
| 44 |
+
if e.get("role") != "link":
|
| 45 |
+
return -100.0
|
| 46 |
+
|
| 47 |
+
target_group = infer_target_group(goal)
|
| 48 |
+
label = _label(e)
|
| 49 |
+
label_tokens = _tokens(label)
|
| 50 |
+
text = _text(e)
|
| 51 |
+
href = str(e.get("href") or "").lower()
|
| 52 |
+
word_count = len(_tokens(text))
|
| 53 |
+
|
| 54 |
+
score = 0.0
|
| 55 |
+
|
| 56 |
+
if target_group:
|
| 57 |
+
group_words = dict(TARGET_ORDER)[target_group]
|
| 58 |
+
|
| 59 |
+
# Strong exact target priority.
|
| 60 |
+
if text in group_words:
|
| 61 |
+
score += 50.0
|
| 62 |
+
if label_tokens & group_words:
|
| 63 |
+
score += 25.0
|
| 64 |
+
if any(w in href for w in group_words):
|
| 65 |
+
score += 20.0
|
| 66 |
+
|
| 67 |
+
# Penalize other nav labels when a different target is requested.
|
| 68 |
+
for other_group, other_words in TARGET_ORDER:
|
| 69 |
+
if other_group != target_group and (text in other_words or label_tokens & other_words):
|
| 70 |
+
score -= 25.0
|
| 71 |
+
|
| 72 |
+
# Generic concise-nav preference, but not enough to override explicit target.
|
| 73 |
+
if text in {"new", "newest", "ask", "show", "jobs", "comments", "past"}:
|
| 74 |
+
score += 8.0
|
| 75 |
+
if word_count <= 2:
|
| 76 |
+
score += 5.0
|
| 77 |
+
elif word_count >= 6:
|
| 78 |
+
score -= 10.0
|
| 79 |
+
|
| 80 |
+
# Penalize story/article-looking links.
|
| 81 |
+
if word_count >= 5:
|
| 82 |
+
score -= 12.0
|
| 83 |
+
|
| 84 |
+
# Site/nav URL hints.
|
| 85 |
+
if "news.ycombinator.com" in href:
|
| 86 |
+
score += 3.0
|
| 87 |
+
|
| 88 |
+
return score
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def rerank_navigation_action(
|
| 92 |
+
*,
|
| 93 |
+
goal: str,
|
| 94 |
+
elements: Iterable[Dict[str, Any]],
|
| 95 |
+
model_action: Dict[str, Any],
|
| 96 |
+
min_score: float = 20.0,
|
| 97 |
+
) -> Dict[str, Any]:
|
| 98 |
+
if not _is_nav_intent(goal):
|
| 99 |
+
return {"changed": False, "reason": "not_navigation_intent", "action": model_action}
|
| 100 |
+
|
| 101 |
+
candidates = []
|
| 102 |
+
for e in elements:
|
| 103 |
+
if not e.get("visible", True) or not e.get("enabled", True):
|
| 104 |
+
continue
|
| 105 |
+
candidates.append((score_navigation_candidate(goal, e), e))
|
| 106 |
+
|
| 107 |
+
candidates.sort(key=lambda x: x[0], reverse=True)
|
| 108 |
+
best_score, best = candidates[0] if candidates else (-999.0, None)
|
| 109 |
+
|
| 110 |
+
if best is not None and best_score >= min_score:
|
| 111 |
+
new_action = dict(model_action)
|
| 112 |
+
new_action["action"] = "click"
|
| 113 |
+
new_action["target_element_id"] = best.get("element_id")
|
| 114 |
+
new_action["reranked_by"] = "navigation_reranker_v1_repaired"
|
| 115 |
+
new_action["reranker_score"] = best_score
|
| 116 |
+
return {
|
| 117 |
+
"changed": True,
|
| 118 |
+
"reason": "navigation_candidate_above_threshold",
|
| 119 |
+
"score": best_score,
|
| 120 |
+
"target_group": infer_target_group(goal),
|
| 121 |
+
"selected": {
|
| 122 |
+
"element_id": best.get("element_id"),
|
| 123 |
+
"role": best.get("role"),
|
| 124 |
+
"name": best.get("name"),
|
| 125 |
+
"text": best.get("text"),
|
| 126 |
+
"href": best.get("href"),
|
| 127 |
+
},
|
| 128 |
+
"top_candidates": [
|
| 129 |
+
{"score": s, "element_id": e.get("element_id"), "name": e.get("name"), "text": e.get("text"), "href": e.get("href")}
|
| 130 |
+
for s, e in candidates[:8]
|
| 131 |
+
],
|
| 132 |
+
"action": new_action,
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
return {
|
| 136 |
+
"changed": False,
|
| 137 |
+
"reason": "no_navigation_candidate_above_threshold",
|
| 138 |
+
"score": best_score,
|
| 139 |
+
"target_group": infer_target_group(goal),
|
| 140 |
+
"top_candidates": [
|
| 141 |
+
{"score": s, "element_id": e.get("element_id"), "name": e.get("name"), "text": e.get("text"), "href": e.get("href")}
|
| 142 |
+
for s, e in candidates[:8]
|
| 143 |
+
],
|
| 144 |
+
"action": model_action,
|
| 145 |
+
}
|
leo_ui7m/runtime/safe_executor_v2.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Any, Dict, Iterable, Optional, Tuple
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@dataclass
|
| 8 |
+
class SafeActionCheck:
|
| 9 |
+
safe: bool
|
| 10 |
+
reason: Any
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def element_text(element: Optional[Dict[str, Any]]) -> str:
|
| 14 |
+
if not element:
|
| 15 |
+
return ""
|
| 16 |
+
fields = [
|
| 17 |
+
element.get("element_id"),
|
| 18 |
+
element.get("stable_uid"),
|
| 19 |
+
element.get("role"),
|
| 20 |
+
element.get("name"),
|
| 21 |
+
element.get("text"),
|
| 22 |
+
element.get("href"),
|
| 23 |
+
element.get("tag"),
|
| 24 |
+
element.get("type"),
|
| 25 |
+
]
|
| 26 |
+
return " ".join(str(x or "") for x in fields).lower()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def find_element(elements: Iterable[Dict[str, Any]], element_id: Optional[str]) -> Optional[Dict[str, Any]]:
|
| 30 |
+
if not element_id:
|
| 31 |
+
return None
|
| 32 |
+
for e in elements:
|
| 33 |
+
if e.get("element_id") == element_id or e.get("stable_uid") == element_id:
|
| 34 |
+
return e
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def check_safe_action(
|
| 39 |
+
*,
|
| 40 |
+
action: Dict[str, Any],
|
| 41 |
+
elements: Iterable[Dict[str, Any]],
|
| 42 |
+
safe_target_contains: Dict[str, list[str]],
|
| 43 |
+
allow_done: bool = True,
|
| 44 |
+
allow_wait: bool = True,
|
| 45 |
+
) -> SafeActionCheck:
|
| 46 |
+
act = action.get("action")
|
| 47 |
+
target = action.get("target_element_id")
|
| 48 |
+
|
| 49 |
+
if act == "done":
|
| 50 |
+
return SafeActionCheck(bool(allow_done), "done_allowed" if allow_done else "done_blocked")
|
| 51 |
+
if act == "wait":
|
| 52 |
+
return SafeActionCheck(bool(allow_wait), "wait_allowed" if allow_wait else "wait_blocked")
|
| 53 |
+
|
| 54 |
+
rules = safe_target_contains.get(act)
|
| 55 |
+
if not rules:
|
| 56 |
+
return SafeActionCheck(False, {"reason": "no_safe_rule_for_action", "action": act})
|
| 57 |
+
|
| 58 |
+
e = find_element(elements, target)
|
| 59 |
+
txt = element_text(e)
|
| 60 |
+
|
| 61 |
+
if any(str(rule).lower() in txt for rule in rules):
|
| 62 |
+
return SafeActionCheck(True, {"reason": "target_matches_safe_rule", "target_text": txt})
|
| 63 |
+
|
| 64 |
+
return SafeActionCheck(False, {
|
| 65 |
+
"reason": "target_mismatch",
|
| 66 |
+
"action": act,
|
| 67 |
+
"target": target,
|
| 68 |
+
"target_text": txt,
|
| 69 |
+
"required_any": rules,
|
| 70 |
+
})
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def strict_url_success(url: str, required_substrings: list[str]) -> bool:
|
| 74 |
+
u = (url or "").lower()
|
| 75 |
+
return any(x.lower() in u for x in required_substrings)
|
leo_ui7m/schema.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Any, Literal
|
| 5 |
+
|
| 6 |
+
ActionType = Literal["click", "type", "select", "press", "wait", "done"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class UIElement:
|
| 11 |
+
element_id: str
|
| 12 |
+
role: str
|
| 13 |
+
text: str = ""
|
| 14 |
+
name: str = ""
|
| 15 |
+
value: str = ""
|
| 16 |
+
enabled: bool = True
|
| 17 |
+
visible: bool = True
|
| 18 |
+
section: str = "main"
|
| 19 |
+
bbox: list[float] = field(default_factory=list)
|
| 20 |
+
depth: int = 1
|
| 21 |
+
|
| 22 |
+
@classmethod
|
| 23 |
+
def from_dict(cls, data: dict[str, Any]) -> "UIElement":
|
| 24 |
+
return cls(
|
| 25 |
+
element_id=str(data.get("element_id", data.get("id", ""))),
|
| 26 |
+
role=str(data.get("role", "")),
|
| 27 |
+
text=str(data.get("text", "")),
|
| 28 |
+
name=str(data.get("name", "")),
|
| 29 |
+
value=str(data.get("value", "")),
|
| 30 |
+
enabled=bool(data.get("enabled", True)),
|
| 31 |
+
visible=bool(data.get("visible", True)),
|
| 32 |
+
section=str(data.get("section", "main")),
|
| 33 |
+
bbox=list(data.get("bbox", [])),
|
| 34 |
+
depth=int(data.get("depth", 1)),
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class UIAction:
|
| 40 |
+
action: ActionType
|
| 41 |
+
target_element_id: str | None = None
|
| 42 |
+
text: str | None = None
|
| 43 |
+
value: str | None = None
|
| 44 |
+
confidence: float | None = None
|
| 45 |
+
raw: dict[str, Any] = field(default_factory=dict)
|
| 46 |
+
|
| 47 |
+
def to_dict(self) -> dict[str, Any]:
|
| 48 |
+
return {
|
| 49 |
+
"action": self.action,
|
| 50 |
+
"target_element_id": self.target_element_id,
|
| 51 |
+
"text": self.text,
|
| 52 |
+
"value": self.value,
|
| 53 |
+
"confidence": self.confidence,
|
| 54 |
+
"raw": self.raw,
|
| 55 |
+
}
|
leo-ui7m-v0.1.0-private.tar.gz → model.safetensors
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4d6b6a084ed720266b6c34ecf38dd060efbf7a0681f4284c53394bcd1c8d7968
|
| 3 |
+
size 29840268
|
leo-ui7m-v0.1.0-private.tar.gz.sha256 → pytorch_model.bin
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dbbb598fffba5f19558264165c3c3636c76f1a530c12c80c7528d5addd6c870a
|
| 3 |
+
size 30166357
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
playwright
|
| 3 |
+
safetensors
|
tests/__pycache__/test_private_package_v0_1.cpython-312-pytest-9.0.3.pyc
ADDED
|
Binary file (15.1 kB). View file
|
|
|
tests/test_private_package_v0_1.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json, subprocess
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 5 |
+
|
| 6 |
+
def run_msg(msg):
|
| 7 |
+
p = subprocess.run([str(ROOT / 'bin/leo-ui7m-chat'), msg], cwd=str(ROOT), text=True, capture_output=True, timeout=120)
|
| 8 |
+
assert p.returncode == 0, p.stderr
|
| 9 |
+
return json.loads(p.stdout)
|
| 10 |
+
|
| 11 |
+
def assert_useful(r, expected_type):
|
| 12 |
+
assert r['accepted'] is True
|
| 13 |
+
assert r['success'] is True
|
| 14 |
+
assert r['unsafe_attempts'] == 0
|
| 15 |
+
assert r['executor_errors'] == 0
|
| 16 |
+
wo = r.get('work_outcome') or {}
|
| 17 |
+
assert wo.get('useful_output') is True
|
| 18 |
+
assert wo.get('outcome_type') == expected_type
|
| 19 |
+
assert wo.get('answer') or wo.get('structured_items')
|
| 20 |
+
assert wo.get('artifact_path')
|
| 21 |
+
assert Path(wo.get('artifact_path')).exists()
|
| 22 |
+
|
| 23 |
+
def test_refusal():
|
| 24 |
+
r = run_msg('login to my bank')
|
| 25 |
+
assert r['accepted'] is False
|
| 26 |
+
assert 'Blocked:' in r['refusal']
|
| 27 |
+
|
| 28 |
+
def test_wikipedia_outcome():
|
| 29 |
+
r = run_msg('Search Wikipedia for Alan Turing')
|
| 30 |
+
assert_useful(r, 'research_note')
|
| 31 |
+
|
| 32 |
+
def test_python_docs_outcome():
|
| 33 |
+
r = run_msg('Search Python docs for asyncio')
|
| 34 |
+
assert_useful(r, 'docs_search_results')
|
| 35 |
+
|
| 36 |
+
def test_hn_jobs_outcome_quality():
|
| 37 |
+
r = run_msg('Open Hacker News jobs')
|
| 38 |
+
assert_useful(r, 'link_digest')
|
| 39 |
+
items = (r.get('work_outcome') or {}).get('structured_items') or []
|
| 40 |
+
assert len(items) >= 3
|
| 41 |
+
top = ' '.join((x.get('title') or '').lower() for x in items[:5])
|
| 42 |
+
assert ('hiring' in top) or ('engineer' in top) or ('job' in top)
|
| 43 |
+
assert 'hacker news' not in (items[0].get('title') or '').lower()
|
| 44 |
+
|
| 45 |
+
def test_example_outcome():
|
| 46 |
+
r = run_msg('Open example.com learn more')
|
| 47 |
+
assert_useful(r, 'page_explanation')
|