localagentbrowser 1.57__tar.gz → 1.61__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.57 → localagentbrowser-1.61}/CHANGELOG.md +41 -2
- {localagentbrowser-1.57/localagentbrowser.egg-info → localagentbrowser-1.61}/PKG-INFO +28 -24
- {localagentbrowser-1.57 → localagentbrowser-1.61}/README.md +27 -23
- {localagentbrowser-1.57 → localagentbrowser-1.61}/agent_browser.py +170 -23
- {localagentbrowser-1.57 → localagentbrowser-1.61/localagentbrowser.egg-info}/PKG-INFO +28 -24
- {localagentbrowser-1.57 → localagentbrowser-1.61}/pyproject.toml +1 -1
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/test_checklist.py +103 -8
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/test_journal.py +7 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/test_main_loop.py +2 -2
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/test_page_text.py +16 -2
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/test_regression.py +6 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tools/regression.py +4 -1
- {localagentbrowser-1.57 → localagentbrowser-1.61}/LICENSE +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/MANIFEST.in +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/localagentbrowser.egg-info/SOURCES.txt +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/localagentbrowser.egg-info/dependency_links.txt +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/localagentbrowser.egg-info/entry_points.txt +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/localagentbrowser.egg-info/requires.txt +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/localagentbrowser.egg-info/top_level.txt +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/setup.cfg +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/conftest.py +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/fakes.py +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/test_decisions.py +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/test_duplicates.py +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tests/test_model_turns.py +0 -0
- {localagentbrowser-1.57 → localagentbrowser-1.61}/tools/live_test.py +0 -0
|
@@ -1,11 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
Every version of `agent_browser.py`, newest first. Each released version is also a tagged commit (`v1.11` … `v1.
|
|
4
|
-
measured on the regression suite and released as part of 1.57 (see
|
|
3
|
+
Every version of `agent_browser.py`, newest first. Each released version is also a tagged commit (`v1.11` … `v1.61`); 1.54 to 1.56 and 1.58 to 1.60 were internal steps
|
|
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.61] - 2026-10-21
|
|
10
|
+
Released together with the internal steps 1.58, 1.59 and 1.60, each measured on the regression suite
|
|
11
|
+
([docs/regression-history.md](docs/regression-history.md) has every step's results, task by task).
|
|
12
|
+
### Added
|
|
13
|
+
- An outline pointer for long pages. On a page long enough to have an outline, the read-on check looks for an unread
|
|
14
|
+
part whose label names a word of an item with nothing recorded yet ("Administration" for "setup and
|
|
15
|
+
administration"), and a plain READ_MORE then reads that part instead of the next one. Told only to use the
|
|
16
|
+
outline, the model went on to the next part 6 times out of 6 on a 10-part comparison page; with the code making
|
|
17
|
+
the jump, it reaches the part every time. Words that say what the whole page is about are ignored (the page title's
|
|
18
|
+
words, words every item shares), as are item verbs and parts that start inside code; hyphenated words stay whole.
|
|
19
|
+
On 1.61 the comparison finished for the first time since 1.56, from that page's Administration part.
|
|
20
|
+
- "Every item has the discoveries it asks for: choose FINISH now" in the note after a discovery, when each item has
|
|
21
|
+
at least what it asks for. When that holds, the read-on check also asks for FINISH instead of offering to read on
|
|
22
|
+
(replayed: read on 6 of 6 before, FINISH 9 of 9 after).
|
|
23
|
+
### Changed
|
|
24
|
+
- The agent is sent back before finishing only for gaps the code verifies: an item nothing was copied for, fewer
|
|
25
|
+
things than the item asks for, or nothing recorded. The model's other verdicts are logged "(not sent back)" and
|
|
26
|
+
only shape the final result. About half of the send-backs for those verdicts had been wrong, costing finishes.
|
|
27
|
+
- Every item that asks for a number is counted, not only items with a condition: an owl task finished with two
|
|
28
|
+
tribes for "at least three cultures". Amounts of time or measure ("the first two weeks") are not counts.
|
|
29
|
+
- A thing not shown to meet a condition stays in the result, marked where it is first named ("**OnlyOffice** (not
|
|
30
|
+
shown in the pages read to be free and open-source)"), instead of being left out: leaving such things out dropped
|
|
31
|
+
correct answers along with wrong ones.
|
|
32
|
+
- A description named by the coverage check as one of a list's things is ignored (it had made two office suites
|
|
33
|
+
count as three).
|
|
34
|
+
- Smaller fresh prompts: refusal reasons in the journal are cut to 120 characters, pages list 8 links instead of 15
|
|
35
|
+
(those with a few words of text first; in four suites the agent opened 39 pages, none from page links), and links
|
|
36
|
+
whose text is HTML are skipped. Fresh prompts are about 2,500 tokens instead of 3,500-4,900.
|
|
37
|
+
- Regression suite: the scorer accepts a forbidden item when the result marks it with the caveat.
|
|
38
|
+
### Fixed
|
|
39
|
+
- When several things shared a caveat, only the first was marked: the identical note next to it counted for all.
|
|
40
|
+
|
|
41
|
+
Regression runs, finished out of 7 (model minutes): 1.58 5 (214), 1.59 5 (209.5), 1.60 6 (170.6), 1.61 6 (222.1).
|
|
42
|
+
What is still open: a fresh prompt (when a conversation reaches its token budget) does not carry the advice to
|
|
43
|
+
finish, and on 1.61 the documentation task read on after one with all four items found; nothing checks that a
|
|
44
|
+
recipe's steps reach their end (on 1.60 cold brew stopped after step 2). Measured and not used: checking a
|
|
45
|
+
discovery's link to its item by shared words (most links without a shared word are right), and a 16,384-token
|
|
46
|
+
context, which would halve the fresh prompts but needs more memory than the default aims for.
|
|
47
|
+
|
|
9
48
|
## [1.57] - 2026-10-19
|
|
10
49
|
Released together with the internal steps 1.54, 1.55 and 1.56, each of which was measured on the regression suite
|
|
11
50
|
but not released on its own. [docs/regression-history.md](docs/regression-history.md) has every step's results,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localagentbrowser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.61
|
|
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
|
|
@@ -199,19 +199,19 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
|
|
|
199
199
|
|
|
200
200
|
Since 1.53, every change is measured on the same seven objectives, one of each kind above, run one after the other
|
|
201
201
|
with a time limit each (`tools/regression.py`, see [Testing](#testing)). Single runs vary with the pages a search
|
|
202
|
-
turns up, so the suite compares versions on all seven at once. 1.54 to 1.56 were internal steps
|
|
203
|
-
and released
|
|
202
|
+
turns up, so the suite compares versions on all seven at once. 1.54 to 1.56 and 1.58 to 1.60 were internal steps
|
|
203
|
+
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)
|
|
204
204
|
has every step's results and what each one showed.
|
|
205
205
|
|
|
206
|
-
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) |
|
|
207
|
-
|
|
208
|
-
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min |
|
|
209
|
-
| 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" |
|
|
210
|
-
| Comparison and recommendation | Time limit | Finished, 39 min | Time limit, after a send-back for "official documentation", which the objective does not ask for |
|
|
211
|
-
| 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 |
|
|
212
|
-
| Explanation (photosynthesis) | Time limit | Finished, 13 min, but thin: no oxygen or water | Finished, 59 min: inputs, outputs and both stages, cited |
|
|
213
|
-
| Opinion (owls) | Time limit | Finished, 38 min | Time limit: its first page covered two of the three cultures asked for |
|
|
214
|
-
| 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 |
|
|
206
|
+
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) | 1.61 (verified send-backs, outline pointer) |
|
|
207
|
+
|---|---|---|---|---|
|
|
208
|
+
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min | Finished, 17 min |
|
|
209
|
+
| 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" |
|
|
210
|
+
| 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 |
|
|
211
|
+
| 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 |
|
|
212
|
+
| 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 |
|
|
213
|
+
| 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 |
|
|
214
|
+
| 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 |
|
|
215
215
|
|
|
216
216
|
## How it works
|
|
217
217
|
|
|
@@ -323,9 +323,9 @@ Measured on an Intel i5-9500 (6 cores, no GPU) with Qwen2.5-7B-Instruct Q4_K_M a
|
|
|
323
323
|
| Coverage check before finishing (all checklist items) | about 1–3 min |
|
|
324
324
|
| Writing the final result (once, at the end) | 50–190 s, depending on how much was found |
|
|
325
325
|
|
|
326
|
-
Whole runs vary with the pages a search turns up. In the regression runs since 1.53, a factual question took
|
|
327
|
-
19 minutes of model time; the list, comparison, how-to, opinion and documentation tasks
|
|
328
|
-
and the explanation 13 to 59. A GPU or a smaller model will be much faster.
|
|
326
|
+
Whole runs vary with the pages a search turns up. In the regression runs since 1.53, a factual question took 9 to
|
|
327
|
+
19 minutes of model time; the list, comparison, how-to, opinion and documentation tasks 17 to 44 when they finished;
|
|
328
|
+
and the explanation 13 to 59. A whole suite of seven tasks took 171 to 272 minutes. A GPU or a smaller model will be much faster.
|
|
329
329
|
|
|
330
330
|
## FAQ
|
|
331
331
|
|
|
@@ -363,15 +363,17 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
363
363
|
- **Judging whether a finding covers an item is the weakest area.** The model's verdict is unreliable both ways:
|
|
364
364
|
on 1.53 the explanation finished from one overview paragraph without oxygen or water, and in the regression runs
|
|
365
365
|
since 1.54 about half of the coverage check's send-backs were for gaps that were not real ("missing official
|
|
366
|
-
documentation" on a comparison that did not ask for it, "H2O, O2" already recorded).
|
|
367
|
-
|
|
368
|
-
|
|
366
|
+
documentation" on a comparison that did not ask for it, "H2O, O2" already recorded). Since 1.58 the agent is sent
|
|
367
|
+
back only for what the code verifies (an item nothing answers, fewer things than asked for); the model's other
|
|
368
|
+
verdicts only shape the final result, so a thin answer can still pass (on 1.61 the comparison's "many
|
|
369
|
+
simultaneous users" part is short).
|
|
369
370
|
- **Conditions in the objective.** A small model tends to drop conditions such as "open source" or "using the
|
|
370
371
|
official documentation": a list of open-source office suites once included FreeOffice (free but not open
|
|
371
372
|
source), and a task that asked for the official Python documentation used a tutorial site. The objective
|
|
372
373
|
checklist keeps the conditions in view (the documentation task uses python.org since 1.55), and a thing not
|
|
373
|
-
shown in the findings to meet a condition is
|
|
374
|
-
|
|
374
|
+
shown in the findings to meet a condition is marked where it is named ("not shown in the pages read to be free
|
|
375
|
+
and open-source"). The mark says what the pages read did not show, not that the thing fails the condition: on
|
|
376
|
+
1.61 it marked LibreOffice and OnlyOffice, which are open source, as well as WPS Office, which is not. Check the
|
|
375
377
|
result against any conditions you set.
|
|
376
378
|
- **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
|
|
377
379
|
secondary sources or stays with one source too long, and results vary with the first pages a search turns
|
|
@@ -402,8 +404,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
402
404
|
- [x] Keep a fixed checklist of the objective's parts instead of re-splitting it at every check (1.53)
|
|
403
405
|
- [ ] Check that a finding really covers its checklist item, including conditions like "open source" (partly, since
|
|
404
406
|
1.57: a coverage check before finishing, with what the code can verify)
|
|
405
|
-
- [
|
|
406
|
-
result (
|
|
407
|
+
- [x] Send the agent back only for gaps the code verifies, and let the model's other verdicts only shape the final
|
|
408
|
+
result (1.58, released in 1.61)
|
|
409
|
+
- [x] Point the agent to the part of a long page whose outline names an item it still needs (1.61)
|
|
410
|
+
- [ ] Keep the advice to finish when a conversation starts over from a fresh prompt (the next step after 1.61)
|
|
407
411
|
- [ ] Rewind dead ends: save the conversation before an expensive step such as reading a long page, and if it
|
|
408
412
|
leads nowhere, return to that point with a short note instead of carrying the unused page text in every later
|
|
409
413
|
prompt (LM Studio reuses its cache up to the saved point)
|
|
@@ -456,8 +460,8 @@ the [Code of Conduct](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/CO
|
|
|
456
460
|
## Project history
|
|
457
461
|
|
|
458
462
|
Every released version is a tagged commit, and [CHANGELOG.md](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/CHANGELOG.md) describes what changed in each one and
|
|
459
|
-
why, with the measurements behind the decisions. Versions 1.54 to 1.56 were internal steps, each
|
|
460
|
-
regression suite and released
|
|
463
|
+
why, with the measurements behind the decisions. Versions 1.54 to 1.56 and 1.58 to 1.60 were internal steps, each
|
|
464
|
+
measured on the regression suite and released with 1.57 and 1.61; their results are in
|
|
461
465
|
[docs/regression-history.md](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/docs/regression-history.md).
|
|
462
466
|
|
|
463
467
|
## Acknowledgements
|
|
@@ -167,19 +167,19 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
|
|
|
167
167
|
|
|
168
168
|
Since 1.53, every change is measured on the same seven objectives, one of each kind above, run one after the other
|
|
169
169
|
with a time limit each (`tools/regression.py`, see [Testing](#testing)). Single runs vary with the pages a search
|
|
170
|
-
turns up, so the suite compares versions on all seven at once. 1.54 to 1.56 were internal steps
|
|
171
|
-
and released
|
|
170
|
+
turns up, so the suite compares versions on all seven at once. 1.54 to 1.56 and 1.58 to 1.60 were internal steps
|
|
171
|
+
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)
|
|
172
172
|
has every step's results and what each one showed.
|
|
173
173
|
|
|
174
|
-
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) |
|
|
175
|
-
|
|
176
|
-
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min |
|
|
177
|
-
| 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" |
|
|
178
|
-
| Comparison and recommendation | Time limit | Finished, 39 min | Time limit, after a send-back for "official documentation", which the objective does not ask for |
|
|
179
|
-
| 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 |
|
|
180
|
-
| Explanation (photosynthesis) | Time limit | Finished, 13 min, but thin: no oxygen or water | Finished, 59 min: inputs, outputs and both stages, cited |
|
|
181
|
-
| Opinion (owls) | Time limit | Finished, 38 min | Time limit: its first page covered two of the three cultures asked for |
|
|
182
|
-
| 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 |
|
|
174
|
+
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) | 1.61 (verified send-backs, outline pointer) |
|
|
175
|
+
|---|---|---|---|---|
|
|
176
|
+
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min | Finished, 17 min |
|
|
177
|
+
| 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" |
|
|
178
|
+
| 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 |
|
|
179
|
+
| 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 |
|
|
180
|
+
| 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 |
|
|
181
|
+
| 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 |
|
|
182
|
+
| 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 |
|
|
183
183
|
|
|
184
184
|
## How it works
|
|
185
185
|
|
|
@@ -291,9 +291,9 @@ Measured on an Intel i5-9500 (6 cores, no GPU) with Qwen2.5-7B-Instruct Q4_K_M a
|
|
|
291
291
|
| Coverage check before finishing (all checklist items) | about 1–3 min |
|
|
292
292
|
| Writing the final result (once, at the end) | 50–190 s, depending on how much was found |
|
|
293
293
|
|
|
294
|
-
Whole runs vary with the pages a search turns up. In the regression runs since 1.53, a factual question took
|
|
295
|
-
19 minutes of model time; the list, comparison, how-to, opinion and documentation tasks
|
|
296
|
-
and the explanation 13 to 59. A GPU or a smaller model will be much faster.
|
|
294
|
+
Whole runs vary with the pages a search turns up. In the regression runs since 1.53, a factual question took 9 to
|
|
295
|
+
19 minutes of model time; the list, comparison, how-to, opinion and documentation tasks 17 to 44 when they finished;
|
|
296
|
+
and the explanation 13 to 59. A whole suite of seven tasks took 171 to 272 minutes. A GPU or a smaller model will be much faster.
|
|
297
297
|
|
|
298
298
|
## FAQ
|
|
299
299
|
|
|
@@ -331,15 +331,17 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
331
331
|
- **Judging whether a finding covers an item is the weakest area.** The model's verdict is unreliable both ways:
|
|
332
332
|
on 1.53 the explanation finished from one overview paragraph without oxygen or water, and in the regression runs
|
|
333
333
|
since 1.54 about half of the coverage check's send-backs were for gaps that were not real ("missing official
|
|
334
|
-
documentation" on a comparison that did not ask for it, "H2O, O2" already recorded).
|
|
335
|
-
|
|
336
|
-
|
|
334
|
+
documentation" on a comparison that did not ask for it, "H2O, O2" already recorded). Since 1.58 the agent is sent
|
|
335
|
+
back only for what the code verifies (an item nothing answers, fewer things than asked for); the model's other
|
|
336
|
+
verdicts only shape the final result, so a thin answer can still pass (on 1.61 the comparison's "many
|
|
337
|
+
simultaneous users" part is short).
|
|
337
338
|
- **Conditions in the objective.** A small model tends to drop conditions such as "open source" or "using the
|
|
338
339
|
official documentation": a list of open-source office suites once included FreeOffice (free but not open
|
|
339
340
|
source), and a task that asked for the official Python documentation used a tutorial site. The objective
|
|
340
341
|
checklist keeps the conditions in view (the documentation task uses python.org since 1.55), and a thing not
|
|
341
|
-
shown in the findings to meet a condition is
|
|
342
|
-
|
|
342
|
+
shown in the findings to meet a condition is marked where it is named ("not shown in the pages read to be free
|
|
343
|
+
and open-source"). The mark says what the pages read did not show, not that the thing fails the condition: on
|
|
344
|
+
1.61 it marked LibreOffice and OnlyOffice, which are open source, as well as WPS Office, which is not. Check the
|
|
343
345
|
result against any conditions you set.
|
|
344
346
|
- **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
|
|
345
347
|
secondary sources or stays with one source too long, and results vary with the first pages a search turns
|
|
@@ -370,8 +372,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
370
372
|
- [x] Keep a fixed checklist of the objective's parts instead of re-splitting it at every check (1.53)
|
|
371
373
|
- [ ] Check that a finding really covers its checklist item, including conditions like "open source" (partly, since
|
|
372
374
|
1.57: a coverage check before finishing, with what the code can verify)
|
|
373
|
-
- [
|
|
374
|
-
result (
|
|
375
|
+
- [x] Send the agent back only for gaps the code verifies, and let the model's other verdicts only shape the final
|
|
376
|
+
result (1.58, released in 1.61)
|
|
377
|
+
- [x] Point the agent to the part of a long page whose outline names an item it still needs (1.61)
|
|
378
|
+
- [ ] Keep the advice to finish when a conversation starts over from a fresh prompt (the next step after 1.61)
|
|
375
379
|
- [ ] Rewind dead ends: save the conversation before an expensive step such as reading a long page, and if it
|
|
376
380
|
leads nowhere, return to that point with a short note instead of carrying the unused page text in every later
|
|
377
381
|
prompt (LM Studio reuses its cache up to the saved point)
|
|
@@ -424,8 +428,8 @@ the [Code of Conduct](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/CO
|
|
|
424
428
|
## Project history
|
|
425
429
|
|
|
426
430
|
Every released version is a tagged commit, and [CHANGELOG.md](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/CHANGELOG.md) describes what changed in each one and
|
|
427
|
-
why, with the measurements behind the decisions. Versions 1.54 to 1.56 were internal steps, each
|
|
428
|
-
regression suite and released
|
|
431
|
+
why, with the measurements behind the decisions. Versions 1.54 to 1.56 and 1.58 to 1.60 were internal steps, each
|
|
432
|
+
measured on the regression suite and released with 1.57 and 1.61; their results are in
|
|
429
433
|
[docs/regression-history.md](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/docs/regression-history.md).
|
|
430
434
|
|
|
431
435
|
## Acknowledgements
|
|
@@ -30,11 +30,13 @@ SEARCH_URL = "https://html.duckduckgo.com/html/?q="
|
|
|
30
30
|
REPORT_FILE = "research_report.md"
|
|
31
31
|
JOURNAL_BATCH = 6 # The prompt shows 6-11 recent journal entries; old ones are dropped 6 at a time
|
|
32
32
|
MAX_JOURNAL_DETAIL_CHARS = 120 # Longest query/URL/request text shown per journal line
|
|
33
|
+
MAX_JOURNAL_REASON_CHARS = 120 # Longest refusal reason shown per journal line (the full text is in research_log.txt)
|
|
33
34
|
MAX_THOUGHT_CHARS = 120 # Old reasoning is trimmed in the prompt; the full text stays in research_log.txt
|
|
34
35
|
MAX_SEARCH_RESULTS = 8
|
|
35
36
|
MAX_SNIPPET_CHARS = 200
|
|
36
37
|
PAGE_PART_CHARS = 2500 # Pages are shown in parts of about this size; READ_MORE shows the next part
|
|
37
|
-
MAX_LINKS =
|
|
38
|
+
MAX_LINKS = 8 # Links listed per page; most pages are reached from search results
|
|
39
|
+
CONTENT_LINK_WORDS = 3 # Links with at least this many words of text are listed before shorter (menu) links
|
|
38
40
|
OUTLINE_MIN_PARTS = 5 # Pages with at least this many parts get an outline of what each part covers
|
|
39
41
|
MAX_OUTLINE_LINES = 40
|
|
40
42
|
MAX_OUTLINE_LABEL = 90
|
|
@@ -75,6 +77,8 @@ MAX_CHECKLIST_ITEM_CHARS = 200
|
|
|
75
77
|
SPLIT_MAX_TOKENS = 300
|
|
76
78
|
COVERAGE_MAX_TOKENS = 400 # Length limit for the check of one checklist item before finishing
|
|
77
79
|
MAX_CONDITION_WORDS = 2 # Longer "conditions" are descriptions of the item, not properties a thing must have
|
|
80
|
+
MAX_THING_NAME_WORDS = 6 # Longer "things" named by the coverage check are descriptions, not names
|
|
81
|
+
UNANSWERED_GAP = "nothing in DISCOVERIES SO FAR answers it"
|
|
78
82
|
MAX_MISSING_CLAIM_WORDS = 4 # Short "missing" claims are checked against the discoveries' words; longer ones are kept
|
|
79
83
|
COVERAGE_ITEM_CHECK = (
|
|
80
84
|
"Check the one CHECKLIST item named at the end against the DISCOVERIES. Give:\n"
|
|
@@ -86,9 +90,10 @@ COVERAGE_ITEM_CHECK = (
|
|
|
86
90
|
"Do not repeat what the evidence already says; '' if the evidence fully answers it;\n"
|
|
87
91
|
"- conditions: the conditions the item sets on the things it asks for, in its own words (for example \"free\", "
|
|
88
92
|
"\"open-source\", \"official\"); [] if it sets none;\n"
|
|
89
|
-
"- things: if the item has conditions, one entry for each thing the
|
|
90
|
-
"
|
|
91
|
-
"
|
|
93
|
+
"- things: if the item asks for a number of things or has conditions, one entry for each different thing the "
|
|
94
|
+
"evidence names for it, with \"proof\": the exact words from the DISCOVERIES that show that thing meets the "
|
|
95
|
+
"conditions, or that name it if there are no conditions ('' if the DISCOVERIES do not say it); [] if the item asks "
|
|
96
|
+
"for neither.")
|
|
92
97
|
COVERAGE_FORMAT = {"type": "json_schema", "json_schema": {"name": "coverage", "strict": True, "schema": {
|
|
93
98
|
"type": "object", "properties": {
|
|
94
99
|
"evidence": {"type": "string"}, "missing": {"type": "string"},
|
|
@@ -364,10 +369,19 @@ async def extract_page_links(page, shown_links=None):
|
|
|
364
369
|
seen = set()
|
|
365
370
|
link_lines = []
|
|
366
371
|
omitted = 0
|
|
372
|
+
# Links with a few words of text (article titles, "Next: ...") before short ones (menu items such as "GUIDES"),
|
|
373
|
+
# each group in page order: the list is capped, and menus come first on most pages. In 4 regression suites the
|
|
374
|
+
# agent opened 39 pages, all from search results and none from page links, while the links were more than half
|
|
375
|
+
# of every new page it was shown
|
|
376
|
+
links = sorted(links, key=lambda link: len(link["text"].split()) < CONTENT_LINK_WORDS)
|
|
367
377
|
for link in links:
|
|
368
378
|
if link["href"] in seen or normalize_url(link["href"]) == normalize_url(page.url):
|
|
369
379
|
continue
|
|
370
380
|
seen.add(link["href"])
|
|
381
|
+
# Image links whose text is the image's HTML ('<img src="...') say nothing and took about a third of a
|
|
382
|
+
# site's link list (shop links, mostly)
|
|
383
|
+
if link["text"].startswith("<"):
|
|
384
|
+
continue
|
|
371
385
|
if shown_links is not None and link["href"] in shown_links:
|
|
372
386
|
omitted += 1
|
|
373
387
|
continue
|
|
@@ -559,6 +573,11 @@ def read_more_target(parameter, page_state):
|
|
|
559
573
|
# The model sometimes writes "2 of 2" or "part 3"; the first number is the part it means
|
|
560
574
|
number = re.search(r'\d+', parameter)
|
|
561
575
|
target = int(number.group()) - 1 if number else page_state["index"] + 1
|
|
576
|
+
# Told by the read-on check to read the outline part that names an open item, the model repeated a plain
|
|
577
|
+
# READ_MORE for the next part in 2 to 3 of 6 replays; a plain READ_MORE after that advice reads the named part
|
|
578
|
+
pointed = page_state.get("pointed")
|
|
579
|
+
if not number and pointed and pointed[0] == (page_state.get("url"), page_state["index"]):
|
|
580
|
+
target = pointed[1]
|
|
562
581
|
# Asking for the part already shown nearly always meant the next one ("READ_MORE 9" while on part 9 came up
|
|
563
582
|
# in most long-page runs, and refusing it cost a retry each time), so it is read as that, except on the last part
|
|
564
583
|
if target == page_state["index"] and target < len(page_state["parts"]) - 1:
|
|
@@ -611,15 +630,81 @@ def read_on_check(page_state, current_url, target, checklist=None):
|
|
|
611
630
|
confirmed.add(index)
|
|
612
631
|
# Naming the items still open: on an explanation task the model answered 'none' for every part of an overview
|
|
613
632
|
# that gave the inputs and outputs it still needed, while it was thinking about the Calvin cycle
|
|
633
|
+
# With every item holding what it asks for, a documentation task got "choose FINISH now" after a discovery and
|
|
634
|
+
# read four more parts anyway, answering each of these checks 'none' and repeating READ_MORE: then this check
|
|
635
|
+
# asks for FINISH first and no longer offers reading on
|
|
636
|
+
if every_item_has_discoveries(checklist):
|
|
637
|
+
return "CONFIRM_READ_ON", (f"Before reading on: every item of the OBJECTIVE CHECKLIST already has the discoveries "
|
|
638
|
+
f"it asks for. Choose FINISH now with the complete final result. Only if part "
|
|
639
|
+
f"{index + 1}, which is shown now, gives something an item still clearly lacks, "
|
|
640
|
+
"DISCOVER that instead.")
|
|
614
641
|
still_open = open_items(checklist)
|
|
615
642
|
serves = f"the OBJECTIVE (above all the checklist items with nothing recorded yet: {still_open})" if still_open else "the OBJECTIVE"
|
|
643
|
+
pointer = outline_pointer(page_state, checklist)
|
|
644
|
+
if pointer and pointer[0] != index + 2:
|
|
645
|
+
page_state["pointed"] = ((page_state.get("url"), index), pointer[0] - 1)
|
|
646
|
+
read_on = (f"repeat your READ_MORE: it will show part {pointer[0]}, not the next part, because the Page Outline "
|
|
647
|
+
f"names '{pointer[1]}' there, from an item with nothing recorded yet" if pointer and pointer[0] != index + 2
|
|
648
|
+
else "repeat your READ_MORE")
|
|
616
649
|
return "CONFIRM_READ_ON", (f"Before reading on: you have recorded nothing from part {index + 1}, which is shown now. "
|
|
617
650
|
f"In thought_process, list each fact in it that serves {serves}, or write 'none'. If any "
|
|
618
|
-
"of them are not in DISCOVERIES SO FAR, DISCOVER them now. If there are none,
|
|
619
|
-
"
|
|
651
|
+
f"of them are not in DISCOVERIES SO FAR, DISCOVER them now. If there are none, {read_on}. "
|
|
652
|
+
"But if DISCOVERIES SO FAR already cover every item of the OBJECTIVE CHECKLIST, choose "
|
|
620
653
|
"FINISH instead of reading on, and if this page is not about the OBJECTIVE at all, leave "
|
|
621
654
|
"it with SEARCH or NAVIGATE.")
|
|
622
655
|
|
|
656
|
+
# Words that say what to do with an item, not what it is about ("Compare ... features" matched "How do they compare")
|
|
657
|
+
ITEM_VERBS = {"compare", "find", "explain", "describe", "detail", "list", "give", "name", "say", "what", "which", "each",
|
|
658
|
+
"about", "into", "does", "make"}
|
|
659
|
+
|
|
660
|
+
def outline_words(text):
|
|
661
|
+
# Hyphenated words stay whole: "open-source" split into "open" matched "Can I open and edit ..." in an FAQ
|
|
662
|
+
return re.findall(r"[a-z0-9]+(?:-[a-z0-9]+)*", str(text).lower())
|
|
663
|
+
|
|
664
|
+
def outline_word_parts(outline):
|
|
665
|
+
"""For each word in the outline labels, the part numbers (1-based) whose label has it."""
|
|
666
|
+
parts = {}
|
|
667
|
+
for number, label in enumerate(outline, 1):
|
|
668
|
+
for word in set(outline_words(label)):
|
|
669
|
+
parts.setdefault(word.rstrip("s"), set()).add(number)
|
|
670
|
+
return parts
|
|
671
|
+
|
|
672
|
+
def outline_pointer(page_state, checklist):
|
|
673
|
+
"""The first unread part whose outline label names a word of an open checklist item, as (part, word), or None.
|
|
674
|
+
|
|
675
|
+
Told to use the outline to reach an open item, the model still read the next part 6 of 6 times on a 10-part
|
|
676
|
+
comparison page whose outline named "Administration" and "Features" in parts 5 and 6. Words in many labels
|
|
677
|
+
("SQLite", "PostgreSQL" in every heading of that page) say nothing about where an item is, so only words in
|
|
678
|
+
at most a third of the labels count.
|
|
679
|
+
"""
|
|
680
|
+
outline = page_state.get("outline") or []
|
|
681
|
+
if len(outline) < OUTLINE_MIN_PARTS:
|
|
682
|
+
return None
|
|
683
|
+
seen = page_state.get("seen_parts") or set()
|
|
684
|
+
parts = page_state.get("parts") or []
|
|
685
|
+
where = outline_word_parts(outline)
|
|
686
|
+
# Topic words point anywhere: the page's own title (its first label: "photosynthesis" on the Photosynthesis
|
|
687
|
+
# article, "free" on a page of free office suites) and words every item shares ("SQLite" in each item of a
|
|
688
|
+
# comparison) say what the whole page is about, not where one item is
|
|
689
|
+
to_find = [item for item in checklist or [] if not OPINION_REQUEST.search(item["text"])]
|
|
690
|
+
topic = {w.rstrip("s") for w in outline_words(outline[0])}
|
|
691
|
+
if len(to_find) > 1:
|
|
692
|
+
topic |= set.intersection(*({w.rstrip("s") for w in outline_words(item["text"])} for item in to_find))
|
|
693
|
+
for item in to_find:
|
|
694
|
+
if item["discoveries"]:
|
|
695
|
+
continue
|
|
696
|
+
for word in outline_words(item["text"]):
|
|
697
|
+
key = word.rstrip("s")
|
|
698
|
+
if len(key) < 4 or word in MISSING_STOPWORDS or word in ITEM_VERBS or key in topic:
|
|
699
|
+
continue
|
|
700
|
+
# A part that starts inside code is labelled by its first code line, not a heading: on the venv docs,
|
|
701
|
+
# "Install pip in the virtual environment." was a docstring in the module's source
|
|
702
|
+
numbers = sorted(n for n in where.get(key, ()) if n - 1 not in seen
|
|
703
|
+
and not str(parts[n - 1] if n <= len(parts) else "").lstrip().startswith("```"))
|
|
704
|
+
if numbers and len(where[key]) <= len(outline) / 3:
|
|
705
|
+
return numbers[0], word
|
|
706
|
+
return None
|
|
707
|
+
|
|
623
708
|
def gap_check(thought, tried):
|
|
624
709
|
# The model is asked to write 'not covered' for missing parts; on a comparison it wrote "Available Libraries:
|
|
625
710
|
# Not covered" and finished anyway. A FINISH whose own reasoning says that is sent back to work on the gap,
|
|
@@ -776,6 +861,29 @@ def checklist_lines(checklist):
|
|
|
776
861
|
lines.append(f"{number}. {item['text']} (discoveries for it: {found or 'none yet'})")
|
|
777
862
|
return "\n".join(lines)
|
|
778
863
|
|
|
864
|
+
def every_item_has_discoveries(checklist):
|
|
865
|
+
"""Whether each item to find has at least the discoveries it asks for: one, or the number it names."""
|
|
866
|
+
items = [item for item in checklist or [] if not OPINION_REQUEST.search(item["text"])]
|
|
867
|
+
needed = [asked_count(item["text"]) or 1 for item in items]
|
|
868
|
+
# As many different discoveries as the items ask for in all: on 1.53 one overview paragraph was linked to all four
|
|
869
|
+
# items of an explanation, and finishing from it gave a thin result
|
|
870
|
+
distinct = {n for item in items for n in item["discoveries"]}
|
|
871
|
+
return (bool(items) and all(len(item["discoveries"]) >= k for item, k in zip(items, needed, strict=True))
|
|
872
|
+
and len(distinct) >= sum(needed))
|
|
873
|
+
|
|
874
|
+
def after_discovery_advice(checklist):
|
|
875
|
+
# With Western, European and Native American views recorded for "at least three cultures", an owl task kept
|
|
876
|
+
# searching for more until its time ran out (twice); told only "if DISCOVERIES SO FAR now cover every item", the
|
|
877
|
+
# model kept judging that they did not. When the code can see each item has what it asks for, it says so; the
|
|
878
|
+
# coverage check before FINISH still looks at what the discoveries say
|
|
879
|
+
if every_item_has_discoveries(checklist):
|
|
880
|
+
return ("Every item of the OBJECTIVE CHECKLIST now has the discoveries it asks for (an item asking for three "
|
|
881
|
+
"things has three). Choose FINISH now with the complete final result, unless one of these discoveries "
|
|
882
|
+
"clearly does not answer its item; do not read further for extra or merely interesting details.")
|
|
883
|
+
return ("If DISCOVERIES SO FAR now cover every item of the OBJECTIVE CHECKLIST, choose FINISH now; do not read further "
|
|
884
|
+
"for extra or merely interesting details. If some items are still not covered, work on those next instead of "
|
|
885
|
+
"adding more detail to items already covered.")
|
|
886
|
+
|
|
779
887
|
def open_items(checklist):
|
|
780
888
|
"""The checklist items nothing has been recorded for yet (opinions are written at FINISH), as one line for prompts."""
|
|
781
889
|
return "; ".join(f"{n}. {item['text']}" for n, item in enumerate(checklist or [], 1)
|
|
@@ -806,10 +914,17 @@ def check_parts(tried):
|
|
|
806
914
|
f"OBJECTIVE CHECKLIST:\n{checklist_lines(checklist)}\n")
|
|
807
915
|
|
|
808
916
|
NUMBER_WORDS = {"two": 2, "three": 3, "four": 4, "five": 5, "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10}
|
|
917
|
+
MEASURE_WORDS = {"second", "seconds", "minute", "minutes", "hour", "hours", "day", "days", "week", "weeks", "month",
|
|
918
|
+
"months", "year", "years", "cup", "cups", "times", "percent"}
|
|
809
919
|
|
|
810
920
|
def asked_count(text):
|
|
811
|
-
"""The number of things an item asks for ("three ...", "5 ..."), or None. "At least three" counts as three.
|
|
812
|
-
|
|
921
|
+
"""The number of things an item asks for ("three ...", "5 ..."), or None. "At least three" counts as three.
|
|
922
|
+
|
|
923
|
+
A number of time units or amounts ("the first two weeks", "2 cups") is not a number of things to find."""
|
|
924
|
+
words = plain_words(text)
|
|
925
|
+
for word, after in zip(words, words[1:] + [""], strict=True):
|
|
926
|
+
if after in MEASURE_WORDS:
|
|
927
|
+
continue
|
|
813
928
|
if word in NUMBER_WORDS:
|
|
814
929
|
return NUMBER_WORDS[word]
|
|
815
930
|
if word.isdigit() and 2 <= int(word) <= 20:
|
|
@@ -892,7 +1007,10 @@ async def coverage_gaps(client, objective, checklist, journal, only=None):
|
|
|
892
1007
|
conditions = [c for c in verdict.get("conditions") or [] if isinstance(c, str) and states(item["text"], c)
|
|
893
1008
|
and len([w for w in plain_words(c) if len(w) > 2]) <= MAX_CONDITION_WORDS
|
|
894
1009
|
and asked_count(item["text"])]
|
|
895
|
-
|
|
1010
|
+
# A thing is a name ("OnlyOffice"), not a sentence: the model also listed ".docx/.xlsx/.pptx compatibility
|
|
1011
|
+
# is genuinely excellent ..." as one, which got a caveat of its own and made two suites count as three
|
|
1012
|
+
things = [t for t in verdict.get("things") or [] if isinstance(t, dict) and str(t.get("name", "")).strip()
|
|
1013
|
+
and "\n" not in str(t["name"]).strip() and len(str(t["name"]).split()) <= MAX_THING_NAME_WORDS]
|
|
896
1014
|
except Exception as err:
|
|
897
1015
|
print(f"⚠️ Coverage check of item {n} failed ({err}); treating it as covered.")
|
|
898
1016
|
continue
|
|
@@ -901,15 +1019,16 @@ async def coverage_gaps(client, objective, checklist, journal, only=None):
|
|
|
901
1019
|
# that item to "the best time to repot is spring and summer", and the final result invented a care routine
|
|
902
1020
|
if not re.search(r"\w", str(verdict.get("evidence", ""))):
|
|
903
1021
|
unanswered.append(n)
|
|
904
|
-
gaps.append(f"Item {n} ({item['text']}):
|
|
1022
|
+
gaps.append(f"Item {n} ({item['text']}): {UNANSWERED_GAP}")
|
|
905
1023
|
elif missing:
|
|
906
1024
|
gaps.append(f"Item {n} ({item['text']}): missing {missing}")
|
|
907
1025
|
# A number the item asks for is counted, not judged: a list run recorded only LibreOffice, twice, and the
|
|
908
1026
|
# check found nothing missing from "three free, open-source alternatives"
|
|
909
1027
|
wanted = asked_count(item["text"])
|
|
910
1028
|
names = list(dict.fromkeys(str(t["name"]).strip() for t in things))
|
|
911
|
-
#
|
|
912
|
-
|
|
1029
|
+
# Also without a condition: an owl task finished from one discovery naming two tribes for "at least three
|
|
1030
|
+
# cultures", with the two named as things and nothing missing, because the count ran only with conditions
|
|
1031
|
+
if wanted and names and len(names) < wanted:
|
|
913
1032
|
gaps.append(f"Item {n} ({item['text']}): only {len(names)} of {wanted} found ({', '.join(names)})")
|
|
914
1033
|
for thing in things:
|
|
915
1034
|
proof = str(thing.get("proof", ""))
|
|
@@ -921,7 +1040,7 @@ async def coverage_gaps(client, objective, checklist, journal, only=None):
|
|
|
921
1040
|
unmet = [c for c in conditions if not (copied and states(proof, c))]
|
|
922
1041
|
if unmet:
|
|
923
1042
|
condition = " and ".join(unmet)
|
|
924
|
-
unproven.append(
|
|
1043
|
+
unproven.append((str(thing["name"]).strip(), condition))
|
|
925
1044
|
gaps.append(f"Item {n}: {thing['name']}: no words in DISCOVERIES SO FAR show it is {condition}. SEARCH "
|
|
926
1045
|
f"for '{thing['name']} {condition}' to check it, or replace it with one that is")
|
|
927
1046
|
return gaps, unanswered, unproven
|
|
@@ -943,9 +1062,17 @@ async def coverage_refusal(client, objective, tried, journal):
|
|
|
943
1062
|
# final result said nothing was found
|
|
944
1063
|
gaps = ["Nothing has been recorded for any checklist item yet"]
|
|
945
1064
|
tried["unanswered"] = [n for n, item in enumerate(checklist, 1) if not OPINION_REQUEST.search(item["text"])]
|
|
1065
|
+
# Only gaps the code can confirm send the agent back: an item nothing was copied for, fewer things than the item
|
|
1066
|
+
# asks for, or nothing recorded at all. About half of the send-backs for the model's other verdicts were wrong in
|
|
1067
|
+
# four suite runs ("missing official documentation" on a comparison, "H2O, O2" already recorded, "free" on a
|
|
1068
|
+
# correct list of open-source suites) and cost a finish each; those verdicts now only shape the final result
|
|
1069
|
+
hard = [g for g in gaps if g.endswith(UNANSWERED_GAP) or re.search(r"\): only \d+ of \d+ found", g)
|
|
1070
|
+
or g.startswith("Nothing has been recorded")]
|
|
1071
|
+
if gaps:
|
|
1072
|
+
print("⚠️ Gaps found:\n" + "\n".join(f" - {g}{'' if g in hard else ' (not sent back)'}" for g in gaps))
|
|
1073
|
+
gaps = hard
|
|
946
1074
|
if not gaps:
|
|
947
1075
|
return None
|
|
948
|
-
print("⚠️ Gaps found:\n" + "\n".join(f" - {g}" for g in gaps))
|
|
949
1076
|
return "BLOCKED_COVERAGE", ("Before finishing, each checklist item was checked against DISCOVERIES SO FAR, and these "
|
|
950
1077
|
"gaps were found:\n" + "\n".join(f"- {g}" for g in gaps) + "\nWork on them first: DISCOVER "
|
|
951
1078
|
"the missing information, or words that show a condition is met, from the current page if "
|
|
@@ -1058,7 +1185,10 @@ def journal_line(e):
|
|
|
1058
1185
|
detail += f" (landed on {destination})"
|
|
1059
1186
|
line = f"T{e.get('turn')} {action} {detail}".rstrip()
|
|
1060
1187
|
if e.get("reason") or e.get("error"):
|
|
1061
|
-
|
|
1188
|
+
# Shortened like the details: the model saw the full refusal when it happened, and repeating it in every
|
|
1189
|
+
# fresh prompt made the journal the largest part of one (up to 7,000 characters, mostly read-on checks
|
|
1190
|
+
# and the finish and coverage checks with their lists), at about 8 tokens a second
|
|
1191
|
+
line += f" -> {shorten(e.get('reason') or e.get('error'), MAX_JOURNAL_REASON_CHARS)}"
|
|
1062
1192
|
thought = e.get("thought", "")
|
|
1063
1193
|
if thought:
|
|
1064
1194
|
line += f" | thought: {thought[:MAX_THOUGHT_CHARS] + '...' if len(thought) > MAX_THOUGHT_CHARS else thought}"
|
|
@@ -1335,8 +1465,9 @@ async def compose_final_result(client, objective, journal, checklist=None, not_f
|
|
|
1335
1465
|
parts += ("### NOT FOUND (no discovery answers these; they are reported after your result)\n"
|
|
1336
1466
|
+ "\n".join(f"- {text}" for text in unfound) + "\n\n")
|
|
1337
1467
|
if unproven:
|
|
1338
|
-
parts += ("### NOT SHOWN TO MEET THE CONDITIONS (do not
|
|
1339
|
-
+ "\n".join(f"- {
|
|
1468
|
+
parts += ("### NOT SHOWN TO MEET THE CONDITIONS (the discoveries do not show these meet them; if you list one, "
|
|
1469
|
+
"say so right after it)\n" + "\n".join(f"- {name}: not shown to be {cond}" for name, cond in unproven)
|
|
1470
|
+
+ "\n\n")
|
|
1340
1471
|
request = (f"### OBJECTIVE\n{objective}\n\n{parts}### DISCOVERIES\n{listing}\n\n"
|
|
1341
1472
|
"Write the complete final result the OBJECTIVE asks for: do exactly what it asks (for example answer "
|
|
1342
1473
|
"the question, write the summary or the plan, or give the opinion). Rules:\n"
|
|
@@ -1385,8 +1516,27 @@ async def compose_final_result(client, objective, journal, checklist=None, not_f
|
|
|
1385
1516
|
text += "\n\n" + (paragraph if paragraph.lower().startswith("my view:") else f"My view: {paragraph}")
|
|
1386
1517
|
if unfound:
|
|
1387
1518
|
text += "\n\nNot found in the pages read:\n" + "\n".join(f"- {t}" for t in unfound)
|
|
1388
|
-
|
|
1389
|
-
|
|
1519
|
+
return with_caveats(text, unproven)
|
|
1520
|
+
|
|
1521
|
+
def with_caveats(text, unproven):
|
|
1522
|
+
"""The final result with each thing not shown to meet a condition marked where it is first named.
|
|
1523
|
+
|
|
1524
|
+
Leaving such things out dropped correct answers along with wrong ones: a page said "open-source" of
|
|
1525
|
+
LibreOffice, ONLYOFFICE and Apache OpenOffice, but not "free". So they stay, marked by the code, which does not
|
|
1526
|
+
rely on the final result to say it; things the result does not name are listed after it.
|
|
1527
|
+
"""
|
|
1528
|
+
unnamed = []
|
|
1529
|
+
for name, condition in unproven:
|
|
1530
|
+
note = f"(not shown in the pages read to be {condition})"
|
|
1531
|
+
match = re.search(re.escape(name) + r"(\*\*)?", text, re.I)
|
|
1532
|
+
if not match:
|
|
1533
|
+
unnamed.append(f"- {name}: {note[1:-1]}")
|
|
1534
|
+
# Only a note right after this name counts as already there: checked against the whole text, the note added
|
|
1535
|
+
# to OnlyOffice left WPS Office, with the same condition, unmarked in a list run's result
|
|
1536
|
+
elif not text[match.end():].lstrip().lower().startswith(note.lower()):
|
|
1537
|
+
text = text[:match.end()] + f" {note}" + text[match.end():]
|
|
1538
|
+
if unnamed:
|
|
1539
|
+
text += "\n\nNot shown in the pages read to meet the conditions:\n" + "\n".join(unnamed)
|
|
1390
1540
|
return text
|
|
1391
1541
|
|
|
1392
1542
|
async def load_page(page, url=None):
|
|
@@ -1582,10 +1732,7 @@ async def main():
|
|
|
1582
1732
|
set_note(conversation, f"RESULT: Your DISCOVER was recorded as discovery [{number}]{linked_note}. "
|
|
1583
1733
|
f"{where_you_are(page_state, current_url)} The page is unchanged.\n"
|
|
1584
1734
|
f"OBJECTIVE CHECKLIST now:\n{checklist_lines(tried['checklist'])}\n"
|
|
1585
|
-
"Do not record the same information again.
|
|
1586
|
-
"every item of the OBJECTIVE CHECKLIST, choose FINISH now; do not read further "
|
|
1587
|
-
"for extra or merely interesting details. If some items are still not covered, "
|
|
1588
|
-
"work on those next instead of adding more detail to items already covered.",
|
|
1735
|
+
f"Do not record the same information again. {after_discovery_advice(tried['checklist'])}",
|
|
1589
1736
|
current_url)
|
|
1590
1737
|
|
|
1591
1738
|
# A single refusal gets a short warning; repeated refusals get an explicit list of other options
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localagentbrowser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.61
|
|
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
|
|
@@ -199,19 +199,19 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
|
|
|
199
199
|
|
|
200
200
|
Since 1.53, every change is measured on the same seven objectives, one of each kind above, run one after the other
|
|
201
201
|
with a time limit each (`tools/regression.py`, see [Testing](#testing)). Single runs vary with the pages a search
|
|
202
|
-
turns up, so the suite compares versions on all seven at once. 1.54 to 1.56 were internal steps
|
|
203
|
-
and released
|
|
202
|
+
turns up, so the suite compares versions on all seven at once. 1.54 to 1.56 and 1.58 to 1.60 were internal steps
|
|
203
|
+
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)
|
|
204
204
|
has every step's results and what each one showed.
|
|
205
205
|
|
|
206
|
-
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) |
|
|
207
|
-
|
|
208
|
-
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min |
|
|
209
|
-
| 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" |
|
|
210
|
-
| Comparison and recommendation | Time limit | Finished, 39 min | Time limit, after a send-back for "official documentation", which the objective does not ask for |
|
|
211
|
-
| 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 |
|
|
212
|
-
| Explanation (photosynthesis) | Time limit | Finished, 13 min, but thin: no oxygen or water | Finished, 59 min: inputs, outputs and both stages, cited |
|
|
213
|
-
| Opinion (owls) | Time limit | Finished, 38 min | Time limit: its first page covered two of the three cultures asked for |
|
|
214
|
-
| 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 |
|
|
206
|
+
| Task | 1.52 | 1.53 (objective checklist) | 1.57 (coverage check) | 1.61 (verified send-backs, outline pointer) |
|
|
207
|
+
|---|---|---|---|---|
|
|
208
|
+
| Factual question | Finished, 12 min | Finished, 14 min | Finished, 13 min | Finished, 17 min |
|
|
209
|
+
| 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" |
|
|
210
|
+
| 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 |
|
|
211
|
+
| 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 |
|
|
212
|
+
| 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 |
|
|
213
|
+
| 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 |
|
|
214
|
+
| 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 |
|
|
215
215
|
|
|
216
216
|
## How it works
|
|
217
217
|
|
|
@@ -323,9 +323,9 @@ Measured on an Intel i5-9500 (6 cores, no GPU) with Qwen2.5-7B-Instruct Q4_K_M a
|
|
|
323
323
|
| Coverage check before finishing (all checklist items) | about 1–3 min |
|
|
324
324
|
| Writing the final result (once, at the end) | 50–190 s, depending on how much was found |
|
|
325
325
|
|
|
326
|
-
Whole runs vary with the pages a search turns up. In the regression runs since 1.53, a factual question took
|
|
327
|
-
19 minutes of model time; the list, comparison, how-to, opinion and documentation tasks
|
|
328
|
-
and the explanation 13 to 59. A GPU or a smaller model will be much faster.
|
|
326
|
+
Whole runs vary with the pages a search turns up. In the regression runs since 1.53, a factual question took 9 to
|
|
327
|
+
19 minutes of model time; the list, comparison, how-to, opinion and documentation tasks 17 to 44 when they finished;
|
|
328
|
+
and the explanation 13 to 59. A whole suite of seven tasks took 171 to 272 minutes. A GPU or a smaller model will be much faster.
|
|
329
329
|
|
|
330
330
|
## FAQ
|
|
331
331
|
|
|
@@ -363,15 +363,17 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
363
363
|
- **Judging whether a finding covers an item is the weakest area.** The model's verdict is unreliable both ways:
|
|
364
364
|
on 1.53 the explanation finished from one overview paragraph without oxygen or water, and in the regression runs
|
|
365
365
|
since 1.54 about half of the coverage check's send-backs were for gaps that were not real ("missing official
|
|
366
|
-
documentation" on a comparison that did not ask for it, "H2O, O2" already recorded).
|
|
367
|
-
|
|
368
|
-
|
|
366
|
+
documentation" on a comparison that did not ask for it, "H2O, O2" already recorded). Since 1.58 the agent is sent
|
|
367
|
+
back only for what the code verifies (an item nothing answers, fewer things than asked for); the model's other
|
|
368
|
+
verdicts only shape the final result, so a thin answer can still pass (on 1.61 the comparison's "many
|
|
369
|
+
simultaneous users" part is short).
|
|
369
370
|
- **Conditions in the objective.** A small model tends to drop conditions such as "open source" or "using the
|
|
370
371
|
official documentation": a list of open-source office suites once included FreeOffice (free but not open
|
|
371
372
|
source), and a task that asked for the official Python documentation used a tutorial site. The objective
|
|
372
373
|
checklist keeps the conditions in view (the documentation task uses python.org since 1.55), and a thing not
|
|
373
|
-
shown in the findings to meet a condition is
|
|
374
|
-
|
|
374
|
+
shown in the findings to meet a condition is marked where it is named ("not shown in the pages read to be free
|
|
375
|
+
and open-source"). The mark says what the pages read did not show, not that the thing fails the condition: on
|
|
376
|
+
1.61 it marked LibreOffice and OnlyOffice, which are open source, as well as WPS Office, which is not. Check the
|
|
375
377
|
result against any conditions you set.
|
|
376
378
|
- **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
|
|
377
379
|
secondary sources or stays with one source too long, and results vary with the first pages a search turns
|
|
@@ -402,8 +404,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
402
404
|
- [x] Keep a fixed checklist of the objective's parts instead of re-splitting it at every check (1.53)
|
|
403
405
|
- [ ] Check that a finding really covers its checklist item, including conditions like "open source" (partly, since
|
|
404
406
|
1.57: a coverage check before finishing, with what the code can verify)
|
|
405
|
-
- [
|
|
406
|
-
result (
|
|
407
|
+
- [x] Send the agent back only for gaps the code verifies, and let the model's other verdicts only shape the final
|
|
408
|
+
result (1.58, released in 1.61)
|
|
409
|
+
- [x] Point the agent to the part of a long page whose outline names an item it still needs (1.61)
|
|
410
|
+
- [ ] Keep the advice to finish when a conversation starts over from a fresh prompt (the next step after 1.61)
|
|
407
411
|
- [ ] Rewind dead ends: save the conversation before an expensive step such as reading a long page, and if it
|
|
408
412
|
leads nowhere, return to that point with a short note instead of carrying the unused page text in every later
|
|
409
413
|
prompt (LM Studio reuses its cache up to the saved point)
|
|
@@ -456,8 +460,8 @@ the [Code of Conduct](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/CO
|
|
|
456
460
|
## Project history
|
|
457
461
|
|
|
458
462
|
Every released version is a tagged commit, and [CHANGELOG.md](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/CHANGELOG.md) describes what changed in each one and
|
|
459
|
-
why, with the measurements behind the decisions. Versions 1.54 to 1.56 were internal steps, each
|
|
460
|
-
regression suite and released
|
|
463
|
+
why, with the measurements behind the decisions. Versions 1.54 to 1.56 and 1.58 to 1.60 were internal steps, each
|
|
464
|
+
measured on the regression suite and released with 1.57 and 1.61; their results are in
|
|
461
465
|
[docs/regression-history.md](https://github.com/SnowFXXX/LocalAgentBrowser/blob/main/docs/regression-history.md).
|
|
462
466
|
|
|
463
467
|
## Acknowledgements
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "localagentbrowser"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.61"
|
|
8
8
|
description = "An autonomous web agent driven by a local LLM (LM Studio) and Playwright. Runs entirely on your own machine."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -72,6 +72,34 @@ class TestChecksUseTheChecklist:
|
|
|
72
72
|
assert "serves the OBJECTIVE (above all the checklist items with nothing recorded yet: 1. What goes in), or write 'none'" in reason
|
|
73
73
|
assert "Where it happens" not in reason
|
|
74
74
|
|
|
75
|
+
def test_read_on_check_points_to_the_outline_part_naming_an_open_item(self):
|
|
76
|
+
tried = {"queries": [], "urls": [], "checklist": checklist("Compare SQLite and PostgreSQL setup and administration",
|
|
77
|
+
"Compare SQLite and PostgreSQL features")}
|
|
78
|
+
outline = ["SQLite vs PostgreSQL / Why", "What is SQLite? / What is PostgreSQL?", "SQLite vs PostgreSQL: Similarities",
|
|
79
|
+
"SQLite vs PostgreSQL: Data types", "SQLite / PostgreSQL / Licenses / Administration",
|
|
80
|
+
"Performance / Features", "Scalability"]
|
|
81
|
+
page = {"url": DOCS, "parts": ["x"] * 7, "outline": outline, "index": 0, "seen_parts": {0}}
|
|
82
|
+
reason = ab.check_decision("READ_MORE", "", discoveries("x"), tried, page, DOCS)[1]
|
|
83
|
+
assert ("If there are none, repeat your READ_MORE: it will show part 5, not the next part, because the Page "
|
|
84
|
+
"Outline names 'administration' there") in reason
|
|
85
|
+
# Repeated plainly, the READ_MORE reads the part named; a part number the model gives is kept
|
|
86
|
+
assert ab.read_more_target("", page) == 4 and ab.read_more_target("2", page) == 1
|
|
87
|
+
assert ab.read_more_target("", dict(page, index=1)) == 2
|
|
88
|
+
# Words in many labels ("sqlite") and item verbs ("compare") point nowhere
|
|
89
|
+
assert ab.outline_pointer(dict(page, seen_parts={0, 4, 5}), tried["checklist"]) is None
|
|
90
|
+
# A label taken from the first line of code is not a heading
|
|
91
|
+
code = dict(page, parts=["x"] * 4 + ["```\n Administration of the pool\n"] + ["x"] * 2)
|
|
92
|
+
assert ab.outline_pointer(code, tried["checklist"]) == (6, "features")
|
|
93
|
+
|
|
94
|
+
def test_read_on_check_asks_for_finish_once_every_item_has_its_discoveries(self):
|
|
95
|
+
tried = {"queries": [], "urls": [], "checklist": checklist("What goes in", "Where it happens")}
|
|
96
|
+
ab.link_discovery(tried["checklist"], [1], 1)
|
|
97
|
+
ab.link_discovery(tried["checklist"], [2], 2)
|
|
98
|
+
page = {"url": DOCS, "parts": ["a", "b"], "index": 0, "seen_parts": {0}}
|
|
99
|
+
reason = ab.check_decision("READ_MORE", "", discoveries("x", "y"), tried, page, DOCS)[1]
|
|
100
|
+
assert "already has the discoveries it asks for. Choose FINISH now" in reason
|
|
101
|
+
assert "repeat your READ_MORE" not in reason
|
|
102
|
+
|
|
75
103
|
|
|
76
104
|
class TestFinalResult:
|
|
77
105
|
def test_checklist_is_given_with_a_rule_to_cover_it(self):
|
|
@@ -119,9 +147,16 @@ class TestCoverageGaps:
|
|
|
119
147
|
"'OnlyOffice free and open-source' to check it, or replace it with one that is"]
|
|
120
148
|
|
|
121
149
|
def test_long_descriptions_are_not_conditions(self):
|
|
122
|
-
reply = verdict(conditions=["in different cultures or religions"],
|
|
150
|
+
reply = verdict(conditions=["in different cultures or religions"],
|
|
151
|
+
things=[("Western cultures", ""), ("Hopi", ""), ("Greece", "")])
|
|
123
152
|
assert gaps(["How at least three different cultures or religions viewed owls"], [reply])[0] == []
|
|
124
153
|
|
|
154
|
+
def test_a_short_count_is_found_without_a_condition(self):
|
|
155
|
+
reply = verdict(things=[("Dakota Hidatsa Indians", "the Dakota Hidatsa"), ("Hopis Indians", "the Hopis")])
|
|
156
|
+
assert gaps(["How at least three different cultures viewed owls"], [reply])[0] == [
|
|
157
|
+
"Item 1 (How at least three different cultures viewed owls): only 2 of 3 found (Dakota Hidatsa Indians, "
|
|
158
|
+
"Hopis Indians)"]
|
|
159
|
+
|
|
125
160
|
def test_conditions_the_item_does_not_state_are_ignored(self):
|
|
126
161
|
reply = verdict(conditions=["open-source"], things=[("WPS Office", "")])
|
|
127
162
|
assert gaps(["What each one is best at"], [reply])[0] == []
|
|
@@ -132,13 +167,22 @@ class TestCoverageGaps:
|
|
|
132
167
|
|
|
133
168
|
|
|
134
169
|
class TestCoverageRefusal:
|
|
135
|
-
def
|
|
136
|
-
client, _ = fake_client([verdict(missing="oxygen")])
|
|
170
|
+
def test_sent_back_once_for_an_item_nothing_answers(self):
|
|
171
|
+
client, _ = fake_client([verdict(evidence="", missing="oxygen")])
|
|
137
172
|
tried = {"checklist": checklist("What comes out")}
|
|
138
173
|
refusal = asyncio.run(ab.coverage_refusal(client, "objective", tried, discoveries("x")))
|
|
139
|
-
assert refusal[0] == "BLOCKED_COVERAGE"
|
|
174
|
+
assert refusal[0] == "BLOCKED_COVERAGE"
|
|
175
|
+
assert "- Item 1 (What comes out): nothing in DISCOVERIES SO FAR answers it" in refusal[1]
|
|
140
176
|
assert asyncio.run(ab.coverage_refusal(client, "objective", tried, discoveries("x"))) is None
|
|
141
177
|
|
|
178
|
+
def test_the_model_s_other_verdicts_do_not_send_back(self):
|
|
179
|
+
reply = verdict(missing="official documentation", conditions=["free"],
|
|
180
|
+
things=[("ONLYOFFICE", ""), ("LibreOffice", ""), ("OpenOffice", "")])
|
|
181
|
+
client, _ = fake_client([reply])
|
|
182
|
+
tried = {"checklist": checklist("Name three free suites")}
|
|
183
|
+
assert asyncio.run(ab.coverage_refusal(client, "objective", tried, discoveries("ONLYOFFICE is open source."))) is None
|
|
184
|
+
assert tried["unproven_items"] == [1], "the unmet condition still counts for the final result"
|
|
185
|
+
|
|
142
186
|
def test_skipped_after_an_exhausted_search(self):
|
|
143
187
|
client, model = fake_client([])
|
|
144
188
|
assert asyncio.run(ab.coverage_refusal(client, "o", {"checklist": checklist("a"), "search_exhausted": True},
|
|
@@ -158,6 +202,8 @@ class TestCountedItems:
|
|
|
158
202
|
assert ab.asked_count("Name three free, open-source alternatives") == 3
|
|
159
203
|
assert ab.asked_count("Find at least 4 examples") == 4
|
|
160
204
|
assert ab.asked_count("How tall is it in meters?") is None
|
|
205
|
+
assert ab.asked_count("Say how to care for the plant during the first two weeks") is None
|
|
206
|
+
assert ab.asked_count("Name two drinks made with 2 cups of water") == 2
|
|
161
207
|
|
|
162
208
|
def test_fewer_different_things_than_asked_is_a_gap(self):
|
|
163
209
|
reply = verdict(conditions=["open-source"], things=[("LibreOffice", "LibreOffice is free and open source"),
|
|
@@ -220,12 +266,25 @@ class TestUnprovenThings:
|
|
|
220
266
|
tried = {"checklist": items, "unproven_items": [1]}
|
|
221
267
|
found = discoveries("LibreOffice is free and open source. WPS Office has a familiar interface.")
|
|
222
268
|
missing, unproven = asyncio.run(ab.not_found_items(client, "o", tried, found))
|
|
223
|
-
assert missing == [] and unproven == ["WPS Office
|
|
269
|
+
assert missing == [] and unproven == [("WPS Office", "open-source")]
|
|
224
270
|
|
|
225
|
-
client, model = fake_client(["1. LibreOffice [1]"])
|
|
271
|
+
client, model = fake_client(["1. **LibreOffice** [1]\n2. **WPS Office**"])
|
|
226
272
|
text = asyncio.run(ab.compose_final_result(client, "o", found, items, [], unproven))
|
|
227
|
-
assert "
|
|
228
|
-
assert text.
|
|
273
|
+
assert "- WPS Office: not shown to be open-source" in model.calls[0][-1]["content"]
|
|
274
|
+
assert text == "1. **LibreOffice** [1]\n2. **WPS Office** (not shown in the pages read to be open-source)"
|
|
275
|
+
|
|
276
|
+
def test_unnamed_things_are_listed_after_the_result_and_notes_are_not_doubled(self):
|
|
277
|
+
text = ab.with_caveats("LibreOffice [1]. ONLYOFFICE (not shown in the pages read to be free).",
|
|
278
|
+
[("ONLYOFFICE", "free"), ("WPS Office", "open-source")])
|
|
279
|
+
assert text == ("LibreOffice [1]. ONLYOFFICE (not shown in the pages read to be free).\n\n"
|
|
280
|
+
"Not shown in the pages read to meet the conditions:\n- WPS Office: not shown in the pages read "
|
|
281
|
+
"to be open-source")
|
|
282
|
+
|
|
283
|
+
def test_things_with_the_same_condition_are_each_marked(self):
|
|
284
|
+
note = "(not shown in the pages read to be free and open-source)"
|
|
285
|
+
text = ab.with_caveats("1. **OnlyOffice** [1]\n2. **WPS Office** [2]",
|
|
286
|
+
[("OnlyOffice", "free and open-source"), ("WPS Office", "free and open-source")])
|
|
287
|
+
assert text == f"1. **OnlyOffice** {note} [1]\n2. **WPS Office** {note} [2]"
|
|
229
288
|
|
|
230
289
|
|
|
231
290
|
def test_opinion_items_are_shown_as_written_at_finish():
|
|
@@ -249,3 +308,39 @@ class TestReallyMissing:
|
|
|
249
308
|
|
|
250
309
|
def test_co2_does_not_count_as_o2(self):
|
|
251
310
|
assert ab.really_missing("O2", discoveries("Plants take in CO2.")) == "O2"
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_descriptions_are_not_things():
|
|
314
|
+
reply = verdict(conditions=["open-source"], things=[
|
|
315
|
+
("LibreOffice", "LibreOffice is free and open source"),
|
|
316
|
+
("OnlyOffice", ""),
|
|
317
|
+
(".docx/.xlsx/.pptx compatibility is genuinely excellent\nBuilt-in real-time collaboration", "")])
|
|
318
|
+
client, _ = fake_client([reply])
|
|
319
|
+
result, _, unproven = asyncio.run(ab.coverage_gaps(client, "o", checklist("Name three free, open-source suites"),
|
|
320
|
+
discoveries("LibreOffice is free and open source.")))
|
|
321
|
+
assert "Item 1 (Name three free, open-source suites): only 2 of 3 found (LibreOffice, OnlyOffice)" in result
|
|
322
|
+
assert unproven == [("OnlyOffice", "open-source")]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class TestAfterDiscoveryAdvice:
|
|
326
|
+
def test_says_finish_when_every_item_has_what_it_asks_for(self):
|
|
327
|
+
items = checklist("Find how at least three cultures viewed owls", "Give your own opinion on why")
|
|
328
|
+
for n in (1, 2):
|
|
329
|
+
ab.link_discovery(items, [1], n)
|
|
330
|
+
assert "If DISCOVERIES SO FAR now cover" in ab.after_discovery_advice(items), "two of three is not enough"
|
|
331
|
+
ab.link_discovery(items, [1], 3)
|
|
332
|
+
assert ab.after_discovery_advice(items).startswith("Every item of the OBJECTIVE CHECKLIST now has the discoveries")
|
|
333
|
+
|
|
334
|
+
def test_an_item_without_discoveries_keeps_the_usual_advice(self):
|
|
335
|
+
items = checklist("Setup", "Features")
|
|
336
|
+
ab.link_discovery(items, [1], 1)
|
|
337
|
+
assert not ab.every_item_has_discoveries(items)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_one_discovery_linked_to_every_item_is_not_enough():
|
|
341
|
+
items = checklist("What goes in", "What comes out", "Light reactions", "Calvin cycle")
|
|
342
|
+
ab.link_discovery(items, [1, 2, 3, 4], 1)
|
|
343
|
+
assert not ab.every_item_has_discoveries(items)
|
|
344
|
+
for n in (2, 3, 4):
|
|
345
|
+
ab.link_discovery(items, [n], n)
|
|
346
|
+
assert ab.every_item_has_discoveries(items)
|
|
@@ -72,3 +72,10 @@ def test_restore_path_escapes():
|
|
|
72
72
|
row = "Medium plants\t4-5 inches\tChoose a pot just 1-2 inches wider"
|
|
73
73
|
assert ab.restore_path_escapes(row) == row, "tabs from a page table stay"
|
|
74
74
|
assert ab.restore_path_escapes("C:" + "\t" + "emp") == r"C:\temp"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_long_refusal_reasons_are_shortened_in_the_journal():
|
|
78
|
+
reason = "Before reading on: you have recorded nothing from part 3. " + "List each fact. " * 40
|
|
79
|
+
line = ab.journal_line({"turn": 8, "action": "REFUSED_IN_TURN", "requested": "READ_MORE", "reason": reason})
|
|
80
|
+
assert line.startswith("T8 REFUSED_IN_TURN READ_MORE -> Before reading on: you have recorded nothing from part 3.")
|
|
81
|
+
assert len(line) < 200 and line.endswith("...")
|
|
@@ -144,10 +144,10 @@ def test_discoveries_are_linked_to_checklist_items(monkeypatch, tmp_path):
|
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
def test_a_coverage_send_back_starts_the_next_turn_fresh(monkeypatch, tmp_path):
|
|
147
|
-
gap = json.dumps({"evidence": "
|
|
147
|
+
gap = json.dumps({"evidence": "", "missing": "the second fact", "conditions": [], "things": []})
|
|
148
148
|
script = [reply("SEARCH", "docs a"), reply("NAVIGATE", A), reply("DISCOVER", "fact from a"),
|
|
149
149
|
reply("FINISH", "done"), reply("FINISH", "done"), # finish check, then repeated
|
|
150
|
-
gap, COVERED, # coverage check:
|
|
150
|
+
gap, COVERED, # coverage check: nothing answers item 1 -> sent back
|
|
151
151
|
reply("FINISH", "done"), reply("FINISH", "done"), # next turn, fresh prompt
|
|
152
152
|
COVERED, COVERED, # nothing linked: both items checked once more
|
|
153
153
|
"Final [1]."]
|
|
@@ -51,7 +51,7 @@ def test_render_part_footer():
|
|
|
51
51
|
assert "[End of page]" in ab.render_part(page)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
MENU = [{"text": f"Menu item {i}", "href": f"https://site.example/menu{i}"} for i in range(
|
|
54
|
+
MENU = [{"text": f"Menu item {i}", "href": f"https://site.example/menu{i}"} for i in range(ab.MAX_LINKS)]
|
|
55
55
|
CONTENT = [{"text": f"Content link {i}", "href": f"https://site.example/content{i}"} for i in range(5)]
|
|
56
56
|
|
|
57
57
|
|
|
@@ -66,9 +66,23 @@ def test_next_page_on_the_same_site_lists_new_links_instead_of_the_menu():
|
|
|
66
66
|
asyncio.run(ab.extract_page_links(FakeLinksPage("https://site.example/a", MENU + CONTENT), shown))
|
|
67
67
|
links = asyncio.run(ab.extract_page_links(FakeLinksPage("https://site.example/b", MENU + CONTENT), shown))
|
|
68
68
|
assert links.count("->") == 5 and "Content link 0" in links and "Menu item" not in links
|
|
69
|
-
assert "(
|
|
69
|
+
assert f"({ab.MAX_LINKS} links already listed" in links
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def test_without_a_running_conversation_the_full_list_is_shown():
|
|
73
73
|
links = asyncio.run(ab.extract_page_links(FakeLinksPage("https://site.example/b", MENU + CONTENT)))
|
|
74
74
|
assert links.count("->") == ab.MAX_LINKS and "not repeated" not in links
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_links_whose_text_is_html_are_skipped():
|
|
78
|
+
images = [{"text": '<img src="https://site.example/x.jpg">', "href": f"https://shop.example/{i}"} for i in range(5)]
|
|
79
|
+
links = asyncio.run(ab.extract_page_links(FakeLinksPage("https://site.example/a", images + CONTENT)))
|
|
80
|
+
assert "<img" not in links and links.count("->") == 5
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_links_with_a_few_words_come_before_menu_items():
|
|
84
|
+
menu = [{"text": f"Menu{i}", "href": f"https://site.example/m{i}"} for i in range(10)]
|
|
85
|
+
article = [{"text": "How to repot a houseplant", "href": "https://site.example/repot"}]
|
|
86
|
+
links = asyncio.run(ab.extract_page_links(FakeLinksPage("https://site.example/a", menu + article)))
|
|
87
|
+
assert links.splitlines()[0] == "- How to repot a houseplant -> https://site.example/repot"
|
|
88
|
+
assert links.count("->") == ab.MAX_LINKS
|
|
@@ -110,3 +110,9 @@ def test_task_ids_are_unique_and_every_kind_has_a_core_task():
|
|
|
110
110
|
ids = [t["id"] for t in rg.TASKS]
|
|
111
111
|
assert len(ids) == len(set(ids))
|
|
112
112
|
assert {t["kind"] for t in rg.TASKS} == {t["kind"] for t in rg.TASKS if t["core"]}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_a_caveated_forbidden_item_is_not_a_missed_condition():
|
|
116
|
+
result = "1. LibreOffice [1]\n2. OnlyOffice [1]\n3. **FreeOffice** (not shown in the pages read to be free and open-source)"
|
|
117
|
+
assert rg.score_run(TASK, journal(result, "LibreOffice and OnlyOffice ..."))["forbidden"] == []
|
|
118
|
+
assert rg.score_run(TASK, journal("3. FreeOffice [1]", "x"))["forbidden"] == ["freeoffice"]
|
|
@@ -210,7 +210,10 @@ def score_run(task, journal, console="", checklist=None):
|
|
|
210
210
|
"unverified_numbers": ab.unverified_numbers(result, found) if finish else [],
|
|
211
211
|
"gap_statements": [m.group(0) for m in GAP_STATEMENT.finditer(result)],
|
|
212
212
|
"missing": ["/".join(group[:3]) for group in task.get("expect", []) if not any(word in low for word in group)],
|
|
213
|
-
|
|
213
|
+
# A forbidden item marked by the agent's own caveat ("FreeOffice (not shown in the pages read to be ...") is
|
|
214
|
+
# flagged to the reader, so it does not count as a missed condition
|
|
215
|
+
"forbidden": [word for word in task.get("forbid", []) if word in low
|
|
216
|
+
and not re.search(re.escape(word) + r"\W{0,3}\(not shown in the pages read", low)],
|
|
214
217
|
# How far a run got, so one that was about to finish is not read the same as one that was stuck
|
|
215
218
|
"finish_attempts": sum(1 for e in journal if e.get("action") in ("FINISH", "BLOCKED_FINISH", "CONFIRM_FINISH")
|
|
216
219
|
or str(e.get("requested", "")).startswith("FINISH")),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{localagentbrowser-1.57 → localagentbrowser-1.61}/localagentbrowser.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{localagentbrowser-1.57 → localagentbrowser-1.61}/localagentbrowser.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|