localagentbrowser 1.63__tar.gz → 1.64__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {localagentbrowser-1.63 → localagentbrowser-1.64}/CHANGELOG.md +57 -1
- {localagentbrowser-1.63/localagentbrowser.egg-info → localagentbrowser-1.64}/PKG-INFO +19 -11
- {localagentbrowser-1.63 → localagentbrowser-1.64}/README.md +18 -10
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/agent.py +9 -3
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/checks.py +17 -2
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/config.py +4 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/coverage.py +106 -7
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/grounding.py +36 -4
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/pages.py +92 -1
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/prompts.py +17 -5
- localagentbrowser-1.64/localagentbrowser/report.py +835 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/text.py +9 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64/localagentbrowser.egg-info}/PKG-INFO +19 -11
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser.egg-info/SOURCES.txt +3 -1
- {localagentbrowser-1.63 → localagentbrowser-1.64}/pyproject.toml +1 -1
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/golden/prompts.json +26 -16
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/test_checklist.py +429 -3
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/test_decisions.py +26 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/test_golden_prompts.py +1 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/test_main_loop.py +5 -3
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/test_model_turns.py +2 -2
- localagentbrowser-1.64/tests/test_page_text.py +165 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/test_regression.py +22 -0
- localagentbrowser-1.64/tests/test_show_report.py +30 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tools/regression.py +58 -14
- localagentbrowser-1.64/tools/windows/show_report.py +155 -0
- localagentbrowser-1.63/localagentbrowser/report.py +0 -316
- localagentbrowser-1.63/tests/test_page_text.py +0 -89
- {localagentbrowser-1.63 → localagentbrowser-1.64}/LICENSE +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/MANIFEST.in +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/agent_browser.py +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/__init__.py +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser/checklist.py +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser.egg-info/dependency_links.txt +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser.egg-info/entry_points.txt +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser.egg-info/requires.txt +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/localagentbrowser.egg-info/top_level.txt +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/setup.cfg +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/conftest.py +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/fakes.py +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/test_duplicates.py +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tests/test_journal.py +0 -0
- {localagentbrowser-1.63 → localagentbrowser-1.64}/tools/live_test.py +0 -0
|
@@ -1,11 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
Every version of LocalAgentBrowser, newest first. Each released version is also a tagged commit (`v1.11` … `v1.
|
|
3
|
+
Every version of LocalAgentBrowser, newest first. Each released version is also a tagged commit (`v1.11` … `v1.63`); 1.54 to 1.56 and 1.58 to 1.60 were internal steps
|
|
4
4
|
measured on the regression suite and released as part of 1.57 and 1.61 (see
|
|
5
5
|
[docs/regression-history.md](docs/regression-history.md)).
|
|
6
6
|
Timings are from live runs on the development machine (Intel i5-9500, 6 cores, no GPU, Qwen2.5-7B-Instruct
|
|
7
7
|
Q4_K_M in LM Studio), where reading the prompt takes about 90% of every model call.
|
|
8
8
|
|
|
9
|
+
## [1.64] - 2026-11-08
|
|
10
|
+
The theme is **silent omission** ([issue #11](https://github.com/SnowFXXX/LocalAgentBrowser/issues/11)): a part of
|
|
11
|
+
the objective dropping out of the final answer without being marked "not found". It was measured on two PCs (PC A
|
|
12
|
+
and PC B, both Intel i5-9500, no GPU) and then on four models with identical code.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **Windows launcher and shortcuts.** `tools/windows/LocalAgentBrowser.cmd` checks that LM Studio is answering,
|
|
16
|
+
runs the agent in its own folder under `Documents\LocalAgentBrowser runs`, and opens the report when the run ends.
|
|
17
|
+
`tools/windows/make_shortcuts.ps1` adds Start-menu and desktop shortcuts with the project icon (from a clone of
|
|
18
|
+
the repository).
|
|
19
|
+
- **The report opens by itself** when a launcher run ends: `research_report.md` is written next to it as a dark web
|
|
20
|
+
page, in the project site's colors, with the final result, checklist and discoveries in separate cards and
|
|
21
|
+
citations shown as tags (`tools/windows/show_report.py`, standard library only). Windows has no program for `.md`
|
|
22
|
+
files by default.
|
|
23
|
+
- Regression suite: every run folder gets `run_info.json` (PC label from `LAB_PC_LABEL`, commit, and a fingerprint
|
|
24
|
+
of the uncommitted changes), so runs on several PCs can be tied to the exact code they ran.
|
|
25
|
+
- Regression suite: `LAB_BLOCKED_SITES` makes NAVIGATE refuse given sites, for benchmark runs that must not read the
|
|
26
|
+
benchmark's own answer pages.
|
|
27
|
+
|
|
28
|
+
### Fixed: parts of the objective dropping out of the answer
|
|
29
|
+
- **The coverage check asks for the quote first.** "Does this discovery answer the item?" is now answered by first
|
|
30
|
+
copying the words that answer it, with a rule that words on the same topic but a different aspect don't count.
|
|
31
|
+
On 30 hand-labelled real cases, 25 right instead of 16 (no false "not found", 5 false "found" left).
|
|
32
|
+
- **A part credited to several items only counts where the item's own words appear**, so one quote can't silently
|
|
33
|
+
cover three parts; such an item is sent back as unanswered.
|
|
34
|
+
- **Missing amounts and concrete missing terms send the agent back.** When an item asks for amounts and no discovery
|
|
35
|
+
for it gives one, or the check names a concrete missing term ("O2") that no discovery contains, the agent is told
|
|
36
|
+
to find it, instead of finishing without it (an explanation once finished in 4 turns without oxygen).
|
|
37
|
+
- **Each fact stays under its own part.** A discovery keeps the page heading it came from, including headings inside
|
|
38
|
+
a discovery that crosses page sections; a heading the discovery starts with isn't replaced by the one above;
|
|
39
|
+
bylines, very long "headings" (numbered sentences) and the prompt's own section names don't count as headings or
|
|
40
|
+
names.
|
|
41
|
+
- **Names no discovery gives are cut,** with a note: a short cited heading's name is checked too, word forms need
|
|
42
|
+
an 80% shared start ("PhotoScape" over a PhotoSplice source), and words in a source's web address count as given.
|
|
43
|
+
- **A sentence under one label copied from another subject's sentence is cut** (SQLite's sentence under
|
|
44
|
+
PostgreSQL).
|
|
45
|
+
- **Numbers in words are checked like digits** ("two weeks" needs a 2 in a source, "one cup" equals "1 cup"), unit
|
|
46
|
+
abbreviations count as their words ("5,895 m (19,341 ft)" answers "in meters" and "in feet"), and a time needs
|
|
47
|
+
the same number as a time in a source ("2 weeks" was taken from a "2L jar").
|
|
48
|
+
- **Citations:** citation lines on their own are joined to the line they cite; citations are moved out of code, and
|
|
49
|
+
unclosed code blocks closed, so correct commands aren't cut as invented.
|
|
50
|
+
- A **"Recommendation" section is labelled as the model's view**, like "My view:".
|
|
51
|
+
- **NAVIGATE refuses text that isn't a web address** (a run lost 5 turns on it).
|
|
52
|
+
|
|
53
|
+
### Fixed: the regression suite's scoring
|
|
54
|
+
- The code's own "Left out, because…" notes are no longer read as the result's claims (commands and uncited lines).
|
|
55
|
+
4 of 78 earlier run folders were over-counted.
|
|
56
|
+
- A bold label ("**1. Creating a virtual environment:**") is a heading, not an uncited claim.
|
|
57
|
+
|
|
58
|
+
### Measured
|
|
59
|
+
- 7B on two PCs: 7 of 7 on both PCs with v11 (297.8 and 269.6 model minutes), with no outside knowledge, invented
|
|
60
|
+
numbers or invented commands in any finished result.
|
|
61
|
+
- Four models, same code (v12, 90 minutes per task): Qwen2.5-7B 6 of 7, Llama 3.1 8B 7 of 7, Qwen2.5-14B 5 of 7,
|
|
62
|
+
Qwen2.5-3B 3 of 7. On 1.63 they were 6, 2, 2 and 0 of 7. No finished result used outside knowledge. Details in
|
|
63
|
+
docs/regression-history.md.
|
|
64
|
+
|
|
9
65
|
## [1.63] - 2026-11-01
|
|
10
66
|
### Added
|
|
11
67
|
- A model call the server fails is tried again after a wait (5, 15, 30, 60 and 120 seconds, about 4 minutes in all),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localagentbrowser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.64
|
|
4
4
|
Summary: An autonomous web agent driven by a local LLM (LM Studio) and Playwright. Runs entirely on your own machine.
|
|
5
5
|
Author: SnowFX
|
|
6
6
|
License-Expression: MIT
|
|
@@ -36,6 +36,7 @@ Dynamic: license-file
|
|
|
36
36
|
decides when it's done, all on your own computer.**
|
|
37
37
|
|
|
38
38
|
[](https://github.com/SnowFXXX/LocalAgentBrowser/actions/workflows/tests.yml)
|
|
39
|
+
[](https://pypi.org/project/localagentbrowser/)
|
|
39
40
|
[](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/LICENSE)
|
|
40
41
|

|
|
41
42
|
-green.svg)
|
|
@@ -58,9 +59,11 @@ and tuned to work well with a **small 7B model on a CPU, no GPU required**.
|
|
|
58
59
|
- ⚡ **Built for slow hardware.** It reuses LM Studio's prompt cache, so most turns process only what changed.
|
|
59
60
|
- 📝 **A readable report every time.** `research_report.md` holds the final result and every finding with its
|
|
60
61
|
source, written however the run ends; `research_log.txt` keeps every action and reason.
|
|
61
|
-
- ✅ **Tested.**
|
|
62
|
+
- ✅ **Tested.** 324 tests on Linux and Windows, Python 3.10–3.13, run in a few seconds with no model or browser,
|
|
62
63
|
plus a [regression suite](#regression-suite) of live runs on seven different kinds of task.
|
|
63
64
|
|
|
65
|
+
**New with 1.64:** parts of the objective no longer drop out of the answer unannounced as often (the coverage check asks for the words first; missing amounts and terms send the agent back), measured on two PCs and then four models with identical code: Qwen2.5-7B 6 of 7, Llama 3.1 8B 7 of 7, Qwen2.5-14B 5 of 7 and Qwen2.5-3B 3 of 7 tasks, with no outside knowledge in any finished result ([regression history](docs/regression-history.md)). On Windows, a launcher with Start-menu and desktop shortcuts opens the report by itself when a run ends.
|
|
66
|
+
|
|
64
67
|
**New with 1.63:** the [project site](https://snowfxxx.github.io/LocalAgentBrowser/), an article,
|
|
65
68
|
[We Can Do Better: A Small AI That Can't Cover Up](https://snowfxxx.github.io/LocalAgentBrowser/article.html), and a
|
|
66
69
|
technical paper, [Verification over Trust](https://snowfxxx.github.io/LocalAgentBrowser/paper.html).
|
|
@@ -135,6 +138,11 @@ six lessons from 30 versions of keeping a small local model on track.
|
|
|
135
138
|
When it finishes, read `research_report.md` in the folder you ran it from. Press Ctrl+C at any time to
|
|
136
139
|
stop; the report is still written with everything found so far.
|
|
137
140
|
|
|
141
|
+
**On Windows**, from a clone of the repository (below), `powershell -ExecutionPolicy Bypass -File
|
|
142
|
+
tools\windows\make_shortcuts.ps1` adds LocalAgentBrowser to the Start menu and the desktop. The launcher checks that
|
|
143
|
+
LM Studio is answering, runs each objective in its own folder under `Documents\LocalAgentBrowser runs`, and opens the
|
|
144
|
+
report as a page in your browser when the run ends.
|
|
145
|
+
|
|
138
146
|
To work on the code instead, clone the repository and install from it:
|
|
139
147
|
|
|
140
148
|
```bash
|
|
@@ -231,15 +239,15 @@ turns up, so the suite compares versions on all seven at once. 1.54 to 1.56 and
|
|
|
231
239
|
measured this way and released with 1.57 and 1.61; [docs/regression-history.md](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/docs/regression-history.md)
|
|
232
240
|
has every step's results and what each one showed.
|
|
233
241
|
|
|
234
|
-
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) | 1.61 (verified send-backs, outline pointer) |
|
|
235
|
-
|
|
236
|
-
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min | Finished, 17 min |
|
|
237
|
-
| List of three office suites | Time limit | Finished, 30 min; but WPS Office is not open source | Time limit: three correct open-source suites by turn 7, sent back because no words showed them to be "free" | Finished, 21 min: LibreOffice, WPS Office and OnlyOffice, each marked "not shown in the pages read to be free and open-source" |
|
|
238
|
-
| Comparison and recommendation | Time limit | Finished, 39 min | Time limit, after a send-back for "official documentation", which the objective does not ask for | Finished, 26 min, setup and administration from the part of a long page the outline pointer chose |
|
|
239
|
-
| How-to (cold brew from 1.55; repotting before) | Time limit | Time limit | Finished, 36 min; but "how long it keeps" answered with the steeping time | Finished, 33 min: every step, the 16-hour steep and the shelf life, cited |
|
|
240
|
-
| Explanation (photosynthesis) | Time limit | Finished, 13 min, but thin: no oxygen or water | Finished, 59 min: inputs, outputs and both stages, cited | Finished, 39 min |
|
|
241
|
-
| Opinion (owls) | Time limit | Finished, 38 min | Time limit: its first page covered two of the three cultures asked for | Finished, 42 min: four cultures, cited, with a view |
|
|
242
|
-
| Documentation (venv, official docs) | Finished, 15 min, from a tutorial site | Time limit (30 min) | Finished, 37 min, all from python.org; but the Windows line repeats the create command instead of the activate one | Time limit: all four items found from python.org, then it read on |
|
|
242
|
+
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) | 1.61 (verified send-backs, outline pointer) | 1.64 (quote-first coverage, send-backs for missing amounts and terms) |
|
|
243
|
+
|---|---|---|---|---|---|
|
|
244
|
+
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min | Finished, 17 min | Finished, 17 min |
|
|
245
|
+
| List of three office suites | Time limit | Finished, 30 min; but WPS Office is not open source | Time limit: three correct open-source suites by turn 7, sent back because no words showed them to be "free" | Finished, 21 min: LibreOffice, WPS Office and OnlyOffice, each marked "not shown in the pages read to be free and open-source" | Finished, 34 min: GIMP, Krita and Darktable (the list task asks for Photoshop alternatives from 1.64) |
|
|
246
|
+
| Comparison and recommendation | Time limit | Finished, 39 min | Time limit, after a send-back for "official documentation", which the objective does not ask for | Finished, 26 min, setup and administration from the part of a long page the outline pointer chose | Finished, 49 min |
|
|
247
|
+
| How-to (cold brew from 1.55; repotting before) | Time limit | Time limit | Finished, 36 min; but "how long it keeps" answered with the steeping time | Finished, 33 min: every step, the 16-hour steep and the shelf life, cited | Finished, 50 min |
|
|
248
|
+
| Explanation (photosynthesis) | Time limit | Finished, 13 min, but thin: no oxygen or water | Finished, 59 min: inputs, outputs and both stages, cited | Finished, 39 min | Finished, 73 min, oxygen included |
|
|
249
|
+
| Opinion (owls) | Time limit | Finished, 38 min | Time limit: its first page covered two of the three cultures asked for | Finished, 42 min: four cultures, cited, with a view | Finished, 49 min |
|
|
250
|
+
| Documentation (venv, official docs) | Finished, 15 min, from a tutorial site | Time limit (30 min) | Finished, 37 min, all from python.org; but the Windows line repeats the create command instead of the activate one | Time limit: all four items found from python.org, then it read on | Time limit: read the reference page part after part without recording (finished in 34 and 42 min in the two v11 runs) |
|
|
243
251
|
|
|
244
252
|
## How it works
|
|
245
253
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
decides when it's done, all on your own computer.**
|
|
5
5
|
|
|
6
6
|
[](https://github.com/SnowFXXX/LocalAgentBrowser/actions/workflows/tests.yml)
|
|
7
|
+
[](https://pypi.org/project/localagentbrowser/)
|
|
7
8
|
[](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/LICENSE)
|
|
8
9
|

|
|
9
10
|
-green.svg)
|
|
@@ -26,9 +27,11 @@ and tuned to work well with a **small 7B model on a CPU, no GPU required**.
|
|
|
26
27
|
- ⚡ **Built for slow hardware.** It reuses LM Studio's prompt cache, so most turns process only what changed.
|
|
27
28
|
- 📝 **A readable report every time.** `research_report.md` holds the final result and every finding with its
|
|
28
29
|
source, written however the run ends; `research_log.txt` keeps every action and reason.
|
|
29
|
-
- ✅ **Tested.**
|
|
30
|
+
- ✅ **Tested.** 324 tests on Linux and Windows, Python 3.10–3.13, run in a few seconds with no model or browser,
|
|
30
31
|
plus a [regression suite](#regression-suite) of live runs on seven different kinds of task.
|
|
31
32
|
|
|
33
|
+
**New with 1.64:** parts of the objective no longer drop out of the answer unannounced as often (the coverage check asks for the words first; missing amounts and terms send the agent back), measured on two PCs and then four models with identical code: Qwen2.5-7B 6 of 7, Llama 3.1 8B 7 of 7, Qwen2.5-14B 5 of 7 and Qwen2.5-3B 3 of 7 tasks, with no outside knowledge in any finished result ([regression history](docs/regression-history.md)). On Windows, a launcher with Start-menu and desktop shortcuts opens the report by itself when a run ends.
|
|
34
|
+
|
|
32
35
|
**New with 1.63:** the [project site](https://snowfxxx.github.io/LocalAgentBrowser/), an article,
|
|
33
36
|
[We Can Do Better: A Small AI That Can't Cover Up](https://snowfxxx.github.io/LocalAgentBrowser/article.html), and a
|
|
34
37
|
technical paper, [Verification over Trust](https://snowfxxx.github.io/LocalAgentBrowser/paper.html).
|
|
@@ -103,6 +106,11 @@ six lessons from 30 versions of keeping a small local model on track.
|
|
|
103
106
|
When it finishes, read `research_report.md` in the folder you ran it from. Press Ctrl+C at any time to
|
|
104
107
|
stop; the report is still written with everything found so far.
|
|
105
108
|
|
|
109
|
+
**On Windows**, from a clone of the repository (below), `powershell -ExecutionPolicy Bypass -File
|
|
110
|
+
tools\windows\make_shortcuts.ps1` adds LocalAgentBrowser to the Start menu and the desktop. The launcher checks that
|
|
111
|
+
LM Studio is answering, runs each objective in its own folder under `Documents\LocalAgentBrowser runs`, and opens the
|
|
112
|
+
report as a page in your browser when the run ends.
|
|
113
|
+
|
|
106
114
|
To work on the code instead, clone the repository and install from it:
|
|
107
115
|
|
|
108
116
|
```bash
|
|
@@ -199,15 +207,15 @@ turns up, so the suite compares versions on all seven at once. 1.54 to 1.56 and
|
|
|
199
207
|
measured this way and released with 1.57 and 1.61; [docs/regression-history.md](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/docs/regression-history.md)
|
|
200
208
|
has every step's results and what each one showed.
|
|
201
209
|
|
|
202
|
-
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) | 1.61 (verified send-backs, outline pointer) |
|
|
203
|
-
|
|
204
|
-
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min | Finished, 17 min |
|
|
205
|
-
| List of three office suites | Time limit | Finished, 30 min; but WPS Office is not open source | Time limit: three correct open-source suites by turn 7, sent back because no words showed them to be "free" | Finished, 21 min: LibreOffice, WPS Office and OnlyOffice, each marked "not shown in the pages read to be free and open-source" |
|
|
206
|
-
| Comparison and recommendation | Time limit | Finished, 39 min | Time limit, after a send-back for "official documentation", which the objective does not ask for | Finished, 26 min, setup and administration from the part of a long page the outline pointer chose |
|
|
207
|
-
| How-to (cold brew from 1.55; repotting before) | Time limit | Time limit | Finished, 36 min; but "how long it keeps" answered with the steeping time | Finished, 33 min: every step, the 16-hour steep and the shelf life, cited |
|
|
208
|
-
| Explanation (photosynthesis) | Time limit | Finished, 13 min, but thin: no oxygen or water | Finished, 59 min: inputs, outputs and both stages, cited | Finished, 39 min |
|
|
209
|
-
| Opinion (owls) | Time limit | Finished, 38 min | Time limit: its first page covered two of the three cultures asked for | Finished, 42 min: four cultures, cited, with a view |
|
|
210
|
-
| Documentation (venv, official docs) | Finished, 15 min, from a tutorial site | Time limit (30 min) | Finished, 37 min, all from python.org; but the Windows line repeats the create command instead of the activate one | Time limit: all four items found from python.org, then it read on |
|
|
210
|
+
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) | 1.61 (verified send-backs, outline pointer) | 1.64 (quote-first coverage, send-backs for missing amounts and terms) |
|
|
211
|
+
|---|---|---|---|---|---|
|
|
212
|
+
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min | Finished, 17 min | Finished, 17 min |
|
|
213
|
+
| List of three office suites | Time limit | Finished, 30 min; but WPS Office is not open source | Time limit: three correct open-source suites by turn 7, sent back because no words showed them to be "free" | Finished, 21 min: LibreOffice, WPS Office and OnlyOffice, each marked "not shown in the pages read to be free and open-source" | Finished, 34 min: GIMP, Krita and Darktable (the list task asks for Photoshop alternatives from 1.64) |
|
|
214
|
+
| Comparison and recommendation | Time limit | Finished, 39 min | Time limit, after a send-back for "official documentation", which the objective does not ask for | Finished, 26 min, setup and administration from the part of a long page the outline pointer chose | Finished, 49 min |
|
|
215
|
+
| How-to (cold brew from 1.55; repotting before) | Time limit | Time limit | Finished, 36 min; but "how long it keeps" answered with the steeping time | Finished, 33 min: every step, the 16-hour steep and the shelf life, cited | Finished, 50 min |
|
|
216
|
+
| Explanation (photosynthesis) | Time limit | Finished, 13 min, but thin: no oxygen or water | Finished, 59 min: inputs, outputs and both stages, cited | Finished, 39 min | Finished, 73 min, oxygen included |
|
|
217
|
+
| Opinion (owls) | Time limit | Finished, 38 min | Time limit: its first page covered two of the three cultures asked for | Finished, 42 min: four cultures, cited, with a view | Finished, 49 min |
|
|
218
|
+
| Documentation (venv, official docs) | Finished, 15 min, from a tutorial site | Time limit (30 min) | Finished, 37 min, all from python.org; but the Windows line repeats the create command instead of the activate one | Time limit: all four items found from python.org, then it read on | Time limit: read the reference page part after part without recording (finished in 34 and 42 min in the two v11 runs) |
|
|
211
219
|
|
|
212
220
|
## How it works
|
|
213
221
|
|
|
@@ -7,7 +7,7 @@ from playwright.async_api import async_playwright
|
|
|
7
7
|
from .config import API_KEY, BASE_URL, MAX_CHEAP_RETRIES, MAX_CONVERSATION_TOKENS, MAX_DISCOVERY_PROMPT_CHARS, MAX_FOLLOWUPS, MAX_JSON_RETRIES, MAX_QUERY_WORDS, MAX_VISITS_PER_URL, MODEL, MODEL_RETRY_WAITS, MODEL_TIMEOUT_SECONDS, TURN_MAX_TOKENS, OBJECTIVE_CHECK_EVERY, REPORT_FILE, STALL_TURNS, TURN_PAUSE_SECONDS
|
|
8
8
|
from .prompts import CUT_OFF, FINAL_RESULT, FIRST_TURN_INSTRUCTION, GIVE_UP_RULE, READ_FIRST, RESPONSE_FORMAT, SYSTEM_PROMPT, TURN_INSTRUCTIONS
|
|
9
9
|
from .text import extract_message_content, was_cut_off
|
|
10
|
-
from .pages import build_environment, count_search_results, determine_url, is_search_page, load_page, normalize_url, read_more_target, render_part, search_query, where_you_are
|
|
10
|
+
from .pages import build_environment, count_search_results, determine_url, is_search_page, load_page, normalize_url, read_more_target, render_part, search_query, section_heading, where_you_are, with_inner_headings
|
|
11
11
|
from .checklist import after_discovery_advice, check_parts, checklist_lines, link_discovery, split_objective
|
|
12
12
|
from .coverage import amounts_not_found, coverage_refusal, not_found_items
|
|
13
13
|
from .checks import check_decision, escalation_message, numbered_discoveries, numbered_lines, refusal_correction, source_note
|
|
@@ -319,7 +319,8 @@ async def main():
|
|
|
319
319
|
missing, unproven = await not_found_items(client, objective, tried, journal)
|
|
320
320
|
no_amounts = amounts_not_found(tried.get("checklist"), journal, skip=missing)
|
|
321
321
|
final = await compose_final_result(client, objective, journal, tried.get("checklist"), missing,
|
|
322
|
-
unproven, no_amounts, tried.get("evidence")
|
|
322
|
+
unproven, no_amounts, tried.get("evidence"),
|
|
323
|
+
tried.get("short_counts"))
|
|
323
324
|
if final:
|
|
324
325
|
finish_entry.update(draft=parameter, finding=final)
|
|
325
326
|
except Exception as err:
|
|
@@ -398,8 +399,13 @@ async def main():
|
|
|
398
399
|
print(f"📝 Logging critical discovery element: {parameter}")
|
|
399
400
|
number = sum(1 for e in journal if e.get("action") == "DISCOVER") + 1
|
|
400
401
|
linked = link_discovery(tried["checklist"], decision.get("checklist_items"), number)
|
|
402
|
+
# Recorded with its section's heading when the copied text does not name it (the heading is page
|
|
403
|
+
# text too): every later check and the final result then see what the lines are about
|
|
404
|
+
heading = section_heading(page_state, parameter)
|
|
405
|
+
inner = with_inner_headings(page_state, parameter)
|
|
406
|
+
recorded = f'(Under the heading "{heading}") {inner}' if heading else inner
|
|
401
407
|
journal.append({"turn": turn_counter, "thought": thought, "action": "DISCOVER", "source": current_url,
|
|
402
|
-
"extracted_data":
|
|
408
|
+
"extracted_data": recorded, "checklist_items": linked})
|
|
403
409
|
page_state["visit_discoveries"] = page_state.get("visit_discoveries", 0) + 1
|
|
404
410
|
page_state.setdefault("recorded_parts", set()).add(page_state["index"])
|
|
405
411
|
# A discovery is when the objective can become complete; on a one-answer question the model
|
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
from .config import GAP_REFUSAL_LIMIT, LONG_PAGE_UNREAD, MAX_CHECK_DISCOVERY_CHARS, MAX_VISITS_PER_URL, OBJECTIVE_ECHO_SHARE
|
|
5
5
|
from .prompts import FINAL_RESULT, GIVE_UP_RULE
|
|
6
6
|
from .text import MISSING_STOPWORDS, shorten
|
|
7
|
-
from .pages import determine_url, is_search_page, normalize_query, normalize_url, outline_pointer, read_more_target, search_query, where_you_are
|
|
7
|
+
from .pages import blocked_site, determine_url, is_search_page, normalize_query, normalize_url, outline_pointer, read_more_target, search_query, where_you_are
|
|
8
8
|
from .grounding import DESCRIBES_PAGE, discovery_lines, grounded_share, grounding_check, is_known_discovery, page_hint
|
|
9
9
|
from .checklist import check_parts, every_item_has_discoveries, open_items
|
|
10
10
|
|
|
@@ -187,6 +187,17 @@ def check_decision(action, parameter, journal, tried, page_state, current_url, t
|
|
|
187
187
|
elif action == "NAVIGATE":
|
|
188
188
|
if not parameter.strip():
|
|
189
189
|
return "BLOCKED_NAVIGATE", "NAVIGATE needs a full URL in the parameter."
|
|
190
|
+
# A list run on Box 1 NAVIGATEd to "discovered", then to sentences ("the size of their budget..."), each opened
|
|
191
|
+
# as https://<text> and failing: four turns lost. Text with spaces or without a dot is no address
|
|
192
|
+
address = parameter.strip().strip("\"'")
|
|
193
|
+
if not re.search(r"https?://\S", address) and not address.startswith("#") and (
|
|
194
|
+
re.search(r"\s", address) or "." not in address):
|
|
195
|
+
return "BLOCKED_NAVIGATE", (f"'{shorten(address, 60)}' is not a URL. NAVIGATE needs a full URL from a link "
|
|
196
|
+
"or a search result (such as https://example.org/page); to look something up, "
|
|
197
|
+
"use SEARCH.")
|
|
198
|
+
if blocked_site(determine_url(action, parameter)):
|
|
199
|
+
return "BLOCKED_NAVIGATE", ("That site is excluded for this run (LAB_BLOCKED_SITES). Find the answer on "
|
|
200
|
+
"another page: SEARCH, or NAVIGATE to a different result.")
|
|
190
201
|
target = normalize_url(determine_url(action, parameter))
|
|
191
202
|
# Shown a long page's outline, the model kept trying to jump to a section with a made-up anchor
|
|
192
203
|
# ("NAVIGATE ...#sqlite-vs-postgresql", then "NAVIGATE #sqlite-vs-postgresql")
|
|
@@ -308,8 +319,12 @@ def check_decision(action, parameter, journal, tried, page_state, current_url, t
|
|
|
308
319
|
return "BLOCKED_DISCOVER_SNIPPET", ("That DISCOVER is the search result's title and description, not what "
|
|
309
320
|
"this page says. DISCOVER the facts the page text shown gives for the "
|
|
310
321
|
"OBJECTIVE, in its own wording, or READ_MORE if they are further on.")
|
|
311
|
-
return grounding_check(parameter, page_state, current_url, tried.get("objective", "")
|
|
322
|
+
return grounding_check(parameter, page_state, current_url, tried.get("objective", ""),
|
|
323
|
+
tried.get("finish_drafts", ()))
|
|
312
324
|
elif action == "FINISH":
|
|
325
|
+
# Kept whole (the journal keeps only 120 characters of a refused one): a refused FINISH's recommendation
|
|
326
|
+
# came back three turns later inside a DISCOVER, appended to a real page line, and was cited as a finding
|
|
327
|
+
tried.setdefault("finish_drafts", []).append(parameter)
|
|
313
328
|
return finish_check(tried, journal, page_state, current_url) or gap_check(thought, tried)
|
|
314
329
|
else:
|
|
315
330
|
return f"UNKNOWN_{action}", f"'{action}' is not a valid action."
|
|
@@ -11,6 +11,10 @@ CONTEXT_LENGTH = int(os.environ.get("LMSTUDIO_CONTEXT_LENGTH", "8192"))
|
|
|
11
11
|
|
|
12
12
|
SEARCH_URL = "https://html.duckduckgo.com/html/?q="
|
|
13
13
|
|
|
14
|
+
# Sites a run must not read, comma-separated (a benchmark run excludes pages that republish its questions and answers):
|
|
15
|
+
# "huggingface.co/datasets,github.com/openai/simple-evals". Empty, so off, unless set
|
|
16
|
+
BLOCKED_SITES = [s.strip() for s in os.environ.get("LAB_BLOCKED_SITES", "").split(",") if s.strip()]
|
|
17
|
+
|
|
14
18
|
REPORT_FILE = "research_report.md"
|
|
15
19
|
|
|
16
20
|
JOURNAL_BATCH = 6 # The prompt shows 6-11 recent journal entries; old ones are dropped 6 at a time
|
|
@@ -16,6 +16,9 @@ AMOUNT_ASKED = re.compile(r"\b(?:quantit(?:y|ies)|amounts?|how (?:much|many|long
|
|
|
16
16
|
AMOUNT_GIVEN = re.compile(r"\d|\b(?:one|two|three|four|five|six|seven|eight|nine|ten|twelve|half|quarter|dozen|"
|
|
17
17
|
r"minutes?|hours?|days?|weeks?|months?|years?|overnight)\b", re.I)
|
|
18
18
|
|
|
19
|
+
AMOUNT_GAP = ("no discovery recorded for it gives the amounts it asks for; DISCOVER the words that give them, as the "
|
|
20
|
+
"page writes them")
|
|
21
|
+
|
|
19
22
|
def amounts_not_found(checklist, journal, skip=()):
|
|
20
23
|
"""The checklist items that ask for amounts while no discovery linked to them gives one, decided in code."""
|
|
21
24
|
found = [e for e in journal if e.get("action") == "DISCOVER"]
|
|
@@ -38,6 +41,16 @@ def quoted_from(quote, text):
|
|
|
38
41
|
pairs = word_pairs(" ".join(plain_words(quote)))
|
|
39
42
|
return bool(pairs) and len(pairs & word_pairs(" ".join(plain_words(text)))) >= 0.8 * len(pairs)
|
|
40
43
|
|
|
44
|
+
def names_copied(quote, text, item):
|
|
45
|
+
"""Whether a quote of short lines (a name per line: 'GIMP', 'carbohydrates') is copied from the text: each line
|
|
46
|
+
of one or two words is in the text, and at least one of them is not a word of the ITEM itself."""
|
|
47
|
+
lines = [plain_words(line) for line in str(quote).splitlines()]
|
|
48
|
+
lines = [words for words in lines if words]
|
|
49
|
+
if not lines or any(len(words) > 2 for words in lines):
|
|
50
|
+
return False
|
|
51
|
+
known, asked = set(plain_words(text)), set(plain_words(item))
|
|
52
|
+
return all(set(words) <= known for words in lines) and any(set(words) - asked for words in lines)
|
|
53
|
+
|
|
41
54
|
async def cited_answer(client, item, cited):
|
|
42
55
|
"""Words from the cited discoveries that say what the item asks for, by a yes-or-no question about them alone;
|
|
43
56
|
'' when the answer is no, the reply is not valid, or its words are not in what was cited."""
|
|
@@ -53,7 +66,8 @@ async def cited_answer(client, item, cited):
|
|
|
53
66
|
return ""
|
|
54
67
|
quote = str(reply.get("quote", ""))
|
|
55
68
|
said = str(reply.get("answer", "")).strip().lower() == "yes"
|
|
56
|
-
return quote if said and re.search(r"\w", quote) and any(
|
|
69
|
+
return quote if said and re.search(r"\w", quote) and any(
|
|
70
|
+
quoted_from(quote, c) or names_copied(quote, c, item) for c in cited) else ""
|
|
57
71
|
|
|
58
72
|
def item_words(text, checklist=()):
|
|
59
73
|
"""An item's own words: content words (s-stripped) that no other checklist item has."""
|
|
@@ -67,6 +81,17 @@ def is_bare_citation(evidence):
|
|
|
67
81
|
evidence = str(evidence)
|
|
68
82
|
return bool(re.search(r"\d", evidence)) and not re.sub(r"\[\d+\]|[\s,;.]|\band\b", "", evidence)
|
|
69
83
|
|
|
84
|
+
CONFIRMED_MISSING = ": named by no discovery; DISCOVER it where a page gives it"
|
|
85
|
+
|
|
86
|
+
def concrete_term(part):
|
|
87
|
+
"""Whether a missing part is a concrete thing a word check can confirm: at most three words, one of them with a
|
|
88
|
+
digit, an all-capitals abbreviation, or a capital inside the phrase ("O2", "GA3P", "PowerShell activation
|
|
89
|
+
command"), not a description ("the specific steps of the Calvin cycle")."""
|
|
90
|
+
words = part.replace(",", " ").split()
|
|
91
|
+
return 0 < len(words) <= 3 and any(
|
|
92
|
+
re.search(r"\d", w) or re.fullmatch(r"[A-Z][A-Z0-9]+", w) or (i > 0 and re.search(r"[A-Z]", w))
|
|
93
|
+
for i, w in enumerate(words))
|
|
94
|
+
|
|
70
95
|
def really_missing(missing, found):
|
|
71
96
|
"""The parts of a "missing" verdict that the discoveries do not already contain.
|
|
72
97
|
|
|
@@ -109,7 +134,7 @@ async def coverage_gaps(client, objective, checklist, journal, only=None, eviden
|
|
|
109
134
|
found = [e for e in journal if e.get("action") == "DISCOVER"]
|
|
110
135
|
listing = "\n\n".join(f"[{n}] {e['extracted_data']}" for n, e in enumerate(found, 1))
|
|
111
136
|
items = "\n".join(f"{n}. {item['text']}" for n, item in enumerate(checklist, 1))
|
|
112
|
-
gaps, unanswered, unproven = [], [], []
|
|
137
|
+
gaps, unanswered, unproven, answered_with = [], [], [], {}
|
|
113
138
|
for n, item in enumerate(checklist, 1):
|
|
114
139
|
if OPINION_REQUEST.search(item["text"]) or (only is not None and n not in only):
|
|
115
140
|
continue
|
|
@@ -177,7 +202,15 @@ async def coverage_gaps(client, objective, checklist, journal, only=None, eviden
|
|
|
177
202
|
unanswered.append(n)
|
|
178
203
|
gaps.append(f"Item {n} ({item['text']}): {UNANSWERED_GAP}")
|
|
179
204
|
elif missing:
|
|
180
|
-
|
|
205
|
+
# A concrete thing ("O2", "GA3P", "Step 3") the check says is missing, and no discovery names, is confirmed
|
|
206
|
+
# by the code, so it sends the agent back: an explanation finished in 4 turns from one overview paragraph,
|
|
207
|
+
# without oxygen, while the check said "missing O2". Vague verdicts ("the specific steps") stay soft: about
|
|
208
|
+
# half were wrong. Of 117 saved "missing" verdicts, 2 were concrete (this one and "Step 3")
|
|
209
|
+
concrete = [p for p in missing.split(", ") if concrete_term(p)]
|
|
210
|
+
gaps.append(f"Item {n} ({item['text']}): missing {missing}"
|
|
211
|
+
+ (f" ({', '.join(concrete)}{CONFIRMED_MISSING})" if concrete else ""))
|
|
212
|
+
if n not in unanswered:
|
|
213
|
+
answered_with[n] = re.sub(r"\[\d+\]", " ", str(verdict.get("evidence", "")))
|
|
181
214
|
# Words the check copied for an answered item, kept where they really are in the discoveries: the final
|
|
182
215
|
# result is shown them, as it said "the discoveries do not specify what goes into photosynthesis" over a
|
|
183
216
|
# discovery that gave water, sunlight and carbon dioxide
|
|
@@ -210,8 +243,63 @@ async def coverage_gaps(client, objective, checklist, journal, only=None, eviden
|
|
|
210
243
|
unproven.append((str(thing["name"]).strip(), condition))
|
|
211
244
|
gaps.append(f"Item {n}: {thing['name']}: no words in DISCOVERIES SO FAR show it is {condition}. SEARCH "
|
|
212
245
|
f"for '{thing['name']} {condition}' to check it, or replace it with one that is")
|
|
246
|
+
for n in shared_evidence_items(answered_with, checklist):
|
|
247
|
+
unanswered.append(n)
|
|
248
|
+
gaps.append(shared_gap(n, checklist[n - 1]["text"]))
|
|
213
249
|
return gaps, unanswered, unproven
|
|
214
250
|
|
|
251
|
+
def shared_gap(n, text):
|
|
252
|
+
"""The gap for an item whose only evidence was another item's words. It ends with UNANSWERED_GAP, the mark of a
|
|
253
|
+
gap that sends the agent back: with the reason after it, v9 found such items on Box 1's comparison but did not
|
|
254
|
+
send the agent back, and the final result filled all three items from memory."""
|
|
255
|
+
return (f"Item {n} ({text}): the words given for it are the same as for another item and say nothing of its own, "
|
|
256
|
+
f"so {UNANSWERED_GAP}")
|
|
257
|
+
|
|
258
|
+
# Units as pages abbreviate them after a number, by the word an item asks with (in item_words' s-stripped form)
|
|
259
|
+
UNIT_WORDS = {"m": "meter", "metres": "metre", "km": "kilometer", "cm": "centimeter", "mm": "millimeter",
|
|
260
|
+
"ft": "feet", "mi": "mile", "in": "inche", "kg": "kilogram", "g": "gram", "lb": "pound", "lbs": "pound",
|
|
261
|
+
"oz": "ounce", "l": "liter", "ml": "milliliter", "min": "minute", "h": "hour", "hr": "hour",
|
|
262
|
+
"hrs": "hour", "c": "celsiu", "f": "fahrenheit"}
|
|
263
|
+
UNIT_AFTER_NUMBER = re.compile(r"\d\s*(" + "|".join(sorted(UNIT_WORDS, key=len, reverse=True)) + r")\b", re.I)
|
|
264
|
+
|
|
265
|
+
def units_written(text):
|
|
266
|
+
"""The unit words a text gives by abbreviation after a number: "5,895 m (19,341 ft)" gives meter and feet. A
|
|
267
|
+
Kilimanjaro sentence answering both "in meters" and "in feet" was dropped for both, as if it said neither."""
|
|
268
|
+
found = {UNIT_WORDS[m.group(1).lower()] for m in UNIT_AFTER_NUMBER.finditer(str(text))}
|
|
269
|
+
return found | ({"foot"} if "feet" in found else set())
|
|
270
|
+
|
|
271
|
+
def shared_evidence_items(answered_with, checklist):
|
|
272
|
+
"""Items whose only evidence is the same words given for other items, none of its own words in them.
|
|
273
|
+
|
|
274
|
+
A comparison finished from one sentence ("I'd pick SQLite for any single-server app under roughly 10,000 daily
|
|
275
|
+
active users, and PostgreSQL the moment concurrent writes ... enter the picture"), given as the evidence for
|
|
276
|
+
setup, many users and features alike, and pasted under all three. Of items sharing the same words, those whose
|
|
277
|
+
own words appear in them keep them; the others count as unanswered. Items about the same thing ("Compare SQLite
|
|
278
|
+
and PostgreSQL features", "Name three features PostgreSQL has") may share words: they share a word of their own,
|
|
279
|
+
one that no more than half of the checklist items have ("compare", "sqlite" and "postgresql" in 3 of 4 do not
|
|
280
|
+
count)."""
|
|
281
|
+
def words_of(text):
|
|
282
|
+
return {w.rstrip("s") for w in plain_words(text) if len(w) > 2} - MISSING_STOPWORDS - ITEM_VERBS
|
|
283
|
+
texts = {n: words_of(item["text"]) for n, item in enumerate(checklist, 1)}
|
|
284
|
+
common = {w for w in set().union(*texts.values()) if sum(w in t for t in texts.values()) > len(texts) / 2}
|
|
285
|
+
dropped = set()
|
|
286
|
+
for n, words in answered_with.items():
|
|
287
|
+
same = [m for m, other in answered_with.items()
|
|
288
|
+
if m != n and quoted_from(words, other) and quoted_from(other, words)]
|
|
289
|
+
if not same or any((texts[n] & texts[m]) - common for m in same):
|
|
290
|
+
continue
|
|
291
|
+
given = item_words(words) | units_written(words)
|
|
292
|
+
overlap = {m: len(item_words(checklist[m - 1]["text"], checklist) & given) for m in [n] + same}
|
|
293
|
+
if overlap[n] == 0 or overlap[n] < max(overlap.values()):
|
|
294
|
+
dropped.add(n)
|
|
295
|
+
return sorted(dropped)
|
|
296
|
+
|
|
297
|
+
SHORT_COUNT = re.compile(r"^Item (\d+) \(.*\): only (\d+) of (\d+) found \(")
|
|
298
|
+
|
|
299
|
+
def short_counts(gaps):
|
|
300
|
+
"""{item number: (things found, things asked for)} from the coverage gaps."""
|
|
301
|
+
return {int(m.group(1)): (int(m.group(2)), int(m.group(3))) for m in map(SHORT_COUNT.match, gaps) if m}
|
|
302
|
+
|
|
215
303
|
async def coverage_refusal(client, objective, tried, journal):
|
|
216
304
|
# Once per run, so a noisy verdict costs one turn at most and a gap that cannot be filled never blocks finishing
|
|
217
305
|
checklist = tried.get("checklist")
|
|
@@ -222,9 +310,15 @@ async def coverage_refusal(client, objective, tried, journal):
|
|
|
222
310
|
print("🔎 Checking each checklist item against the discoveries before finishing...")
|
|
223
311
|
gaps, tried["unanswered"], unproven = await coverage_gaps(client, objective, checklist, journal,
|
|
224
312
|
evidence=tried.setdefault("evidence", {}))
|
|
313
|
+
tried["short_counts"] = short_counts(gaps)
|
|
225
314
|
# Items whose named things lacked proof of a condition are checked again before the final result
|
|
226
315
|
tried["unproven_items"] = sorted({n for n, item in enumerate(checklist, 1) if any(
|
|
227
316
|
g.startswith(f"Item {n}: ") and "no words in DISCOVERIES SO FAR show it is" in g for g in gaps)})
|
|
317
|
+
# Amounts asked for and given by no discovery of the item, decided in code: a cold brew run on Box 1 had the
|
|
318
|
+
# check's "missing quantities" (not sent back), its own FINISH drafts gave "1 cup (113 grams)", and the
|
|
319
|
+
# final result said the amounts were not found, because nothing made it record the card that gave them
|
|
320
|
+
gaps += [f"Item {n} ({checklist[n - 1]['text']}): {AMOUNT_GAP}"
|
|
321
|
+
for n in amounts_not_found(checklist, journal, skip=set(tried["unanswered"]))]
|
|
228
322
|
else:
|
|
229
323
|
# A factual run finished with nothing recorded (its answer was refused as not on the page shown) and its
|
|
230
324
|
# final result said nothing was found
|
|
@@ -235,7 +329,8 @@ async def coverage_refusal(client, objective, tried, journal):
|
|
|
235
329
|
# four suite runs ("missing official documentation" on a comparison, "H2O, O2" already recorded, "free" on a
|
|
236
330
|
# correct list of open-source suites) and cost a finish each; those verdicts now only shape the final result
|
|
237
331
|
hard = [g for g in gaps if g.endswith(UNANSWERED_GAP) or re.search(r"\): only \d+ of \d+ found", g)
|
|
238
|
-
or g.startswith("Nothing has been recorded")
|
|
332
|
+
or g.startswith("Nothing has been recorded") or g.endswith(AMOUNT_GAP)
|
|
333
|
+
or g.endswith(CONFIRMED_MISSING + ")")]
|
|
239
334
|
if gaps:
|
|
240
335
|
print("⚠️ Gaps found:\n" + "\n".join(f" - {g}{'' if g in hard else ' (not sent back)'}" for g in gaps))
|
|
241
336
|
gaps = hard
|
|
@@ -264,10 +359,14 @@ async def not_found_items(client, objective, tried, journal):
|
|
|
264
359
|
return [], []
|
|
265
360
|
candidates = sorted({n for n, item in enumerate(checklist, 1) if not item["discoveries"]
|
|
266
361
|
and not OPINION_REQUEST.search(item["text"])} | set(tried.get("unanswered") or []))
|
|
267
|
-
|
|
362
|
+
# Items short of the number they ask for are counted again too: what the agent recorded after being sent back
|
|
363
|
+
# may complete them
|
|
364
|
+
short = tried.get("short_counts") or {}
|
|
365
|
+
rechecked = sorted(set(candidates) | set(tried.get("unproven_items") or []) | set(short))
|
|
268
366
|
if not rechecked or not any(e.get("action") == "DISCOVER" for e in journal):
|
|
269
367
|
return candidates, []
|
|
270
368
|
print(f"🔎 Checking checklist item(s) {', '.join(map(str, rechecked))} once more before writing the final result...")
|
|
271
|
-
|
|
272
|
-
|
|
369
|
+
gaps, unanswered, unproven = await coverage_gaps(client, objective, checklist, journal, only=set(rechecked),
|
|
370
|
+
evidence=tried.setdefault("evidence", {}))
|
|
371
|
+
tried["short_counts"] = {n: count for n, count in short_counts(gaps).items() if n not in unanswered}
|
|
273
372
|
return [n for n in unanswered if n in candidates or n in rechecked], unproven
|
|
@@ -3,7 +3,7 @@ names never mentioned, quoted when one is refused."""
|
|
|
3
3
|
import re
|
|
4
4
|
import unicodedata
|
|
5
5
|
from .config import GROUNDED_PAIR_SHARE, GROUNDED_SENTENCE_SHARE, KNOWN_LINE_THRESHOLD, MAX_CLOSEST_CHARS, MIN_GROUNDING_PAIRS, MIN_SENTENCE_PAIRS, UNGROUNDED_PAIR_SHARE, UNGROUNDED_SENTENCE_SHARE
|
|
6
|
-
from .text import plain_words, shorten, word_pairs
|
|
6
|
+
from .text import plain_words, shorten, with_digits, word_pairs
|
|
7
7
|
from .pages import normalize_url
|
|
8
8
|
|
|
9
9
|
QUOTE_STYLES = str.maketrans({"‘": "'", "’": "'", "“": "'", "”": "'", '"': "'", "`": "'"})
|
|
@@ -24,8 +24,9 @@ def discovery_lines(text):
|
|
|
24
24
|
|
|
25
25
|
def grounded_share(text, parts):
|
|
26
26
|
"""The share of a discovery's consecutive word pairs that also appear in the given page parts."""
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
# "1 cup" and the page's "one cup" are the same words
|
|
28
|
+
mine = word_pairs(with_digits(text).lower())
|
|
29
|
+
return len(mine & word_pairs(with_digits(" ".join(parts)).lower())) / len(mine) if mine else 1.0
|
|
29
30
|
|
|
30
31
|
def code_text(text):
|
|
31
32
|
# Case, spacing, quote style and placeholder brackets ("<venv>/bin/activate") do not make code different
|
|
@@ -77,7 +78,31 @@ def command_lines(commands, parts):
|
|
|
77
78
|
heads = [h for h in heads if len(h) > 3]
|
|
78
79
|
return [shorten(line, MAX_CLOSEST_CHARS) for line in dict.fromkeys(x for x in lines if any(h in x.lower() for h in heads))][:2]
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
OWN_DRAFT_SHARE = 0.6 # A DISCOVER sentence sharing this much of its word pairs with an earlier FINISH draft...
|
|
82
|
+
OWN_PAGE_SHARE = 0.35 # ...and less than this with the page itself (without the objective) is the agent's own text
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def own_draft_sentence(parameter, page_parts, drafts):
|
|
86
|
+
"""A sentence of the DISCOVER the agent wrote itself in an earlier FINISH, and the page does not give.
|
|
87
|
+
|
|
88
|
+
Its FINISH "For a hobby project with a few hundred users, PostgreSQL would be the recommended choice..." was
|
|
89
|
+
refused; three turns later that sentence came back appended to a real page line, as a DISCOVER, and the result
|
|
90
|
+
cited it for its recommendation. It scored as grounded because the objective counts as page text, and the
|
|
91
|
+
sentence is mostly the objective's own words; scored against the page alone, it is not there."""
|
|
92
|
+
if not drafts:
|
|
93
|
+
return None
|
|
94
|
+
mine = [word_pairs(" ".join(plain_words(d))) for d in drafts]
|
|
95
|
+
for s in re.split(r"(?<=[.!?])\s+|\n+", parameter):
|
|
96
|
+
pairs = word_pairs(" ".join(plain_words(s)))
|
|
97
|
+
if len(pairs) < MIN_SENTENCE_PAIRS:
|
|
98
|
+
continue
|
|
99
|
+
from_draft = any(len(pairs & d) >= OWN_DRAFT_SHARE * len(pairs) for d in mine)
|
|
100
|
+
if from_draft and grounded_share(s, page_parts) < OWN_PAGE_SHARE:
|
|
101
|
+
return s.strip()
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def grounding_check(parameter, page_state, current_url, objective="", drafts=()):
|
|
81
106
|
# Shown part 2 of a guide, which ended "Just follow these 6 easy steps", the model recorded six steps it
|
|
82
107
|
# wrote itself (the real ones were in part 3), then a care routine the page never gave. Real discoveries
|
|
83
108
|
# from earlier runs shared 64-100% of their word pairs with the page; these shared 11-44%, and a rerun's
|
|
@@ -100,6 +125,13 @@ def grounding_check(parameter, page_state, current_url, objective=""):
|
|
|
100
125
|
f"write it. Commands are copied exactly as the page writes them{said}. "
|
|
101
126
|
"DISCOVER that in the page's own words, or READ_MORE if a later part gives "
|
|
102
127
|
"the command.")
|
|
128
|
+
# Refused outright, before the share checks: repeating it must not confirm it, since the agent wrote it
|
|
129
|
+
own = own_draft_sentence(parameter, shown[:-1], drafts)
|
|
130
|
+
if own:
|
|
131
|
+
return "BLOCKED_DISCOVER_OWN_WORDS", (f"\"{shorten(own, 160)}\" is what you wrote yourself when you tried to "
|
|
132
|
+
"FINISH, not what the page says. DISCOVER only the page's own words; your "
|
|
133
|
+
"conclusion or recommendation belongs in the final result, written at "
|
|
134
|
+
"FINISH.")
|
|
103
135
|
long_enough = len(word_pairs(parameter)) >= MIN_GROUNDING_PAIRS
|
|
104
136
|
whole = grounded_share(parameter, shown) if long_enough else 1.0
|
|
105
137
|
# An invented sentence can hide behind copied ones: an opinion task's discovery was 75% copied, but its last
|