localagentbrowser 1.47__tar.gz → 1.52__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.47 → localagentbrowser-1.52}/CHANGELOG.md +91 -0
- {localagentbrowser-1.47/localagentbrowser.egg-info → localagentbrowser-1.52}/PKG-INFO +15 -13
- {localagentbrowser-1.47 → localagentbrowser-1.52}/README.md +14 -12
- {localagentbrowser-1.47 → localagentbrowser-1.52}/agent_browser.py +81 -15
- {localagentbrowser-1.47 → localagentbrowser-1.52/localagentbrowser.egg-info}/PKG-INFO +15 -13
- {localagentbrowser-1.47 → localagentbrowser-1.52}/pyproject.toml +1 -1
- {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_decisions.py +36 -15
- {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_journal.py +11 -1
- {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_model_turns.py +13 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/LICENSE +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/MANIFEST.in +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/SOURCES.txt +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/dependency_links.txt +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/entry_points.txt +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/requires.txt +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/top_level.txt +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/setup.cfg +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/conftest.py +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/fakes.py +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_duplicates.py +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_main_loop.py +0 -0
- {localagentbrowser-1.47 → localagentbrowser-1.52}/tests/test_page_text.py +0 -0
|
@@ -4,6 +4,97 @@ Every version of `agent_browser.py`, newest first. Each version is also a tagged
|
|
|
4
4
|
Timings are from live runs on the development machine (Intel i5-9500, 6 cores, no GPU, Qwen2.5-7B-Instruct
|
|
5
5
|
Q4_K_M in LM Studio), where reading the prompt takes about 90% of every model call.
|
|
6
6
|
|
|
7
|
+
## [1.52] - 2026-10-16
|
|
8
|
+
### Changed
|
|
9
|
+
- The note after each DISCOVER also says to work on parts of the objective that are not covered yet before
|
|
10
|
+
adding detail to parts that are. On the explanation task (how photosynthesis works), 1.51 recorded the
|
|
11
|
+
Calvin cycle enzyme by enzyme while the light reactions and the inputs and outputs were still missing.
|
|
12
|
+
|
|
13
|
+
Rerun: after two Calvin cycle discoveries the agent left for a page on the light reactions and recorded where
|
|
14
|
+
each stage happens and what the light reactions do. It still did not finish within 65 minutes: it lost about
|
|
15
|
+
20 minutes reading the history and evolution sections of a long encyclopedia article twice, answering "none"
|
|
16
|
+
to each read-on check, and never recorded the inputs and outputs that article's first part gives.
|
|
17
|
+
|
|
18
|
+
Regression runs on 1.51 for the other kinds of task: the list task (three free, open-source alternatives to
|
|
19
|
+
Microsoft Office) finished in 7 turns and 15.5 minutes of model time (49.3 on 1.46), naming LibreOffice,
|
|
20
|
+
OnlyOffice and Apache OpenOffice, all open source; its final result wrongly says FreeOffice was "not
|
|
21
|
+
mentioned" in the discoveries while leaving it out.
|
|
22
|
+
|
|
23
|
+
## [1.51] - 2026-10-16
|
|
24
|
+
### Fixed
|
|
25
|
+
- On the repotting how-to, 1.50 sent back the model's invented steps once (part 2 still only said "follow
|
|
26
|
+
these 6 easy steps"), the model sent nearly the same steps again, and they went through as a confirmed
|
|
27
|
+
repeat. A DISCOVER far below anything real is now refused outright, and repeating it does not help: below
|
|
28
|
+
40% of its word pairs on the page text shown, or with a sentence below 25%. Real discoveries in the
|
|
29
|
+
earlier runs scored at least 50% overall and 40% per sentence; these invented steps scored 32%, one of
|
|
30
|
+
them 9%. Between those levels and 70% / 35%, the one-time confirmation stays.
|
|
31
|
+
|
|
32
|
+
Rerun: the invented steps were refused, the agent read on and recorded the guide's real materials and steps,
|
|
33
|
+
and FINISH came on turn 7 after 28.5 minutes of model time (67.5 on 1.44). The final result has every step,
|
|
34
|
+
including putting the plant in the new pot, and says that no care instructions beyond the first watering
|
|
35
|
+
were found. Regression on the comparison task with 1.50: 8 turns and 24.9 minutes of model time (38.2 on
|
|
36
|
+
1.42), all parts covered and cited, with a recommendation.
|
|
37
|
+
|
|
38
|
+
## [1.50] - 2026-10-16
|
|
39
|
+
Three runs of the strict documentation objective (Playwright test agents, "do not FINISH until ... fully
|
|
40
|
+
validated", including "parameter schemas" the documentation does not have).
|
|
41
|
+
### Changed
|
|
42
|
+
- `READ_MORE` with the number of the part already shown reads the next part. The model did this in most
|
|
43
|
+
long-page runs, meaning the next part, and refusing it cost a retry each time. On the last part it is still
|
|
44
|
+
refused.
|
|
45
|
+
- The read-on check also says to leave a page that is not about the objective: shown an unrelated "agent"
|
|
46
|
+
project's command reference, the model read on part after part.
|
|
47
|
+
### Fixed
|
|
48
|
+
- After being told twice that a search was exhausted and that it could finish with the gap named, the model
|
|
49
|
+
tried FINISH, the finish check sent it to work on the uncovered part, and it repeated the search again (five
|
|
50
|
+
times in one run). Once a search is exhausted, FINISH goes through without the finish and gap checks.
|
|
51
|
+
|
|
52
|
+
Third run: FINISH on turn 10 (25.2 minutes of model time), the first time this objective has finished; earlier
|
|
53
|
+
versions searched for over an hour. The result has the official `npx playwright init-agents --loop=vscode`
|
|
54
|
+
command and the file layout from the official docs, but it is thin: the finish check was answered with
|
|
55
|
+
"covered by [1] covered by [2] covered by [3]" without naming the parts, one discovery was off topic, and the
|
|
56
|
+
result does not say that no parameter schemas were found.
|
|
57
|
+
|
|
58
|
+
## [1.49] - 2026-10-16
|
|
59
|
+
A documentation task on 1.48 (create a Python virtual environment with venv, activate it on Windows and on
|
|
60
|
+
macOS or Linux, and install packages) garbled commands in two places.
|
|
61
|
+
### Fixed
|
|
62
|
+
- Recording the Windows command `python -m venv C:\path\to\new\virtual\environment`, the model wrote single
|
|
63
|
+
backslashes in its JSON reply: `\t`, `\n` and `\r` became a tab, a newline and a carriage return, and since
|
|
64
|
+
the response format does not allow `\v`, the model turned `\virtual\environment` into `\real\name`. The
|
|
65
|
+
system prompt now asks for every backslash to be written twice, and a tab, carriage return, backspace or
|
|
66
|
+
form feed right before a letter is turned back into its backslash sequence when the text also has a
|
|
67
|
+
backslash or a drive letter (page tables keep their real tabs).
|
|
68
|
+
- The final result turned the recorded `.\venv\Scripts\activate` and `source venv/bin/activate` into
|
|
69
|
+
`.venv\Scripts\activate` and `source .venv/bin/activate`, which do not match the `python -m venv venv` it
|
|
70
|
+
gave first. Rule 1 of the final result now says to copy commands, code, file paths and names exactly.
|
|
71
|
+
Replaying that step three times on 1.48 changed the commands twice; with the rule, all three were exact.
|
|
72
|
+
|
|
73
|
+
Rerun with the backslash changes: FINISH on turn 6 (15.1 minutes of model time) with the create, activate
|
|
74
|
+
and install commands, the Windows path intact. It used a tutorial site rather than the official
|
|
75
|
+
documentation the objective named; checking conditions like that is still a weakness.
|
|
76
|
+
|
|
77
|
+
## [1.48] - 2026-10-16
|
|
78
|
+
An opinion task on 1.47 (how at least three cultures or religions have viewed owls, then an opinion on why
|
|
79
|
+
they attract such different meanings) showed three problems.
|
|
80
|
+
### Fixed
|
|
81
|
+
- A discovery copied two sentences from the page and added a third the page never said ("...depicted
|
|
82
|
+
alongside other wise figures like the prophet Samuel"); copied text lifted the whole discovery to 75%, past
|
|
83
|
+
the page-match check. Each sentence of 8 or more word pairs is now also scored, and one below 35% gets the
|
|
84
|
+
same cheap retry. Measured on the earlier runs' real discoveries, sentences scored 40-100%; invented ones
|
|
85
|
+
0-41%.
|
|
86
|
+
- With three cultures recorded where three were asked for, the read-on check pushed the agent to keep
|
|
87
|
+
collecting. It now also says to choose FINISH if the discoveries already cover every part of the objective.
|
|
88
|
+
- The final result listed many traditions but gave no opinion, although the objective asked for one. When the
|
|
89
|
+
objective asks for an opinion, view or recommendation and the result has no "My view:" paragraph, a short
|
|
90
|
+
second call writes only that paragraph from the result.
|
|
91
|
+
- The report's number check treated citation ranges like "[1-4]" as numbers.
|
|
92
|
+
|
|
93
|
+
Rerun with the sentence and finish changes: FINISH on turn 11 (37.9 minutes of model time) with the views of
|
|
94
|
+
Greek, English and many Native American peoples from a detailed source, all discoveries matching the page. Its
|
|
95
|
+
three page-match checks were condensed summaries of real sentences; the model repeated each and it went
|
|
96
|
+
through, at 37-61 s per check. Replaying the final-result step on that run added a "My view:" paragraph.
|
|
97
|
+
|
|
7
98
|
## [1.47] - 2026-10-16
|
|
8
99
|
A list task on 1.46 (three free, open-source alternatives to Microsoft Office and what each is best at)
|
|
9
100
|
finished on its own in 18 turns and 49 minutes of model time. LibreOffice and OnlyOffice were right, but the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localagentbrowser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.52
|
|
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
|
|
@@ -56,7 +56,7 @@ and tuned to work well with a **small 7B model on a CPU, no GPU required**.
|
|
|
56
56
|
- ⚡ **Built for slow hardware.** It reuses LM Studio's prompt cache, so most turns process only what changed.
|
|
57
57
|
- 📝 **A readable report every time.** `research_report.md` holds the final result and every finding with its
|
|
58
58
|
source, written however the run ends; `research_log.txt` keeps every action and reason.
|
|
59
|
-
- ✅ **Tested.**
|
|
59
|
+
- ✅ **Tested.** 112 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
|
|
60
60
|
plus live runs on seven different kinds of task (see [results](#results-on-different-kinds-of-tasks)).
|
|
61
61
|
|
|
62
62
|
📖 **The story behind it:** [Teaching a 7B model on a CPU to finish what it starts](https://dev.to/snowfxxx/teaching-a-7b-model-on-a-cpu-to-finish-what-it-starts-1amc),
|
|
@@ -184,14 +184,15 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
|
|
|
184
184
|
| Kind of task | Objective (abridged) | Version | Outcome |
|
|
185
185
|
|---|---|---|---|
|
|
186
186
|
| Documentation research | Setup commands and file layout for Playwright's test agents | 1.27 | Finished on its own in 13 turns (~36 min) with commands, roles and file layout |
|
|
187
|
-
| Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40 |
|
|
187
|
+
| Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40, 1.50 | 1.38–1.40: its most detailed findings (per-client setup, seed test, agent definition files), but it kept searching for schemas the docs do not contain and did not finish within an hour. 1.50: finished in 10 turns (25 min) with the official init command and file layout, but with thin findings and without saying the schemas were not found |
|
|
188
188
|
| Opinion from several sources | How spiritual traditions view cats, then form an opinion | 1.30 | Six traditions from two sources and a stated opinion, in 18 turns (37 min of model time) |
|
|
189
|
+
| Opinion from several sources | How at least three cultures or religions have viewed owls, then an opinion on why | 1.47–1.48 | Greek, English and many Native American views from one detailed source in 11 turns (38 min); 1.48 adds the opinion the result had left out and catches an invented sentence in a discovery |
|
|
189
190
|
| Factual question | Tallest mountain in Africa, height in meters and feet | 1.35, 1.40, 1.46 | Correct, cited answer in 4 turns (about 12 min of model time) in all three versions; before 1.35 it kept reading after finding it |
|
|
190
|
-
| Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min) |
|
|
191
|
+
| Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42, 1.50 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min); on 1.50 in 8 turns (25 min) |
|
|
191
192
|
| List of items | Three free, open-source Photoshop alternatives and the strengths of each | 1.37–1.38 | GIMP, Krita and Darktable with their strengths, all cited, in 9 turns (24 min); 1.37 listed a fourth and added an unrequested opinion, which 1.38 fixes |
|
|
192
|
-
| List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words |
|
|
193
|
+
| List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46, 1.51 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words; on 1.51: LibreOffice, OnlyOffice and Apache OpenOffice, all open source, in 7 turns (16 min) |
|
|
193
194
|
| Step-by-step how-to | Make a sourdough starter, with quantities and what to do each day | 1.36–1.37 | A clear plan in 7 turns (19 min), but it invented the first water amount; 1.37 stops guessing and flags unsupported numbers, and the agent then went looking for the missing details |
|
|
194
|
-
| Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44
|
|
195
|
+
| Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44, 1.51 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 recorded the guide's real steps but took 68 min and its result left one out. 1.51: every step from the page, in 7 turns (29 min), and it says that no care instructions beyond the first watering were found |
|
|
195
196
|
| Explanation of a topic | How photosynthesis works: inputs and outputs, both stages and where each takes place | 1.41–1.42 | A complete, cited explanation in 18 turns (45 min); 1.41 read four parts of a relevant page without recording them and had not finished after 49 min |
|
|
196
197
|
|
|
197
198
|
## How it works
|
|
@@ -229,8 +230,9 @@ flowchart LR
|
|
|
229
230
|
check: record something, or repeat the action to leave. Moving on to the next part of a page works the same
|
|
230
231
|
way, and the model must first list what the part it is on says about the objective (or "none").
|
|
231
232
|
- **Findings must match the page.** A finding that shares too few word pairs with the page text shown so far
|
|
232
|
-
is sent back once, asking for only what the page says in its own words
|
|
233
|
-
|
|
233
|
+
(or has a sentence that does) is sent back once, asking for only what the page says in its own words, and
|
|
234
|
+
one far from anything on the page is refused outright. A small model will otherwise "complete" a list from
|
|
235
|
+
its own knowledge when a page says "follow these 6 steps" and the steps come later.
|
|
234
236
|
- **Numbered findings.** Findings are numbered in the prompt and listed with every check, and the model names
|
|
235
237
|
the finding that covers each part of the objective ("covered by [2]") instead of just saying "covered".
|
|
236
238
|
- **Knowing when it's done.** After every new finding the model is asked whether the objective is now fully
|
|
@@ -325,10 +327,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
325
327
|
- **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
|
|
326
328
|
secondary sources or stays with one source too long, and results vary with the first pages a search turns
|
|
327
329
|
up; every finding is in `research_report.md` regardless.
|
|
328
|
-
- **Very strict objectives
|
|
329
|
-
|
|
330
|
-
and
|
|
331
|
-
|
|
330
|
+
- **Very strict objectives.** An objective that forbids finishing until everything is "fully validated" used
|
|
331
|
+
to keep the agent looking for something the web may not contain. On 1.50 it finished such an objective in 25
|
|
332
|
+
minutes, but with thin findings and without naming what it could not find, so check the result against the
|
|
333
|
+
objective.
|
|
332
334
|
- **Check the citations.** The final result is told to use only recorded findings, but a 7B model sometimes
|
|
333
335
|
puts the wrong number on a citation, adds a line no finding supports or leaves out a step, so verify a
|
|
334
336
|
claim against the listed findings before relying on it.
|
|
@@ -345,7 +347,7 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
345
347
|
- [x] Write the final result in a separate, rule-bound step and flag unsupported numbers (1.32–1.38)
|
|
346
348
|
- [x] Finish as soon as the objective is covered (1.35)
|
|
347
349
|
- [x] Get the model to weigh each part of a long page before reading on (1.42)
|
|
348
|
-
- [ ] Let very strict objectives end with the gaps named instead of searching indefinitely
|
|
350
|
+
- [ ] Let very strict objectives end with the gaps named instead of searching indefinitely (they finish since 1.50; naming the gaps is still open)
|
|
349
351
|
- [ ] Try more kinds of objectives (seven so far) and adjust the rules that don't generalise
|
|
350
352
|
- [ ] Make fresh prompts (the slowest kind of turn) cheaper still (31% cheaper since 1.34)
|
|
351
353
|
- [ ] Interacting with pages (clicking, forms) for tasks beyond reading
|
|
@@ -24,7 +24,7 @@ and tuned to work well with a **small 7B model on a CPU, no GPU required**.
|
|
|
24
24
|
- ⚡ **Built for slow hardware.** It reuses LM Studio's prompt cache, so most turns process only what changed.
|
|
25
25
|
- 📝 **A readable report every time.** `research_report.md` holds the final result and every finding with its
|
|
26
26
|
source, written however the run ends; `research_log.txt` keeps every action and reason.
|
|
27
|
-
- ✅ **Tested.**
|
|
27
|
+
- ✅ **Tested.** 112 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
|
|
28
28
|
plus live runs on seven different kinds of task (see [results](#results-on-different-kinds-of-tasks)).
|
|
29
29
|
|
|
30
30
|
📖 **The story behind it:** [Teaching a 7B model on a CPU to finish what it starts](https://dev.to/snowfxxx/teaching-a-7b-model-on-a-cpu-to-finish-what-it-starts-1amc),
|
|
@@ -152,14 +152,15 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
|
|
|
152
152
|
| Kind of task | Objective (abridged) | Version | Outcome |
|
|
153
153
|
|---|---|---|---|
|
|
154
154
|
| Documentation research | Setup commands and file layout for Playwright's test agents | 1.27 | Finished on its own in 13 turns (~36 min) with commands, roles and file layout |
|
|
155
|
-
| Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40 |
|
|
155
|
+
| Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40, 1.50 | 1.38–1.40: its most detailed findings (per-client setup, seed test, agent definition files), but it kept searching for schemas the docs do not contain and did not finish within an hour. 1.50: finished in 10 turns (25 min) with the official init command and file layout, but with thin findings and without saying the schemas were not found |
|
|
156
156
|
| Opinion from several sources | How spiritual traditions view cats, then form an opinion | 1.30 | Six traditions from two sources and a stated opinion, in 18 turns (37 min of model time) |
|
|
157
|
+
| Opinion from several sources | How at least three cultures or religions have viewed owls, then an opinion on why | 1.47–1.48 | Greek, English and many Native American views from one detailed source in 11 turns (38 min); 1.48 adds the opinion the result had left out and catches an invented sentence in a discovery |
|
|
157
158
|
| Factual question | Tallest mountain in Africa, height in meters and feet | 1.35, 1.40, 1.46 | Correct, cited answer in 4 turns (about 12 min of model time) in all three versions; before 1.35 it kept reading after finding it |
|
|
158
|
-
| Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min) |
|
|
159
|
+
| Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42, 1.50 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min); on 1.50 in 8 turns (25 min) |
|
|
159
160
|
| List of items | Three free, open-source Photoshop alternatives and the strengths of each | 1.37–1.38 | GIMP, Krita and Darktable with their strengths, all cited, in 9 turns (24 min); 1.37 listed a fourth and added an unrequested opinion, which 1.38 fixes |
|
|
160
|
-
| List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words |
|
|
161
|
+
| List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46, 1.51 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words; on 1.51: LibreOffice, OnlyOffice and Apache OpenOffice, all open source, in 7 turns (16 min) |
|
|
161
162
|
| Step-by-step how-to | Make a sourdough starter, with quantities and what to do each day | 1.36–1.37 | A clear plan in 7 turns (19 min), but it invented the first water amount; 1.37 stops guessing and flags unsupported numbers, and the agent then went looking for the missing details |
|
|
162
|
-
| Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44
|
|
163
|
+
| Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44, 1.51 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 recorded the guide's real steps but took 68 min and its result left one out. 1.51: every step from the page, in 7 turns (29 min), and it says that no care instructions beyond the first watering were found |
|
|
163
164
|
| Explanation of a topic | How photosynthesis works: inputs and outputs, both stages and where each takes place | 1.41–1.42 | A complete, cited explanation in 18 turns (45 min); 1.41 read four parts of a relevant page without recording them and had not finished after 49 min |
|
|
164
165
|
|
|
165
166
|
## How it works
|
|
@@ -197,8 +198,9 @@ flowchart LR
|
|
|
197
198
|
check: record something, or repeat the action to leave. Moving on to the next part of a page works the same
|
|
198
199
|
way, and the model must first list what the part it is on says about the objective (or "none").
|
|
199
200
|
- **Findings must match the page.** A finding that shares too few word pairs with the page text shown so far
|
|
200
|
-
is sent back once, asking for only what the page says in its own words
|
|
201
|
-
|
|
201
|
+
(or has a sentence that does) is sent back once, asking for only what the page says in its own words, and
|
|
202
|
+
one far from anything on the page is refused outright. A small model will otherwise "complete" a list from
|
|
203
|
+
its own knowledge when a page says "follow these 6 steps" and the steps come later.
|
|
202
204
|
- **Numbered findings.** Findings are numbered in the prompt and listed with every check, and the model names
|
|
203
205
|
the finding that covers each part of the objective ("covered by [2]") instead of just saying "covered".
|
|
204
206
|
- **Knowing when it's done.** After every new finding the model is asked whether the objective is now fully
|
|
@@ -293,10 +295,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
293
295
|
- **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
|
|
294
296
|
secondary sources or stays with one source too long, and results vary with the first pages a search turns
|
|
295
297
|
up; every finding is in `research_report.md` regardless.
|
|
296
|
-
- **Very strict objectives
|
|
297
|
-
|
|
298
|
-
and
|
|
299
|
-
|
|
298
|
+
- **Very strict objectives.** An objective that forbids finishing until everything is "fully validated" used
|
|
299
|
+
to keep the agent looking for something the web may not contain. On 1.50 it finished such an objective in 25
|
|
300
|
+
minutes, but with thin findings and without naming what it could not find, so check the result against the
|
|
301
|
+
objective.
|
|
300
302
|
- **Check the citations.** The final result is told to use only recorded findings, but a 7B model sometimes
|
|
301
303
|
puts the wrong number on a citation, adds a line no finding supports or leaves out a step, so verify a
|
|
302
304
|
claim against the listed findings before relying on it.
|
|
@@ -313,7 +315,7 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
313
315
|
- [x] Write the final result in a separate, rule-bound step and flag unsupported numbers (1.32–1.38)
|
|
314
316
|
- [x] Finish as soon as the objective is covered (1.35)
|
|
315
317
|
- [x] Get the model to weigh each part of a long page before reading on (1.42)
|
|
316
|
-
- [ ] Let very strict objectives end with the gaps named instead of searching indefinitely
|
|
318
|
+
- [ ] Let very strict objectives end with the gaps named instead of searching indefinitely (they finish since 1.50; naming the gaps is still open)
|
|
317
319
|
- [ ] Try more kinds of objectives (seven so far) and adjust the rules that don't generalise
|
|
318
320
|
- [ ] Make fresh prompts (the slowest kind of turn) cheaper still (31% cheaper since 1.34)
|
|
319
321
|
- [ ] Interacting with pages (clicking, forms) for tasks beyond reading
|
|
@@ -49,9 +49,15 @@ MAX_DISCOVERY_PROMPT_CHARS = 250 # Discoveries are shortened to this in fresh p
|
|
|
49
49
|
MAX_CHECK_DISCOVERY_CHARS = 150 # Shorter still in the numbered list that comes with the finish and progress checks
|
|
50
50
|
GROUNDED_PAIR_SHARE = 0.7 # A DISCOVER sharing fewer of its word pairs with the page text shown is confirmed first
|
|
51
51
|
MIN_GROUNDING_PAIRS = 20 # Shorter discoveries are not checked: a few connecting words sink their score
|
|
52
|
+
GROUNDED_SENTENCE_SHARE = 0.35 # Any one sentence matching the page text shown less than this is confirmed too
|
|
53
|
+
MIN_SENTENCE_PAIRS = 8 # Shorter sentences are not scored on their own
|
|
54
|
+
UNGROUNDED_PAIR_SHARE = 0.4 # Below this (whole) or the next (any sentence) a DISCOVER is refused outright
|
|
55
|
+
UNGROUNDED_SENTENCE_SHARE = 0.25
|
|
52
56
|
TURN_PAUSE_SECONDS = 1
|
|
53
57
|
STALL_TURNS = 4 # Turns without a new discovery before the model is asked to check its progress
|
|
54
58
|
COMPOSE_MAX_TOKENS = 900 # Length limit for the separately written final result
|
|
59
|
+
VIEW_MAX_TOKENS = 250 # Length limit for a 'My view:' paragraph added when the result left it out
|
|
60
|
+
OPINION_REQUEST = re.compile(r"\b(opinion|your (own )?view|recommend\w*|what do you think)\b", re.I)
|
|
55
61
|
OBJECTIVE_CHECK_EVERY = 8 # Turns between scheduled objective checks, which happen even while discoveries continue
|
|
56
62
|
READ_FIRST = ("First, if the CURRENT ENVIRONMENT (or an unread part of this page, via READ_MORE) contains information "
|
|
57
63
|
"relevant to the objective that is not yet in DISCOVERIES SO FAR, DISCOVER it now.")
|
|
@@ -88,7 +94,7 @@ Strictly follow these rules:
|
|
|
88
94
|
2. Never repeat a search query, and never NAVIGATE to the page you are already on. You may return to a previously visited URL when it still has information you need, at most 3 visits per URL. These limits are enforced automatically.
|
|
89
95
|
3. Prefer primary, authoritative sources over copies and summaries of them.
|
|
90
96
|
4. Gather only what the objective asks for. As soon as your discoveries cover every part of it, FINISH; do not keep reading for extra or merely interesting details.
|
|
91
|
-
5. Output ONLY raw JSON. No markdown wrappers, no backticks, no conversational filler."""
|
|
97
|
+
5. Output ONLY raw JSON. No markdown wrappers, no backticks, no conversational filler. Inside JSON strings, write every backslash twice (a Windows path C:\\Users\\me is written "C:\\\\Users\\\\me")."""
|
|
92
98
|
|
|
93
99
|
RESPONSE_FORMAT = {
|
|
94
100
|
"type": "json_schema",
|
|
@@ -416,13 +422,25 @@ def grounding_check(parameter, page_state, current_url, objective=""):
|
|
|
416
422
|
parts = page_state.get("parts") or []
|
|
417
423
|
if not (current_url and page_state.get("url") == normalize_url(current_url) and parts):
|
|
418
424
|
return None
|
|
419
|
-
if len(word_pairs(parameter)) < MIN_GROUNDING_PAIRS:
|
|
420
|
-
return None
|
|
421
425
|
# The objective's wording counts as well: "Mount Kilimanjaro is the tallest mountain in Africa..." echoed the
|
|
422
426
|
# question's "tallest" where the page says "highest", and was sent back although it was accurate
|
|
423
427
|
shown = [parts[i] for i in sorted(page_state.get("seen_parts", {0})) if i < len(parts)] + [objective]
|
|
424
|
-
|
|
428
|
+
long_enough = len(word_pairs(parameter)) >= MIN_GROUNDING_PAIRS
|
|
429
|
+
whole = grounded_share(parameter, shown) if long_enough else 1.0
|
|
430
|
+
# An invented sentence can hide behind copied ones: an opinion task's discovery was 75% copied, but its last
|
|
431
|
+
# sentence ("...depicted alongside other wise figures like the prophet Samuel") was not on the page (31%).
|
|
432
|
+
# Real sentences scored 40-100% against their page, invented ones 0-41%.
|
|
433
|
+
sentences = [s for s in re.split(r"(?<=[.!?])\s+|\n+", parameter) if len(word_pairs(s)) >= MIN_SENTENCE_PAIRS]
|
|
434
|
+
lowest = min((grounded_share(s, shown) for s in sentences), default=1.0)
|
|
435
|
+
if whole >= GROUNDED_PAIR_SHARE and lowest >= GROUNDED_SENTENCE_SHARE:
|
|
425
436
|
return None
|
|
437
|
+
# Far below anything real (whole discoveries 50%+, sentences 40%+), repeating does not confirm it: asked once,
|
|
438
|
+
# the model sent its invented repotting steps again (32% overall, one step 9%) and they went through
|
|
439
|
+
if whole < UNGROUNDED_PAIR_SHARE or lowest < UNGROUNDED_SENTENCE_SHARE:
|
|
440
|
+
return "BLOCKED_DISCOVER", ("That DISCOVER is mostly not in the page text you have been shown, so it comes from "
|
|
441
|
+
"your own knowledge. DISCOVER only what the parts shown actually say, in the page's own "
|
|
442
|
+
"wording. If what you need is not there, READ_MORE (a later part may have it) or look "
|
|
443
|
+
"elsewhere.")
|
|
426
444
|
# Only repeating the same DISCOVER confirms it: asked again, the model once sent a different text that was
|
|
427
445
|
# still invented ("water sparingly", which the page never said), and a per-part pass let it through
|
|
428
446
|
asked = page_state.setdefault("grounding_asked", [])
|
|
@@ -431,7 +449,7 @@ def grounding_check(parameter, page_state, current_url, objective=""):
|
|
|
431
449
|
return None
|
|
432
450
|
asked.append(mine)
|
|
433
451
|
# Pointed to READ_MORE as well, the model left a part holding the real materials list and steps unrecorded
|
|
434
|
-
return "CONFIRM_GROUNDED", (f"
|
|
452
|
+
return "CONFIRM_GROUNDED", (f"Some of that DISCOVER does not match the page text you have been shown, so part of it "
|
|
435
453
|
"seems to come from your own knowledge. DISCOVER again with only what the page actually "
|
|
436
454
|
f"says in part {page_state['index'] + 1} (shown now) or the parts before it, using the "
|
|
437
455
|
"page's own wording, for example its lists and steps. Do not add steps, amounts or "
|
|
@@ -470,7 +488,12 @@ def is_known_discovery(parameter, journal):
|
|
|
470
488
|
def read_more_target(parameter, page_state):
|
|
471
489
|
# The model sometimes writes "2 of 2" or "part 3"; the first number is the part it means
|
|
472
490
|
number = re.search(r'\d+', parameter)
|
|
473
|
-
|
|
491
|
+
target = int(number.group()) - 1 if number else page_state["index"] + 1
|
|
492
|
+
# Asking for the part already shown nearly always meant the next one ("READ_MORE 9" while on part 9 came up
|
|
493
|
+
# in most long-page runs, and refusing it cost a retry each time), so it is read as that, except on the last part
|
|
494
|
+
if target == page_state["index"] and target < len(page_state["parts"]) - 1:
|
|
495
|
+
target += 1
|
|
496
|
+
return target
|
|
474
497
|
|
|
475
498
|
def leave_check(action, page_state, current_url, journal=()):
|
|
476
499
|
# The model often left content pages without recording what it needed from them. The first attempt to
|
|
@@ -505,7 +528,9 @@ def read_on_check(page_state, current_url, target):
|
|
|
505
528
|
# asking it to DISCOVER relevant information first: on an explanation task it read four parts of a page that
|
|
506
529
|
# had the inputs, outputs and cell locations it needed and recorded them only when a progress check fired.
|
|
507
530
|
# Moving on from a part nothing was recorded from gets one cheap retry that makes the model name what the
|
|
508
|
-
# part says about the objective; repeating the READ_MORE then goes through.
|
|
531
|
+
# part says about the objective; repeating the READ_MORE then goes through. It also offers FINISH: with three
|
|
532
|
+
# cultures recorded where three were asked for, the check pushed an opinion task to keep collecting more, and
|
|
533
|
+
# it offers leaving: shown an unrelated "agent" project page, the model read on part after part.
|
|
509
534
|
index = page_state["index"]
|
|
510
535
|
# Only moving forward counts: going back to an earlier part is covered by the re-read check
|
|
511
536
|
if target <= index or index in page_state.get("recorded_parts", set()):
|
|
@@ -517,13 +542,16 @@ def read_on_check(page_state, current_url, target):
|
|
|
517
542
|
return "CONFIRM_READ_ON", (f"Before reading on: you have recorded nothing from part {index + 1}, which is shown now. "
|
|
518
543
|
"In thought_process, list each fact in it that serves the OBJECTIVE, or write 'none'. If any "
|
|
519
544
|
"of them are not in DISCOVERIES SO FAR, DISCOVER them now. If there are none, repeat your "
|
|
520
|
-
"READ_MORE."
|
|
545
|
+
"READ_MORE. But if DISCOVERIES SO FAR already cover every part of the OBJECTIVE, choose "
|
|
546
|
+
"FINISH instead of reading on, and if this page is not about the OBJECTIVE at all, leave "
|
|
547
|
+
"it with SEARCH or NAVIGATE.")
|
|
521
548
|
|
|
522
549
|
def gap_check(thought, tried):
|
|
523
550
|
# The model is asked to write 'not covered' for missing parts; on a comparison it wrote "Available Libraries:
|
|
524
551
|
# Not covered" and finished anyway. A FINISH whose own reasoning says that is sent back to work on the gap,
|
|
525
552
|
# at most GAP_REFUSAL_LIMIT times per run so a part that cannot be found does not block finishing for good
|
|
526
|
-
if "not covered" not in str(thought).lower() or tried.get("gap_refusals", 0) >= GAP_REFUSAL_LIMIT
|
|
553
|
+
if ("not covered" not in str(thought).lower() or tried.get("gap_refusals", 0) >= GAP_REFUSAL_LIMIT
|
|
554
|
+
or tried.get("search_exhausted")):
|
|
527
555
|
return None
|
|
528
556
|
tried["gap_refusals"] = tried.get("gap_refusals", 0) + 1
|
|
529
557
|
return ("BLOCKED_FINISH", "Your own check marks a part of the objective as 'not covered'. Do not finish yet: work on "
|
|
@@ -543,6 +571,10 @@ def check_decision(action, parameter, journal, tried, page_state, current_url, t
|
|
|
543
571
|
repeats = tried.setdefault("repeated_searches", {})
|
|
544
572
|
repeats[normalize_query(query)] = repeats.get(normalize_query(query), 0) + 1
|
|
545
573
|
if repeats[normalize_query(query)] >= 2:
|
|
574
|
+
# Told to finish with the gap, the model tried FINISH, was asked by the finish check to work on the
|
|
575
|
+
# uncovered part, and repeated the search again (five times on a strict objective); from here a
|
|
576
|
+
# FINISH goes straight through
|
|
577
|
+
tried["search_exhausted"] = True
|
|
546
578
|
return "BLOCKED_SEARCH", (f"You have now tried to repeat the search '{query}' "
|
|
547
579
|
f"{repeats[normalize_query(query)]} times. It was already done and did not find "
|
|
548
580
|
"what you need. Either SEARCH with clearly different words, or accept that it "
|
|
@@ -616,7 +648,7 @@ def finish_check(tried, journal, page_state=None, current_url=None):
|
|
|
616
648
|
# Asked to judge whether its discoveries "satisfy" the objective, a small model says yes too easily: on an
|
|
617
649
|
# opinion task it finished after one source and gave a summary instead of the opinion asked for. The first
|
|
618
650
|
# FINISH of each turn gets a cheap retry asking for a part-by-part check; repeating FINISH then goes through.
|
|
619
|
-
if tried.get("finish_confirmed"):
|
|
651
|
+
if tried.get("finish_confirmed") or tried.get("search_exhausted"):
|
|
620
652
|
return None
|
|
621
653
|
tried["finish_confirmed"] = True # Reset at the start of every turn
|
|
622
654
|
return "CONFIRM_FINISH", (f"{source_note(journal)}{unread_note(page_state or {}, current_url)}"
|
|
@@ -817,6 +849,19 @@ Active URL: {current_url if current_url else "None (Loop Initializing)"}
|
|
|
817
849
|
{"role": "user", "content": user_payload}
|
|
818
850
|
]
|
|
819
851
|
|
|
852
|
+
PATH_ESCAPES = {"\t": "\\t", "\r": "\\r", "\b": "\\b", "\f": "\\f"}
|
|
853
|
+
|
|
854
|
+
def restore_path_escapes(text):
|
|
855
|
+
"""Undoes JSON escapes the model did not mean: recording the Windows command "python -m venv
|
|
856
|
+
C:\\path\\to\\new\\virtual\\environment", it wrote single backslashes, and its \\t, \\n and \\r became a tab, a
|
|
857
|
+
newline and a carriage return. A tab, carriage return, backspace or form feed right before a letter is
|
|
858
|
+
treated as such a path; a newline is only restored where it sits between word characters (as in "to\\new").
|
|
859
|
+
Only text that also has a backslash or a drive letter is touched: page tables are read with real tabs."""
|
|
860
|
+
if not re.search(r"[\t\r\b\f][A-Za-z]", text) or not re.search(r"\\|\b[A-Za-z]:[\\\"\t\r\b\f\n]", text):
|
|
861
|
+
return text
|
|
862
|
+
text = re.sub(r"(?<=\w)\n(?=[a-z])", r"\\n", text)
|
|
863
|
+
return re.sub(r"[\t\r\b\f](?=[A-Za-z])", lambda m: PATH_ESCAPES[m.group()], text)
|
|
864
|
+
|
|
820
865
|
async def request_decision(client, messages, journal, tried, page_state, current_url):
|
|
821
866
|
# Cheap retries: a reply that is invalid or would be refused is answered with a short correction
|
|
822
867
|
# appended to the same conversation. Everything before the correction is identical to what LM Studio
|
|
@@ -842,6 +887,9 @@ async def request_decision(client, messages, journal, tried, page_state, current
|
|
|
842
887
|
action_decision = json.loads(raw_content)
|
|
843
888
|
if not isinstance(action_decision, dict):
|
|
844
889
|
raise ValueError("the reply was JSON but not an object")
|
|
890
|
+
for key in ("parameter", "thought_process"):
|
|
891
|
+
if isinstance(action_decision.get(key), str):
|
|
892
|
+
action_decision[key] = restore_path_escapes(action_decision[key])
|
|
845
893
|
except (json.JSONDecodeError, ValueError) as parse_err:
|
|
846
894
|
json_failures += 1
|
|
847
895
|
print(f"⚠️ Model returned invalid JSON (attempt {json_failures}/{MAX_JSON_RETRIES}): {raw_content[:200]}")
|
|
@@ -883,7 +931,7 @@ def unverified_numbers(text, discoveries):
|
|
|
883
931
|
A small model writing a how-to filled a gap with an invented quantity ("45 grams of lukewarm water (about
|
|
884
932
|
1/4 cup)"); numbers are where such errors do the most harm and the easiest place to check them.
|
|
885
933
|
"""
|
|
886
|
-
text = re.sub(r"\[\d+(?:\s*[
|
|
934
|
+
text = re.sub(r"\[\d+(?:\s*[-–,;]\s*\d+)*\]", " ", str(text)) # Citation markers: [2], [4, 5], [1-4]
|
|
887
935
|
text = re.sub(r"(?m)^\s*\d+[.)]\s", " ", text) # Numbered list items
|
|
888
936
|
known = {n.replace(",", "") for e in discoveries for n in NUMBER_PATTERN.findall(e["extracted_data"])}
|
|
889
937
|
unverified = []
|
|
@@ -937,7 +985,8 @@ async def compose_final_result(client, objective, journal):
|
|
|
937
985
|
"the question, write the summary or the plan, or give the opinion). Rules:\n"
|
|
938
986
|
"1. Use only the information in the DISCOVERIES. Do not fill gaps from your own knowledge or with "
|
|
939
987
|
"guesses: no amounts, numbers, steps or advice that the discoveries do not contain. Where a detail the "
|
|
940
|
-
"objective asks for is missing, say that it was not found
|
|
988
|
+
"objective asks for is missing, say that it was not found. Copy commands, code, file paths and "
|
|
989
|
+
"names exactly as the discoveries give them.\n"
|
|
941
990
|
"2. A discovery about one source's own situation (for example a company's workload in a case study) "
|
|
942
991
|
"describes that situation, not the one in the OBJECTIVE: say whose situation it is, and do not assume "
|
|
943
992
|
"details of the OBJECTIVE's situation that it does not state.\n"
|
|
@@ -959,7 +1008,21 @@ async def compose_final_result(client, objective, journal):
|
|
|
959
1008
|
text = str(reply.get("parameter") or reply.get("thought_process") or text)
|
|
960
1009
|
except (json.JSONDecodeError, AttributeError):
|
|
961
1010
|
pass
|
|
962
|
-
|
|
1011
|
+
text = text.strip()
|
|
1012
|
+
# Asked to "give your own opinion" about owls, the result listed the traditions and ended without one,
|
|
1013
|
+
# although rule 6 asks for a 'My view:' paragraph whenever the objective wants a view. A short second call
|
|
1014
|
+
# writes only that paragraph, from the result just written
|
|
1015
|
+
if OPINION_REQUEST.search(objective) and "my view:" not in text.lower():
|
|
1016
|
+
view = await client.chat.completions.create(model=MODEL, temperature=0.3, max_tokens=VIEW_MAX_TOKENS, messages=[
|
|
1017
|
+
{"role": "system", "content": COMPOSE_SYSTEM},
|
|
1018
|
+
{"role": "user", "content": f"### OBJECTIVE\n{objective}\n\n### RESULT\n{text}\n\nThe OBJECTIVE asks for your "
|
|
1019
|
+
"own view, opinion or recommendation, and the RESULT does not give it yet. Write "
|
|
1020
|
+
"only that, as one short paragraph that starts with 'My view:', based on the "
|
|
1021
|
+
"RESULT. Plain text."}])
|
|
1022
|
+
paragraph = extract_message_content(view).strip()
|
|
1023
|
+
if paragraph:
|
|
1024
|
+
text += "\n\n" + (paragraph if paragraph.lower().startswith("my view:") else f"My view: {paragraph}")
|
|
1025
|
+
return text
|
|
963
1026
|
|
|
964
1027
|
async def load_page(page, url=None):
|
|
965
1028
|
"""Loads a URL (or reloads the page) and returns Playwright's response, which may be None."""
|
|
@@ -1130,13 +1193,16 @@ async def main():
|
|
|
1130
1193
|
page_state.setdefault("recorded_parts", set()).add(page_state["index"])
|
|
1131
1194
|
# A discovery is when the objective can become complete; on a one-answer question the model
|
|
1132
1195
|
# recorded the full answer, said "the objective has been met", then kept reading for
|
|
1133
|
-
# "interesting" details, so completion is checked right here
|
|
1196
|
+
# "interesting" details, so completion is checked right here. On an explanation task it recorded
|
|
1197
|
+
# the Calvin cycle step by step while the light reactions were still missing, so the note also
|
|
1198
|
+
# points to the parts not yet covered
|
|
1134
1199
|
number = sum(1 for e in journal if e.get("action") == "DISCOVER")
|
|
1135
1200
|
set_note(conversation, f"RESULT: Your DISCOVER was recorded as discovery [{number}] in DISCOVERIES SO FAR. "
|
|
1136
1201
|
f"{where_you_are(page_state, current_url)} The page is unchanged. "
|
|
1137
1202
|
"Do not record the same information again. If DISCOVERIES SO FAR now cover "
|
|
1138
1203
|
"every part of the OBJECTIVE, choose FINISH now; do not read further for extra "
|
|
1139
|
-
"or merely interesting details.
|
|
1204
|
+
"or merely interesting details. If some parts are still not covered, work on "
|
|
1205
|
+
"those next instead of adding more detail to parts already covered.", current_url)
|
|
1140
1206
|
|
|
1141
1207
|
# A single refusal gets a short warning; repeated refusals get an explicit list of other options
|
|
1142
1208
|
last_entry = journal[-1]["action"] if journal else ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: localagentbrowser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.52
|
|
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
|
|
@@ -56,7 +56,7 @@ and tuned to work well with a **small 7B model on a CPU, no GPU required**.
|
|
|
56
56
|
- ⚡ **Built for slow hardware.** It reuses LM Studio's prompt cache, so most turns process only what changed.
|
|
57
57
|
- 📝 **A readable report every time.** `research_report.md` holds the final result and every finding with its
|
|
58
58
|
source, written however the run ends; `research_log.txt` keeps every action and reason.
|
|
59
|
-
- ✅ **Tested.**
|
|
59
|
+
- ✅ **Tested.** 112 tests on Linux and Windows, Python 3.10–3.13, run in about a second with no model or browser,
|
|
60
60
|
plus live runs on seven different kinds of task (see [results](#results-on-different-kinds-of-tasks)).
|
|
61
61
|
|
|
62
62
|
📖 **The story behind it:** [Teaching a 7B model on a CPU to finish what it starts](https://dev.to/snowfxxx/teaching-a-7b-model-on-a-cpu-to-finish-what-it-starts-1amc),
|
|
@@ -184,14 +184,15 @@ does not generalise; the [changelog](https://github.com/SnowFXXX/LocalAgentBrows
|
|
|
184
184
|
| Kind of task | Objective (abridged) | Version | Outcome |
|
|
185
185
|
|---|---|---|---|
|
|
186
186
|
| Documentation research | Setup commands and file layout for Playwright's test agents | 1.27 | Finished on its own in 13 turns (~36 min) with commands, roles and file layout |
|
|
187
|
-
| Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40 |
|
|
187
|
+
| Documentation research, strict | The same, but "do not FINISH until fully validated", including "parameter schemas" | 1.38–1.40, 1.50 | 1.38–1.40: its most detailed findings (per-client setup, seed test, agent definition files), but it kept searching for schemas the docs do not contain and did not finish within an hour. 1.50: finished in 10 turns (25 min) with the official init command and file layout, but with thin findings and without saying the schemas were not found |
|
|
188
188
|
| Opinion from several sources | How spiritual traditions view cats, then form an opinion | 1.30 | Six traditions from two sources and a stated opinion, in 18 turns (37 min of model time) |
|
|
189
|
+
| Opinion from several sources | How at least three cultures or religions have viewed owls, then an opinion on why | 1.47–1.48 | Greek, English and many Native American views from one detailed source in 11 turns (38 min); 1.48 adds the opinion the result had left out and catches an invented sentence in a discovery |
|
|
189
190
|
| Factual question | Tallest mountain in Africa, height in meters and feet | 1.35, 1.40, 1.46 | Correct, cited answer in 4 turns (about 12 min of model time) in all three versions; before 1.35 it kept reading after finding it |
|
|
190
|
-
| Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min) |
|
|
191
|
+
| Comparison and recommendation | Python vs Rust for command-line tools; recommend one for a beginner. Later: SQLite vs PostgreSQL for a small web app, then a recommendation for a hobby project | 1.36, 1.42, 1.50 | Python vs Rust: cited comparison recommending Python in 16 turns (33 min), Rust's libraries not covered. SQLite vs PostgreSQL: all three parts covered and cited, with a recommendation, in 13 turns (38 min); on 1.50 in 8 turns (25 min) |
|
|
191
192
|
| List of items | Three free, open-source Photoshop alternatives and the strengths of each | 1.37–1.38 | GIMP, Krita and Darktable with their strengths, all cited, in 9 turns (24 min); 1.37 listed a fourth and added an unrequested opinion, which 1.38 fixes |
|
|
192
|
-
| List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words |
|
|
193
|
+
| List of items | Three free, open-source alternatives to Microsoft Office and what each is best at | 1.46, 1.51 | LibreOffice and OnlyOffice with their strengths, cited, in 18 turns (49 min); the third, FreeOffice, is free but not open source, and LibreOffice was recorded four times in different words; on 1.51: LibreOffice, OnlyOffice and Apache OpenOffice, all open source, in 7 turns (16 min) |
|
|
193
194
|
| Step-by-step how-to | Make a sourdough starter, with quantities and what to do each day | 1.36–1.37 | A clear plan in 7 turns (19 min), but it invented the first water amount; 1.37 stops guessing and flags unsupported numbers, and the agent then went looking for the missing details |
|
|
194
|
-
| Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44
|
|
195
|
+
| Step-by-step how-to | Repot a houseplant: what you need, the steps, and care for the first two weeks | 1.43–1.44, 1.51 | 1.43 recorded six steps it wrote itself before reading the part that had them; 1.44 recorded the guide's real steps but took 68 min and its result left one out. 1.51: every step from the page, in 7 turns (29 min), and it says that no care instructions beyond the first watering were found |
|
|
195
196
|
| Explanation of a topic | How photosynthesis works: inputs and outputs, both stages and where each takes place | 1.41–1.42 | A complete, cited explanation in 18 turns (45 min); 1.41 read four parts of a relevant page without recording them and had not finished after 49 min |
|
|
196
197
|
|
|
197
198
|
## How it works
|
|
@@ -229,8 +230,9 @@ flowchart LR
|
|
|
229
230
|
check: record something, or repeat the action to leave. Moving on to the next part of a page works the same
|
|
230
231
|
way, and the model must first list what the part it is on says about the objective (or "none").
|
|
231
232
|
- **Findings must match the page.** A finding that shares too few word pairs with the page text shown so far
|
|
232
|
-
is sent back once, asking for only what the page says in its own words
|
|
233
|
-
|
|
233
|
+
(or has a sentence that does) is sent back once, asking for only what the page says in its own words, and
|
|
234
|
+
one far from anything on the page is refused outright. A small model will otherwise "complete" a list from
|
|
235
|
+
its own knowledge when a page says "follow these 6 steps" and the steps come later.
|
|
234
236
|
- **Numbered findings.** Findings are numbered in the prompt and listed with every check, and the model names
|
|
235
237
|
the finding that covers each part of the objective ("covered by [2]") instead of just saying "covered".
|
|
236
238
|
- **Knowing when it's done.** After every new finding the model is asked whether the objective is now fully
|
|
@@ -325,10 +327,10 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
325
327
|
- **A small model's judgement is the ceiling.** It sometimes drifts to loosely related pages, prefers
|
|
326
328
|
secondary sources or stays with one source too long, and results vary with the first pages a search turns
|
|
327
329
|
up; every finding is in `research_report.md` regardless.
|
|
328
|
-
- **Very strict objectives
|
|
329
|
-
|
|
330
|
-
and
|
|
331
|
-
|
|
330
|
+
- **Very strict objectives.** An objective that forbids finishing until everything is "fully validated" used
|
|
331
|
+
to keep the agent looking for something the web may not contain. On 1.50 it finished such an objective in 25
|
|
332
|
+
minutes, but with thin findings and without naming what it could not find, so check the result against the
|
|
333
|
+
objective.
|
|
332
334
|
- **Check the citations.** The final result is told to use only recorded findings, but a 7B model sometimes
|
|
333
335
|
puts the wrong number on a citation, adds a line no finding supports or leaves out a step, so verify a
|
|
334
336
|
claim against the listed findings before relying on it.
|
|
@@ -345,7 +347,7 @@ turn that brings in a new page takes a few minutes. See [Performance](#performan
|
|
|
345
347
|
- [x] Write the final result in a separate, rule-bound step and flag unsupported numbers (1.32–1.38)
|
|
346
348
|
- [x] Finish as soon as the objective is covered (1.35)
|
|
347
349
|
- [x] Get the model to weigh each part of a long page before reading on (1.42)
|
|
348
|
-
- [ ] Let very strict objectives end with the gaps named instead of searching indefinitely
|
|
350
|
+
- [ ] Let very strict objectives end with the gaps named instead of searching indefinitely (they finish since 1.50; naming the gaps is still open)
|
|
349
351
|
- [ ] Try more kinds of objectives (seven so far) and adjust the rules that don't generalise
|
|
350
352
|
- [ ] Make fresh prompts (the slowest kind of turn) cheaper still (31% cheaper since 1.34)
|
|
351
353
|
- [ ] Interacting with pages (clicking, forms) for tasks beyond reading
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "localagentbrowser"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.52"
|
|
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"
|
|
@@ -132,7 +132,8 @@ class TestReadOnCheck:
|
|
|
132
132
|
page = unrecorded_page(DOCS)
|
|
133
133
|
first = decide("READ_MORE", "", page, DOCS)
|
|
134
134
|
assert first[0] == "CONFIRM_READ_ON" and "recorded nothing from part 1" in first[1]
|
|
135
|
-
assert "write 'none'" in first[1]
|
|
135
|
+
assert "write 'none'" in first[1] and "choose FINISH instead of reading on" in first[1]
|
|
136
|
+
assert "not about the OBJECTIVE at all, leave it" in first[1]
|
|
136
137
|
assert decide("READ_MORE", "", page, DOCS) is None, "repeating it goes through"
|
|
137
138
|
|
|
138
139
|
def test_no_check_after_recording_from_the_shown_part(self):
|
|
@@ -156,10 +157,11 @@ class TestReadOnCheck:
|
|
|
156
157
|
page.update(index=4, seen_parts={0, 1, 2, 3, 4}, reread_confirmed={2})
|
|
157
158
|
assert decide("READ_MORE", "3", page, DOCS) is None
|
|
158
159
|
|
|
159
|
-
def
|
|
160
|
-
|
|
161
|
-
assert
|
|
160
|
+
def test_asking_for_the_part_already_shown_means_the_next_one(self):
|
|
161
|
+
assert ab.read_more_target("1", unrecorded_page(DOCS)) == 1
|
|
162
|
+
assert decide("READ_MORE", "1", recorded_page(DOCS, parts=("a", "b")), DOCS) is None
|
|
162
163
|
last = recorded_page(DOCS, parts=("a", "b"), index=1)
|
|
164
|
+
assert ab.read_more_target("2", last) == 1, "on the last part it stays a request for that part"
|
|
163
165
|
assert "last part" in decide("READ_MORE", "2", last, DOCS)[1]
|
|
164
166
|
|
|
165
167
|
|
|
@@ -248,14 +250,13 @@ class TestGroundingCheck:
|
|
|
248
250
|
page = unrecorded_page(GUIDE, parts=(self.PAGE, "Step 1: Water the plant the day before."))
|
|
249
251
|
return page
|
|
250
252
|
|
|
251
|
-
def
|
|
253
|
+
def test_a_discover_the_page_does_not_say_is_refused_even_when_repeated(self):
|
|
252
254
|
page = self.page()
|
|
253
255
|
invented = ("1. Gently remove the plant from its pot. 2. Loosen any tangled roots and trim dead ones. "
|
|
254
256
|
"3. Place fresh potting mix in the bottom of the new pot.")
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
assert decide("DISCOVER", invented, page, GUIDE) is None, "repeating it goes through"
|
|
257
|
+
for _ in range(2):
|
|
258
|
+
result = decide("DISCOVER", invented, page, GUIDE)
|
|
259
|
+
assert result[0] == "BLOCKED_DISCOVER" and "comes from your own knowledge" in result[1]
|
|
259
260
|
|
|
260
261
|
def test_a_discover_copied_from_the_page_passes(self):
|
|
261
262
|
copied = "Choose a pot one to two inches wider than the current one, with drainage holes."
|
|
@@ -264,10 +265,10 @@ class TestGroundingCheck:
|
|
|
264
265
|
def test_short_discoveries_are_not_checked(self):
|
|
265
266
|
assert decide("DISCOVER", "Use a bigger pot.", self.page(), GUIDE) is None
|
|
266
267
|
|
|
267
|
-
def
|
|
268
|
+
def test_real_advice_mixed_with_invented_steps_is_refused(self):
|
|
268
269
|
mixed = ("Choose a pot one to two inches wider than the current one, with drainage holes. The steps are: "
|
|
269
270
|
"loosen and inspect the roots, prune any dead roots, then fill with fresh potting mix.")
|
|
270
|
-
assert decide("DISCOVER", mixed, self.page(), GUIDE)[0] == "
|
|
271
|
+
assert decide("DISCOVER", mixed, self.page(), GUIDE)[0] == "BLOCKED_DISCOVER"
|
|
271
272
|
|
|
272
273
|
|
|
273
274
|
def test_recorded_parts_are_remembered_across_visits(monkeypatch):
|
|
@@ -290,15 +291,14 @@ def test_recorded_parts_are_remembered_across_visits(monkeypatch):
|
|
|
290
291
|
assert decide("READ_MORE", "", state, GUIDE) is None, "part 1 was recorded on the earlier visit"
|
|
291
292
|
|
|
292
293
|
|
|
293
|
-
def
|
|
294
|
+
def test_a_different_ungrounded_discover_is_refused_again():
|
|
294
295
|
page = TestGroundingCheck().page()
|
|
295
296
|
first = ("1. Gently remove the plant from its pot. 2. Loosen any tangled roots and trim dead ones before you "
|
|
296
297
|
"start. 3. Place fresh potting mix in the bottom of the new pot.")
|
|
297
298
|
second = ("During the first two weeks water your plant sparingly and avoid fertilizing it until it settles in. "
|
|
298
299
|
"Keep it out of direct sun and check the soil every few days.")
|
|
299
|
-
assert decide("DISCOVER", first, page, GUIDE)[0] == "
|
|
300
|
-
assert decide("DISCOVER", second, page, GUIDE)[0] == "
|
|
301
|
-
assert decide("DISCOVER", second, page, GUIDE) is None, "repeating the same one goes through"
|
|
300
|
+
assert decide("DISCOVER", first, page, GUIDE)[0] == "BLOCKED_DISCOVER"
|
|
301
|
+
assert decide("DISCOVER", second, page, GUIDE)[0] == "BLOCKED_DISCOVER"
|
|
302
302
|
|
|
303
303
|
|
|
304
304
|
def test_the_objectives_wording_counts_as_grounded():
|
|
@@ -311,3 +311,24 @@ def test_the_objectives_wording_counts_as_grounded():
|
|
|
311
311
|
page["grounding_asked"] = []
|
|
312
312
|
tried["objective"] = "What is the tallest mountain in Africa, and how tall is it in meters and in feet?"
|
|
313
313
|
assert ab.check_decision("DISCOVER", finding, [], tried, page, DOCS) is None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_an_invented_sentence_behind_copied_ones_is_confirmed():
|
|
317
|
+
page = unrecorded_page(GUIDE, parts=(
|
|
318
|
+
"Owls have played a crucial role in Native American cultures since time immemorial, with sightings often "
|
|
319
|
+
"associated with shapeshifters, spiritual messages and supernatural signs. In Christianity, owls symbolize "
|
|
320
|
+
"wisdom and appear in several places.",))
|
|
321
|
+
finding = ("Owls have played a crucial role in Native American cultures since time immemorial, with sightings often "
|
|
322
|
+
"associated with shapeshifters, spiritual messages and supernatural signs. In Christianity, owls symbolize "
|
|
323
|
+
"wisdom and are often depicted alongside other wise figures like the prophet Samuel.")
|
|
324
|
+
first = decide("DISCOVER", finding, page, GUIDE)
|
|
325
|
+
assert first[0] == "CONFIRM_GROUNDED" and "DISCOVER again with only what the page actually says in part 1" in first[1]
|
|
326
|
+
assert decide("DISCOVER", finding, page, GUIDE) is None, "repeating a borderline one goes through"
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_finish_goes_through_once_a_search_is_exhausted():
|
|
330
|
+
tried = {"queries": ["schemas"], "urls": []}
|
|
331
|
+
page = recorded_page(DOCS)
|
|
332
|
+
for _ in range(2):
|
|
333
|
+
assert ab.check_decision("SEARCH", "schemas", [], tried, page, DOCS)[0] == "BLOCKED_SEARCH"
|
|
334
|
+
assert ab.check_decision("FINISH", "result", [], tried, page, DOCS, "Schemas: not covered") is None
|
|
@@ -55,4 +55,14 @@ def test_old_entries_are_dropped_in_batches():
|
|
|
55
55
|
|
|
56
56
|
def test_unverified_numbers_ignores_grouped_citations():
|
|
57
57
|
found = [{"extracted_data": "LibreOffice offers good compatibility."}]
|
|
58
|
-
assert ab.unverified_numbers("LibreOffice has good compatibility [4, 5] and more [2;3].", found) == []
|
|
58
|
+
assert ab.unverified_numbers("LibreOffice has good compatibility [4, 5] and more [2;3] [1-4].", found) == []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_restore_path_escapes():
|
|
62
|
+
# What json.loads makes of "C:\path\to\new\real" written with single backslashes
|
|
63
|
+
mangled = "python -m venv C:\\path" + "\t" + "o" + "\n" + "ew" + "\r" + "eal"
|
|
64
|
+
assert ab.restore_path_escapes(mangled) == r"python -m venv C:\path\to\new\real"
|
|
65
|
+
assert ab.restore_path_escapes("First line.\nSecond line.") == "First line.\nSecond line.", "real newlines stay"
|
|
66
|
+
row = "Medium plants\t4-5 inches\tChoose a pot just 1-2 inches wider"
|
|
67
|
+
assert ab.restore_path_escapes(row) == row, "tabs from a page table stay"
|
|
68
|
+
assert ab.restore_path_escapes("C:" + "\t" + "emp") == r"C:\temp"
|
|
@@ -93,12 +93,25 @@ class TestComposeFinalResult:
|
|
|
93
93
|
assert "unless the objective explicitly asks for your view" in user["content"] and "Do not fill gaps" in user["content"]
|
|
94
94
|
assert "give exactly that many" in user["content"]
|
|
95
95
|
assert "describes that situation, not the one in the OBJECTIVE" in user["content"]
|
|
96
|
+
assert "Copy commands, code, file paths and names exactly" in user["content"]
|
|
96
97
|
assert "DRAFT" not in user["content"], "the agent's unverified draft is not shown"
|
|
97
98
|
|
|
98
99
|
def test_unwraps_a_json_reply(self):
|
|
99
100
|
client, _ = fake_client([reply("FINISH", "My view: supported by [1].")])
|
|
100
101
|
assert asyncio.run(ab.compose_final_result(client, "objective", discoveries("x"))) == "My view: supported by [1]."
|
|
101
102
|
|
|
103
|
+
def test_adds_a_missing_view_when_the_objective_asks_for_one(self):
|
|
104
|
+
client, model = fake_client(["Owls mean wisdom in Greece [1].", "Their night habits invite mystery."])
|
|
105
|
+
text = asyncio.run(ab.compose_final_result(client, "Find views on owls, then give your own opinion.",
|
|
106
|
+
discoveries("Greece: wisdom")))
|
|
107
|
+
assert text == "Owls mean wisdom in Greece [1].\n\nMy view: Their night habits invite mystery."
|
|
108
|
+
assert "does not give it yet" in model.calls[1][-1]["content"]
|
|
109
|
+
|
|
110
|
+
def test_no_extra_call_when_no_view_is_asked_for(self):
|
|
111
|
+
client, model = fake_client(["Three items [1]."])
|
|
112
|
+
asyncio.run(ab.compose_final_result(client, "Name three items.", discoveries("x")))
|
|
113
|
+
assert len(model.calls) == 1
|
|
114
|
+
|
|
102
115
|
|
|
103
116
|
class TestConversation:
|
|
104
117
|
def test_turns_continue_one_conversation(self, scrapes):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{localagentbrowser-1.47 → localagentbrowser-1.52}/localagentbrowser.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{localagentbrowser-1.47 → localagentbrowser-1.52}/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
|