qox commited on
Commit
99f00fe
·
verified ·
1 Parent(s): fee76e4

Convert to standard model repository layout

Browse files
Files changed (42) hide show
  1. .gitattributes +2 -2
  2. MODEL_REPO_MANIFEST.json +50 -0
  3. README.md +18 -14
  4. config.json +7 -6
  5. evidence/leo_ui7m_final_clean_release_v1.json +0 -7
  6. examples/run_agent.py +3 -0
  7. leo_ui7m/__init__.py +6 -0
  8. leo_ui7m/__pycache__/__init__.cpython-312.pyc +0 -0
  9. leo_ui7m/__pycache__/api.cpython-312.pyc +0 -0
  10. leo_ui7m/__pycache__/cli.cpython-312.pyc +0 -0
  11. leo_ui7m/__pycache__/schema.cpython-312.pyc +0 -0
  12. leo_ui7m/adapters/__init__.py +0 -0
  13. leo_ui7m/adapters/__pycache__/__init__.cpython-312.pyc +0 -0
  14. leo_ui7m/adapters/__pycache__/playwright_dom.cpython-312.pyc +0 -0
  15. leo_ui7m/adapters/__pycache__/real_web_v2.cpython-312.pyc +0 -0
  16. leo_ui7m/adapters/playwright_dom.py +41 -0
  17. leo_ui7m/adapters/real_web_v2.py +192 -0
  18. leo_ui7m/agent/__pycache__/chat_agent_v1.cpython-312.pyc +0 -0
  19. leo_ui7m/agent/chat_agent_v1.py +395 -0
  20. leo_ui7m/api.py +45 -0
  21. leo_ui7m/cli.py +38 -0
  22. leo_ui7m/config.json +7 -0
  23. leo_ui7m/executors/__init__.py +0 -0
  24. leo_ui7m/executors/__pycache__/__init__.cpython-312.pyc +0 -0
  25. leo_ui7m/executors/__pycache__/playwright_executor.cpython-312.pyc +0 -0
  26. leo_ui7m/executors/playwright_executor.py +63 -0
  27. leo_ui7m/outcomes/__pycache__/work_outcome_v1.cpython-312.pyc +0 -0
  28. leo_ui7m/outcomes/work_outcome_v1.py +305 -0
  29. leo_ui7m/runtime/__init__.py +0 -0
  30. leo_ui7m/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  31. leo_ui7m/runtime/__pycache__/model.cpython-312.pyc +0 -0
  32. leo_ui7m/runtime/__pycache__/navigation_reranker_v1.cpython-312.pyc +0 -0
  33. leo_ui7m/runtime/__pycache__/safe_executor_v2.cpython-312.pyc +0 -0
  34. leo_ui7m/runtime/model.py +198 -0
  35. leo_ui7m/runtime/navigation_reranker_v1.py +145 -0
  36. leo_ui7m/runtime/safe_executor_v2.py +75 -0
  37. leo_ui7m/schema.py +55 -0
  38. leo-ui7m-v0.1.0-private.tar.gz → model.safetensors +2 -2
  39. leo-ui7m-v0.1.0-private.tar.gz.sha256 → pytorch_model.bin +2 -2
  40. requirements.txt +3 -0
  41. tests/__pycache__/test_private_package_v0_1.cpython-312-pytest-9.0.3.pyc +0 -0
  42. tests/test_private_package_v0_1.py +47 -0
.gitattributes CHANGED
@@ -1,2 +1,2 @@
1
- *.tar.gz filter=lfs diff=lfs merge=lfs -text
2
- leo-ui7m-v0.1.0-private.tar.gz.sha256 filter=lfs diff=lfs merge=lfs -text
 
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
- LEO-UI7M v0.1-private is a private bounded useful browser-agent release.
17
 
18
- Correct parameter count: **7,458,567**.
19
 
20
- ## Artifact
21
 
22
- - File: `leo-ui7m-v0.1.0-private.tar.gz`
23
- - SHA256: `4cb33cb984cbfe273c27e0d7636e7be6d2d478c3bf86fe9a0b6d6ec9408f8648`
24
-
25
- ## Validated capabilities
 
 
 
 
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
- - Forbidden login/payment/sensitive command to refusal
32
-
33
- ## Non-claims
34
 
35
- - Not a general Fara replacement
36
- - Not screenshot-based visual computer-use
37
- - Not arbitrary-web automation
38
- - Not public-product-ready
 
 
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
- "release_type": "private_useful_browser_agent",
6
- "artifact": "leo-ui7m-v0.1.0-private.tar.gz",
7
- "sha256": "4cb33cb984cbfe273c27e0d7636e7be6d2d478c3bf86fe9a0b6d6ec9408f8648",
 
8
  "python_package": "leo_ui7m",
9
- "cli": "bin/leo-ui7m-chat",
10
- "checkpoint": "checkpoints/leo_ui7m_v4_best.pt",
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:4cb33cb984cbfe273c27e0d7636e7be6d2d478c3bf86fe9a0b6d6ec9408f8648
3
- size 27834754
 
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:d1a8bbfe11c982f9c2135b9dc25b7934e4c7bc56d51310581eb78dab47a1fc5f
3
- size 97
 
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')