Skip to content

Latest commit

 

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

browsergraph

CI License: MIT Python 3.10+ Core deps: none Tests Studio Kaggle

Write a browser automation once. Run it on any engine — or on none.

Playwright, Patchright, Selenium, undetected-chromedriver, SeleniumBase, nodriver, zendriver, pydoll, Botasaurus, rebrowser, Camoufox — or engine=http, which fetches with a real browser's TLS fingerprint and no browser at all. Same graph, no code change.

from browsergraph import Engine, Graph, Spec, run
from browsergraph.drivers import build
from browsergraph.nodes.actions import Click, Extract, Navigate, WaitFor

graph = (Graph("quote")
         .add(Navigate("https://example.com"))
         .add(WaitFor("#quote"))
         .add(Click("#quote"))
         .add(WaitFor("#result", name="confirm"))   # <- proves the click landed
         .add(Extract("#result", into="quote")))

spec = Spec(engine=Engine.PLAYWRIGHT)               # swap to HTTP, SELENIUM, ...
print(run(graph, spec, build(spec)).context.data)

Install

pip install "browsergraph @ git+https://github.com/aidonerightcorp/browsergraph.git"
browsergraph doctor           # what works here, and the command to fix what doesn't
browsergraph bootstrap        # only if you want a browser; the core needs none

To pin, add @v0.4.0 — or take the wheel and its checksums from the release.

v0.4.0 is the first release that matches this page. The v0.3.0 wheel predates execute, solve, viz, templates, explore, journal, bounded and quick — most of what is described below — so following the old install line gave you a build that could not run the examples on it. If you pinned 0.3.0, upgrade; CHANGELOG.md lists what changed and what was broken.

The wheel has no dependencies at all — installing it into an empty environment pulls exactly one package. Every engine, OCR backend and plotting library is an optional extra, so a graph can be built, drawn, linted, compiled and mock-run with nothing else installed.

With an engine, if you want one:

pip install "browsergraph[playwright] @ git+https://github.com/aidonerightcorp/browsergraph.git"

A job is a graph, and the graph is the point

Steps say what has to happen. Candidates say what could do each one. A route picks one candidate per step, and the picture is what tells you two of these steps are independent:

graph LR
  load["Load the table<br/>2 options"]
  numeric["Encode the numbers<br/>2 options"]
  categorical["Encode the categories<br/>2 options"]
  fit["Fit the model<br/>2 options"]
  load --> numeric
  load --> categorical
  numeric -->|numeric| fit
  categorical -->|categorical| fit
Loading

numeric and categorical sit side by side because nothing connects them — so they can run at once, and they fail separately. That is derived from the ports, not asserted by whoever drew it. Sixteen routes through four steps, and the two arrows into fit each carry the name of the port they land on, because an unlabelled join is the bug this repository started with.

from browsergraph import demo, viz
bench = demo.tabular()
viz.dag(bench)                  # the same thing as SVG, in a notebook

Or without writing any Python at all:

browsergraph draw  job.json -o job.html                    # every figure, one file
browsergraph solve job.json --runtime mine:RUNTIME --stage extract
browsergraph packs                                          # domains already written
browsergraph packs tabular --solve                          # ...and run one

solve tries routes, runs them, judges the output, and hands back the best one plus a fallback. It refuses to run until you say what a good answer looks like — the default would be "nothing raised", and a route that returns an empty result passes that with full marks. The full command list is in AGENTS.md.

Is my problem one of the known shapes?

Engineering pipelines are not infinitely various. Backend request handling, front-end analytics, cleaning, geographic and temporal enrichment, model training, synthetic data, LLM harnesses — most of it falls into about forty shapes, and the shapes repeat across domains with nothing else in common. docs/PIPELINE_TAXONOMY.md is the map: 41 categories in 9 families, classified by shape and failure mode rather than by subject, because that is the classification that helps.

browsergraph taxonomy --search address    # which category is this?
browsergraph taxonomy enrich.geo          # its shape, and how it fails
browsergraph taxonomy --coverage          # what has code, what does not

