需 Python 3.11 以上且 Docker 处于运行状态,模型写的代码在容器里执行;首次启动要构建执行器镜像并安装分析库,耗时数分钟;需在 ~/bambooai/.env 中填模型密钥,默认 cost 预设用 OpenRouter key,performance/max 预设还需 Anthropic key。

BambooAI
pgalkoMIT活跃维护一个可审计的 AI 数据分析工作台,能查看结果背后的代码和证据、回放验证计算过程,并支持本地开源模型等多种模型选择。
AI-powered data analysis workbench built for transparency, reproducibility and user control. Inspect the code and evidence behind results, verify calculations through replay, and choose from a wide range of models, including local open-weight models.
pip install bambooai
bambooai serveREADME 未说明显存或 GPU 型号要求;支持通过 Ollama 或 vLLM 本地部署开源权重模型,也可用云端 API 模型。
可审查、可复现的 AI 数据分析工作台,保留代码与证据,支持本地模型
一个可审计的 AI 数据分析工作台,能查看结果背后的代码和证据、回放验证计算过程,并支持本地开源模型等多种模型选择。
核心能力
- 在持久 Python 内核中写码执行并迭代分析
- 记录代码、输出、图表与模型交互全过程
- 用全新内核重放计算以验证结果可复现
- Adaptive 模式引入独立 Reviewer 模型质询结论
- 把完成的分析固化为可复用 routine 换数据重跑
容易踩的坑
- v2.7.2最新正式版2026-11-08 · 2026-11-08 05:26 UTC
BambooAI 2.7.2
Routines run by the calendar, keep less and compare better; data goes back four years, a year at a time. No database migration; the executor image stays v74.
Routines
- Runs by interval. A routine runs on a day, a week, a month or a year, and fills the units not run yet from one load. The Summary shows the runs of an interval as a table — one row per unit, a thumbnail of each line over the year in the column head, the enlarged chart on a click — and the run view's Trend tab carries the same thumbnails.
- The lines a run compares. When an analysis is saved as a routine, the promoter picks the result lines a run should compare with the runs before it — the final form of each quantity, not an earlier draft or a check — and names them as table heads. Series are picked whole; lines that restate a quantity on part of the data are checks. Identifiers of the saved data (session ids, names found by looking) are never inputs; the chain's own rule for finding them is used instead.
- Less code, same results. After the save is verified, the method is condensed from the code itself: cells
nothing rests on and the prints go, and the condensed code replaces the chain's only when it reproduces run zero's
lines and figures on the same data. A slow method is rewritten once by the
Condenserseat (now in the shipped configuration template, on the analyst's model) under the same checks, and stays provisional until the first run on new data agrees with the old method. The code as written stays downloadable. - The Summary redesigned. One column, one scroll: the facts (purpose, inputs, what a run records, the version with its verdict) and then the runs as a table; a row opens the run, a failed unit is a row with its reason, the values show without the plus sign, and the gap line offers to fill the units not run.
- Custom runs. Open and Delete on a run's head; the Compare row offers any earlier custom run, so a period can be compared with the same period of another year directly.
- The save shows what it is doing while you wait (the chain read, the data checked, the model's turn with a clock); the Run dialog is slimmer, with a progress band while a load runs.
Data
- Reach and window per plan. A load may start as far back as the plan's reach and cover at most the plan's window: free — a year back, three months at a time; cost — two years, six months; performance, max and the self-hosted edition — four years, a year. The store keeps an athlete's data as far back as the reach. The same rule in the three places a window is picked — the Intervals.icu dialog, the Dataset cache's loads, a routine's custom run — with the bounds on the date pickers and a plain correction when Load is pressed on a longer window.
- Dataset cache. Under an athlete: From → To, quick picks 3 m · 6 m · 1 y, Load all datasets (or one dataset's Load Wellness), and Clear the cache. Days not in the cache are downloaded; the rest come from the store.
Also
- The README's introduction and overview rewritten, with the Usage dialog's screens.
Patches 0259–0280 on top of 2.7.1.
- v2.7.12026-10-31 · 2026-10-31 02:57 UTC
查看更新说明
BambooAI 2.7.1 — the Usage dialog, rebuilt
Every number BambooAI shows about itself should be as inspectable as the numbers it shows about your data. 2.7.1 rebuilds the Usage dialog (gear menu → Usage) on that principle: what the analyses cost and where the money and the time went, computed from the record of model calls, nothing written by a model.
What it shows
A period — the last 24 hours, 7, 30 or 90 days, this month or last month, in your own days — against the period before it, and Export CSV for the rows themselves.
Overview — the tiles: model cost, analyses, model calls, tokens in (with the share served from the prompt cache and what it saved), tokens out, model time, and on the hosted service the compute charge; each with its change against the period before and a sparkline. Under them a chart, one column a day, stacked by seat, model or mode and measuring cost, tokens, calls or time, with the legend as a table beside it — click a row to see that part alone — and a tooltip per column. Then three lines of what stands out: the heaviest day and what made it, the cache, the failures.
Seats & models — who spent what: the Analyst writes the cells, the Reviewer judges them, the Reader reads documents, the Coder codes the context, the Rewriter writes the plain version, the Figures seat draws, the Promoter makes routines. Sortable, with the configuration's list prices per model.
Analyses — every analysis of the period with what it cost and how it ended, grouped by day, searchable by a word of the question, a mode or an outcome. Pick one for its breakdown: its figures, its seats ranked, its calls laid out in time (one row per seat, a mark per call — hover for the model, tokens and cost), its models, and open the analysis when it is saved.
Calls — the raw rows, newest first, under a frozen header.
What is recorded
Each model call's row now keeps the prompt-cache split (tokens read from the cache, tokens written to it), what the cache saved against an uncached prompt, and the search and image fees inside the cost. Rows from before read as not recorded, never as zero, and the dialog says how many calls of a period carry the figures.
A small usage ledger (
storage/<user>/usage_ledger.jsonl) keeps one line per analysis as it ends — the question, the mode, how it ended — so an analysis keeps its name in Usage even when its thread was not saved as a favourite (an unsaved thread's full record is still discarded at the next start, as before). Analyses from before this release that were not saved show as An analysis that was not saved.The outcome words, since they are easy to misread: a finished analysis is answered; its replay then re-runs the cells and checks the cited numbers — reproduced when they match, differed when they do not, replay failed when it could not run. "Answered" alone means no replay on record. Every chip says this on hover.
Upgrading
pip install -U bambooai bambooai serveThe local database adds the five columns by itself; no executor rebuild (the image stays v74). Self-hosters of the hosted layout run
docs/sql/usage_cache_fields.sqlonce on their Supabase project before starting 2.7.1.Patches 0255–0258. The design is in
docs/USAGE_DESIGN.md. - v2.7.02026-10-30 · 2026-10-30 06:32 UTC
查看更新说明
BambooAI 2.7.0 — datasets by athlete
A coach works with several athletes. 2.7.0 makes the athlete the unit everywhere data is kept, loaded, cached and run: one store per athlete on the executor's disk, one Intervals.icu key per athlete, one athlete at a time on the Data tab, and routines whose history and comparisons stay within one athlete's runs. The executor image is rebuilt (v74).
The store, per athlete
Every Intervals.icu load is filed under the athlete it belongs to - the id and the name the profile gives, never the key - and feeds a store that only grows: each activity's streams, the activity list, the laps and the chat under each activity, the wellness log by day, the calendar by entry, the rider profile. A window asked twice is fetched once; a window that grows fetches only the days the store lacks; the dialog says what came from the store and what was downloaded. The store is trimmed by age (older than 400 days and unused for a month), and the per-run window files leave after a month.
The Dataset cache is a tree: source → athlete → dataset. A click opens the details - what is held, since when, the span, the columns, what is on the Data tab now - with three actions per line: Load (the stored version, at once, nothing fetched), Load … to today (the stored version extended to today, only the missing days fetched) and Forget (off the Data tab and out of the cache). One dataset can be loaded alone. Uploads and generated files keep their own groups.
One athlete at a time on the Data tab. A load for one athlete lets the other athletes' pills go first, after asking; the thread's ledger says when an earlier chain was on another subject's data, so the analyst never mixes two athletes in one answer.
Keys, per athlete
Settings → Intervals ICU keeps one key per athlete. A key is named by Intervals.icu the moment it is saved; the dialog lists the athletes with their key, each with Remove, and Add for the next. The Intervals.icu load dialog opens on a roster: one row per athlete with what the store holds for them, the last loaded first; a click proposes the window from the day after their last held day. Every load runs on the picked athlete's own key; a load asked for one athlete under another's key is refused before anything is written. The Dataset cache lists an athlete with a key and nothing held yet, with a way into the load dialog; without a key, the to today loads are disabled and loads from the store still work.
Routines, by athlete
The run dialog of an Intervals.icu routine picks the athlete above the window - the roster again, the athlete the routine was made on picked first. The window is loaded for that athlete with their key, or taken from the executor's cache when it already holds it for them. Each run records whose data it was on:
- Compare with offers the run before on that athlete, and run 0 only when the original data was theirs. The first run on an athlete compares with nothing. Runs are never compared across athletes.
- The Comparison tab says whose and when: the athlete in its heading, each run's window under its column, the result labels whole.
- The history in the manager shows one athlete's runs at a time when a routine has run on several - a pick in the Results across runs heading switches - and the arrows between runs stay within the athlete.
Routines on an upload, an Endura race or a SweatStack load work as before: one group, compared among themselves.
Also in this release
- A bar under the top bar when the app was updated under an open page - BambooAI was updated… reload - so a page never runs older scripts unnoticed.
- The pill strip scrolls at the wheel's pace.
- Load buttons on every details pane of the Dataset cache; the small Intervals.icu calls wait out a rate limit.
Upgrading
pip install -U bambooai bambooai serveserverebuilds the executor image on first use (the tag moves - v2.6.12026-10-28 · 2026-10-28 09:59 UTC
查看更新说明
BambooAI 2.6.1 — the reach record
The kernel now records what each cell of an analysis touched outside the data, and the app reports it when the chain ends. The analysis itself is unchanged; the executor image is rebuilt (v65).
What is recorded
For every cell the analyst runs, the kernel keeps a record of the doors it went through:
- Network. Each host the cell connected to, with the port, the bytes sent and received (TLS included), a listening port it opened, a name it resolved without connecting, a connection that failed.
- Files. A file opened or a folder listed outside the data — the dataset, your own datasets folder, the generated folder, the plots folder, temp and Python's own trees are the data;
/etc, home folders, dotfiles and the box's own places are not. - Environment. A variable the cell read, or the whole environment walked.
- Programs. A program the cell started.
Each record carries the cell line it came from and is marked asked for when the question — or an earlier question of the thread — named the host, the file or the variable. "Download it from https://example.org/x.csv" names the host; "fetch it from the internet" does not, and the card says so. The rule is a plain substring test, deliberately: nothing is inferred.
The record observes; it does not block. The one address the kernel now refuses is the cloud instance metadata service (169.254.169.254), which answers with the box's own credentials. The restricted-module list is unchanged in substance, with two faults fixed: the
os.execandos.spawnentries had matched nothing, and the text of an f-string counted as code, sof"http://{host}/x"was refused as thehttpmodule.Where it shows
- The closing card. Under the replay line: how many reaches outside the data, how many not asked for, how many cells not run; one line per reach with its cell and tag; the cells the executor refused; and the box in one line (user, root or not, capabilities worth a word, seccomp, the mounts, whether the metadata address answers). A chain that reached nothing says so in one faint line.
- The cell rows. A cell that reached out carries a short tag after its output line —
↓ 1.3 MB from host,↑ 42 KB to host:port,read /etc/passwd,env HOME— plain when the question asked for it, in the kernel's colour when not. - Refused cells. A cell the executor refused (the restricted-module list) is shown as its own state —
⊘ In [–] not run: restricted operation (socket)— not as a failure, in the pane, the Investigation tab and the counts. - The code map. The same tag on the step's row, a refused attempt as its own row, and a Reaches outside the data section in the summary panel, each line opening its step.
- The tally.
python3 tools/reaches_report.py --root <working folder> --days 14reads the stored threads and the run logs and prints the record per model: runs, cells, fetched and sent (asked for or not), files, environment names, programs, refused cells, the hosts not asked for, and the lines not asked for with their question.
Nothing of the record enters a prompt: the analyst never sees it.
Not seen
A connection made by a C library (libpq, pycurl); a library's own choice of path outside the box's sensitive places; the replay and routines, which re-run the committed cells on the script route — they were observed when the chain ran.
docs/REACH_DESIGN.mdhas the design and the decisions.Upgrading
pip install -U bambooai bambooai serveserverebuilds the executor image on first use (the tag moves to…-v65; a few minutes). Threads, favourites, memory and routines are unchanged. Chains run before the upgrade carry no record: their cards show nothing for it, and their maps say the record was not kept.Full changelog: https://github.com/pgalko/BambooAI/compare/v2.5.1...v2.6.0
- v2.5.12026-10-24 · 2026-10-24 04:33 UTC
查看更新说明
BambooAI 2.5.1 — the code map, reachable from the report and the notebook
2.5.0 added the code map: for each chain, how every recorded number was made, read mechanically from the cells that ran. 2.5.1 is about getting to it, and from it to the code. There is no change to the analysis, the kernel or the executor image.
The code map
- From the report. A
[cell n]citation in the Answer, or in the plain-language version, opens the map on that number's recipe: the diagram drawn, the panel open. A cell with one recorded number opens on it; with several, the one the sentence names, allowing for the report's rounding; if the sentence names none of them, the Steps view opens at the cell with its numbers a click away. The chip no longer opens the cell's code directly — that is Open in the notebook on the panel, one click further — and theIn [n]lines in the stream pane still open cells. - From the notebook. Each committed cell in the Investigation tab carries an eye at the right of its header; it opens the map on the Steps view at that cell. Saved favourites keep it.
- To the code. Open in the notebook from a number's recipe lands on the cell and lights the lines the number rests on — every statement behind it, in every cell of its recipe, with the RESULT line itself marked strongly. The lines come from the same record the map is built from; where the map could not trace, nothing is lit. Opening a cell any other way clears them.
- Between the two views. A number's panel carries Recipe diagram in the Steps view and Show in the steps on the diagram, beside Open in the notebook.
- Zoom. The diagram zooms as the thread map does: − / level / + / fit at the end of its header, Ctrl/Cmd and the mouse wheel, 30–200 %. A diagram is fitted to the frame when first shown, never enlarged; your zoom stays while the same diagram is on screen.
- Colour. The
[cell n]chip and the panel's buttons take the result colour the map's chips use; the light theme has a proper result border of its own (it had been inheriting the dark theme's, pale on white).
The README
The figure is four screenshots in one: a run beside its data, the report's figures, the chain's code map, the recipe of one of its numbers.
Upgrading
pip install -U bambooai bambooai serveNo executor rebuild: the image from 2.5.0 stands. A chain's map stored by 2.5.0 is rebuilt from the chain's record the first time it is opened (map version 3); nothing is rewritten. Threads, favourites, memory and routines are unchanged.
On the hosted service there is nothing to do.
Full changelog: https://github.com/pgalko/BambooAI/compare/v2.5.0...v2.5.1
- From the report. A
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
BambooAI
Hosted version: bambooai.org
BambooAI is an AI-powered data analysis workbench designed to make the analytical process transparent, reproducible and open to scrutiny. It gives you visibility into how an AI reaches its conclusions, along with the tools to inspect, challenge and refine its work.
At its core is an LLM Analyst working in a persistent Python environment. Given a dataset and a question, the Analyst explores the data, writes and executes code, examines results and develops its analysis iteratively. You can follow the investigation as it unfolds, ask follow-up questions, revisit earlier steps or branch into a different line of enquiry without losing the previous work.
BambooAI preserves the complete computational record: executed code, outputs, figures, intermediate results, working notes and model interactions. Numerical findings are linked to the code that produced them, while a code map helps trace how results were derived. At the end of an analysis, the supporting computation is replayed in a fresh Python kernel to check reproducibility.
You retain control over the models, the depth of analysis and the direction of the investigation. Adaptive mode introduces a separate Reviewer model that can challenge assumptions, question the evidence and request further tests. Completed analyses can also be turned into reusable routines and applied to new datasets.
The underlying principle is simple: AI-generated analysis should be open to the same scrutiny as any other analytical work. Reproducible calculations are an important part of that, but the choice of methods, assumptions and interpretation must also remain open to examination.

1 · a run: the turns and cells on the left, the data on the right · 2 · the report's figures · 3 · the chain's code map · 4 · the recipe of one of its numbers.

5 · Usage: what a month of analyses cost and where the money and the time went, by day and by seat · 6 · one analysis: its seats, its calls in time, its models.
Table of Contents
Overview
BambooAI is built around the idea that an AI-generated analysis should be something you can examine, reproduce and build upon. Rather than treating the final report as the only output, it preserves the analytical work that led to it.
How the analysis works
A primary Analyst model investigates a question through a series of steps, using a persistent Python kernel. It can explore datasets, write and execute code, examine results, consult documents and external sources, and decide what to investigate next. Each step builds on what has already been learned.
The Python environment retains variables, dataframes and intermediate calculations throughout the investigation. Successful code executions are committed to the workspace; failed executions are rolled back to the previous state. Follow-up questions can reuse earlier work, while branching allows alternative approaches to be explored without losing the original analysis.
Inspecting and verifying the work
BambooAI records the analytical process as it happens. The user can inspect executed code, outputs, figures, intermediate results, working notes and model interactions, including token usage, execution time and cost.
Numerical estimates are recorded alongside the computations that produced them. Reports reference those calculations, and an automatically generated code map helps trace results through the underlying data transformations and intermediate steps.
Once an analysis is complete, BambooAI reconstructs the supporting computation in a fresh Python kernel and runs it again against the original data. This replay verification checks whether the reported numerical results can be reproduced.
Reproducibility is an important safeguard, but it does not establish that a statistical method was appropriate or that a conclusion was justified. BambooAI makes the underlying evidence available so those questions can be examined independently.
Review and user control
The analytical workflow is configurable. Users can select models, reasoning settings and analysis depth, and intervene as an investigation develops.
In Adaptive mode, a separate Reviewer model examines the available evidence at regular intervals. It can challenge the Analyst's approach, request additional tests, question assumptions or recommend narrowing a conclusion. The Analyst and Reviewer operate under separate instructions and can use different models.
Other specialised roles handle supporting tasks such as document reading, figure generation and report rewriting. These roles can be configured independently.
From exploration to reusable analysis
An investigation does not have to end with a report. BambooAI can turn completed analyses into reusable routines, preserving the recorded method while allowing selected inputs to change. The original computation can be used to check the routine before applying it to new datasets, without requiring another LLM-driven investigation.
The application supports a range of commercial and open-weight models, including locally hosted models served through Ollama or vLLM. Python execution runs in an isolated container by default, independently of model inference. BambooAI can be deployed locally or used through the hosted service.
BambooAI originated in sports science and physiological data analysis, where repeated measurements, statistical uncertainty and the ability to trace results back to their source are particularly important. Its integrations with Intervals.icu, Endura and SweatStack reflect those origins, but the underlying analytical environment is general-purpose and works with ordinary tabular data, supporting documents and external sources.
Installation
Requirements: Python 3.11 or newer, with Docker running (Docker Desktop on macOS and Windows, Docker Engine on Linux). Docker is where the model-written code runs; see Where the Code Runs for the alternative.
Option 1, pip (recommended)
pip install bambooai
bambooai serve
Option 2, from the repository
git clone https://github.com/pgalko/BambooAI.git && cd BambooAI
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
bambooai serve
Option 3, hosted. bambooai.org runs the same analyst as a service, with nothing to install.
Quick Start
- Start the app with
bambooai serve. On first use it creates the working folder~/bambooai, writes a.envfile, builds the executor image from the Dockerfile in the package, starts the container, and openshttp://127.0.0.1:5001. The initial build takes several minutes: it installs pandas, numpy, scipy, statsmodels, scikit-learn, matplotlib, plotly and the other analysis libraries. - Stop the app with Ctrl+C and add your model key to
~/bambooai/.env. An OpenRouter key is enough for the default preset,cost; theperformanceandmaxpresets put the Analyst, Figures, Rewriter and Promoter seats on Claude and need an Anthropic key as well. AddGEMINI_API_KEYto enable web search. For locally hosted models, use the Ollama or vLLM configuration below instead. - Run
bambooai serveagain. Upload a CSV, Parquet, JSON or XLSX file and ask a question. Choose Quick, Deep or Adaptive in the composer. - Open Models & Compute from the gear menu to choose and save a preset:
cost,performanceormax. Usage shows what the analyses cost and where the money and the time went.
The working folder:
~/bambooai/
├── .env keys and settings; read at start, never written by the app
├── LLM_CONFIG_template.json the seats per preset: model, provider, reasoning effort, max_tokens, prices
├── bambooai.sqlite chains, labels, usage, the saved preset, integration credentials (encrypted)
├── config/<user>/LLM_CONFIG.json the configuration built from the template for the chosen preset
├── storage/<user>/threads/ one JSON per thread: runs, cells, reports, reproduction results (a thread not saved is discarded at the next start)
├── storage/<user>/usage_ledger.jsonl one line per analysis as it ended - question, mode, outcome - for the Usage dialog; kept
├── storage/<user>/favourites/ saved chains
├── storage/<user>/routines/ saved routines: versions, runs and their records
├── storage/<user>/context/ the codebook and the coded passages of each Intervals.icu subject
├── memory/<user>/ the memory pack
└── logs/<user>/ bambooai_run_log.json: every model call with tokens, time and cost
serve uses this folder for your configuration and saved work. Upgrading the package does not change it;
deleting it starts you fresh. Use BAMBOO_HOME or bambooai serve --home DIR to choose another location.
The data the executor loads lives elsewhere: in the Docker volume bambooai-datasets, which outlives the
container (see The Dataset cache and the store).
Features
- Three analysis modes. Quick for a lookup or definition, Deep for a full analysis within a turn budget, and Adaptive for longer investigations with periodic review.
- Follow-up questions. Continue with the calculations and variables from the previous run, or start a new question with a fresh workspace. Runs form chains you can step through; see Follow-up questions for what happens when the data changes.
- Reports and figures. Keep both the technical report and a plain-language version for non-technical readers. A separate, cheaper model can handle the rewrite. Ask about a plot using its chat button; an image-capable Analyst can receive the picture along with your question.
- Data and documents together. Work with several datasets, load training data from Intervals.icu, Endura or SweatStack, and browse loaded data in the Data tab. Attach up to four papers, notes or transcripts per thread as PDF, Word, Markdown or text; their passages can be read and cited alongside the analysis.
- The words beside the numbers. An Intervals.icu load can bring the messages, notes, calendar entries and planned workouts written around the training. They are coded against a codebook, read in the kernel like any table, quoted verbatim in the report, and set against the numbers; see The words beside the numbers.
- Routines. Save a finished analysis as a method with a few inputs and run it again on another window or file, without a model. Each run is checked against the data it gets, recorded, and compared with the run before; see Routines.
- A Dataset cache that remembers. Loaded frames and files stay in the executor and are reused; Intervals.icu activities are kept once parsed, so a new window downloads only what is missing. Datasets the analysis writes appear in the same cache.
- Web search with sources. With a Gemini key, the Analyst can look up published figures using Google AI grounding, compare them with your data, and cite the sources in its report.
- Memory across runs. Saving a run records what was learned about the dataset, including column quirks and approaches that worked, for use in later analyses of the same data.
- Your choice of models. Use OpenRouter, OpenAI, Anthropic, Google, Groq, Mistral, xAI, DeepSeek, Ollama or vLLM. Each role, such as Analyst, Reviewer or Rewriter, has its own model and reasoning settings. See Models and Seats for configuration.
- A workspace you can return to. Save chains, organise them with labels, review usage and costs, and export reports as PDF. The app has light and dark themes.
How It Works
A run is the work needed to answer one question. It consists of turns: on each turn the Analyst reads the question, the dataset's description, its working note, the cells and their outputs, and its remaining budget. Long outputs are shortened in the prompt and can be reopened when needed. It then takes one action:
| Action | What it does |
|---|---|
CELL | Run one Python cell in the kernel; its printed output becomes input for the next turn. |
SHOW n | Reopen the full output of an earlier cell. |
NAMES | List the variables the kernel holds. |
RECALL | Consult the memory pack for this dataset. |
SEARCH | Search the web with Google AI grounding; return a summary with sources. |
READ | Read relevant passages from attached documents, or from the words loaded beside Intervals.icu data, through the Reader role, when configured. |
ASK | Ask you a question; your answer continues the thread in the next run. |
REPORT | Write the report and end the analysis turns. |
The Analyst follows the rules in analyst/contract.md, shared across models. These ask for a
best estimate and its uncertainty, the conditions it holds under, how it was established,
and references to the cells that support it. They also ask the Analyst to make exclusions visible,
distinguish association from causation, account for repeated observations rather than treating them
as independent subjects, and say what the available data or budget could not establish.
During the analysis, cells perform calculations rather than draw plots. Figures are created once the report is written.
Reports and checks
After REPORT:
- Figures. The
Figuresrole receives the report and the cells, then writes one Plotly cell drawing the one to three figures the reader needs from the objects already in the kernel. It can replyNONEwhen a number or table is sufficient. A failed cell is returned with its error for up to three corrections. The report gains a Figures section citing the cell and discloses when the requested figures could not be drawn. - Plain-language version. The
Rewriterrole produces a version for a non-technical reader. Both versions are kept, and both open with the report's title - its first line, written by the Analyst as a statement of what was found - which heads the Answer tab and the PDF export. The export's top section puts it above the question as the Analyst understood it (its own statement of what it answered, so a follow-up reads whole without the thread), what data the report stands on (the source, the window, the files), and the day, mode and reproduction verdict, so a report can be handed to someone who has never seen BambooAI or the data; the body is whichever version is on screen, with its figures on a print layout and its citations marked. The browser offers the title as the file's name. - Reproduction check. The successful cells are assembled in execution order, through the last cited cell or the run's last figure cell, and rerun against the original data in a fresh kernel. Earlier cells are retained so their imports, variables and changes to the data are available. Numerical values from the original cited cell outputs are compared with the fresh output. The closing card shows the outcome and how many numbers were checked; the figures produced by the replay appear in the Plots tab.
BambooAI also checks the technical report's numbers against its recorded evidence and the plain-language version's numbers against the technical report. Unmatched numbers are flagged for the reader rather than silently changed. These checks help expose discrepancies, but an analysis can reproduce and still use an unsuitable method or draw an unsupported conclusion.
-
Code map. When a chain ends, BambooAI builds a map of how it got from its data to its numbers, read mechanically from the committed cells and the kernel's record - no model is asked, so the same chain always gives the same map, and nothing of it reaches a prompt. Code map on the closing card (or the eye on a chain's box in the thread map) opens it, and a
[cell n]citation in the report opens it on that number's recipe: one row per cell with the inputs it read, what came out (the RESULT lines by status, the figures), and for each number its recipe - the data it rests on, the tables and values the code made in order, the settings typed into the code, the choices (seeds, thresholds, resample counts) - with a short list worth checking (an input never read, a value typed into the code that every result rests on, a result that came out empty or was replaced). The map shows how a number was made, not whether it is right.docs/CODEMAP_DESIGN.mdhas the design. -
Reach report. The kernel records what each cell touched outside the data: the hosts it connected to and the bytes each way, the files it opened outside the data, the environment names it read, the programs it started - and whether the question asked for it (the host, the file or the name appears in the question). The closing card lists the reaches with their cells, the cells the executor refused (the restricted-module list; a refused cell is shown as not run, not as a failure), and the box the cells ran on in one line; the same tags sit on the cell rows, in the Investigation tab and in the code map.
tools/reaches_report.pytallies the record over the stored threads per model. Nothing of it reaches a prompt; it observes and reports, it does not prevent (the one address the kernel refuses is the cloud instance metadata service).docs/REACH_DESIGN.mdhas the design.
Modes and budgets
Each mode sets a turn budget and a dollar budget the Analyst can see. The budgets are set per
preset through tier_properties in the configuration. In the performance preset, Quick allows
4 turns, Deep 15, and Adaptive 50, with a review after every 8th turn.
In Adaptive mode, the Reviewer has its own brief and can use a stronger model. It reads the
question, the working note, the results so far, the Analyst's turns since the last review, and the
code and output of the cells supporting the current answer. The session supplies that evidence
in one call per review. The Reviewer returns one verdict: run another test, narrow the conclusion,
or write the report now. A verdict to report is binding only when every cell it cites has been
checked by a review.
After the report, in Adaptive mode, the same Reviewer checks it against the original question and adds a short note for the reader.
Follow-up questions
A follow-up continues with the variables the previous run left in the Python workspace; a new question starts fresh. Runs form chains that you can step through.
If the loaded data has changed since the earlier chain, for example another date window or a replaced file, the follow-up starts in a fresh kernel with the data loaded now. The earlier chains remain in the analysis history as context, marked as using other data. The replay and any routine made from the follow-up use the cells computed on the current data, not those earlier calculations. Reuploading the same file, including in a new session or when reopening a favourite, lets the follow-up continue as before.
Ask about a plot
The chat button on a plot sends the picture with your question when the Analyst's model supports
images and its model_properties entry has "multimodal": "true". Otherwise, only the question is
sent, and the app tells you that the model has not received the picture.
Memory
When you save a run, the Knowledge Distiller role writes a card about the dataset: what the
columns mean in practice, what to watch for, and what worked. It is stored in
memory/<user>/memory_pack.yaml; RECALL reads it in later runs on the same data.
Routines
A finished analysis can be kept as a method and run again on other data of the same shape. Save as
routine (the save menu on a chain) hands the chain to the Promoter seat, which reads it once and
names the constants that would have to change for other data - a boundary date, a threshold, a
band - as inputs, at most five; identifiers of this data are never inputs. The routine is stored
only once a run zero on the original data reproduces every RESULT line the chain recorded. Then
the script is condensed from the code itself, without a model: the cells nothing rests on and the
prints go, and the condensed code replaces the chain's only when it reproduces run zero's lines
and figures on the same data; otherwise the script stays as written, and the routine's Summary
says which. A slow method is also rewritten once by the Condenser seat - vectorised, computed
once - under the same checks, and the rewrite stays provisional until the routine's first run on
new data has run the old method beside it and found them to agree.
A run is then a dialog with the inputs and the load, and no model: the data is loaded the way the original was (a window from Intervals.icu for the athlete picked in the dialog - a coach's athletes are listed with what the store holds for them - or a file from the Dataset cache), checked against the routine - the columns fit, the auxiliary files are bound by kind, the chosen values are in the data, what differs is said in sentences - and the script runs. Every RESULT line is compared with the run before on the same athlete (or run zero, when it is theirs): moved beyond its interval, within it, or not reached; the first run on an athlete has nothing to compare with yet, and runs are never compared across athletes. The Comparison tab names the athlete and both runs' windows. Runs are recorded with their data, inputs, results, figures and differences, and can be stepped through like chains - the arrows within one athlete's runs; the manager shows one athlete's runs at a time when a routine has run on several. Data without an athlete (an upload, a race) behaves as one group. A window inside a load the executor still holds is cut from that load rather than downloaded again.
The contract's rule that makes this possible is that a value read off the data - a date in it, an
identifier, a count - is computed into a variable, never typed as a literal, so the method holds
when the data changes. docs/ROUTINES_DESIGN.md has the design.
Configuration
Environment Variables
Set these in ~/bambooai/.env. Lines starting with # are ignored; do not put comments after a value.
| Variable | Meaning |
|---|---|
OPENROUTER_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, GROQ_API_KEY, XAI_API_KEY, MISTRAL_API_KEY, DEEPSEEK_API_KEY | Provider keys; add only those needed by your configured models |
GEMINI_API_KEY | also enables web search (Google AI grounding); without it SEARCH is off |
REMOTE_OLLAMA, REMOTE_VLLM | base URLs of local model servers, e.g. http://localhost:11434 |
BAMBOO_LEVEL | cost (the default) / performance / max: the preset before one is saved in the app |
BAMBOO_COMPUTE | docker (default) or local |
EXECUTOR_PORT | the container's published port (default 5055) |
BAMBOO_EXECUTOR_DATASETS | a folder for the executor's datasets (the loaded windows, the Intervals.icu store) in place of the Docker volume bambooai-datasets |
EXECUTION_MODE, EXECUTOR_API_BASE_URL | to use an executor you run yourself: api and its URL. BAMBOO_COMPUTE is then ignored |
BAMBOO_USER | the identity that names the per-user folders (default local) |
SWEATSTACK_CLIENT_ID, SWEATSTACK_CLIENT_SECRET | your own SweatStack app registration; without them the SweatStack integration is not offered |
APP_PORT | the web app's port (default 5001) |
SESSION_COOKIE_NAME, FLASK_SECRET, LLM_CONFIG_ENCRYPTION_KEY | the server; the last two are generated on first use |
Models and Seats
A seat is a role in the workflow, such as Analyst, Reviewer or Rewriter. Each can use its own model and settings; using one primary Analyst does not mean every task must use the same model.
LLM_CONFIG_template.json in the working folder defines the seats for each of four presets
(free_agent_configs, cost_agent_configs, performance_agent_configs, max_agent_configs), the
turn budgets per preset (tier_properties), and the properties of every model named
(model_properties). The performance preset as shipped:
"performance_agent_configs": [
{"agent": "Analyst", "details": {"model": "claude-sonnet-5-5", "provider": "anthropic", "reasoning_effort": "xhigh", "max_tokens": 32000, "temperature": 0}},
{"agent": "Figures", "details": {"model": "claude-sonnet-5-5", "provider": "anthropic", "reasoning_effort": "low", "max_tokens": 16000, "temperature": 0}},
{"agent": "Rewriter", "details": {"model": "claude-sonnet-5-5", "provider": "anthropic", "reasoning_effort": "low", "max_tokens": 16000, "temperature": 0}},
{"agent": "Reader", "details": {"model": "deepseek/deepseek-v4.1-flash", "provider": "openrouter", "reasoning_effort": "low", "max_tokens": 16000, "temperature": 0}},
{"agent": "Coder", "details": {"model": "deepseek/deepseek-v4.1-flash", "provider": "openrouter", "reasoning_effort": "none", "max_tokens": 16000, "temperature": 0}},
{"agent": "Reviewer", "details": {"model": "openai/gpt-6.1-sol", "provider": "openrouter", "reasoning_effort": "high", "max_tokens": 48000, "temperature": 0}},
{"agent": "Promoter", "details": {"model": "claude-sonnet-5-5", "provider": "anthropic", "reasoning_effort": "medium", "max_tokens": 32000, "temperature": 0}},
{"agent": "Knowledge Distiller", "details": {"model": "deepseek/deepseek-v4.1-flash", "provider": "openrouter", "reasoning_effort": "none", "max_tokens": 24000, "temperature": 0}},
{"agent": "Google Search Executor", "details": {"model": "gemini-flash-latest", "provider": "gemini", "reasoning_effort": "none", "max_tokens": 8000, "temperature": 0}},
{"agent": "Google Search Summarizer", "details": {"model": "deepseek/deepseek-v4.1-flash", "provider": "openrouter", "reasoning_effort": "none", "max_tokens": 12000, "temperature": 0}},
{"agent": "Image Generator", "details": {"model": "deepseek/deepseek-v4.1-flash", "provider": "openrouter", "reasoning_effort": "none", "max_tokens": 28000, "temperature": 0}}
],
"tier_properties": {
"performance": {"analyst_turns_quick": 4, "analyst_turns_deep": 15, "analyst_turns_adaptive": 50, "analyst_review_every": 8}
},
"model_properties": {
"claude-sonnet-5-5": {"capability": "reasoning", "multimodal": "true", "templ_formating": "xml",
"prompt_tokens": 0.002, "completion_tokens": 0.01, "cache_read_tokens": 0.0001, "cache_write_tokens": 0.0025,
"reasoning_style": "effort", "reasoning_efforts": ["low", "medium", "high", "xhigh", "max"],
"thinking_off": "between_tools", "no_sampling": true}
}
The cost preset, the default, keeps every seat on OpenRouter (DeepSeek, with Grok reviewing);
max moves the Claude seats to Opus. multimodal indicates whether a model accepts images. For Ask about a plot,
set it to "true" for an image-capable Analyst model so the picture is sent with the question.
The available roles:
| Seat | Role |
|---|---|
Analyst | Carries out the analysis: each turn, each cell and the report. |
Reviewer | Reviews Adaptive runs at the configured interval (every 8th turn in the example), and again after the report. Uses its own brief and can use a stronger model. |
Figures | Draws the figures after the report, using one cell and the objects already in the kernel. Falls back to the Analyst when absent. |
Rewriter | Writes the plain-language version of the report. |
Reader | Reads relevant document or context passages for the Analyst through READ. Called once per read; suitable for a cheaper model. |
Coder | Reads the words loaded beside Intervals.icu data once, in batches, and writes the codes table against the codebook; renders passages not in English. Falls back to the Reader when absent. |
Promoter | Handles Save as routine: reads a finished chain once and identifies its inputs as find/replace edits in a JSON reply. Falls back to the Analyst when absent. |
Condenser | After a routine's run zero and the pruning, rewrites the method once as lean code when the method takes 15 s or more; the rewrite is kept only when it reproduces run zero's lines and figures, and stays provisional until the first run on new data confirms it against the method it replaced. The template puts it on the Analyst's model at medium effort. No fallback: remove the entry and the step does not run. |
Knowledge Distiller | Writes the memory card when a run is saved. |
Google Search Executor | Performs the grounded search, using a Gemini model. |
Google Search Summarizer | Condenses search results into a digest with citations. |
Image Generator | Turns the report into an infographic shown with the answer, using a YAML specification rendered to SVG. |
Use a low reasoning effort for Figures: it needs a short code reply, and prolonged reasoning can
consume the token allowance before any code is returned. For Promoter, use a moderate effort and
a generous max_tokens allowance so its JSON reply can finish.
Seat fields: model, provider, reasoning_effort (none, low, medium, high, xhigh;
mapped to each provider's own vocabulary, or to a thinking budget for models that use one),
max_tokens, temperature. Every model a seat names must appear in model_properties, which
supplies its prices per 1K tokens (used for the running cost, cached tokens priced separately), its
formatting (xml or text) and its reasoning interface; the remaining keys of an entry are the
provider's details, used as the template ships them.
The app builds config/<user>/LLM_CONFIG.json from the template for the chosen preset, and rebuilds
it when the template file is newer than the built one.
Ollama
BambooAI can use models running locally through Ollama, or hosted in Ollama's cloud.
Model configuration
In LLM_CONFIG_template.json, set the agent's provider to ollama and name the model. For example,
to use gpt-oss:20b as the Analyst:
{
"agent": "Analyst",
"details": {
"model": "gpt-oss:20b",
"provider": "ollama",
"reasoning_effort": "high",
"max_tokens": 16000,
"temperature": 0
}
}
Add a matching entry under model_properties in the same file:
"gpt-oss:20b": {
"capability": "reasoning",
"templ_formating": "text",
"prompt_tokens": 0.0,
"completion_tokens": 0.0,
"context_window": 65536,
"reasoning_efforts": ["low", "medium", "high"]
}
Context length. For local models, context_window sets the context length used for each request
(Ollama's num_ctx). Set it to at least 32,768 tokens for the Analyst; the example uses 65,536.
Without it, Ollama applies its own default, which depends on the machine's memory and can be as low as
4,096 tokens. A prompt longer than the limit is truncated rather than rejected; BambooAI shows a
warning when it detects this. Cloud models manage their own context length and do not need the
setting.
Reasoning. List the thinking levels the model supports in reasoning_efforts, and pick one with
the agent's reasoning_effort. ollama show <model> lists the levels a model defines. For models
that can only switch thinking on or off, BambooAI sends the switch instead; reasoning_effort: none
turns thinking off where the model allows it. Models without thinking support receive no setting.
Repetition penalty. Ollama penalises recently used tokens by default (repeat_penalty 1.1), which
degrades code and structured output, so BambooAI sends 1.0 (no penalty) with each request. To use a
different value for a model, add repeat_penalty to its model_properties entry.
Costs. Set prompt_tokens and completion_tokens to 0.0 for local models. Cloud usage is
covered by your Ollama account and plan.
Connecting to Ollama
By default, BambooAI connects to Ollama on your own computer at http://localhost:11434. To use
Ollama on another machine, set its address in your .env file:
REMOTE_OLLAMA=http://<server-address>:11434
All agents using the ollama provider share this connection. Pull local models on the machine
running that Ollama server, and to use cloud models through it, sign in there with ollama signin.
For access from another machine, start the Ollama server with OLLAMA_HOST=0.0.0.0, and restrict
access to a trusted network.
You can also connect to Ollama's cloud directly, without running an Ollama server. Add these settings
to your .env file and choose cloud models in your agent configuration:
REMOTE_OLLAMA=https://ollama.com
OLLAMA_API_KEY=your_api_key
vLLM
BambooAI can use models served by vLLM on a machine with a suitable GPU. vLLM serves a model as an OpenAI-compatible API; BambooAI connects to it the way it connects to any provider.
Serving a model
The command below serves Qwen3.8-27B in 8-bit on a single 48 GB GPU, with a 64k context and the model's thinking returned separately from its answers. It is the configuration BambooAI was tested against (vLLM 0.30, an RTX A6000):
vllm serve ~/models/Qwen3.8-27B \
--quantization fp8 --linear-backend marlin \
--language-model-only \
--compilation-config '{"mode": 0, "cudagraph_mode": "FULL"}' \
--speculative-config '{"method": "mtp", "num_speculative_tokens": 2}' \
--max-model-len 65536 --max-num-seqs 8 \
--reasoning-parser qwen3 \
--served-model-name qwen3.8-27b \
--host 0.0.0.0 --port 8000
A few of these flags are worth knowing about. --reasoning-parser is what lets BambooAI show the
model's thinking apart from the answer. --served-model-name gives the model the short name you use
in the agent configuration. --linear-backend marlin selects the 8-bit kernels that run on Ampere
GPUs such as the A6000 and A100; on Ada and Hopper GPUs it is not needed. --language-model-only
skips the model's vision components for this text-only setup; it will not receive plot images.
The --compilation-config line runs the model with CUDA graphs but without torch.compile, which
fails in the tested setup; it is markedly
faster than --enforce-eager. The --speculative-config line uses the model's own
multi-token-prediction head to draft tokens: the same output, noticeably faster. Drop it for a
model without an MTP head.
Model configuration
In LLM_CONFIG_template.json, set the agent's provider to vllm and use the served model name:
{
"agent": "Analyst",
"details": {
"model": "qwen3.8-27b",
"provider": "vllm",
"reasoning_effort": "high",
"max_tokens": 16000,
"temperature": 0
}
}
Add a matching entry under model_properties:
"qwen3.8-27b": {
"capability": "reasoning",
"templ_formating": "text",
"prompt_tokens": 0.0,
"completion_tokens": 0.0,
"context_window": 65536,
"reasoning_efforts": ["low", "medium", "xhigh"]
}
Context length. vLLM sets the context length when the server starts (--max-model-len) and
refuses a prompt that does not fit, rather than truncating it. Set context_window to the same
value and BambooAI warns before sending a prompt that cannot fit, with the options for fixing it.
Reasoning. With --reasoning-parser, the agent's reasoning_effort is passed to the model;
list the levels it supports in reasoning_efforts, as the model names them (Qwen3.8 takes low,
medium and xhigh). An agent's effort the model does not define is mapped to the nearest level
it does. reasoning_effort: none turns thinking off.
Costs. Set prompt_tokens and completion_tokens to 0.0; the server does not bill.
Connecting to vLLM
Set the server's address in your .env file:
REMOTE_VLLM=http://<server-address>:8000/v1
If the server was started with --api-key, add VLLM_API_KEY with the same value.
With every role configured to use models hosted on your own network, LLM requests stay on that network. Ollama cloud is not local hosting; web search and external data integrations also make requests to their respective services.
Data and Integrations
- Primary dataset: CSV, Parquet, JSON or XLSX, loaded into the kernel as
df. Browse its rows in the Data tab. - Auxiliary datasets: additional files uploaded alongside the primary dataset, available to the cells by path.
- Intervals.icu and Endura: an API key entered in the app (stored encrypted in the working folder's database). For Intervals.icu a coach keeps one key per athlete: Settings → Intervals ICU lists the athletes with a key (Intervals.icu is asked whose a key is when it is saved), and the load dialog opens on the roster - each athlete with what the store holds for them, the last loaded first; picking one proposes the window from their last held day. Choose a date range; the data is loaded into the executor and described to the analyst - the frame's schema and the heads of the files that came with it. An Intervals.icu load can add the activity summary, the wellness log, the activity intervals and, with the Context option, the words and the planned workouts.
- SweatStack: OAuth; needs your own app registration with SweatStack (
SWEATSTACK_CLIENT_ID,SWEATSTACK_CLIENT_SECRET). - Web search:
GEMINI_API_KEY.
Documents
A thread can carry up to four documents, such as papers, notes, meeting or interview transcripts, as PDF,
Word (.docx), Markdown or plain text, each up to 10 MB. Attach one from the paperclip menu
(Document). The file is parsed once, into passages with locators: the page for a PDF, the heading
trail for the other formats, each passage numbered D1.17 (document 1, passage 17). Tables become
rows the kernel can load. A blue Documents pill in the top bar shows how many are attached; hover
it to see them and remove one, click it to open the Documents view, which shows each document's map
and its text passage by passage.
Rather than receiving each document in full, the Analyst starts with a short map of it: the
outline, the tables and the figure captions. In the kernel each document is an object: D1.text,
D1.page(n), D1.grep(pattern, context=1), D1.units(a, b), D1.table(n) (a dataframe),
D1.outline(). A cell can search a transcript for a name, read a section, or load a table and check it
against the paragraph that describes it, and the report cites the cell as usual.
For questions that need more than a text search, the Analyst can use READ. It names a document
(or all of them, or a stretch of one) and what it is looking for; the passages that match go to the Reader seat, which
quotes the ones that answer, each checked word for word against the document and returned with its
locator. A READ counts as a turn, like a web search, with up to three reads per run.
The report cites a passage as [D1.17] beside [cell n]; on the page the citation shows the
passage on hover and opens the Documents view at it on click.
Documents are kept as files under the working folder's storage/<user>/documents/<thread>/, not in
the database; every chain in the thread sees them, and they are removed with the thread. The parsers
are pdfplumber and python-docx, installed with the package. A template from before the Reader
seat keeps working: add the seat from the shipped template to yours to turn READ on. A PDF without a
text layer (a scan) is refused: OCR is not supported. Parsed text above 2 MB is refused as well; the
pill shows the reason.
The words beside the numbers
Training data comes with writing: what the athlete wrote under a ride, what the coach replied, the
plan's prose, a note on a day, a sick or holiday entry, a tag, a tick. With the Context option
of an Intervals.icu load (on by default) those become a table of passages beside the numbers - one
row each, with its day, its kind, its author's role and the text as written - and a second table of
codes: the Coder seat reads every passage once against a codebook (fatigue, recovery, instruction,
deviation, …), labels it, and renders a passage not in English into English, kept beside the original.
The coded passages are stored per subject, so a window seen before is coded in seconds. A third
file, the planned workouts, carries the prescription on the numbers side: each planned session, the
activity it was done as, its compliance, the plan's duration, load and targets, the step lines and
the structure.
The Analyst sees a map of the words in its data description - the span, the passages by kind and
month, the authors, the languages, the codes with their counts - never the text. In the kernel the
words are CTX: CTX.frame() and CTX.codes() are the two tables, CTX.grep(pattern),
CTX.around(date, days=3), CTX.between(a, b) and CTX.on(activity_id) find passages (as text to
print; as_frame=True returns the rows), CTX.text(ref) reads one whole. READ CTX what puts a question to the Reader over the passages, as over documents.
The report cites a passage as [C.417], quoted verbatim and marked rendered when it was translated;
a count of codes is reported as the model's reading, not as what was said.
The codebook is yours. When no code fits a passage the Coder proposes one, uses it meanwhile with
its rows marked, and leaves the decision to you: the Context card of the Intervals.icu load dialog
says how many proposals wait and opens the codebook (the Context pill carries the count too), which
shows each proposal with the passages it came from - confirm it, confirm it under another name,
merge it into a code the book has, or decline it - and the codes by group with their definition,
status and the last load's uses.
The book can be exported and imported as JSON. A confirmed change applies from the next load;
nothing already coded is re-coded. The design is docs/CONTEXT_DESIGN.md.
The settings the data is read against
The Rider profile option of an Intervals.icu load (on by default) writes the account's settings
as a file beside the data: the rider's weight, height, resting HR and age; per sport, the FTP, the
thresholds and the zones with their bounds, and the power-duration model's estimates; every custom
zone set the account defines, zone by zone; and the meaning of every custom field - its code, type,
units and options - so the columns of the other files explain themselves. When the account defines
custom zone sets, a second file carries the time in each custom zone per ride. The intervals, wellness
and summary files carry every custom field under its code - a select as its option's text, with the
number beside it. Everything comes from the account's own record, nothing is named in code, and
nothing identifying goes in (the record's e-mail, names and key stay out). The profile also says which end of the
wellness file's 1–4 scales is the good one for each scale the account tracks - 1, by the platform's convention. The Analyst reads the thresholds from the file's rows in its data
description, and the summary file carries the settings in force on each day. The design is
docs/RIDER_PROFILE_DESIGN.md.
The Dataset cache and the store
The executor keeps what it has loaded - the frames with their window and metrics, the files a load
writes, the datasets an analysis writes with DS.save - and the Dataset cache dialog (the rail)
draws it as a tree: the source, the athlete under it, and under the athlete one line per dataset the
Intervals.icu dialog offers plus the frame - Activities, Activity summary, Activity intervals, Wellness,
Context (chats and calendar), Rider profile - with its span, what is held and its size, and whether it is
on the Data tab now or in the store only. A click on a line opens its details - what it is, what is held, the
files a load writes and their columns - and the actions: Load (the span held, from the
store alone - nothing is fetched), Load to today (through Intervals.icu, only the missing
days come down) and Forget (its pill off the Data tab and the data out of the cache,
after asking); the athlete's line has the same for everything held. Loading one dataset leaves the
frame as it is; loading Activities brings the datasets on the Data tab with it, to the same window; the
Data tab holds one athlete at a time (a load for another athlete lets the first's pills go, after
asking; the thread stays, the kernel starts fresh on the new data, and the model's ledger marks the
earlier chains as on another subject's data); a to today load runs on the athlete's own key, and one
for an athlete without a key is refused, saying so; a pill's × takes a dataset off the
Data tab without deleting it. Uploads and generated files keep their own groups. A routine whose window lies inside a
load the executor still holds runs from the cache without a download.
Underneath, every Intervals.icu activity the executor has parsed is kept in a store under the athlete's folder (the source and the athlete id the profile gives), with the chats and the laps fetched for it, the wellness log by day, the calendar by entry and the profile's snapshots. A load is a view over the store: the window's activity list is fetched, the activities the store lacks (or whose file changed) are downloaded, the rest are assembled from disk, and the chats and laps of activities older than sixty days are reused; the wellness and calendar of the window come down and are merged, so the span held is the union of every load. Everything again in the load dialog fetches the whole window again. A load covers at most a year at a time and reaches back four years (three months and a year on the free tier, six months and two years on the cost tier); activities fall out of the store once older than the plan's reach unless a load used them in the last thirty days; the window files a load writes leave after a month.
The store and the loaded files live in the Docker volume bambooai-datasets, which outlives the
container and its image: an upgrade keeps them. docker volume rm bambooai-datasets starts the
executor empty; BAMBOO_EXECUTOR_DATASETS in .env names a folder on the host instead of the
volume.
Where the Code Runs
The analysis kernel, the reproduction run and the integrations' data fetching all run in the
executor container: a Flask API in front of the persistent kernel, from containers/executor/.
bambooai serve builds the image (bambooai-executor:<package version>-<executor build>, rebuilt when
either changes) on first use, starts the container published on 127.0.0.1:5055 with the datasets
volume mounted at /app/datasets, waits until it is ready, and stops it when the app stops
(--keep-executor leaves it running). The Restart control in the app restarts it.
Alternatives:
bambooai serve --compute local(orBAMBOO_COMPUTE=local): the kernel runs in a subprocess of the web app. No Docker needed; no isolation; the integrations are not available, since they fetch inside the executor.- An executor you run yourself,
docker run -p 5055:5000 -v bambooai-datasets:/app/datasets bambooai-executor:2.3.0-v56on this or another machine, withEXECUTION_MODE=apiandEXECUTOR_API_BASE_URLin.env. Use the image tagbambooai servebuilt for your installed package (docker images bambooai-executorlists it); the one here is an example. Restrict access to trusted hosts.
The web app listens on 127.0.0.1 unless started with --host 0.0.0.0. Model provider keys live in
.env and are read at start; the app does not have a model-key entry form. Integration credentials
are entered separately as described above.
Local code execution and local model inference are separate choices. When you use a remote model provider, the prompts sent to it include analysis context, such as dataset descriptions and cell outputs. Use locally hosted models for local LLM inference; Docker isolation alone does not make model requests local.
Command Line
bambooai serve [--home DIR] [--port N] [--host H] [--compute docker|local] [--keep-executor] [--no-browser]
bambooai init [--home DIR] create the working folder and its files without starting
bambooai where print the working folder
Architecture
browser ── HTTP / SSE ── web_app (Flask) ── HTTP ── executor container (Flask API + persistent kernel)
│
├── analyst/ the turn loop (session.py), the contract, the notebook,
│ the report, the reproduction run, routines, the context
├── bambooai/ model adapters (models/), log manager, config builder,
│ the local store (db/local_store.py, SQLite), the hosts
│ of routines and of the context's coding pass
├── delve/ the persistent kernel: a subprocess with a checkpoint per
│ committed cell and rollback of a failed one
└── containers/executor/ the executor image
analyst/contract.mdcontains the Analyst's base instructions.analyst/session.pyruns the loop and assembles each prompt with the question, data, working state and any applicable document or review instructions.delve/kernel.pyruns cells in a subprocess and keeps a checkpoint per committed cell.analyst/routines.pyis the pure part of routines (the promoter's prompt, the inputs, the cut, the comparison) andbambooai/routines_host.pyruns them;analyst/context.pyandanalyst/coder.pyare the words' tables and the coding pass,containers/executor/intervals_store.pythe store.bambooai/models/adapts each provider, including reasoning-effort vocabularies and prompt caching.bambooai/db/local_store.pyis a SQLite client with the same query interface the hosted edition uses against Supabase, so the routes above it are shared between the two.tools/stack/runs the app, an executor and stand-ins for the external services with a scripted analyst, so the product can be driven end to end offline - including Intervals.icu and Endura, served byfake_apis.pyfrom a calendar with the loaders' own variation (FIT files the executor's parser reads, windows without a sensor, a wellness log that ends, a race without power).
Logging and Usage
logs/<user>/bambooai_run_log.json: every model call of the current thread, including seat, model, prompt, response, tokens, time and cost, overwritten when a new thread starts.logs/<user>/consolidated_logs.json: the same across threads, rotated at 5 MB.- In the app: Usage (gear menu) reads the call records for a period - the last 24 hours, 7, 30 or 90 days, this month or last month - against the period before: the model cost, analyses, calls, tokens in (with the share served from the prompt cache and what it saved), tokens out and model time as tiles; a column chart by day stacked by seat, model or mode, measuring cost, tokens, calls or time; the seats and models as sortable tables with the configuration's list prices; every analysis with what it cost and how it ended, and for the one picked its seats, its calls laid out in time and its models; the raw rows; a CSV export. Every number comes from the records; nothing in it is written by a model. The closing card of every run shows its cells, cost and time and opens the per-call telemetry; the prompt button on any turn shows the exact prompt that produced it.
Development
git clone https://github.com/pgalko/BambooAI.git && cd BambooAI
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]" && python -m playwright install chromium
./run_battery.sh # unit suites: analyst, kernel, notebook, report, replay
python tests/e2e/test_stack.py --edition local --compute local # the real app, page and kernel; a scripted model, no network
python tests/e2e/test_stack.py --edition local --compute direct # the same against an executor process
python tests/e2e/test_integrations.py # the loaders against the fake Intervals.icu and Endura; a routine run on the next window and the next race
python tests/e2e/test_local_store.py # the store survives a server restart
python tests/e2e/test_date_inputs.py # Chromium over the loaders' date inputs
python tests/e2e/test_pill_strip.py # Chromium over the top bar's chip row
python tests/e2e/test_package.py # the wheel in a fresh virtualenv; serve with a docker stand-in
python tools/render_gallery.py --out /tmp/gallery # every page state, both themes, for visual comparison
CI (.github/workflows/ci.yml) runs the battery and builds the executor image on every push; the
browser suites and the package test are run locally before a push. A tag v* publishes the package
to PyPI (.github/workflows/release.yml).
Design notes: docs/DESIGN_CHECKLIST.md (what every change must keep true),
docs/OSS_DESIGN.md (the decisions behind this edition), docs/ROUTINES_DESIGN.md,
docs/CONTEXT_DESIGN.md and docs/INCREMENTAL_LOADS_DESIGN.md (routines, the words, the store),
docs/CODEMAP_DESIGN.md (the code map), docs/REACH_DESIGN.md (the reach record),
docs/DATASETS_DESIGN.md (the store and the keys per athlete), docs/USAGE_DESIGN.md (the usage records and
the dialog), HANDOVER.md (the state of the code).
Contributing
Issues and pull requests are welcome. Keep changes small and readable, run the battery and the end-to-end story before opening a pull request, and say in the description what you tested.
Contact
[email protected] · Issues · Discussions
BambooAI 1.x was a Python library used from notebooks (from bambooai import BambooAI). Version 2 replaces it. The 1.x releases remain installable with pip install "bambooai<2"; the source is on the v1 branch.