The local-first claim is already stated in the README, so the dashed
"100% local — your data never leaves here" badge in the sidebar foot was
redundant. Drop the markup, its now-empty foot container, the i18n string and
the dead CSS.
Enlarge nav icons, icon the board, align top controls
Three chrome polish tweaks from a UI review:
- Enlarge the sidebar nav icons from 22px to 28px so they read clearly; the
sidebar already scrolls (overflow-y: auto), so taller rows can't break the
layout, and flex-shrink:0 keeps the glyphs from squashing.
- Show the matching category icon on each Summary board row, reusing the same
/assets/tabs art as the nav (the clients row borrows the resumo icon). The
dashboard now mirrors the sidebar instead of being a bare list of labels.
- Align the three top controls (language toggle, Upload, Clear) to one 38px
height with shared centering and an 8px radius, so they read as a single
action group rather than three differently-sized buttons.
The pipeline's next-action descriptions are shown in the dashboard, so they
are user-facing PT-BR and must carry accents like the rest of the UI. Fix
the folded ones: "orcamento" -> "orçamento" in both send-quote variants and
"concluida" -> "concluída" in the trip-complete action.
The PT Q&A system prompt was ASCII-folded (Voce, portugues, informacao,
nao), the one model-facing PT string that broke the project's stated quality
bar: extraction_glossary.py spells out that the model is cued and trained on
correctly accented text, never on ASCII-folded approximations, and the DSPy
demos already follow that. Feeding the PT fine-tune off-distribution
instructions is exactly what that note warns against.
Restore the accents across the PT prompt and the quote/order/booking glossary
added for the cotar-vs-booking fix, and accent the Portuguese cue words
(cotação, orçamento) quoted inside the EN prompt. Two test assertions pinned
the old folded spellings (portugues, nao invente); point them at the accented
forms.
Teach the Q&A model the agency quote/order/booking vocabulary
The small Q&A model conflated "cotar"/"cotacao" with bookings, when in the
agency's domain a cotacao is an orcamento — a not-yet-purchased price
estimate — and is distinct from a reserva (booked flight, PNR), a pedido
(invoice for a closed sale) and an embarque (boarding pass after check-in).
The system prompt defined none of these terms, leaving the model to guess.
Add a short domain glossary to both the PT and EN system prompts spelling
out the four terms and stating explicitly that a quote is never a booking,
matching the accent-free style the rest of the prompt already uses.
Add a "Limpar"/"Clear" button next to Enviar that resets the current
browser session to an empty workspace. The bridge GETs /clear?session=...,
which evicts that session's container server-side (dropping its in-memory
repo and data), then reloads the page so it repaints from the empty hero.
The session id is unchanged; only its data is dropped. New i18n token
chat.clear (Limpar/Clear) and a styled outline button matching the send
control.
Fix identity demo holder name (matched transcription)
The identity demo's holder output still read FULANA TESTE while its
transcription was changed to JOÃO PEREIRA — an input/output mismatch that
would teach the model to emit the constant. Align the holder to the
transcription.
Three extraction-debug changes:
- Log the Stage-2 output per artifact: "[extract] <kind> stage2 raw=..."
(model JSON) and "... payload=..." (coerced domain payload), so the
constructed Stage-2 pipeline output is visible in the Space logs.
- Fix the "Fulana Teste" leak: every few-shot demo used the same
placeholder name in its name fields, so the model copied that constant
instead of extracting the real name. Vary them to distinct realistic
PT-BR names (Mariana Costa, Rafael Oliveira, Beatriz Andrade, JOÃO
PEREIRA, Camila Rocha) so the demos teach extraction, not a constant.
- Add VIVAMAIS_EXTRACTION_DEMOS=0 to run extraction with no few-shot
demos at all, for A/B testing whether the demos help or hurt.
Default back to fine-tunes; preload the transformers checkpoints
The base-model defaults were a temporary before/after demo aid. Restore
the published fine-tunes as the runtime defaults: vision
marinarosa/minicpmv4.6-vivamais-v1 (sft_v4) + its GGUF, Q&A
marinarosa/minicpm5-1b-vivamais-v4 + its GGUF.
Point preload_from_hub at the transformers fine-tunes the Space actually
loads on ZeroGPU (minicpmv4.6-vivamais-v1, minicpm5-1b-vivamais-v4),
dropping the GGUF + base entries that ggml-less ZeroGPU never uses — so
startup no longer cold-downloads the fine-tune weights on first request.
Log which model serves each inference, not just at load
Add a run-time "[model] <role> RUN <backend> <repo>" line at every
inference entry point so the active checkpoint is verifiable per request,
not only from the load-time line: vision (transformers extract batch/
stream/chat, llama.cpp extract_ocr) and Q&A (transformers generate/
stream_phases, llama.cpp generate/stream_phases). Makes a base-vs-
fine-tune swap unambiguous in the logs.
Flip the runtime defaults back to the upstream base checkpoints so the
app starts from base unless pointed at a fine-tune via env:
- vision transformers: openbmb/MiniCPM-V-4.6 (no subfolder)
- vision GGUF: openbmb/MiniCPM-V-4.6-gguf (Q4_K_M + mmproj-model-f16)
- text Q&A transformers: openbmb/MiniCPM5-1B
- text Q&A GGUF: openbmb/MiniCPM5-1B-GGUF (Q4_K_M)
The fine-tunes remain one env var away: VIVAMAIS_VISION_MODEL /
VIVAMAIS_VISION_GGUF_* and VIVAMAIS_QA_TRANSFORMERS_MODEL /
VIVAMAIS_QA_GGUF_*. ruff + mypy clean.
Make QA GGUF env-overridable + fix stale preload list
The llama.cpp Q&A model repo was hardcoded, so the deploy could not be
pointed at a different checkpoint without a code change. Add
VIVAMAIS_QA_GGUF_REPO/FILE overrides (parity with the vision GGUF), so the
Space can run the base openbmb/MiniCPM5-1B-GGUF for a before/after demo.
Refresh preload_from_hub, which was stale: it preloaded the base vision
GGUF and the v1 text GGUF while the runtime now defaults to the v1 vision
and v4 text fine-tunes (cold-downloaded at request time). Preload both the
production fine-tunes and the base GGUFs so neither the live app nor the
base-model demo pays a cold download.
Point the runtime defaults at the published fine-tunes:
- vision (transformers): marinarosa/minicpmv4.6-vivamais-v1, subfolder
sft_v4 (added to the merged-subfolder resolver);
- vision (llama.cpp GGUF): marinarosa/minicpmv4.6-vivamais-v1-GGUF with the
PTBR-v4 Q4_K_M + mmproj files;
- text Q&A (transformers): marinarosa/minicpm5-1b-vivamais-v4;
- text Q&A (llama.cpp GGUF): marinarosa/minicpm5-1b-vivamais-v4-GGUF.
All remain overridable via the VIVAMAIS_VISION_MODEL / _GGUF_* / QA env
vars. ruff + mypy clean.
Consolidate vision finetune files under finetune/vision/
Move all vision-specific finetune assets into finetune/vision/ so the
vision fine-tuning work lives in one place, separate from the text-gen
work: the synthetic generator, dataset builders, schema/contract/prompts,
configs/, patches/, fixtures/, the model + dataset cards, the vision
reports, and the repo-root vision runlog all relocate. Update every
importer (modal/, evals/, tests/, intra-package) to finetune.vision.* and
the moved path strings (configs, patches, fixtures, cards, reports) in the
Modal scripts and Makefile.
Kept at finetune/ root: the text-gen files (minicpm5/qa/whatsapp) and
redact.py, which is shared — the text distill pipeline copies it too, so
it is not vision-specific. modal/ and evals/ stay top-level shared infra.
make check green: 626 passed, 1 skipped; ruff + format clean.
Wire v3 train, merge, GGUF export, and runtime selection
Land the v3 plumbing so the whole pipeline is one parametrized run away.
Configs: minicpmv4_6_lora_sft_v3.yaml and _export_v3.yaml (v2 hyperparams
with v3 dataset keys, output dir, and repos).
modal/train.py and modal/merge_export.py take a version argument
(default v2), bake both v2+v3 configs, and derive every path from it.
package_modal_dataset gains --v3 via a generalized version knob (v1 stays
unsuffixed for back-compat). New modal/vision_gguf_export.py builds
llama.cpp at b9049, converts the merged model to F16 then quantizes
Q4_K_M, keeps the mmproj at F16, smoke-tests with llama-mtmd-cli
--reasoning off on the fixture receipt, and publishes to gguf-v3.
Runtime: vision_model_subfolder now resolves a merged-v3 repo to its
sft_v3 subfolder, and the llama.cpp VARIANT reads
VIVAMAIS_VISION_GGUF_REPO/FILE/MMPROJ_FILE. Production defaults stay on
the published v2 so the app and make check keep working until v3 is
trained and published; going live is then a one-line / one-env flip.
…
ruff format follow-up to the shared-glossary change: add the blank line
ruff expects before TranscribeAndClassify now that the inline glossary
constant was removed. No behavior change; keeps `make check` (ruff
format --check) green.
Re-accent extraction glossary and prompts (shared constant)
Brazilian-Portuguese accent fidelity is this project's top quality bar,
but the classification glossary was written de-accented (orcamento,
sessao, cartao, portao, transferencia, Itinerario, Cod. reserva) in two
drifting copies and the dspy payload field hints, training and cueing
the model to read and emit ASCII-folded Portuguese.
Extract the glossary into a single dependency-light source of truth,
src/vivamais/adapters/models/extraction_glossary.py, with correct
accents and a new PTBR_FIDELITY_INSTRUCTION (preserve accents; R$
1.234,56; DD/MM/AAAA; CPF/CNPJ masks). It imports nothing heavy (no
dspy), so finetune/ can reuse it and train == serve can never drift
again.
Wire both consumers to it: dspy_extractor.py drops its inline copy and
appends the fidelity instruction to the transcribe+classify signature;
finetune/prompts.py imports the glossary and adds the fidelity line to
the stage-1 and stage-2 prompts. Re-accent the de-accented field hints
in dspy_payloads.py (destinatario->d…
Use fp16 for the torch transcriber on pre-Ampere GPUs
The transformers whisper pipeline loaded in bf16 on any CUDA device. On
a T4 (compute 7.5, no native bf16) that yields NaN/empty transcriptions
- the live Space transcribed 15 audios to nothing. Gate dtype on the
device compute capability: bf16 only on Ampere+ (>= 8.0) or ZeroGPU's
A10G, else fp16. Mirrors the embedder dtype gate.
A full-res default (MAX_IMAGE_DIM=0) worked on the transformers vision
path because it caps slices at VIVAMAIS_VISION_MAX_SLICES=9, but the
llama.cpp mmproj handler slices by image size with no cap. A long
full-res receipt/ticket screenshot then emits more image tokens than the
vision context holds (seen as vision_error / "llama_decode returned -1",
the artifact demoted to UNCATEGORIZED) and inflates VRAM on the shared
T4. Default MAX_IMAGE_DIM to 1280: receipts stay legible while slices are
bounded to ~10. 0 still disables resizing; VIVAMAIS_MAX_IMAGE_DIM
overrides.
Prefer the torch transcriber over CTranslate2 on the GPU backend
faster-whisper's CTranslate2 engine is built for CUDA 12, but torch>=2.8
pulls in CUDA 13 on the T4, risking a cuDNN/CUDA version mismatch that
would fail to load - and the whisper adapter raises instead of falling
back (models never run on CPU), which would block startup. The torch
TransformersTranscriber shares torch's CUDA stack and the same PT-BR
whisper weights, so prefer it on the llama.cpp GPU backend; faster-whisper
remains the fallback when transformers is unavailable.
Gate embedder bf16 on GPU compute capability, not is_bf16_supported
torch.cuda.is_bf16_supported() returns True even on a T4 (compute 7.5,
no native bf16) because it counts emulated support, so the embedder
still loaded bf16 on the live Space (logged device=cuda
dtype=torch.bfloat16) and ran the slow emulated path. Gate on the device
compute capability instead: bf16 only on Ampere+ (>= 8.0) or ZeroGPU's
A10G, else fp16.
Shrink QA context to fit the T4 alongside other models
On the live t4-small Space the QA model crashed at startup of its
llama_context: "Failed to create llama_context". The 15 GB T4 hosts the
vision GGUF + f16 mmproj and the torch Nemotron embedder simultaneously,
and the QA model's 32768-token KV cache pushed past free VRAM.
The RAG prompt is bounded (domain facts + top-k chunks + recent tail),
so QA_N_CTX_DEFAULT drops 32768 -> 8192, cutting the KV cache ~4x.
VIVAMAIS_QA_N_CTX still overrides. Confirmed from the runtime traceback
(MiniCPM5TextModel._ensure_loaded -> Llama -> Failed to create
llama_context).
The Nemotron embedder loaded in bf16 on any CUDA device. A T4 (compute
7.5) has no native bf16 and would emulate it slowly, so prefer bf16 only
when torch reports native support (and on ZeroGPU's A10G, where the
current behavior is unchanged), else fp16. fp32 stays the off-GPU
fallback.
The faster-whisper adapter was hardcoded to device="cpu"/int8. Since
models never run on CPU and this adapter is wired only on the llama.cpp
GPU backend, resolve the device to CUDA/float16 and raise if no CUDA is
available rather than silently degrading. ZeroGPU is rejected too: its
CUDA virtualization patches torch only, so CTranslate2 cannot use the
GPU (and the ZeroGPU backend uses the transformers transcriber anyway).
VIVAMAIS_WHISPER_DEVICE forces a device when needed.
Bake the live Space's production models as defaults
The deployed marinarosa/vivamais Space overrode the Q&A model to
marinarosa/minicpm5-1b-vivamais-v1 and set VIVAMAIS_MAX_IMAGE_DIM=0,
while the code defaults still pointed at the stale MiniCPM5-1B-PTBR-v3
and a 672px resize cap. Bake the production choices in so the defaults
match what actually runs and the env overrides become redundant.
- llama.cpp QA_VARIANT -> marinarosa/minicpm5-1b-vivamais-v1-GGUF
(the published Q4_K_M GGUF), used for both Q&A and the
grammar-constrained stage-2 extractor.
- transformers DEFAULT_QA_MODEL -> marinarosa/minicpm5-1b-vivamais-v1
for the ZeroGPU alternate.
- MAX_IMAGE_DIM default -> 0 (no resize). Inference only runs on GPU
now, so the model's own slicer handles full resolution and receipt
legibility is preserved.
Vision keeps the base openbmb/MiniCPM-V-4.6 GGUF on llama.cpp: no GGUF
of the fine-tuned vivamais-vision-merged-v2 exists yet, an accepted
tradeoff for now.
Drop transformers-v46 backend; route cuda runtime to llama.cpp
Models now run only under two GPU modes: ZeroGPU (all-torch) and
llama.cpp on a real GPU. The torch-on-real-CUDA transformers-v46
backend is removed and the cuda runtime defaults to llamacpp-v46, so a
T4 Space exercises llama.cpp at runtime (genuinely earning the Llama
Champion badge). The cpu runtime maps to mock: real models never run on
CPU.
KNOWN_BACKENDS drops transformers-v46; the legacy "transformers" alias
is removed (unknown values still fall back to the detected runtime, so
stale Space vars do not crash startup). _RUNTIME_DEFAULT_BACKEND becomes
zerogpu->zerogpu-v46, cuda->llamacpp-v46, cpu->mock. The embedder is no
longer gated behind gpu=False/VIVAMAIS_EMBED_ENABLE: both GPU backends
build it and it selects CUDA internally. Dead _create_qa_text_model and
_create_transformers_adapters are deleted.
Show honest counts on Resumo and translate presenter chrome
Rebuild render_board to surface only what we can count reliably: a
Clientes row plus one row per artifact section (mirroring the sidebar),
all with a neutral status dot. Drop the Recebido/Pendente money rows,
the Total pago/Saldo/Lucro ledger, and the pipeline rail + "Proxima
acao" banner — these all derived from quote/reservation reconciliation
that is not reliably extracted. render_route_rail, render_ledger, and
the pipeline parameter on render_tick are removed, and the dead
.vm-route* / .vm-ledger / .vm-printer / .vm-stamp CSS goes with them.
Thread a `lang` argument (default English) through the text-bearing
builders so the board, customer strip, section titles, chat, Q&A
thinking label, and trace headers render from the i18n table. Per-kind
table column headers stay Portuguese for now (out of scope).
Update the hypermedia tests to the new contract and add an i18n test
pinning the English default, Portuguese lookup, and key fallback.
Answer Q&A in the selected UI language (EN default)
Add an English twin of the Q&A system prompt and select it by the new
`lang` argument threaded through `_build_prompt` / `stream` /
`stream_phases`. English is the request default; Portuguese stays the
fallback so existing callers and dashboard tests are unaffected. Only the
answer-language instruction and the "Question:"/"Pergunta:" user-prompt
prefix change — the RAG context assembly (customer facts, chunk labels,
conversation header) stays Portuguese per the agreed scope, so the model
answers in English from Portuguese context.
Pin the behavior with TestPromptLanguage: EN selects the English system
prompt and "Question:" prefix; the default selects the Portuguese pair.
Introduce a tiny presenter-layer i18n module: a Lang literal
("en"/"pt"), DEFAULT_LANG="en", a vm_lang cookie name, normalize_lang
(anything not "pt" collapses to English), and a TRANSLATIONS dict keyed
by stable slugs (nav.*, empty.*, board.*, topbar.*, chat.*, hero.*,
trace.*, status.*, stage.*) read through t(key, lang).
This is the foundation for serving the UI in English by default with a
permanent PT-BR/EN toggle. The table lives in the adapters/presenters
layer so the application layer never imports it (the Q&A prompt switch
takes a plain lang string instead), keeping the dependency rule intact.
t() falls back to the key itself on a miss and to English when a Portuguese
entry is absent, so a partial table degrades gracefully rather than raising.
Move the Viva Mais dashboard QA system prompt to Portuguese and make the grounding rule explicit: answer from the supplied context and refuse missing facts instead of inventing names, dates, values, hotels, flights, documents, or payments. Reuse that prompt for MiniCPM5 SFT rows so training and production prompts stay aligned.
Add targeted refusal examples with nearby distractor facts, broaden the unknown gate to accept natural Portuguese context-absence wording, and render SFT text with MiniCPM's no-thinking assistant prefix. The previous training text skipped that generation prefix, which made the learned answer position differ from production inference.
Checked with the local SFT smoke command and make check: 630 passed, 1 skipped.
Co-authored-by: Codex <codex@openai.com>
Expose a persisted-work counter on every upload status tick
Each status tick now carries data-work-done: the count of persisted
artifacts, transcribed audio messages, and indexed corpus chunks. The
counter is monotonic within an upload because every pipeline stage
persists its results and re-runs skip what is already stored.
This gives the browser's resume loop a stall signal: a ZeroGPU resume
pass that could not reach the GPU (expired proxy token, queue timeout)
leaves the counter flat, while any productive pass advances it. The
marker is emitted by _tick on every yield, so error ticks carry it
too. Pinned by a smoke test asserting the counter starts at zero on a
fresh container and only ever grows across ticks.
Model answers (e.g. a client report) arrive as Markdown but were
escaped and shown as literal text under white-space: pre-wrap, so
headings, bold spans and bullet lists read as raw asterisks and
hashes. Add markdown_lite, an escape-first renderer for exactly the
subset the Q&A model produces: headings (mapped to h4-h6), bold,
italic, inline code, bullet and numbered lists, paragraphs with line
breaks, and horizontal rules. The text is HTML-escaped before any tag
is emitted, so model output can never inject markup.
render_qa_answer now wraps the answer in a div (block elements are
invalid inside the old span) and the bubble drops pre-wrap in favor of
compact CSS for the rendered elements; the thinking trace stays
escaped plain text. Considered vendoring a client-side renderer
(marked) but a server-side subset keeps the fragment contract testable
in pytest and adds no dependency to the Space runtime.
Add Nemotron Embed VL adapter and wire it per backend
Implement the Embedder port over nvidia/llama-nemotron-embed-vl-1b-v2
(Llama 3.2 1B + SigLip2, 2048-dim multimodal embeddings), the NVIDIA
Nemotron model powering the RAG pipeline. Loaded via transformers
AutoModel with trust_remote_code, sdpa attention (no flash-attn
dependency), bf16 on GPU and float32 on CPU. A spike against the
repo's pinned transformers 5.10 confirmed the model's remote Eagle
code imports, instantiates, and runs all three encode paths (query,
text doc, image+text doc), so no vendoring was needed; the model card
processor settings (p_max_length 10240, 6 tiles + thumbnail) are
applied at load.
The adapter mirrors transformers_text exactly for ZeroGPU: weights
load eagerly in the main process at startup (the CUDA emulation only
intercepts .to("cuda") then), every forward runs in a @spaces.GPU
function (duration 120s, override VIVAMAIS_EMBED_GPU_DURATION), with
the nested-task guard and one retry on expired proxy tokens. All
vectors come back L2-normalized so retriev…
Answer dashboard questions with hybrid retrieved context
DashboardInteractor gains an optional Embedder and routes prompt
assembly: when an embedder is wired and the corpus index is non-empty,
the context is retrieved instead of stuffed. The RAG context layers,
in order:
1. Domain matches - customers whose name, passengers, reservation
codes, or transaction ids appear in the question contribute their
full live facts block, always and first. This deterministic backstop
keeps grounding intact even if semantic recall misses.
2. Semantic top-k (k=6) chunks by cosine over the stored vectors,
labeled by source (Dados do cliente / Trecho da conversa /
Documento extraido), skipping facts chunks already included and
respecting the 12k-char budget.
3. A 1.5k-char recent-conversation tail via the existing _trim_oldest,
so "what did she just say?" questions keep working.
The full-context prompt survives verbatim as _full_context and remains
the path whenever no embedder is wired (cpu/mock-off) or the index is
empty (traced as rag.…
Add the IndexCorpus interactor: it builds chunk specs from the repo,
diffs their content-addressed ids against what is already stored
(deleting stale rows, skipping unchanged ones), and embeds only the
pending chunks - image documents in batches of 4 through
embed_image_documents, text chunks in batches of 16. Every batch is
persisted before the next starts and progress is yielded as
(done, total).
BuildCrmCard gains an optional indexer that runs as a new "index"
stage after assembly, when all customers, messages, and artifacts are
persisted. On ZeroGPU a proxy-token death during indexing propagates
to the existing resumable-tick + chained-resume mechanism unchanged:
the resume pass skips extraction (cached) and continues indexing from
the chunks already saved, so no new resume protocol is needed.
Wiring: app.py builds IndexCorpus from the new "embedder" adapter key
(mock backend ships MockEmbedder; real backends land with the Nemotron
adapter), the upload status shows "Indexando con…
Add deterministic MockEmbedder for offline retrieval tests
A bag-of-words embedder satisfying the Embedder port: each token
hashes (sha256) to a fixed pseudo-random 64-dim vector and a text
embeds as the L2-normalized sum of its token vectors. Texts sharing
tokens with a query therefore score higher cosine, which is exactly
enough signal for unit tests to assert retrieval ranking without any
model. Image documents embed their companion text, mirroring how the
real multimodal adapter folds OCR text into the document embedding.
Deterministic and dependency-free, so the full suite stays offline
and reproducible. Part of the RAG pipeline for dashboard Q&A (UC8).
Store corpus chunks with embeddings in both repositories
Extend the Repository port with chunk persistence (save_chunk,
list_chunk_ids, list_chunks_with_embeddings, delete_chunks,
count_chunks) and implement it in SqliteRepository and
InMemoryRepository.
SQLite gets an additive corpus_chunks table (CREATE TABLE IF NOT
EXISTS, so existing databases upgrade transparently) with the vector
stored as a float32 BLOB encoded via the stdlib array module - no
numpy in the adapter. INSERT OR REPLACE keyed by the content-addressed
chunk id makes re-saving idempotent; the indexer diffs stored ids
against desired ids and deletes the stale ones. count_chunks gives the
dashboard a cheap "is the index ready?" probe.
The in-memory implementation mirrors the same surface with a dict, so
the Space default (no VIVAMAIS_DB_PATH) keeps the index for the
session lifetime, consistent with every other aggregate.
Part of the RAG pipeline for dashboard Q&A (UC8).
Add Embedder port, corpus chunker, and hybrid retrieval logic
Lay the pure-logic foundation of the RAG pipeline that will replace
the dashboard Q&A stuff-everything prompt. Three pieces, all offline
and model-free:
- Embedder port (application/ports.py): a Protocol embedding queries,
text documents, and (image_bytes, text) pairs into one L2-normalized
vector space, so retrieval is a plain dot product. Only bytes/str
cross the boundary; image decoding stays in the adapter.
- Chunker (application/chunking.py + domain/retrieval.py CorpusChunk):
turns persisted data into retrieval documents - one facts block per
customer, sliding 12-message windows (stride 8) over the
conversation, and one chunk per artifact pairing a payload-identifier
header with full_transcription (previously unused by Q&A) and the
source image bytes when readable. Ids are content-addressed
(sha256 prefix) so re-indexing unchanged data is a no-op.
- Retrieval (application/retrieval.py): brute-force cosine top-k
(corpus is hundreds of chunks; no index needed) p…
Show the thinking trace greyed out under a Pensando label
While the Q&A model reasons, the answer bubble now streams the trace
in muted slate under an uppercase 'Pensando' label with pulsing
ellipsis, separated from the answer body by a dashed rule; the trace
stays visible (greyed) once the answer streams in. render_qa_answer
gains a thinking parameter and handle_search accumulates the two
phases separately from dashboard.stream_phases.
Before this, thinking mode meant a silent bubble for the whole
reasoning phase - the trace was stripped from the stream and the user
saw nothing until the first answer token.
Stream Q&A phases through the dashboard interactor
stream_phases becomes the dashboard's primary generator: it forwards
the model's thinking and answer chunks to the caller while keeping the
existing keyword shortcuts, generate() fallback, and help text as
answer-phase chunks. stream() is now a filter over it, so search() and
the BDD contract are unchanged.
Traces record the answer only, never the reasoning: the exported
traces (Sharing is Caring badge) must stay leakage-free, matching the
eval gate that treats reasoning leakage as a hard failure. A test pins
both the passthrough and the answer-only trace payload.
Add stream_phases to the TextModel protocol, yielding (phase, text)
tuples with THINKING_PHASE/ANSWER_PHASE constants defined in ports so
the application layer can consume the reasoning trace without
importing adapters. split_reasoning_stream in text_postprocess does
the tag-aware splitting (chronological, partial tags held back across
chunks); stream_without_reasoning becomes a filter over it so the
answer-only behavior is pinned by the same buffering logic.
Implementations: the transformers adapter streams phases through the
ZeroGPU boundary (the @spaces.GPU generator now yields tuples); the
llama.cpp MiniCPM5 adapter splits its raw token stream the same way;
the mock yields a canned thinking phase so the offline backend
exercises the UI. A truncated trace now streams as thinking to the
end instead of being dropped, which is the right surface once the UI
shows the trace.
Groundwork for showing 'Pensando' reasoning in the Q&A bubble; stream()
behavior is unchanged for existing calle…
The system prompt now states that the user asking questions is the
travel agent who runs Viva Mais, that her conversation messages are
labeled 'Agente de viagens', and that everyone else is a customer. The
conversation block renders her side with that label instead of
'{contact} (a agencia)', keyed on from_me so mock adapters and
previously persisted messages get the label too.
Together with alias-aware ingest this stops answers that treat the
agent as 'Cliente B': the model no longer sees her export label at all
and is told explicitly which side of the conversation she is.
Recognize the agent by alias and rename her at ingest
WhatsApp exports label the agent's own messages with her profile name
(currently the letter B in real exports), not the configured owner
name, so her messages parsed as a regular participant: from_me stayed
False, identify_customer counted her as a contact, and the Q&A model
answered about 'Cliente B' as if the agent were a customer.
parse_chat now accepts owner aliases (VIVAMAIS_OWNER_ALIASES env,
comma-separated, threaded through Config and both app.py call sites)
and any owner-or-alias sender is marked from_me and renamed to the
canonical TRAVEL_AGENT_LABEL ('Agente de viagens') domain constant.
counterpart and identify_customer now exclude messages by the from_me
flag instead of name matching, so the rename cannot reintroduce the
agent as a customer.
Audio transcriptions inherit the fix for free: they attach to messages
whose contact is already normalized at parse time.
Raise the generation budget from 768 to 2048 tokens in both the
transformers and llama.cpp Q&A adapters. With thinking enabled the
reasoning trace consumes output budget before the answer starts, and a
768-token cap risks truncating the trace, which the strippers now
convert into an empty answer rather than leaked reasoning.
The 32k context window leaves ample room, and the 1B model on the
ZeroGPU a10g stays well inside the 90-second GPU task duration even at
the full budget.
Flip enable_thinking in the transformers Q&A adapter from hardcoded
False to a VIVAMAIS_QA_THINKING env flag that defaults to on, and
thread the flag into the reasoning strippers as opened_in_prompt so
the prompt-embedded <think> tag is handled on both the generate and
stream paths. Only final answers reach the user; the trace never
streams into the answer box.
The 2026-06-12 Viva Mais QA evals motivate the default: thinking lifts
the upstream base from 0.620 to 0.677 average score (pass rate 0.531
to 0.625) and candidate 002 to 0.711 with zero leakage failures. The
deployed v3 fine-tune was not itself A/B'd in thinking mode, so the
env flag keeps a one-variable rollback on the Space if it regresses.
The llama.cpp Q&A adapter is unchanged: llama-cpp-python offers no
chat-template kwarg passthrough, and the deployed ZeroGPU Space runs
the transformers stack.
Support prompt-embedded think tags in reasoning strippers
With thinking enabled, MiniCPM's chat template emits the opening <think>
tag inside the prompt, so generated text and streams begin mid-reasoning
and end with a bare </think> before the answer. The existing strippers
only suppressed blocks that opened inside the generation, which would
leak the trace into the user's answer box.
Add an opened_in_prompt keyword to strip_reasoning and
stream_without_reasoning that starts them in the suppressed state. A
trace truncated before its closing tag now yields an empty answer
instead of leaking reasoning, mirroring the eval scorer fix in a26193a.
This is groundwork for enabling thinking in the Q&A adapter; behavior
is unchanged for existing callers.
Point both Q&A runtimes at the new v3 fine-tune: the transformers
adapter (ZeroGPU path) now defaults to marinarosa/MiniCPM5-1B-PTBR-v3
and the llama.cpp adapter (cuda/cpu path) pulls
MiniCPM5-1B-PTBR-v3-Q4_K_M.gguf from the matching -GGUF repo. The
README preload_from_hub entry is updated so the Space caches the new
weights at build time instead of the retired WhatsApp-v1 checkpoint.
Both repos were verified live on the Hub before the switch and the
GGUF filename matches the published artifact. Env overrides
(VIVAMAIS_QA_TRANSFORMERS_MODEL, VIVAMAIS_QA_MODEL_PATH) still allow
falling back to any other checkpoint without a code change.
Build the hero desk objects (skeuomorphic tier 2)
The conversa replay now lives inside a phone lying on the desk: dark
gradient bezel with earpiece slit and home indicator, slight resting
rotation, the chat canvas inset behind glass shadow. Rendering the
user's WhatsApp data inside a literal phone is the strongest physical
metaphor in the app.
The departure board becomes a machined fixture: corner bolts
(layered radial-gradients), a recessed face with inner shadow, a
specular highlight across each flap's top half, hinge shadows per
cell and a soft mango bloom on the glyphs.
The ledger becomes a thermal receipt emerging from a printer slot
(gradient metal bar with a dark mouth), tipped 2.5deg through a
perspective container, with the ink fading faintly toward the loose
end via a mask.
Pinned by a presenter test asserting the printer slot precedes the
ledger rows.
Offer the synthetic example from the hero dropzone
The first-run hero now carries a Mango "Teste com dados sinteticos"
button under the drop target. bridge.js fetches the bundled zip from
the static mount, wraps it in a File and feeds it through the exact
same processUpload flow as a user upload — queue, streaming ticks,
ZeroGPU resume loop and all — so the demo path exercises precisely
what a real export would. The button disables itself while the run is
in flight.
Verified in the browser on a fresh session: 64 ticks streamed, status
reached done/success, the customer strip picked up the synthetic
client, and the conversa replay rendered her conversation.