AI documentation¶
Optional and opt-in: linexcel is a lineage analyser first, and every figure in the report is computed without a model. What AI adds is prose — a card per node and an overview per workbook, written from the deterministic dossier the analysis already produced.
Pick a provider first — see Choosing an AI provider. Every example below uses a local Ollama runtime; substitute your own endpoint.
from linexcel import analyze
result = analyze("workbook.xlsx")
docs = result.document(base_url="http://localhost:11434/v1", model="qwen3.8")
result.save_html("out.html", docs=docs)
Why the output is checkable¶
The model receives the node's dossier — the exact formula, its step-by-step evaluation, its precedents and their values, its dependents, the extent of a stretched group, any VBA link — and the system prompt forbids asserting anything absent from it. Missing information must be written as "not determined by lineage" rather than guessed.
The card sits next to that evidence in the report, so a reader can check a sentence against its formula and values. A prompt is not an accuracy guarantee: local reviews have found incorrect explanations despite complete responses.
Source name definitions include constants and worksheet scope, independently of
which names become graph nodes. A missing graph edge or a #NAME? result does
not establish that the source definition is absent. Metadata too large to
inspect or include is marked incomplete. Group values explicitly identify their
representative cell; they are not group totals or values for unsampled members.
The dossier distinguishes group membership from value sampling. A represented group can fill its entire bounding rectangle while only a few member values are included. It also separates an input value read by the engine from a formula result, and gives range dimensions as rows, columns and cells. Missing evaluated step values do not by themselves establish whether evaluation was attempted.
Omissions describe the bounded documentation payload. A neighbor removed to fit that payload can still exist in the graph. Workbook overviews receive directed connections with their own coverage counts; ordering formula examples does not establish a calculation chain. These facts help review the explanation, but do not prevent a model from misinterpreting them.
For literal AGGREGATE selectors, the dossier records their
Excel meanings.
For example, function 9 selects SUM and option 6 excludes errors, not hidden
rows. Dynamic selectors remain undetermined. These facts describe the formula;
they do not independently verify the engine's implementation or result.
Formula quotation checks¶
Documentation checks complete formula quotations in Markdown against source formulas in the exact dossier sent to the model. An unsupported quotation adds a visible notice identifying the quotation; the generated explanation is not silently corrected. Literal illustrations and incomplete snippets can remain uncheckable. Processing limits are reported explicitly.
This is a bounded lexical check, not a factual verdict. A supported quotation does not establish correct attribution, arithmetic, causality, value provenance or the truth of surrounding prose. Screenshot descriptions require separate visual review.
Both documentation APIs accept an optional evidence collector:
checks = {}
docs = result.document(base_url=..., model=..., validation_results=checks)
overview = result.document_workbook(
base_url=..., model=..., validation_results=checks,
)
Entries are keyed by node ID, with workbook reserved for the overview. Each
entry retains the raw model response, quotation checks, limitations and a
qualified or unverified status; neither means factually approved. Keep this
evidence private when its source workbook is private. The manual validation
runner saves it alongside the generated documentation for independent review.
In local review, this check exposed a quoted /100 where the source used
/1000. It also qualified conceptual shorthand such as =C{row} * D{row}:
that is not a literal source formula, but is not necessarily a false
explanation. Separately, generated prose miscounted sampled cells and described
a complete group as potentially containing gaps. Those prose errors are
outside this check's scope and still require comparison with source evidence.
A subsequent review using the explicit group, dimension and connection facts observed correct complete-group descriptions, branching topology, range row counts and input provenance in the selected cases. It still found false claims about external caches, ambiguous localized illustrative formulas and invented visual discrepancies. The six unsupported quotations in that run are lexical observations, not a count of proven factual errors. Neither the additional source facts nor a complete generation run certifies the explanation.
Node cards¶
# Every calculation node (cells, groups, VBA procedures)
docs = result.document(base_url=..., model=...)
# Or a chosen few
docs = result.document(["c:Summary!B3"], base_url=..., model=...)
document() issues max_workers requests in parallel (4 by default):
Documenting a large workbook is long and often billed, so a node that fails does
not discard the ones that succeeded. The successful cards are returned and a
UserWarning reports how many nodes were dropped; AiDocError is raised only
when every node failed.
Workbook overview¶
overview = result.document_workbook(base_url=..., model=..., language="en")
result.save_html("out.html", docs=docs, workbook_doc=overview, language="en")
The dossier behind the overview carries two things. The lineage says how the workbook computes: sheet statistics, the largest formula patterns, defined names, VBA procedures, unresolved references. The workbook context says what it looks like: the first rows of each sheet, cell comments, merged ranges, frozen panes, hidden columns — the same cues the sheet screenshots show a human reader.
That second half is what lets the model write about the file rather than about
a graph. A title sitting two rows above a table, a column called Price that
somebody hid, a comment reading "exported product category" — no formula
records any of it, and an overview written without them describes a structure
nobody recognises.
The images themselves stay put for this call: openpyxl reads the same facts
deterministically, so the "cite only the dossier" rule still holds and no vision
model is required. Sending them is a separate, explicit step — see below.
# Lineage only — cell contents stay on your machine
overview = result.document_workbook(base_url=..., model=..., include_context=False)
The dossier is capped at aidoc.MAX_WORKBOOK_DOSSIER_CHARS. A workbook that
exceeds it sheds detail in order — long previews shrink, then the tail of the
pattern and VBA lists, and only as a last resort are previews and comments
dropped — so a small workbook loses nothing.
Describing the screenshots¶
Everything above is grounded in the graph. This one is not: a screenshot shows what no extraction reaches — colour conventions such as blue inputs against black formulas, conditional formatting, charts, the shape of a layout — and a model looking at the picture is the only way to put them into words.
shots = result.save_screenshots("shots/") # LibreOffice renders them
seen = result.describe_screenshots(shots, base_url=..., model="<a vision model>")
result.save_html("out.html", screenshots=shots, screenshot_docs=seen)
linexcel analyze book.xlsx --screenshots shots/ --vision-docs \
--base-url http://localhost:11434/v1 --vision-model "<a vision model>"
Each description appears under the image it describes, in the Sheets tab, badged read from the screenshot rather than as ordinary AI documentation — the reader can tell a claim about pixels from a claim about the lineage.
Three things worth knowing:
- The model must accept images.
model=here is where a vision model is named when it differs from the writing one (--vision-modelon the command line). A text-only endpoint raisesAiDocErrorrather than having the picture silently dropped from the request. - The prompt confines it to what is visible — the model may ignore it. This is the one card in the report with no deterministic counterpart to check it against: nothing in the lineage can contradict a sentence about colours. A weak vision model invents confidently — one local model described a three-column sheet as having six, with a total row that was not there — so read a description against its own image before trusting the model on the next one.
- This is the widest thing linexcel sends. A picture of a sheet shows every row on it, including those no dossier would have quoted — see Data handling.
Images go one at a time, so an image that fails is skipped with a
UserWarning and token_budget is checked before each call. A vision request
is expensive and endpoints do not always report what an image cost, so the
tally may read as estimated.
Token usage¶
Every AI call is tallied on the result:
docs = result.document(base_url=..., model="qwen3.8")
overview = result.document_workbook(base_url=..., model="qwen3.8")
print(result.token_usage)
# 48,210 tokens (44,900 in + 3,310 out) over 5 request(s) [openai-compatible/qwen3.8]
usage = result.token_usage
usage.input_tokens, usage.output_tokens, usage.total, usage.requests
Counts come from the provider when it reports them — the OpenAI-compatible
usage block is read directly, so the figure is the one you are billed on. When
a provider reports nothing (a custom callable, or a local runtime that omits the
block), the tokens are approximated instead and usage.estimated is True;
str(usage) then prefixes the numbers with ~.
Tokens already spent are counted even when a later node fails, since they are
billed regardless. The tally accumulates across every call made on the result;
build a fresh TokenUsage and pass it as usage= to
aidoc.document_nodes() if you need to scope it more finely.
Tokens, not currency
TokenUsage deliberately carries no price. Rates differ per provider,
model and region, and a table baked into the package would silently go
stale. Multiply by your own current rate — or run locally, where the rate
is zero.
Capping the bill¶
token_usage reports what a run cost after it ran. token_budget= decides
what it is allowed to cost before it starts:
The budget is a ceiling on the total tokens of the run — input and output
together, every request combined. It is deliberately not a per-node allowance:
one node's card is never the question, and a workbook with 900 formula patterns
is 900 requests whose sum is the only figure that turns up on an invoice. Use
max_tokens= when you want to bound an individual response.
Because a request's cost is only known once it has answered, the budget is
enforced between requests. Nodes still queued when the tally reaches the
ceiling are never sent, the cards already written are returned, and a
UserWarning names how many nodes were left undocumented:
UserWarning: Token budget of 200,000 tokens reached after 201,447:
612 of 900 nodes documented, 288 never sent. Raise token_budget to document the rest.
Requests already in flight are allowed to finish, so the final tally can exceed
the ceiling by up to max_workers responses — set a budget as an order of
magnitude, not to the token.
The ceiling is counted against result.token_usage, which spans the result's
lifetime. One budget covers document_workbook() and document() together, so
the figure to choose is what the whole workbook is worth to you:
result.document_workbook(base_url=..., model=..., token_budget=200_000)
result.document(base_url=..., model=..., token_budget=200_000) # same ceiling
A budget already spent by earlier calls raises AiDocError rather than sending
a request that would breach it. token_budget=0 — or any non-positive value —
raises ValueError; to send nothing at all, do not call document().
Estimating before you spend
A dry run costs nothing: point provider= at a callable that records its
prompt and returns "". Empty documents are rejected, so this deliberately
raises AiDocError after processing the nodes. result.token_usage still
holds the estimated input cost of those calls.
Empty replies and responses that the endpoint marks as truncated or filtered are failures. Reported token usage from rejected replies remains counted toward the budget. Partial node or image batches still return completed documents with a warning, so acceptance checks must compare their keys with the requested set.
Language¶
language= selects the system prompt sent to the model, and the same value
drives the viewer interface. Nine are available; see
Languages.
docs = result.document(base_url=..., model=..., language="ja")
result.save_html("out.html", docs=docs, language="ja")
What gets sent¶
Node dossiers and, for the overview, the workbook context. See Data handling for the payload of each call and where it goes.