Product comparison and receipt collaboration¶
Query caching and accepted items¶
Brave raw search results are persisted in enrichment.brave_query_cache and reused
across items and jobs. The key includes the exact trimmed query, endpoint/version,
country, language and result count. API keys are never stored. Results are rescored
against each item's original text and retailer; a cached score from another item
is never reused as proof. Positive results expire after 90 days and empty
results after one hour. Set BRAVE_CACHE_DAYS=180 on the Make command to retain
newly fetched positive responses longer (1–3650 days). Existing entries keep
the expiry recorded when fetched; changing the setting does not rewrite history.
Failed requests are not cached. Concurrent jobs serialize
a shared cache miss to avoid duplicate requests.
Normal enrichment and collaboration skip accepted items with complete product
fields before applying LIMIT, including receipt-scoped runs. Comparison mode
deliberately still includes accepted items to compare both models. In collaboration,
a skipped accepted item makes no image, model or search request. Cached Brave
results do not suppress Qwen processing for pending items.
# Process pending items using cached search evidence where available.
make enrich-products COLLABORATE=1 LIMIT=100 RECEIPT='receipt.pdf'
# Deliberately include accepted items and fetch fresh search evidence.
make enrich-products COLLABORATE=1 LIMIT=1 RECEIPT='receipt.pdf' REFRESH=1
REFRESH=1 can incur fresh API calls. It retains normal review/acceptance
requirements and deduplicates a repeated query within the job. With COMPARE=1,
it refreshes search evidence without changing accepted products. The CLI equivalent
is ENRICHMENT_REFRESH=1. DRY_RUN=1 on the Make target validates the Job and
makes no provider or cache calls; CLI runs without --write-db still perform
searches and populate the query cache, but do not save product decisions.
enrichment_brave_cache logs identify memory_hit, persistent_hit, miss or
refresh, with original fetch time and item/run identifiers where available.
Collaboration and comparison payloads retain cache provenance; summaries report
brave_api_requests (request attempts) and brave_cache_hits. The graph's search
queries retain cache status and fetch time. Reused evidence is historical evidence,
not another independent search result. The cache table is created additively for
existing deployments when a job starts and is included in database bootstrap.
Paired comparison¶
Use make enrich-products COMPARE=1 to run both models on the same database
receipt items. This is product enrichment after OCR, not a comparison of OCR
engines. It calls OpenAI and Arsene's Ollama service on every selected item,
including items already enriched. It does not wait for the normal AI fallback.
The comparison first searches Brave using the receipt text and a C/O OCR
variant where applicable. Both models receive the same top five unique retailer
results (titles, URLs and snippets), identical prompts and JSON schemas, and must
propose one to three expanded searches. This brings the comparison into line with
the normal enrichment path, which already searched Brave before its AI fallback. An empty response gets one retry with a shared clarification;
two empty responses are recorded as a model-output error with null confidence,
not a zero-score product match. Neither provider borrows the other's proposals.
Brave retrieves retailer evidence. The scorer evaluates all candidates against
the original item text, not the model's expanded words. Identical queries
share their search response within an item. Each provider can select from the
shared baseline and its own proposed searches, never the other provider's new
candidates. candidate_origin, baseline_confidence and confidence_gain show
whether a model improved on the baseline; ties can simply preserve that baseline.
Scoring version receipt-evidence-v5 records per-token evidence: exact words
weigh 1, word beginnings 0.9, brand initials 1, a single C/O OCR ambiguity in
brand initials 0.95, and sauce/salsa category synonyms 0.85. It averages these
weights; any unmatched meaningful token caps the score at 0.84. It requires a
retailer product page and at least two meaningful tokens, or an exact barcode.
Recorded package units match despite spacing (1KG and 1 kg), and recorded
percentages must match. Slash contractions (S/Berry and strawberry) and exact
consonant abbreviations (Rstrnt and restaurant) weigh 0.9. Conflicting or missing
recorded package sizes and percentages remain unmatched.
Singular/plural forms weigh 0.95. Consonant prefixes, a single omitted interior
consonant, or one omitted vowel in a short word prefix weigh 0.85; all printed
letters must remain in order. Adjacent OCR chunks such as HoneyMus tar can
match honey mustard at 0.9 without dropping either chunk. 1EA denotes one
item sold each and stays in the receipt evidence rather than imposing a package
dimension; other printed counts and dimensions remain binding. Negations such as
no salt must apply to the same following descriptor in the retailer evidence.
Snippets remain discovery context; they cannot supply missing identity tokens
when the product title and URL describe a different product.
Collaboration can resolve one known OCR glyph ambiguity, such as Lin versus
Lim, only when a closely related retained OCR observation records the alternate
reading. That token weighs 0.85 and saves its original spelling, observed spelling
and source IDs. Model proposals cannot supply that correction, and printed
package sizes or percentages cannot be changed by this path.
Recipes, brand listings and categories score zero. It no longer matches Pic
inside spice. Normal enrichment also uses this scorer and rechecks cached
candidates against the original receipt text before accepting them.
Confidence remains a lexical evidence heuristic, not a calibrated probability
of correctness. Matching printed values does not establish purchase identity. Review
URLs, titles, snippets and the evidence.tokens explanation alongside scores.
Prompt version product-queries-v3 adds the shared Brave context; filter by run
UUID when comparing experiments rather than mixing scores from earlier versions.
The job records append-only pairs in enrichment.product_comparisons and emits
structured logs for Grafana. It does not change receipt items, accepted product
cache entries or enrichment audit records. Provider/search failures are recorded
with null confidence and an incomplete pair; they do not count as model wins.
The job finishes with a nonzero exit status if any pairs were incomplete.
Deploy the comparison mode¶
Merge and deploy the application changes through the normal GitOps release
workflow. The existing database PreSync upgrade creates the comparison and run-summary tables;
the managed product-enrichment CronJob supplies the application image and
database, OpenAI and Brave secret references. It remains suspended, so comparisons
run only when requested. The OpenAI model uses the existing AI_PRODUCT_MODEL
setting/default. Qwen uses OLLAMA_PRODUCT_MODEL=qwen3.8:27b and
OLLAMA_BASE_URL=http://192.168.2.201:11436 from the CronJob manifest.
Apply the Grafana dashboard from this repository on your Mac:
make monitoring-gitops-check
make monitoring-gitops-apply
The dashboard is Home Budget Product Model Comparison, in the private Home
Budget folder at
Grafana.
It uses the existing Loki datasource and Fluent Bit logs. Select a time range
covering the experiment; paste its run_uuid into the dashboard filter to isolate
one run. Logs are limited by Loki retention, while the database pairs remain.
If panels show no data, set the time range to Last 6 hours and the Run UUID
filter to .*. In Grafana Explore, select Loki and check the raw events:
{namespace="home-budget", container="enrich", collection="fluent-bit"}
|= "enrichment_model_result"
| json
Fluent Bit emits the application JSON directly. The dashboard also handles
wrapped message JSON by unwrapping only when that field exists. If Explore
finds no events, check log collection before rerunning the experiment.
Make Ollama reachable¶
On Arsene, inspect the listener:
ss -ltnp | grep 11436
The job must reach the service from Kubernetes. Manage Arsene's listener and source-restricted firewalld rules through the Git-controlled Ansible playbook:
make ollama-gitops-check
make ollama-gitops-apply
Run these Make commands on your Mac. The role configures the shared GPU service at
192.168.2.201:11436; when firewalld is active it allows the private LAN and K3s
pod network in the default zone. For a dedicated LAN zone, set
ollama_firewall_zone in ops/ollama/inventory/production.yml. Other firewall
implementations require their own managed rule. Keep the unauthenticated endpoint
on the private network. Check reachability from your Mac before running the job:
curl --fail --max-time 5 http://192.168.2.201:11436/api/tags
Qwen loads automatically on the first request and stays loaded for five minutes after requests. Model calls time out after 180 seconds, including cold loading. The job runs providers/items sequentially and uses one Ollama model instance. The caller Pod needs no GPU allocation; Ollama on Arsene owns the RTX 3090. Check actual offload on Arsene during the experiment:
export OLLAMA_HOST=http://192.168.2.201:11436
ollama ps
watch -n 1 'ollama ps; nvidia-smi'
The CLI defaults to localhost. Set OLLAMA_HOST to the managed LAN listener;
do not start a second ollama serve process to inspect the running service.
The role also installs a private read-only GPU telemetry service at
192.168.2.201:11435/gpu. It runs a fixed nvidia-smi query as the existing
Ollama service user; the same source restrictions cover both ports. Apply the
host role before running the new application image:
curl --fail --max-time 5 http://192.168.2.201:11435/gpu
During Qwen requests, the job polls that endpoint once per second and emits
enrichment_gpu_sample with receipt, item and run identifiers. Model results
include average/peak GPU utilization, sample coverage, sampling failures and
approximate observed active seconds. Missing samples remain unknown, not zero.
These are host GPU observations during the request, so concurrent workloads
can contribute. Active seconds estimate sampled activity, not CUDA kernel time.
The memory panel still reports allocation from Ollama /api/ps size_vram.
Ollama's own load, prompt_eval, eval and total durations are recorded in
seconds, accumulated across retries. The job requests a 4096-token context by
default (OLLAMA_CONTEXT_TOKENS), with a 1200-token output budget.
Run a bounded comparison¶
Run these on your Mac after deployment. Start with one item:
make enrich-products COMPARE=1 LIMIT=1 DRY_RUN=1
make enrich-products COMPARE=1 LIMIT=1
Select a receipt filename or database receipt ID:
make enrich-products COMPARE=1 LIMIT=10 \
RECEIPT='2026-08-14/receipts_20260814_0001.pdf'
LIMIT caps the selected line items even for a receipt. Comparison defaults to
10 items; normal enrichment keeps its existing default of 100. Each comparison
item normally uses two model calls and up to eight Brave searches (two shared
baseline queries and up to three per model, with identical queries reused). Each provider
may retry an empty response once; token totals and latency include that retry.
OpenAI and Brave usage
can incur charges. Run one experiment at a time to keep GPU and service load
bounded. The Make target follows logs and reports completion/failure; Ctrl+C
stops log following but leaves this finite Job running.
The dashboard shows evidence scores, accepted matches at the 0.85 threshold, acceptance rates, score wins/ties, exact product URL agreement, model response times (including cold load), observed Qwen GPU memory and utilization, sampling errors, Ollama evaluation time, receipt enrichment time and per-item paired evidence. Failed providers are excluded from score and acceptance summaries. Model latency excludes Brave searches so shared-search cache reuse does not favor the second model.
To inspect durable results, run this SQL using your usual database client:
SELECT run_uuid, expense_item_id, compared_at,
payload->>'winner' AS winner,
payload->>'agreement' AS agreement,
payload->'results' AS provider_evidence
FROM enrichment.product_comparisons
ORDER BY compared_at DESC, expense_item_id;
enrichment_receipt_timing reports total enrichment time for the selected items
in each receipt, with shared Brave and provider breakdowns. complete_receipt
is true only when every database line item was selected. LIMIT=1 usually means
a partial receipt; use a sufficiently large limit with RECEIPT for a whole
receipt. These durations exclude queue waiting and OCR. OCR already records
processing_seconds and extraction/structuring stage timings in its receipt
cache metadata; the new duration measures the subsequent comparison stage.
Run summaries are persisted in enrichment.product_comparison_runs.summary:
SELECT run_uuid, completed_at, summary->'receipts' AS receipt_timings
FROM enrichment.product_comparison_runs
ORDER BY completed_at DESC;
Collaborate using receipt evidence¶
After the GitOps release, run a bounded two-round collaboration:
make enrich-products COLLABORATE=1 LIMIT=1 DRY_RUN=1
make enrich-products COLLABORATE=1 LIMIT=1 RECEIPT='20260214_sobeys_363_95.pdf'
COLLABORATE=1 defaults to one line item. It is separate from COMPARE=1;
select one mode per Job. The mode reads the latest stored OCR run for each receipt
source, including Tesseract, Paddle and any persisted vision-engine passes. It
retrieves probable matching lines, consensus text, line geometry, quantities,
prices, receipt totals and arithmetic checks. When asynchronous pass records lack
text, it reads the current run's recorded ocr-cache artifact from the mounted
cache directory. It verifies the recorded file size and SHA-256, source SHA and
reference, run UUID, and pass identity before recovering alternate text. Sources
retain their pass/run IDs and artifact URI/digest for audit and graph navigation.
It does not rerun missing OCR engines or silently substitute older OCR runs.
Only PVC artifacts beneath the configured cache root are supported by this
reader. Missing, overwritten, unsupported or invalid artifacts are reported in
artifact_errors and block automatic recommendations while leaving available
text usable for review. Old runs without a recorded artifact are not guessed
from filenames. The loader never writes recovered text back to PostgreSQL.
Round one gives each available model the same retrieved text and Brave baseline.
The baseline searches the original phrase and a bounded OCR C/O alternative.
Additional expansions are learned at runtime from the same retailer's accepted
enrichment.product_cache matches with confidence at least 0.9 and a product
page that still scores at least 0.9 against its original receipt text. For example,
an accepted Oep Pic Med → Old El Paso Salsa Picante Medium pair can teach the
brand initialism and the Pic/Med prefixes for later searches. There is no
fixed brand or abbreviation dictionary. Conflicting learned expansions are
bounded alternatives, not votes or proof of product identity. Review, rejected,
error, recipe, other-retailer and experimental model results cannot teach rules.
Learned searches record cache IDs, original text, product titles and URLs in
learned_searches in the logs and comparison/collaboration payloads. The graph
links these prior matches to the searches they generated. The history is reread
for each item, so accepting or revoking a cache match changes future searches
without a code change. Candidate scores still use the current original receipt
text; prior matches do not count as current receipt observations or independent
model votes. On an empty accepted cache, the system uses original/OCR-alternative
searches plus model-generated proposals; it does not invent learned mappings.
Vision profiles also receive up to two receipt page images, verified against the
source SHA-256 and read from the mounted receipt inbox. Text-only Qwen sees the
OCR evidence. Models propose readings and search queries with source citations.
The default product and receipt collaboration model is qwen3.8:27b on Arsene.
It supports vision and receives the same receipt page images as OpenAI. To
install the default model on another managed host:
OLLAMA_HOST=http://192.168.2.201:11436 ollama pull qwen3.8:27b
The previous qwen3-vl-receipts:30b-instruct alias remains available for explicit
selection and evaluation baselines. The host role maintains its ChatML template
and stop tokens from the installed qwen3-vl:30b-a3b-instruct-q4_K_M weights.
Large scanned PDFs up to 128 MiB are hash-verified in a stream and rendered
without loading the entire source into a Python byte buffer. Uploaded evidence
remains limited to two page images, each at most 2048 pixels per dimension.
Image failures include a machine-readable reason such as source_too_large or
source_hash_mismatch; source paths and credentials are not logged.
Model prompts group duplicate readings and use compact citation metadata. Full
artifact URIs, digests and pass provenance remain in the saved evidence bundle
and graph instead of being repeated for every observation in the prompt. The
character budget and request guard reserve 4096 tokens per attached image plus
the configured output budget; these are conservative estimates, not exact model
tokenization. Prompts request concise readings, explanations and citations.
Logs include prompt_bytes, reserved_image_tokens, and Ollama finish_reason
for incomplete outputs. A prompt-budget or truncation failure remains incomplete
and cannot become an accepted recommendation.
Brave searches those proposals; round two exposes every provider's proposal and
the ranked product candidates to every reviewer. Responses cannot cite unseen
sources or invent candidate IDs.
Production collaboration uses two RabbitMQ rounds. Every enabled provider gets its own discovery and review task against the same immutable receipt/OCR snapshot. The coordinator publishes all tasks before waiting; provider workers run independently, with Qwen inference on Arsene. After all tasks finish (or time out), the coordinator pools retailer results and each provider's findings, including abstentions, and dispatches a second review to every participant. Each model can revise its first review after comparing the collected evidence. There is no Qwen-first short-circuit in the queued path.
The final decision requires original receipt/product evidence and agreement
across two provider families when multiple families participate. Qwen variants
count as one family. A Qwen-only run can recommend a supported candidate with
one family. Errors, truncated evidence, disagreement and weak citations remain
in review. Peer assertions never become receipt observations; they cannot
create evidence or increase the heuristic score. Both rounds and dissent remain
in the append-only run record. Recommendations do not overwrite canonical items.
The four model-worker-* Deployments consume provider-specific durable quorum
queues with prefetch one. RabbitMQ messages carry task IDs; PostgreSQL retains
inputs and results in enrichment.model_collaboration_tasks. Workers commit
results before confirmed completion publication and ACK. Duplicate deliveries
reuse committed results; interrupted tasks have a bounded retry budget, and
late/missing workers become incomplete results after a 900-second round deadline
(COLLAB_TASK_TIMEOUT_SECONDS, 30–3600 seconds). Broker heartbeats continue
while inference runs. Tasks/results contain no provider API keys. Each worker
receives only its own provider credential plus the existing Brave key.
COLLABORATION_TRANSPORT=rabbitmq is the production/default transport and
requires RABBITMQ_URL. Explicit inline retains the old workflow for offline
fixtures and legacy comparisons; there is no silent fallback when the broker is
unavailable. Informational evaluation currently calls the legacy inline path;
it does not evaluate queue scheduling or the new evidence-exchange round.
The new managed receipt-model-profiles ConfigMap supplies cloud model IDs and
Qwen budgets. OpenAI uses the existing key/model setting. Anthropic and Gemini
participate when the optional anthropic-api / ANTHROPIC_API_KEY and
gemini-api / GEMINI_API_KEY Secrets exist in home-budget. Missing credentials
are reported in skipped_profiles; they never become zero-confidence votes.
The defaults are pinned Claude Sonnet 4.6 and Gemini 3.8 Flash; change the model
IDs in Git for your accounts. Provider access and image support must be available.
See the official Claude model reference
and Gemini model catalog.
Increase budgets or select multiple already installed Arsene models:
make enrich-products COLLABORATE=1 LIMIT=1 \
CONTEXT_TOKENS=16384 OUTPUT_TOKENS=4096 \
QWEN_MODELS='qwen3:30b,qwen3-vl:8b'
The default collaboration context/retrieval budget is 16,384 tokens and output
budget is 2,048 tokens, larger than the short product-query comparison. Qwen uses
these as num_ctx and num_predict; cloud providers receive the output limit and
a bounded evidence prompt. Context sizing for cloud providers is a retrieval
budget, not an API setting that changes their native context capacity. Images
also consume context. A conservative character check rejects oversized prompts;
it is not an exact provider tokenizer. Inspect actual usage and truncation errors.
Larger budgets are experiments, not guaranteed accuracy improvements.
Collaboration keeps distinct retrieved OCR readings as separate
reading_hypotheses, with their source IDs. It searches each reading on the
retailer site before model proposals, reusing cached queries. For example,
Cep Pic Med and Oep Pic Med remain separate hypotheses even when both occur
in the same OCR pass. Repeated passes do not add independent votes. Up to eight
distinct readings are searched; exceeding that budget records an error and keeps
the item in review. Hypotheses and search queries are retained in the run payload,
and hypotheses appear in enrichment_collaboration_evidence logs.
Models are asked to explore unresolved readings and abbreviation expansions; there is no fixed mapping from Oep to a brand. Discovery results remain scored against the original item. Review selections are constrained to supplied product IDs or null, with local citation validation still enforced. A successful search or model agreement alone does not approve a product.
Before the shared proposal/review rounds, each unresolved reading receives a
separate text-only expansion call, trying configured models in order until
discovery finds a strong retailer product match with supporting OCR. That call receives
the target reading, its citations, the merchant and up to six ranked Brave
results. The result list refreshes after each discovery search; titles, URLs and
snippets are untrusted evidence, not other models' unverified brand guesses.
When the brand is uncertain, the model is asked to search expanded product/style/size
words without the brand rather than repeat the literal abbreviations. Partial queries are tested against retailer and OCR evidence first. Unverified
queries retaining short receipt tokens are omitted from full expansions; Qwen
moves to another reading while paid fallback, when enabled, may correct once. The heuristic can also reject real short product words;
an unresolved expansion is recorded rather than accepted as a product fact.
Expanded queries are pooled and deduplicated for Brave verification, with scores
still calculated against the original receipt item. Only product pages can be
selected in the final review; brand indexes and store pages remain discovery
evidence. Within each independent worker task, discovery steps can depend on
previous search results. Different providers run concurrently. Strong matches
can skip additional discovery within that provider's task, but cannot skip
another participant or the shared evidence-exchange round. Paid providers are
disabled by default. PAID_FALLBACK=1 enrolls every configured paid provider in
both queue rounds, rather than waiting for Qwen to fail first.
Qwen receives the JSON schema in its prompt as well as Ollama's format argument.
Review schemas restrict citations to supplied source IDs and request at most six.
If a provider still returns more than the legacy validator limit of 20 citations,
the full list must contain only supplied IDs before it can be reduced to six,
retaining a citation that supports the selected product. The original list and
counts are saved under citation_normalization in model logs and attempt history.
Unknown citations, mismatched product titles, and missing OCR support still fail
validation; excess valid citations alone do not require another model call.
Expansion reading/query strings are limited to 160 characters, reasons to 300,
and citations to six. Partial queries containing receipt abbreviations are searched
before requesting corrections. Only retailer product pages with the existing score
and independent OCR support can stop discovery; a partial model guess cannot.
If those queries do not verify, Qwen moves to the next OCR reading instead of
spending another reasoning call on the same partial expansion. Other providers
retain one semantic correction attempt, and a failed correction preserves the
valid first response and both attempt records.
Production uses the explicitly named Instruct model rather than the ambiguous
30b tag. Thinking-only truncations (no answer content, nonempty thinking)
skip the larger-budget retry and record retry_skipped_reason; expansion then
opens its existing provider circuit and allows fallback discovery. Review also
skips that unproductive retry. Other truncated responses get one concise retry. Qwen recovery requests may
double the output budget, capped at 16384 tokens and the context ceiling.
A retry is skipped when the budget cannot grow; the existing context and image
budget guard still applies. Default Qwen profiles use 32768 context tokens and
8192 output tokens, independently of OpenAI's 2048-token output default.
Thinking and the final answer share Ollama's output budget. Override only Qwen
with make enrich-products COLLABORATE=1 LIMIT=1 QWEN_CONTEXT_TOKENS=32768 QWEN_OUTPUT_TOKENS=8192.
Shared CONTEXT_TOKENS/OUTPUT_TOKENS settings still apply to all defaults unless
the Qwen-specific values are supplied; explicit JSON profiles retain their budgets.
Larger contexts use more GPU memory and may cause CPU offload on the 24 GB card.
If both attempts truncate, expansion calls stop for that Qwen profile rather than
repeating failures across every remaining OCR reading. Other configured profiles
can continue discovery. Qwen reviews also get one bounded truncation retry.
Truncated Ollama responses include incomplete_output in the model-result log
and saved attempt history. content_head and content_tail retain at most 1024
response characters combined (768 from the beginning and 256 from the end for
long responses); content_chars and omitted_content_chars describe the full
response length without storing its middle. thinking_chars counts returned
thinking text without retaining that text; null means Ollama did not return a
string in that field and does not prove that no reasoning occurred. Request
bodies and attached images are not copied into these diagnostics. Token usage,
finish_reason, requested output budget and Ollama timing remain available for
comparison. Inspect the sample for repetition or unfinished JSON before raising
budgets again. Historical runs cannot recover response text that was discarded.
Incomplete responses are never parsed as valid evidence. Persistent failures
remain recorded, including failures later superseded by verified discovery and
a successful review.
Reviews require candidate_title to exactly match the retailer title for
candidate_id; both fields must be null when abstaining. The title schema is
restricted to supplied retailer titles, and title/ID mismatches require a fresh
review. Unverified expansion names are retained in discovery history but are
not repeated as product identities in the review summary.
product_source_id must equal candidate_id (both null when abstaining); source_ids must cite supporting OCR evidence. The schema restricts
both product fields to supplied candidate IDs. Missing or mismatched product
citations or missing supporting OCR citations get one new model review. Both attempts remain recorded; citations are never inserted
by the application. Unresolved evidence failures continue to block recommendations.
An expansion IncompleteModelOutput can be classified as recovered only after
verified retailer discovery, complete evidence, passing item and receipt
arithmetic, and a valid, cited review from that same model profile. The review
must meet the normal product-page, score, OCR-citation and title checks. Other
search/model failures, missing evidence and failed reviews remain blocking.
The original errors list is retained; recovered_errors and blocking_errors
explain their effect on the final decision. Run incomplete counts use blocking
errors, so successful recovery does not fail the Job. Logs and graph decision
properties retain the recovered discovery failures.
A model-generated quoted Brave search with no results gets one retry after
removing phrase quotes. Every search term and the retailer site restriction remain;
the broader query uses the original receipt item for scoring. Successful quoted
searches are not broadened. Both queries are cached and retained, with
query_fallbacks and enrichment_collaboration_search_fallback recording the
reason. Search errors remain errors and do not masquerade as empty results.
Inspect enrichment_collaboration_expansion for target_reading, attempts,
expansion_state, queries and GPU usage. The payload retains expansions, and
the knowledge graph links their proposals to observations and actual searches.
This restores the successful August 22 interactive method: OpenAI proposed
Old El Paso medium picante salsa, Brave returned the Sobeys product page,
and the former enrichment flow accepted it. That earlier run used OpenAI;
Qwen now participates in the same expansion process. Historical acceptance
does not override the evidence requirements of a new collaboration run.
Models on your Mac are not automatically available on Arsene. The Job never
pulls models. Qwen profiles containing vl are treated as vision capable by the
default configuration; use explicit profiles for custom model names. GPU requests
run sequentially and keep a requested model loaded for five minutes after each
call to reuse it across stages; Ollama manages replacement when another model
is requested. The managed Ollama host
limits loaded models and parallel requests to one. Apply the updated host role
with make ollama-gitops-check / make ollama-gitops-apply. No new replica count or
worker CPU allocation is required.
For different context sizes of the same Qwen model, set RECEIPT_MODEL_PROFILES
in the ConfigMap to JSON with unique profile names. An explicit list replaces all
default profiles, including the cloud providers:
[
{"name":"openai","provider":"openai","model":"gpt-5.6-terra","vision":true},
{"name":"qwen-8k","provider":"qwen","model":"qwen3:30b","context_tokens":8192,"output_tokens":2048},
{"name":"qwen-16k","provider":"qwen","model":"qwen3:30b","context_tokens":16384,"output_tokens":4096},
{"name":"qwen-vision","provider":"qwen","model":"qwen3-vl:8b","context_tokens":16384,"output_tokens":2048,"vision":true}
]
At most eight profiles participate, with two requests per item and no automatic
provider retries. Each profile can propose up to three searches. Calls are
sequential to bound service/GPU load; cloud and Brave calls can incur charges.
The source selection is bounded to four documents, 80 latest-run OCR passes and
relevant item excerpts. Repeated readings are deduplicated while preserving their
pass citations. All included sources, search results, proposals, reviews, usage,
image hashes and omissions remain in enrichment.receipt_collaborations.payload;
run/receipt timing is in enrichment.receipt_collaboration_runs.summary.
Grafana adds Collaborative decisions and disagreements, Collaborative proposals and reviews, and Shared OCR and image evidence coverage. Apply with the monitoring GitOps targets and filter by the new run UUID. The GPU timeline and receipt timing panels also include collaboration. In Loki Explore:
{namespace="home-budget", container="enrich"}
| json
| event=~"enrichment_collaboration_.*"
SELECT run_uuid, expense_item_id, completed_at,
payload->'decision' AS decision,
payload->'proposals' AS proposals,
payload->'reviews' AS reviews,
payload->'evidence_bundle' AS evidence
FROM enrichment.receipt_collaborations
ORDER BY completed_at DESC;
Review and accept saved results in Ledger¶
A completed collaboration records a recommendation, not an accepted product. Open its receipt in Ledger and inspect Recommendation evidence, the product URL, the search evidence and the retained OCR readings. An evidence score is a matching score rather than a calibrated probability. Choose Accept recommendation for this item to save the description and product URL with an audit event. Acceptance applies to one item; it does not automatically accept other identical rows. Supported category rules are applied to unresolved categories during acceptance, while established categories and manual overrides are retained.
To repair older rows, use Apply accepted matches and category rules on the receipt page. It reuses accepted exact retailer/text cache matches, rechecks their product-page evidence, and fills only empty product fields without a per-item collaboration review. It makes no provider calls and audits each change.
To gather evidence for remaining items without an accepted match, run a larger receipt-scoped collaboration:
make enrich-products COLLABORATE=1 LIMIT=100 RECEIPT='receipt.pdf' DRY_RUN=1
make enrich-products COLLABORATE=1 LIMIT=100 RECEIPT='receipt.pdf'
Replace the filename with your receipt. LIMIT=100 permits up to 100 items;
LIMIT=1 only processes one. Review and accept eligible saved recommendations
in Ledger. The cached repair action does not generate descriptions for unknown
products or classify every item from a grocery merchant as Groceries.
If a supported reading correction was accepted, the item label leads with
Accepted interpretation, for example Oep Pic Med. Import history retains
the selected import text, such as Cep Pic Med. Neither value describes every
OCR pass. Related OCR observations retains actual readings and provenance,
with row association unverified when the observations were retrieved by text
similarity. The imported item text, financial amount and recorded observations
are preserved. Product labels omit shopping prompts while the full source title
remains inspectable.
Use make receipts-graph after a new collaboration or acceptance to refresh the
projection, then open View receipt knowledge graph from the receipt. Select
the item and run to follow evidence, expansion, search, verification and the final
recommendation/acceptance. See the user guide
and graph manual for navigation and provenance limits.
Local-only collaboration¶
Discovery also searches the descriptive tokens without the uncertain brand,
then builds up to three follow-up queries from full words actually matched in
retailer titles/URLs/snippets. For example, prefix matches can expand a style
and size without a fixed brand or product glossary. The discovery log records
descriptive_queries and grounded_descriptive_queries.
The uncertain brand is identified from its original position before scoring
filters short or noise tokens. Thus ep Pic Med still requires evidence for
both Pic and Med; a match for medicine alone cannot produce a grounded
query. Matches for picante and medium preserve both words in the query.
When the combined suffix search produces no hit explaining all descriptive
tokens, discovery tries up to three distinct individual descriptor searches.
These broader hits still must explain every descriptor on the same result
before supplying full words for a grounded follow-up query or guiding Qwen.
The discovery log records these probes as relaxed_descriptive_queries.
Verified product evidence skips the expansion and proposal rounds and goes
directly to Qwen review with the receipt images and OCR.
When searches still lack a verified product, one small Qwen lexical task per
distinct descriptor suffix (at most three) generates brand-free full-word
hypotheses, or an empty list when no defensible expansion exists. The full reading
and merchant context preserve descriptor meaning; expanded words need not start
with literal OCR fragments, which may omit internal letters. The grammar bounds
phrases to lowercase words and excludes numbers and punctuation. The application
validates that grammar again before searching. Sampling uses temperature 0.7, top-p 0.8, top-k 20 and
seed 42 for this task; final review retains temperature 0. The discovery log
records descriptive_expansion_calls and verified_descriptive_match. These
hypotheses remain unverified until a retailer product page passes original-item
scoring and independent OCR support. Discovery citations are constrained to the
IDs actually supplied in each prompt.
If discovery remains unresolved, the fallback proposal prompt contains one citation per distinct OCR reading, rather than repeated engine observations. This round generates plausible full-word grocery search hypotheses without requiring prior search confirmation; its first query should omit the uncertain brand. Final review receives the full OCR evidence and still requires a verified retailer product page, original-item scoring and independent OCR support. Qwen requests keep the model loaded for five minutes after each call so adjacent stages can reuse it instead of paying the model load cost each time.
Search hits that do not explain every descriptive receipt token are excluded from model discovery context, preventing brand-only hits from steering Qwen. Selectable review products must pass the original-item score threshold, explain every meaningful token, and have independent OCR support. Other hits stay unverified and cannot be selected merely because a brand prefix matches.
make enrich-products COLLABORATE=1 LIMIT=1
Production collaboration uses Qwen, receipt images/OCR and Brave without calling
OpenAI, Anthropic or Gemini. Skipped paid profiles are recorded with
paid_fallback_disabled. A local-only run requires at least one Qwen profile;
insufficient evidence remains in review without changing the accepted product.
Explicit opt-in enrolls configured paid providers alongside Qwen:
make enrich-products COLLABORATE=1 LIMIT=1 PAID_FALLBACK=1
COMPARE=1 still compares OpenAI and Qwen; this local-only policy applies to
collaboration, not comparison or other receipt pipeline stages.