repair-guy / core /constants.py
airayven7's picture
Sync from GitHub 00f6e1b
93700d9 verified
Raw
History Blame Contribute Delete
14.3 kB
import os
# ---------------------------------------------------------------------------
# Shared (both approaches)
# ---------------------------------------------------------------------------
# Page rendering (embedding/parsing at index time, and the answering model).
RENDER_DPI = 150
# Answering model (runs locally on ZeroGPU). Revisions are pinned because all
# models here load trust_remote_code; bump deliberately after reviewing
# upstream changes.
MINICPM_MODEL_ID = os.environ.get("MINICPM_MODEL_ID", "openbmb/MiniCPM-V-4_5")
MINICPM_REVISION = os.environ.get(
"MINICPM_REVISION", "fd3209b2e0580e346fc33d2c6f85b6e9332eecda"
)
ANSWER_MAX_NEW_TOKENS = 2048
# Let MiniCPM-V reason (legend → callout-number → leader-line → part) before
# committing to a grounding box for "circle the <thing>". This is only the
# DEFAULT — the UI settings panel sends a per-request override (see api_find's
# `think`). Off by default: it roughly multiplies grounding latency (64 → ~512
# generated tokens) and mainly helps exploded-diagram callouts, not the
# dense-table wrong-row misses.
GROUND_ENABLE_THINKING = os.environ.get("GROUND_ENABLE_THINKING", "").lower() in (
"1",
"true",
"yes",
)
# Token budgets for one grounding generation. A bare <box> fits in 64; a think
# trace does not (it gets cut off before the box), so the budget tracks whether
# thinking is on for that call.
GROUND_BOX_MAX_NEW_TOKENS = 64
GROUND_THINK_MAX_NEW_TOKENS = 512
# Agent brain: MiniCPM5-1B — a standard LlamaForCausalLM (no trust_remote_code),
# 131k context. The small TEXT model that drives the find-and-point loop: each
# step it picks ONE tool from the conversation so far, the manual's table of
# contents, and the whole text of the page being viewed. The MiniCPM-V VLM above
# stays the "eyes" (grounding the circle + the ingest figure/table descriptions);
# this is the "brain". Revision is left unpinned (standard architecture, no
# remote code to drift) but overridable; pin a commit before a real deploy.
MINICPM_AGENT_MODEL_ID = os.environ.get("MINICPM_AGENT_MODEL_ID", "openbmb/MiniCPM5-1B")
MINICPM_AGENT_REVISION = os.environ.get("MINICPM_AGENT_REVISION", "") or None
# Selectable agent brains, offered in the UI settings panel. ONE model is meant
# to be resident in VRAM at a time — switching evicts the previous and loads the
# next (models/minicpm_agent.use_model). The FIRST/default brain is loaded at
# import (ZeroGPU's startup phase) and "packed" into the forked GPU worker, so it
# stays resident for the whole process and the common (no-switch) turn pays NO
# per-turn load cost. A brain SWITCHED IN at runtime is built inside the GPU
# window instead (not packed), so its first turn after a switch is slower.
#
# The default is MiniCPM4.1-8B in bf16 (~16 GiB). It fits because the ColEmbed
# visual retriever (~8 GiB) is NO LONGER packed at import — it lazy-loads only
# when the "visual" search index is used (models/colembed.py). So the resident set
# is the MiniCPM-V "eyes" + the Nemotron text embedder + this 8B brain, which
# leaves room for the grounding spike on the 48 GiB slice. Because the 8B and
# ColEmbed cannot BOTH be resident, the 8B sets forbid_visual: a turn that asks for
# visual search while it is active is served by the parsed index instead (enforced
# in app.py). Smaller brains leave room for ColEmbed to lazy-load, so they keep
# visual search.
#
# Each loads as an AutoModelForCausalLM; `trust_remote_code` (default False) flags
# the ones that ship custom modeling code (MiniCPM3 / MiniCPM4.1). `thinking` flags
# whether the chat template accepts enable_thinking (Qwen3, MiniCPM5, MiniCPM4.1 do
# — tool routing passes it False; MiniCPM3 does not). The FIRST entry is the
# default at boot; the minicpm5-1b entry stays selectable and still tracks the
# MINICPM_AGENT_MODEL_ID/REVISION env overrides. Only one brain is resident at a time.
AGENT_MODELS = [
{
"key": "minicpm4.1-8b",
"label": "MiniCPM4.1 8B",
"model_id": "openbmb/MiniCPM4.1-8B",
# DEFAULT brain. Hybrid-reasoning 8B in bf16 (~16 GiB) — the best eval
# config (0.90 tool / 0.90 args, and it unlocks the v3 coincidence fix).
# Loaded at import so ZeroGPU packs it (no per-turn reload) and it decodes
# in bf16 (faster than a bnb-int8 build). trust_remote_code custom modeling
# (sparse "InfLLM v2" attention); runs clean on current transformers, unlike
# MiniCPM3-4B — pin a reviewed commit (revision) before a real deploy.
"revision": None,
"thinking": True,
"trust_remote_code": True,
# Too large to keep the ColEmbed visual retriever resident alongside it, so
# visual search is disabled while this brain is active (falls back to parsed).
"forbid_visual": True,
},
{
"key": "minicpm5-1b",
"label": "MiniCPM5 1B",
"model_id": MINICPM_AGENT_MODEL_ID,
"revision": MINICPM_AGENT_REVISION,
"thinking": True,
},
{
"key": "qwen3-1.7b",
"label": "Qwen3 1.7B",
"model_id": "Qwen/Qwen3-1.7B",
"revision": None,
"thinking": True,
},
{
"key": "qwen3-0.6b",
"label": "Qwen3 0.6B",
"model_id": "Qwen/Qwen3-0.6B",
"revision": None,
"thinking": True,
},
{
"key": "qwen3-4b",
"label": "Qwen3 4B",
"model_id": "Qwen/Qwen3-4B",
"revision": None,
# Native Qwen3 arch (no trust_remote_code → immune to the custom-modeling
# rot that breaks MiniCPM3-4B on current transformers). ~4B / ~8.1 GiB bf16,
# same footprint class as MiniCPM3-4B, which was VRAM-vetted to fit; routing
# passes enable_thinking=False.
"thinking": True,
},
{
"key": "minicpm3-4b",
"label": "MiniCPM3 4B",
"model_id": "openbmb/MiniCPM3-4B",
# trust_remote_code model — pin a reviewed commit before a real deploy
# (see use_model). Left unpinned here so the entry tracks latest.
"revision": None,
# MiniCPM3 has no hybrid-reasoning mode; its chat template doesn't take
# enable_thinking, so leave it off (the kwarg is then omitted).
"thinking": False,
"trust_remote_code": True,
# ~4B / ~8.6 GiB in bf16: with the resident VLM+ColEmbed+embedder (~28 GiB)
# and the un-evictable default brain, it loads into the ~15.6 GiB free at
# switch time with ~5 GiB to spare after the grounding spike. The 8B (16
# GiB) didn't fit — see core/vram.py / the find-turn VRAM logs.
},
]
DEFAULT_AGENT_MODEL = AGENT_MODELS[0]["key"]
# A tool-call decision is short JSON; a rerank reply is a single number. 96 was
# too tight — the 1B writes verbose search queries and was getting CUT OFF
# mid-string (unterminated JSON → parse fail → wasted retry), seen live.
AGENT_MAX_NEW_TOKENS = 128
# Backstop on tool steps within one turn, so a confused loop can't run forever.
AGENT_MAX_STEPS = 6
# ColEmbed shortlist size the search tool retrieves (the eval default).
AGENT_SEARCH_CANDIDATES = 5
# How many of the search shortlist's top pages get their FULL TEXT fed back after
# a search, for the brain to RERANK and recover over — it circles the target on
# whichever candidate actually has it, not just retrieval's #1 (the recovery path
# when the right page is rank 2/3). The rest of the shortlist still shows as
# thumbnails. Capped low because each page's text is large; raising it (or the k
# slider) deepens the recovery pool at the cost of context/latency. The smarter 8B
# reranks reliably where the old 1B (rerank 0.68 < top-1 0.84) could not.
AGENT_RERANK_CANDIDATES = 3
# Which index the search tool ranks against. Both retrievers return the same
# (doc, page, score) shape, so the agent loop is identical either way:
# visual — ColEmbed late-interaction MaxSim over page-image embeddings
# parsed — Nemotron dense cosine over parsed section/figure/table chunks
# Exposed as a UI setting (settings panel); the parsed index wins on spec/table
# lookups, so it is the default for now.
RETRIEVAL_MODES = [
{"key": "parsed", "label": "Parsed (text)"},
{"key": "visual", "label": "Visual (ColEmbed)"},
]
DEFAULT_RETRIEVAL_MODE = RETRIEVAL_MODES[0]["key"]
# Past turns of conversation fed back as memory (resolve "the other one", "go
# back"); the live turn carries the full current page text (no table of contents).
AGENT_HISTORY_TURNS = 6
# One ZeroGPU call covers the whole question: query embedding + retrieval +
# page rendering + answer generation.
ASK_GPU_DURATION = 120
# Pages handed to the answering model.
DEFAULT_TOP_K = 3
MAX_TOP_K = 5
# ---------------------------------------------------------------------------
# Find-and-point (pipelines/agent_ask.py) — every non-obvious request is one
# GPU turn: the 1B agent loops over tools (search / go_to_page / circle) against
# the current page's text — no table of contents is injected.
# The frontend's breadcrumb section nav is client-side and never reaches the GPU.
# ---------------------------------------------------------------------------
# One ZeroGPU call covers a whole agent turn: up to AGENT_MAX_STEPS tool-choice
# generations, ColEmbed retrieval + page rendering and the 1B rerank inside a
# search step, and one MiniCPM-V grounding generation for a circle.
FIND_GPU_DURATION = 180
# ---------------------------------------------------------------------------
# Visual approach: ColEmbed late-interaction page embeddings + MaxSim
# ---------------------------------------------------------------------------
COLEMBED_MODEL_ID = os.environ.get(
"COLEMBED_MODEL_ID", "nvidia/nemotron-colembed-vl-4b-v2"
)
COLEMBED_REVISION = os.environ.get(
"COLEMBED_REVISION", "0ed152d91f8ad4c5d48296b51c220f686641a398"
)
# sdpa works for this model and needs no extra wheels on ZeroGPU; set
# COLEMBED_ATTN=flash_attention_2 if flash-attn is installed.
COLEMBED_ATTN = os.environ.get("COLEMBED_ATTN", "sdpa")
# Indexing: pages embedded per GPU call, and model batch size within a call.
EMBED_PAGES_PER_CALL = 64
EMBED_BATCH_SIZE = 8
EMBED_GPU_DURATION = 240
# Retrieval: MaxSim is computed on GPU over fixed-size batches of pages
# streamed from the on-disk store.
SCORE_PAGES_PER_BATCH = 32
# ---------------------------------------------------------------------------
# Parsed approach: Nemotron Parse -> MiniCPM figure/table descriptions ->
# section chunks -> dense embeddings + cosine retrieval
# ---------------------------------------------------------------------------
# Nemotron Parse requires transformers==5.6.1, which is incompatible with
# ColEmbed/MiniCPM (<5). It therefore runs only in the dedicated parse stage
# on Modal (scripts/index_modal.py) and is never imported on the Space.
NEMOTRON_PARSE_MODEL_ID = os.environ.get(
"NEMOTRON_PARSE_MODEL_ID", "nvidia/NVIDIA-Nemotron-Parse-v1.2"
)
NEMOTRON_PARSE_REVISION = os.environ.get(
"NEMOTRON_PARSE_REVISION", "2bd0189bffd6cdded6280d9f22a4077b25a504e3"
)
# Pages parsed per generate call at index time (identical task prompt per
# page, so the batch needs no padding).
PARSE_BATCH_SIZE = 16
# Dense bi-encoder for chunk/query embeddings (2048-dim, mean-pooled).
# Its remote code supports transformers 4.56+ — same env as the Space.
NEMOTRON_EMBED_MODEL_ID = os.environ.get(
"NEMOTRON_EMBED_MODEL_ID", "nvidia/llama-nemotron-embed-vl-1b-v2"
)
NEMOTRON_EMBED_REVISION = os.environ.get(
"NEMOTRON_EMBED_REVISION", "0c6f636ed4c022e427277c4c336054d6cdffaa87"
)
EMBED_TEXT_MAX_LENGTH = 8192 # processor token budget for text-only inputs
EMBED_TEXT_BATCH_SIZE = 8
# MiniCPM descriptions generated at ingest for Picture/Table elements.
# Each description is conditioned on document context (manual name, section
# heading, adjacent caption, page text) so it uses the manual's terminology;
# the page-text part of that context is capped at this many characters.
DESCRIBE_MAX_NEW_TOKENS = 256
DESCRIBE_CONTEXT_MAX_CHARS = 1200
# Figure/table descriptions generated per batched MiniCPM chat() call.
DESCRIBE_BATCH_SIZE = 16
# Picture bboxes with either side smaller than this (pixels at RENDER_DPI)
# are skipped — icons, bullets, print artifacts.
FIGURE_MIN_SIDE_PX = 40
# Section chunking (core/chunking.py): a Title/Section-header closes the
# current section unless it is still under SECTION_MIN_CHARS (sparse pages
# merge forward into the next section); sections over SECTION_MAX_CHARS split
# at element boundaries with the heading repeated.
SECTION_MIN_CHARS = 200
SECTION_MAX_CHARS = 6000
# Retrieval: chunk candidates scored by cosine before the page budget
# (top_k pages) is applied via parent-document lookup.
PARSED_TOP_CHUNKS = 8
# ---------------------------------------------------------------------------
# Stores
# ---------------------------------------------------------------------------
# HF Spaces persistent storage mounts at /data; fall back to a local directory
# for development. The library dataset mirrors PREINDEXED_DIR: one top-level
# directory per indexing method, one doc directory per manual under it.
_DATA_ROOT = os.environ.get("DATA_ROOT") or (
"/data"
if os.path.isdir("/data")
else os.path.join(os.path.dirname(os.path.dirname(__file__)), "data")
)
PREINDEXED_DIR = os.path.join(_DATA_ROOT, "preindexed")
VISUAL_SUBDIR = "visual"
PARSED_SUBDIR = "parsed"
LIBRARY_DATASET_ID = os.environ.get(
"LIBRARY_DATASET_ID", "build-small-hackathon/repair-guy-library"
)
# ---------------------------------------------------------------------------
# Local mock mode (UI iteration with no GPU / model downloads / HF sync)
# ---------------------------------------------------------------------------
# With MOCK_MODELS set, app.py serves canned answers grounded in real, rendered
# pages of any PDF dropped into MOCK_PDF_DIR. Nothing on the mock path imports
# torch / spaces / the model modules (which load CUDA at import), so the Gradio
# UI boots instantly on a laptop. See pipelines/mock_ask.py.
MOCK_MODELS = os.environ.get("MOCK_MODELS", "").lower() in ("1", "true", "yes")
MOCK_PDF_DIR = os.environ.get("MOCK_PDF_DIR") or os.path.join(_DATA_ROOT, "mock_pdfs")