Every category records how it fails while reporting success, and that field is the load-bearing one — a category with no characteristic silent failure is a topic with a nice name, and a test refuses to let one exist. Coverage is counted from the registries on every run: 41 categories have a checkable shape, 13 have code that runs, and the 28 gaps are listed rather than rounded up.

Audit the judge before you believe the dashboard

If you ship a feature built on a language model you almost certainly have a judge. It probably produces a number every day, and it has probably never been checked against a person.

python -m assay.cli judge --demo
python -m assay.cli judge --data labels.csv --model verdict --human truth
JUDGE AUDIT — FAIL

BELOW_CHANCE — kappa -0.207 on 120 items
  raw agreement 65.0%, but chance alone gives 71.0% on this distribution. The
  judge agrees with people less often than a coin weighted to the commonest
  answer would.

RUBRIC-DEPENDENT — kappa spans 0.694 across 3 rubrics
  +0.487  is anything in it false
  +0.000  rate the overall quality
  -0.207  did it answer the question asked

FAIL  length
  score and answer length correlate at rho +0.95

Four ways a judge is wrong while looking right: it agrees at chance, it scores length, it scores position, or the rubric decides instead of the input. All four fall out of one realistic mechanism in the demo, and every number above is printed by the command rather than typed into this page.

CANNOT_CHECK is a verdict, not an error. A single-class human sample cannot validate a judge, and reporting that as the judge's fault convicts it of the evaluation's own sampling.

assay now lives in its own repository — github.com/aidonerightcorp/assay is the canonical home. The copy in this tree is vendored and frozen so the wheel stays self-contained and the published notebooks keep working; from browsergraph import duecare and from browsergraph import taxonomy still mean the same objects and will keep doing so.

Example 14 · assay/README.md · canonical repo

Controls, and evaluations you could defend

The obligations of an evaluation are usually habits. assay.obligations makes them values: nine obligations, each discharged with evidence, waived with a stated reason, failed, or visibly outstanding — and a verdict computed while a blocking obligation is outstanding is PROVISIONAL, never a pass.

from assay import obligations          # was browsergraph.duecare; both work

ledger = obligations.Ledger.standard()
ledger.discharge("holdout", "cases 0-199 were never used in development")
ledger.record(obligations.check_negative_control(real=0.91, broken=0.89))
print(ledger.verdict(0.91).text())
# FAIL — 0.910  [50ed4cc56a8c]
#   failed:      negative_control — a deliberately broken variant scored 0.890
#                against 0.910: a gap of 0.020 does not clear 0.050. This
#                harness cannot separate the system from a broken one, so its
#                previous results do not mean what they appeared to

Three states, and the middle one is the point. PASS — every blocking obligation met. PROVISIONAL — the number exists and nobody may act on it yet; this is the honest description of most evaluations. FAIL — a check ran and came back no, which is worse news than not having run it, because it means the previous results were noise.

"Could not check" is a fourth thing and is kept distinct from "checked and failed": a single-class human sample cannot validate a grader, and reporting that as the grader's fault would convict it of the evaluation's own sampling.

A waiver needs a reasonwaive() refuses an empty one — and waivers appear in the report as prominently as discharges. The ledger's digest is a hash of the standard rather than the score, so obligations.compare() will tell you two numbers are not comparable when the standard slipped between them.

The other half is the loop: each round's failures become permanent regression cases, and the outcome folds into route evidence, so the search learns from the same observation that graded the output. It reports new failures per round rather than total, because a total that goes down is also what deleting the hard cases looks like.

And a harness with no controls at all cannot report better than PROVISIONALassay.controls enforces that, because "we did not check whether this harness works" is the accurate description and having a word for it is what stops it being rendered as a pass.

Example 09 runs all of it.

Twelve findings a green test suite would have missed

Every one of these was hit while building this repository, usually while writing a demonstration of the feature it broke. None raised an exception.

