Skip to content

Model card — Sprout

Sprout is a grounded, evaluated, multilingual (EN/ES) houseplant-care assistant. It answers only from a versioned, cited horticulture corpus, never certifies a plant "safe," abstains below a confidence threshold, and ships with a public evaluation harness as its headline artifact. This card documents the system as deployed — a retrieval pipeline with a swappable generator — not a trained model. Sprout trains nothing.

  • Author: Chelsea Kelly-Reif
  • Project status: reference implementation · independent personal open-source project · Apache-2.0 · unaffiliated with any employer or client; contains no proprietary or client material
  • Version: sprout 0.1.0
  • Card date: 2026-06-22
  • Repository layout: package at src/sprout/; eval harness at src/sprout/eval/
  • License: Apache-2.0 (code); bundled corpus and eval datasets are synthetic and CC0-1.0

This card follows the Hugging Face model-card format and the portfolio AI-EVALUATION-STANDARD §4 (model/data cards as committed, release-regenerated artifacts). It references the cross-cutting standards rather than restating them.


Intended use

Sprout answers everyday household plant-care questions — diagnosis ("why are my Monstera's leaves yellowing?"), routine care ("how often should I water in winter?"), and toxicity lookups ("is Pothos toxic to cats?") — and grounds every substantive sentence in a retrieved, dated, licensed corpus passage with an inline citation, or refuses and points elsewhere.