browsergraph cases                              # the twelve
browsergraph cases --id judge-below-chance      # one, with its numbers
study what looked fine what was happening
judge-below-chance 73% agreement with people chance alone gives 76.6%; answering "good" every time beats it
empty-result-passes exits 0 every night for weeks the selector matches nothing and a 0-byte file overwrites yesterday
timezone-rolls-the-year every timestamp stored in UTC 23:30 on 31 December in Denver is reported in the following year
the-copier-wins best fidelity and best utility it is returning the input; the privacy it existed for is gone
label-belongs-to-a-response one human label per case, reused the label describes the incumbent, so every system is scored on matching it
blocking-hides-a-pair 73% fewer comparisons, same duplicates found pairs split across blocks are unreachable at any threshold

The full set is docs/CASE_STUDIES.md, which is generated: each study's numbers are computed when the document is rendered, and tests/test_casestudies.py fails if the committed copy drifts from what the code produces.

Thirteen domains with the code already written

A template is a shape with typed ports and zero candidates. A pack is the other half: real candidates with real implementations, so a domain goes from expressible to runnable in two calls. Standard library only, and a test runs every route of every pack — not a sample.

browsergraph packs                    # the catalogue
browsergraph packs harness --solve    # run one
pack the job the finding it exists to show
harness grade a system on cases four graders, one measuring answer length — and only the controls can tell them apart
judge a model grading other models 70% raw agreement, and a kappa below zero; and the rubric matters more than the judge
redteam attack a system on purpose 32 attacks on one family found nothing; 20 across five found four holes
agents supervisor, workers, critic the four-field answer to a three-field document is the best-looking output and the only wrong one
synth synthetic tabular data five generators, five ways to score well; the copier tops fidelity and utility
models linear / tree / boosted / MLP / attention no winner column — the answer depends on the data-generating process
geo addresses into checked places a format check accepts Denver, XZ 80202 and a nonexistent ZIP
spacetime place × time enrichment the better rainfall figure is a leak; midnight in Denver lands in next year
quality a gate that may refuse three adjudicators that disagree on purpose
migrate move data and prove it arrived equal counts with swapped contents look identical
files do the same thing to every file one parser per folder is not enough, and only a verifier that counts records knows
tabular fit a model on a table the model has to carry its own encoding, or the wrong encoder wins
clean repair a messy table repair.drop empties the table, every check passes, and only the row count says so

Each pack's docstring carries a table of measured numbers, and tests/test_packs.py turns every one of those sentences into arithmetic. A pack that argues something in prose and cannot demonstrate it is a pack making a claim, which is the thing this repository is arranged against.

Explore it in your browser, installing nothing: the live studio — all 166 candidates across 14 sub-steps, five synchronized views, one offline file.

Try it without installing anything: the Kaggle notebook installs a browser, drives it, and shows the screenshots and video it captured.

Eleven notebooks that solve a real job

Start here. Each takes real input, runs real code, and writes real files you can open afterwards. Standard library plus numpy and Pillow, so they run anywhere.

Notebook The job Run it
12 Browse and scrape a live page in, clean product rows out Kaggle
13 Ingest into a schema mixed formats in, typed rows plus rejects with reasons Kaggle
14 Check and process an image images in, a report and resized copies out Kaggle
15 Clean up messy data a messy table in, a clean one plus every change recorded Kaggle
16 Fit a model a dataset in, regression and classification scores out not yet
17 Batch many files forty files in, a total and the broken ones named not yet
18 Retry and fall back a flaky source, and a run that succeeds anyway Kaggle
19 Find duplicates one customer list in, matched pairs out Kaggle
20 Forecast next month two years of sales in, three months out Kaggle
21 Watch for changes a thing to check, and noise only when it moved Kaggle
22 Sort text into categories support messages in, the right team out Kaggle

What they actually produce, not what they claim to: 3 products parsed from three different price formats with the priceless one dropped and named; 6 records accepted and 3 rejected with a reason each; a blank PNG caught by colour spread where size and file bytes look fine; a $1,340.00 row repaired after its own comma split it in half; four model routes that score identically, which the notebook reports as "this decision does not matter" rather than crowning a winner; a batch that names item 11 instead of failing forty files at once; blocking that saves 89% of the comparisons and says which pair it can therefore never find.

Eleven notebooks about the model itself

The claim that this is a general model is only worth something if it works somewhere else. Each of these runs in seconds, installs nothing but the library, and touches no dataset or network. Every cell is executed and its output committed.

Notebook What it argues Run it
01 Express a problem as a graph stages, ports, candidates, and the four checks Kaggle
02 Search without enumerating 3.8 trillion routes is 41.8 bits, not an obstacle Kaggle
03 A domain that is not browsing the same machinery on document work Kaggle
04 A Kaggle pipeline is a graph numeric ∥ categorical encoding is a join Kaggle
05 Two readings of one document text and layout are independent extractions Kaggle
06 A workflow with no data science effects, permissions, and one step that reaches outside Kaggle
07 A gate that can say no schema and drift meeting at one decision Kaggle
08 Build, verify, release a gate with two inputs cannot be half-skipped Kaggle
09 Retrieval is two searches dense and lexical recall, joined Kaggle
10 The leak you cannot see in CV the split as a node with two named outputs Kaggle
11 The domain this started in completed ≠ worked Kaggle

Twenty-three of the twenty-five are on Kaggle, and those ran there to completion — the library is installed from this repository in cell one, so what you see rendered is what the code in main actually does. The two that are not say "not yet" and link the committed notebook, rather than a kernel that does not exist. python notebooks/check_published.py --fix reconciles this table against the account in both directions, because a row claiming "not yet" about a published notebook misleads exactly as much as a dead link.

Two picture books

Mostly diagrams. Useful if you would rather see the thing than read about it.

Notebook What it shows
24 Every picture and why seven figures on one job, and what the flat version of each would hide
25 The shapes a job comes in nine shapes — chain, fan-out, diamond, map, branch, gate, fallback, reuse, tournament — each drawn, each with the lines that build it

The eighth figure, the learning curve, needs a loop to draw, so it lives in 23 · Get better by doing it — a hundred and twenty passes of the evidence loop against the best route there is and the one you would get by guessing.

Both found real bugs while being written, which is the argument for drawing things: 24 caught solve reporting a champion "out of 48 possible" on a space it could only ever draw 24 routes from, and a per-step evidence chart reading flat zero because a run's verdict was never reaching the per-candidate posteriors — so a reader that returned nothing and a reader that returned two records came out believed equally.

Regenerate and verify them with:

python notebooks/build_notebooks.py && python notebooks/build_domains.py
python notebooks/build_workflows.py
python notebooks/build_gallery.py && python notebooks/build_shapes.py
python notebooks/execute.py          # runs every cell, under a memory cap

Why this exists

The interesting problem in browser automation is not clicking things. It is that a run which reports success can have accomplished nothing — and you find out weeks later.

This library was written after an incident where 551 emails reported "sent" successfully and produced zero posts. Every layer said success. Nothing checked the destination. So the linter's flagship rule is BG003 — changes remote state, never verifies the outcome — and the rest of the design follows from there.

from browsergraph.lint import lint, report
print(report(lint(graph)))
# [WARN] BG003 click: graph changes remote state but never verifies the outcome
#        — a silent failure will look like success

New here? Read this first

HOW_IT_WORKS.md — the whole architecture in plain English: what a stage is, how settings expand into a matrix of concrete options, how four trillion routes get narrowed to one, and what actually happens when a step fails. No jargon, no prior context.

Don't write the steps. Write down what has to be true, list everything that could make it true, and let the program pick — from evidence, with reasons it can show you.

Sub-steps: the combinatorics a coarse diagram hides

Six stages, but "Acquire inputs" is not one decision — it is three. Work out what to fetch, open a session capable of fetching it, read a payload out of that session. Each has its own matrix.

one stage decomposed into its sub-steps

One stage, its three sub-steps, all 80 candidates, three routes traced. The full 14-sub-step network →

Draw each stage as a single pooled choice and you count 85,747,200 routes. The sub-steps those same stages are actually made of expose 3,802,314,700,800 — the coarse view was hiding 44,343× of the space. Same task, same registry, same code.

6 stages / 14 sub-steps · 57 definitions · 166 atomic candidates
3,802,314,700,800 complete routes · 1,337 adjacent transitions