Primary uses

  • Interactive Q&A over the bundled (or an adopter's substituted) horticulture corpus, via the sprout CLI, the JSON/SSE API, or the accessible chat UI.
  • A worked reference implementation of grounded RAG with structural groundedness, a never-certify-"safe" safety guard, calibrated abstention, and EN/ES parity.
  • A corpus-agnostic evaluation harness: the eval runner and suites measure groundedness, safety, calibration, refusal, and multilingual parity against any care corpus that follows the same manifest contract.

Intended users: developers and reviewers studying responsible-AI RAG patterns; hobbyist plant owners using the demo; teams adapting the harness to their own grounded-QA domain.

Out-of-the-box behavior: the offline deterministic stack is the default. No cloud account, API key, or network is required to ingest the corpus, answer questions, or regenerate the eval report.

Out-of-scope use

Sprout is not any of the following, and these uses are explicitly unsupported:

  • Veterinary, medical, or emergency advice. Sprout does not diagnose or treat animal or human poisoning. Toxicity questions are routed to a vet / poison-control line; the assistant reports what a cited source says and never substitutes for a professional.
  • A safety certifier. Sprout will never tell you a plant is safe, non-toxic, or fine to chew. The never-certify-"safe" guard (below) is structural, not a guideline.
  • A general-knowledge chatbot. Out-of-scope questions (cooking, legal, anything the corpus does not cover) are refused by the retrieval threshold gate, not answered from model priors.
  • A source of facts beyond the cited corpus. Sprout cannot answer about a species, pest, or product absent from the loaded corpus; it refuses rather than extrapolating.
  • An authority for high-stakes or commercial horticulture (agricultural dosing, pesticide application rates, regulated-substance guidance). The corpus is general household plant care.
  • A multilingual service beyond EN/ES. Only English and Spanish are supported and parity-tested; other languages are out of scope until added as a localization module.
  • A personalization service in v1. The Family Greenhouse household-data path (per-user plants, care history) is deferred to a later phase and is not part of this release.

The two stacks: offline deterministic default vs. the Claude-on-Bedrock seam

Sprout's pipeline is fixed; only the generator (and optionally the embedder) is swappable behind a config switch (config/sprout.yaml). The pipeline contract is the same in both modes: guards(input) → retrieve (hybrid, threshold-gated) → generate → citation guard → never-certify-safe guard → confidence/abstention → answer-or-refuse.

Offline deterministic stack (default — provider: deterministic)

  • Embedding: HashingEmbedding — a signed SHA-256 token-hashing bag-of-tokens projection, L2-normalised, 512-d. SHA-256 is used purely as a stable token→dimension map (a non-cryptographic use). The same text always yields a byte-identical vector, so the index is reproducible in CI. It is a retrieval baseline, not a semantic model.
  • Retrieval: pure-Python Okapi BM25 fused with dense cosine via Reciprocal Rank Fusion, a conservative species/topic filter, and a single min_score gate that decides answer-vs-refuse.
  • Generator: ExtractiveGenerator — selects the most query-relevant sentences verbatim from retrieved chunks, each tagged with its chunk id. There is no text path by which it can fabricate.
  • Why it matters: groundedness and citation coverage are 100% by construction — the generator can only copy retrieved text, and the citation guard re-verifies it. The whole project, including the eval, runs with no network and no cloud account, for free.

Opt-in offline semantic embedder (embedding_provider: static)

  • Embedding: StaticEmbedding — a curated, precomputed lookup table (EN+ES plant-care vocabulary clusters, data/embeddings/) with a hashing fallback for any token outside the table, so coverage is total. Same determinism guarantee as HashingEmbedding: no network, no training, byte-identical output for identical input. See ADR-0017.
  • Generator and everything else unchanged — this only swaps the embedder; generation.provider stays deterministic (or any other value) independently.
  • Why it exists: it closes part of the offline recall gap HashingEmbedding has by construction (see "Offline retrieval is a baseline" below) without giving up any offline guarantee. It is not the default because it does not yet clear the suite's 0.95 excellence bar on its own — see ADR-0017's measured delta and honest analysis of what it does and does not fix.

Claude-on-Bedrock production seam (provider: bedrock) and native Anthropic (provider: anthropic)

  • Answer model: Claude Haikuanthropic.claude-haiku-4-5-20251001-v1:0 on Bedrock, or claude-haiku-4-5-20251001 via the native Anthropic Messages API. Temperature 0.0; the prompt constrains the model to quote the numbered sources and never certify a plant "safe." Bedrock activation requires this priced model to be set explicitly; the older constructor fallback is intentionally rejected as unpriced rather than silently substituted.
  • Embedding (Bedrock): Amazon Titan amazon.titan-embed-text-v2:0.
  • Same failure posture as offline. Any exception, timeout, or malformed/empty model response logs and returns an empty candidate list, so a provider outage degrades to a refusal — never an ungrounded answer. A circuit breaker / fail-closed parse wraps the client.
  • Still grounded. Model-generated sentences are attributed to their best-overlap retrieved chunk and then independently re-verified by the same citation guard, so a hallucinated or mis-attributed sentence is dropped, not shown. Groundedness does not depend on the model's good behavior; it depends on the guard.
  • Cost & dependencies: A separate operational lifecycle wrapper shared-table prices Claude answer calls and fails closed before transport when unpriced or above the configured per-answer ceiling; it forwards provider inputs unchanged. Titan uses the exact shared AWS catalog rate for the configured Bedrock region and rejects missing/unsupported regions rather than borrowing a rate. boto3 and httpx are lazily imported and their clients cached only when selected, so a plain install runs offline end to end. These paths require live credentials and are tested via injected clients.

The generator is interchangeable; the guards are not. Swapping in Claude raises fluency and recall but cannot weaken groundedness, the never-certify-safe rule, or abstention — those are enforced downstream of generation. Per the README, the citation guard, the never-certify-safe guard, the abstention thresholds, and the fail-closed eval loader change only behind an ADR + CODEOWNERS review.

Never-certify-"safe" and calibrated abstention

These are the two behaviors that make Sprout honest about its limits.

Never certify "safe"

  • A deny-list output guard (guards.asserts_safety) drops any rendered sentence that certifies a plant "safe" / "non-toxic" in either language, after generation, regardless of which generator produced it.
  • Toxicity/ingestion questions are detected on input (is_safety_query) and carry a routing directive to a vet / poison-control line on both the answer and the refusal paths.
  • Sprout reports what a cited toxicity reference says and, when the corpus is silent, says so — it does not infer safety from absence of evidence.
  • The routing directive is a standardized safety message shown on every toxicity answer and refusal, in EN and ES (PromptConfig.safety_directive_for), with three parts in urgency order: (1) an urgency-forward routing line — if a pet or child may have eaten part of a plant, treat it as urgent and call now, rather than leading with reassurance; (2) a "not listed as toxic is not safe" caveat — any plant material can cause vomiting or GI upset and individual animals vary, so a source's silence is not a guarantee against harm; and (3) a standardized escalation card naming the public authorities — the ASPCA Animal Poison Control Center and the Pet Poison Helpline (with their published numbers and official pages) — plus the three facts a clinician needs: the plant, how much was eaten, and when. None of this asserts safety, and the caveat is source-attributed so the never-certify-"safe" guard leaves it intact.

Calibrated abstention

  • Confidence is a transparent function of retrieval evidence — the best passage's cosine score, nudged by its margin over the runner-up, through a fixed logistic. It deliberately does not depend on answer fluency, which would reward confident nonsense.
  • Below abstain_threshold (default 0.25) the assistant refuses rather than guesses. Below low_confidence_threshold (default 0.50) it answers but flags the answer for human review. (Per ADR-0012, which supersedes ADR-0005; the earlier 0.45/0.62 figures never matched the shipped, calibrated values and are corrected here as of 2026-07-05.)
  • The harness checks that these stated confidences track correctness via a reliability diagram and Expected Calibration Error (ECE) in the calibration suite — a confidence Sprout cannot back up is a test failure, not a footnote.

Training data

None. Sprout performs no training and no fine-tuning. It is a retrieval system over a corpus, plus a swappable, pinned, third-party generator used only at inference.

  • Knowledge source: the bundled corpus/ — cleaned, chunked passages carrying per-document source, license, fetch_date, and language in corpus/manifest.yaml. The bundled corpus and all eval datasets are synthetic and CC0-1.0 — authored for this project, containing no scraped or proprietary text — so the demo is fully redistributable. See docs/cards/data-card-corpus.md for the datasheet.
  • No user-query persistence in the demo; secrets come from the environment, never the corpus or config.
  • Adopters substitute their own corpus via config/sprout.yaml; doing so changes the knowledge source but not the pipeline, the guards, or this card's structural claims.

Evaluation

The eval harness is the headline artifact. The committed, release-regenerated report lives at docs/audits/eval-report.md (with .html, .json, JUnit, and SARIF companions). Runs are content-hashed and byte-identical for identical inputs; the run identity changes if the dataset, judge model, prompt version, temperature, or thresholds change.

Scoring blends deterministic checks (citation resolves to the corpus; forbidden "safe" certifications absent; "as of" date shown; language matches) with an LLM-as-judge for groundedness/helpfulness. The judge model is deliberately different from the answer model:

  • Answer model: Claude Haiku (cloud seam) or the offline ExtractiveGenerator (default).
  • Judge model: Claude Sonnetclaude-sonnet-4-6 — at temperature 0.0, with a committed, versioned judge prompt (PROMPT_VERSION). Malformed judge output raises (fail-closed).

A 10% human-agreement sample is reported with raw agreement and Cohen's κ (portfolio gate: κ ≥ 0.60); the judge is only trusted insofar as it tracks human labels.

The 9 suites

Suite What it asks
groundedness Is every claim entailed by the cited passage? (flags contradicted vs unsupported)
safety For toxicity/ingestion questions: cite a toxicity reference, never certify "safe," route to vet/poison-control
calibration Do stated confidences track correctness? (reliability diagram, ECE; abstains below threshold)
refusal Out-of-scope, "just tell me it's fine," and prompt-injection embedded in questions
multilingual Spanish answers preserve the facts and citations of their English mirror (per-case, against each case's own English anchor)
language-parity Do the language slices pass at the same rate? Scores every case — English anchors included — as its own language slice and gates the aggregate |EN − ES| pass-rate gap. A different quantity from multilingual, which never scores the anchors
completeness Does a multi-facet question's answer cover every authored expected_fact, not just whichever ranked first? (cases with two or more; single-fact cases are groundedness's job)
conversation Replayed through the same bounded SessionMemory the server uses: do the follow-up's citations resolve to the right species, do a prior turn's forbidden_terms stay out, and can history never turn an out-of-scope follow-up into an answer?
toxicity-coverage Corpus-level: does every ASPCA-listed pet-toxic plant the corpus covers carry an English "## Toxicity" section that routes to a vet and a poison-control line?

Target 120+ YAML cases, each carrying id, question, expected behavior (answer / partial / refuse-and-redirect), required citation or fact, language tag, and rationale. Everything is fail-closed: a dataset-hash mismatch, a malformed case, an empty suite, or a malformed judge response fails the run rather than passing quietly (a zero-item suite can never report PASS). Each suite carries a written MetricDefinition (name, definition, threshold, direction) reproduced verbatim in the report — no opaque scores.

The numeric model-index values above are illustrative of the offline deterministic stack's by-construction guarantees (groundedness, citation coverage = 1.0; forbidden certifications = 0.0), with one exception: en-es-parity is a measured per-run score, and tests/test_model_card.py fails the build if it drifts from the language-parity score in the committed docs/audits/eval-report.json. Per-run scores — including ECE, which depends on the judge and the case set — remain authoritative in the committed report, not here.

Limitations and disclaimers

A passing evaluation is NOT a safety guarantee. This is NOT veterinary advice.

Read this section before relying on any answer.

  • A green eval is evidence, not a warranty. The suites measure groundedness, the absence of "safe" certifications, calibration, refusal behavior, and EN/ES parity on a finite case set against a specific corpus and judge. Passing them means Sprout behaved correctly on those cases. It does not prove an answer is correct, complete, current, or safe for your specific plant, pet, child, or situation.
  • Sprout is not veterinary, medical, or emergency advice and must not be used as such. If an animal or person may have ingested a plant, contact a veterinarian or poison-control line immediately. Sprout deliberately refuses to certify safety precisely because that judgment is not its to make.
  • Grounded ≠ true. The citation guard guarantees every rendered sentence is supported by a retrieved passage. It does not guarantee the passage is correct — plant-care lore is contradictory and seasonal, and the bundled corpus is synthetic (authored for testing), so it must not be treated as horticultural ground truth. With a real corpus, answers are only as good, current, and unbiased as that corpus.
  • Corpus-bounded by design. Anything outside the loaded corpus is refused, including newer species, products, or guidance. Coverage gaps look like refusals, not best guesses — which is the intended failure mode, but a limitation nonetheless.
  • "Not listed as toxic" is not "safe," and a cited answer can be true-but-incomplete. A source that does not list a plant as toxic is not a clean bill of health: any plant material can cause vomiting or GI upset and individual animals vary. Because generation is extractive, a rendered sentence is faithful to its passage yet that passage may legitimately omit a toxin or a caveat — a correct retrieval that happens to be incomplete is the failure that matters most here. Sprout mitigates this by never certifying safety and by attaching the non-toxic caveat and the vet/poison-control escalation card to every toxicity answer and refusal, but the residual risk is real: treat reassuring-sounding silence as unverified and call a professional.
  • Photo-ID can be confidently wrong; reminders are local-only. When the optional photo path is used, a visual match is a selector, not a fact — it never renders as a citation and the care answer still flows through the grounded pipeline, but a confident wrong identification of an in-corpus species can yield a correct, cited answer about the wrong plant (mitigated by the confidence gate and the "a visual match, not a cited fact" label; see ADR-0010). Care reminders are stored only on the user's device (var/reminders.json): no sync, no push delivery (ADR-0011). A local review console for flagged/refused traces (sprout review) is opt-in and off by default; enabling it stores question text on the maintainer's own machine (var/review/queue.json) — see ADR-0020 and RESPONSIBLE-TECH-AUDITS.md §C before enabling it.
  • Offline retrieval is a baseline. The default HashingEmbedding is lexical, not semantic; paraphrased or synonym-heavy questions may under-retrieve and refuse where a semantic embedder would answer. This trades recall for zero-dependency reproducibility. An opt-in offline alternative, StaticEmbedding (embedding_provider: static), closes part of that gap with a curated EN/ES vocabulary table while staying fully offline and deterministic — see ADR-0017 for the measured eval delta (refusal 0.9118 → 0.9412, groundedness unchanged) and its remaining limits (unknown-species and persona-override cases are not recall problems it can fix). It is not yet the default; Titan/Bedrock remains the path for maximum recall when a network and cloud account are available.
  • Calibration is empirical and corpus-specific. Confidence is derived from retrieval evidence and tuned to the bundled data; on a different corpus the thresholds should be re-calibrated and the reliability diagram re-checked.
  • Cloud-mode residual risk. The offline default cannot fabricate (it copies source sentences verbatim), so its grounding/never-certify-safe guarantees hold by construction. With a Claude generator the model composes free text, and the citation guard becomes the sole structural defense. An adversarial review hardened that path: the guard now rejects negation-polarity inversion ("X is not toxic" attributed to a "toxic" chunk) and content-free fragments, and the never-certify-safe guard is negation-aware (catching "no risk"/"pet-safe") while still permitting honest source-attributed statements ("the cited reference does not list X as toxic"). One residual remains: lexical (bag-of-token) verification can still admit a same-plant recombination (e.g. swapping which attribute applies), bounded in practice because retrieval is species-scoped to one plant. An optional NLI-grade cross-encoder entailment verifier now exists behind the citation guard for exactly this residual (generation.support_verifier: nli, cloud path only, ADR-0018) — it is an additional gate, applied after the lexical check, that must also judge the cited chunk as entailing the sentence. It defaults off; enabling it requires a pinned, hash-verified model weight file (fail-closed on a hash mismatch) and is currently EN-validated only — off-the-shelf NLI checkpoints are typically EN-strong/ES-weaker, and per-language false-reject rates have not yet been measured, so do not assume ES parity for this gate. The guards remain the safety boundary either way, not the model's instruction-following.
  • Prompt-injection is handled structurally, not perfectly. Injection attempts are labeled for the refusal suite and logs; the actual defense is that an injected instruction cannot produce a sentence entailed by a retrieved chunk. Novel attacks should be filed as eval cases.

Fairness and EN/ES parity

Sprout's fairness commitment is language parity: a Spanish-speaking user must get the same facts, the same citations, and the same safety behavior as an English-speaking user.

  • Parity is a gate, but it is a per-case gate. The multilingual suite checks that each Spanish answer preserves the facts and citations of its English mirror, and the AI-evaluation gate enforces per-case structural parity at ≥ 0.85: the fraction of non-reference-language cases that match their English anchor on the refuse/answer decision and the cited-plant set (multilingual-parity, src/sprout/eval/suites/multilingual.py). Falling below that floor fails CI, and every failing case is listed by item id in the report, so no case hides inside the average.
  • The aggregate |EN − ES| pass-rate gap is now gated too — as a separate suite. The language-parity suite (src/sprout/eval/suites/language_parity.py) scores every recorded case, English anchors included, as its own language slice and gates the gap between the slices' pass rates at ≤ 0.05 (5 pp). It is a deliberately separate suite from multilingual because it is a different quantity: one is a per-case structural check that never scores the English anchors, the other is an aggregate rate comparison that scores both sides. Conflating them is the error this card recorded on 2026-08-29 and it is now structurally hard to repeat — each has its own written MetricDefinition and its own row in the report. The report is also disaggregated by language at last: the suite emits a pass rate · <language> segment per slice.
  • What the measured gap does and does not license us to say. The committed run measures 0.011 (1.1 pp) — EN 0.826 over 121 cases, ES 0.838 over 37 — which clears the 5 pp target. Two caveats travel with that number and are printed in the report beside it. First, the 95% Newcombe interval on the gap is [0.000, 0.127]: with only 37 Spanish cases, this corpus cannot rule out a real gap well above the target, so the gate currently catches a large disparity, not a small one. Second, the EN and ES case sets are not matched — English carries case families (all 24 calibration cases, most groundedness cases) that Spanish does not — so the pooled gap mixes a language effect with a case-mix effect. The suite therefore also publishes report-only diagnostics that recompute the same gap inside strata common to both languages, and on this corpus all of them exceed 5 pp: matched pairs 0.083, behavior=answer 0.068, behavior=refuse-and-redirect 0.052. They do not gate — the ledger's declared metric is the aggregate — but they are published rather than averaged away, and they say plainly that the honest fix is a deeper Spanish slice, not a better-looking headline.
  • Same guards in both languages. The never-certify-"safe" deny-list, the safety-query detector, and the vet/poison-control routing all operate in EN and ES; a "safe" certification is blocked in Spanish exactly as in English.
  • Localization is modular. All user-facing strings live in per-language bundles; adding a language is a localization module plus its parity cases — but until a language has both, it is out of scope rather than silently degraded.
  • Known parity limit. Parity is measured on the synthetic corpus's EN/ES mirror; on an adopter's corpus, parity is only as good as the translation fidelity of that corpus, and must be re-measured.

Environmental footprint

No training or fine-tuning is performed, so there is no training-time compute or emissions to report. The default stack runs offline on CPU at negligible energy cost. The optional Claude seam incurs ordinary third-party inference cost bounded by the per-answer preflight ceiling; per-token inference emissions are governed by the provider, not this project. Per the AI-evaluation standard, the environmental-footprint metric is N/A-with-reason for this API-only / no-train repo.

How to cite / reproduce

git clone https://github.com/ChelseaKR/sprout.git && cd sprout
uv sync
uv run sprout ingest               # build the index from the bundled CC0 corpus
uv run sprout ask "Why are my Monstera's leaves yellowing?"
make eval                          # regenerate docs/audits/eval-report.* fully offline
make verify                        # the full CI gate set: lint · type · test >=90% · security · a11y · eval

The model card, data card, and eval report are regenerated and re-committed on each release — the same audit-as-artifact discipline applied across the portfolio.