Sub-steps are recursive — a sub-step can decompose again, to any depth. A stage is either a leaf that holds candidates or a composite that holds sub-steps, never both, because otherwise "one choice per stage" stops being well defined and that sentence is what the whole model rests on.

browsergraph workbench -o studio.html   # five interactive views, one offline file
browsergraph route --compare            # greedy vs beam vs exhaustive, measured
browsergraph route --gates              # what a policy blocks, and why

Policy is a hard gate that runs before scoring: under a locked-down policy (no browser, no network, no LLM, no external effects, deterministic only) the space drops from 3.8 trillion to 1,959,552,000 routes — 99.95% removed before a single score is computed, every removal stating its reason. Blocked candidates stay visible; filtering them out silently would answer "what could perform this step" with "what the policy left".

Measured, not asserted — the full report includes the profile-ranking bug this found (all four objective profiles were secretly identical) and the beam-search bug that made width buy nothing:

profile greedy (71 evals) beam (512 evals)
Balanced 0.8156 0.8156
Quality first 1.0149 1.0149
Speed first 0.9608 1.0304
Cost first 0.3443 0.3443

Greedy scores each sub-step in isolation; route quality compounds, so it loses whenever the trade-off is real. Decomposition also pushed the gated space past the enumeration limit — exhaustive is no longer an option, which is exactly when the strategy choice starts to matter.

The architecture

A task decomposes into ordered stages. Each stage offers every candidate that could perform it; a route picks one per stage. That model is written down in full — generalized past browsers, with portable manifests, contract validation, typed feedback and optimization profiles — in UNIVERSAL_GRAPH_SYSTEM.md.

browsergraph workbench -o studio.html    # 6 stages, 149 candidates, 32,864,832 routes

The demonstration registry is domain-neutral on purpose: the same primitives describe document ingestion, image processing, data cleaning and machine learning. Browser adapter alone expands to 60 atomic candidates (5 controllers × 6 binaries × 2 display modes) — because drawing that as one box hides fifty-nine decisions.

A task decomposes into planes. Each plane offers several interchangeable ways to answer it. A route through them is one candidate solution — and the route is chosen from evidence, not fixed in advance.

task planes and candidate routes

With no evidence the cheapest route wins: http → dwell → css → click → screenshot → extract — no browser, no model. After a few dozen runs against a defended, JavaScript-rendered site the same machinery picks patchright → wait_for → healing → … and can say why: 6/6 steps measured.

The planes are derived from node contracts, not written down. click is on act because it declares mutates; adding a node adds a candidate and the diagram changes with nobody editing it.

browsergraph planes --demo               # the planes and the chosen route
browsergraph planes --html planes.html   # both routes drawn over all the others

Failure is a first-class path

When a configuration fails, the next one is tried — and the diagnosis chooses what to try next, which is what makes it more than a retry loop.

1. http        ok=False  timeout        wait_retry
2. http        ok=False  timeout        wait_retry
3. playwright  ok=True                          → extracted: $49.00

A missing element on an engine with no JavaScript runtime suggests a different engine, not a longer wait. Retries are bounded per spec — an unbounded retry never reaches the rest of the ladder. A terminal diagnosis (CAPTCHA, block) stops immediately rather than escalating into a ban, and SiteMemory puts the winner first next time.

Every engine, every browser, headless and headed

Measured, not declared — a real launch matrix against a served page. browsergraph doctor reports the same for your machine.

engine chromium chrome firefox webkit brave headless headed xvfb
playwright
playwright_stealth
patchright
rebrowser
selenium
selenium_uc
seleniumbase
botasaurus
camoufox (isolated)
nodriver / zendriver / pydoll (CDP)
http (no browser) n/a n/a n/a n/a n/a

126 verified combinations. What does not work is documented in ENGINES.md with the reason — a bare protocol with no client library, a chromedriver/snap version skew, a dependency that ships broken source.

What comes in the box

Contracts nodes declare what they read, write and mutate — enforced at import, at composition and at run time (CONTRACTS.md)
Linter BG001–BG009 over a graph, before a browser starts
Escalation diagnose the failure, try the next configuration, remember the winner
Learning outcomes generalise site → org → sector → platform → global
Token reduction 8 preprocessing strategies, then keyword focus with neighbour expansion
Extraction conservative, deterministic contacts / NAICS / articles — no model needed
Politeness per-domain, process-wide rate limiting that honours robots Crawl-delay
Isolation conflicting engines in per-engine virtualenvs, over a worker protocol
Notebooks Jupyter/Kaggle/Colab run cells inside an asyncio loop; the sync API is driven from a worker thread so it just works
Universal graph portable node manifests, atomic candidates, stage/route validation and a five-view studio — UNIVERSAL_GRAPH_SYSTEM.md
Evidence per-candidate, per-context posteriors; Thompson-samples a route at sum cost instead of enumerating, and reports how many bits of the choice remain
Pictures seven figures of any workbench — shape, route space, funnel, per-step evidence, run timeline, solve scoreboard, learning trend — as self-contained SVG, Mermaid, JSON or matplotlib. Domain-neutral: viz takes a graph and knows nothing else
Domain packs thirteen non-browser domains with the code already written. Standard library only, every route of every pack executed by a test: browsergraph packs harness --solve
Taxonomy 41 pipeline categories in 9 families, classified by shape and silent failure; coverage counted from the registries, gaps listed — docs/PIPELINE_TAXONOMY.md
Due care what an evaluation owes, as values: nine obligations, PASS/PROVISIONAL/FAIL, waivers that need a reason, and a feedback loop whose regression cases are permanent
Route search policy gates first, then greedy / beam / exhaustive over the eligible space, reporting how much of it was actually examined
Capabilities each engine declares what it can do — press, select, upload, download, frames, cookies, viewport, PDF — checked against a graph before a browser launches, with the engines that could run it
Receipts every run writes durable evidence: route, engine, per-step timing, artifacts with content hashes, which steps verified, and a pasteable replay line — for failures too
Model router ten jobs (extract, verify, locate, read-image, classify, embed, rerank, code, plan) routed to the right model with a recorded reason, instead of one default for everything
Binaries fetches a browser or a driver matched to the browser it will drive — the fix for "cannot connect to chrome"
OCR (optional) read a page from its pixels when the DOM cannot answer — canvas text, baked-in images, and "does this screenshot contain any text at all"
LLM (optional) Ollama-compatible; the model is resolved from the host by capability, never hardcoded

Documentation

HOW_IT_WORKS.md start here — the architecture in plain English
AGENTS.md instructions for an LLM harness, written as checkable constraints
docs/TOWARD_A_GENERAL_MODEL.md a critical review — what is still wrong, and what to fix first
QUICKSTART.md first graph, first real browser, first task
UNIVERSAL_GRAPH_SYSTEM.md stages, candidates, routes, contracts, feedback, optimization
docs/PIPELINE_TAXONOMY.md the 41 shapes engineering work comes in, and which have code
docs/ROUTE_SEARCH_REPORT.md policy gating and route search, measured end to end
ARCHITECTURE.md the Protocol-vs-base-class seam
CONTRACTS.md what a node promises, and the three moments it is checked
ENGINES.md every engine, what it is for, and what does not work
DIMENSIONS.md the axes, and why verification matters most
ISOLATION.md conflicting engines in separate virtualenvs
PLUGINS.md the open plugin format
CONTRIBUTING.md how to add an engine, a node or an extraction path

Related

extractgraph — the other half. browsergraph reaches the page; extractgraph gets the data out of it, with several independent paths, provenance, and the disagreements kept.

Contributing

Issues and pull requests welcome — see CONTRIBUTING.md. The most useful contributions are a new engine adapter, a new extraction path, or a page that breaks something.

pip install -e ".[dev]"
pytest -q                                  # 1,848 tests; browser suites skip when absent
mypy browsergraph --ignore-missing-imports
ruff check browsergraph tests

MIT.

About

Composable browser automation: one graph runs on Playwright, Patchright, Selenium, undetected-chromedriver, Camoufox or no browser at all. Self-tuning, multi-model, plugin-based.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages