Retrieval fused a bi-encoder and BM25 by reciprocal rank. A bi-encoder embeds a document long before the question exists, so the two never meet: it is good at "same topic" and mediocre at "answers this". A cross-encoder reads the pair. The proxy already serves three — `cohere-rerank-v4.0-pro` is the default and measurably better than the fast variant. Query text goes exactly where the embeddings already go, and nothing new was signed up for. It found a defect nobody was looking for. In AI Mode each finder scored `1/(1+rank)` *within its own corpus*, so the best article, section, question and card all scored 1.0 and the shortlist was a meaningless round-robin. A cross-encoder is the first thing in this system that can compare a question with a section. Candidates per kind widened so it can select rather than merely reorder. Measured against labels neither ranker produced. Questions, 60 disease tags: precision@3 0.394 → 0.483. Sections, 60 article titles: 0.772 → 0.833. "Management of bronchiolitis" led with influenza transmission and a pregnancy question; "when do you image a first febrile seizure" returned the definition rather than the sentence saying imaging is unnecessary. And the honest negative, in docs/reranking.md: board vignettes are written *not* to name their diagnosis, so on "what causes croup" it prefers a question that says the word in passing over the barking-cough vignette that never says it. Some of the bi-encoder's strength is traded away. Not on the typeahead. A page of results is a choice being made and worth a third of a second; a typeahead is a word being finished, runs on every keystroke, and has nothing to judge yet. The three-state thresholds stay on cosine, argued at the constant: a reranker only ever sees a shortlist and structurally cannot answer the corpus-wide question those numbers ask, and whether an answer claims to come from the library is a promise that must not depend on a network hop. Every failure returns None and leaves the order alone — unconfigured, no proxy, connect error, bare 502, timeout, non-JSON, a duplicate or out-of-range index, a non-numeric score, a list the wrong length. Verified against the running site with a bogus model name: same results, fused order, no error to the reader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
|
|
DATABASE_URL: str = "sqlite:///./quiz.db"
|
|
SECRET_KEY: str = "change-me-to-a-random-secret-key-in-production"
|
|
ALGORITHM: str = "HS256"
|
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
|
|
|
|
REDIS_URL: str = "redis://localhost:6379/0"
|
|
|
|
LITELLM_MODEL: str = "gpt-4o-mini"
|
|
LITELLM_API_KEY: str = ""
|
|
LITELLM_API_BASE: str = ""
|
|
LITELLM_EMBEDDING_MODEL: str = ""
|
|
# Cross-encoder reranker, named as the proxy serves it. Blank turns
|
|
# reranking off and leaves every result list in the order rank fusion
|
|
# produced, which is what a deployment without this proxy gets.
|
|
LITELLM_RERANK_MODEL: str = "cohere-rerank-v4.0-pro"
|
|
OPENAI_API_KEY: str = ""
|
|
ELEVENLABS_API_KEY: str = ""
|
|
GOOGLE_TTS_API_KEY: str = ""
|
|
LOCAL_SPEECH_GATEWAY_URL: str = "http://127.0.0.1:8110"
|
|
AWS_ACCESS_KEY_ID: str = ""
|
|
AWS_SECRET_ACCESS_KEY: str = ""
|
|
AWS_REGION: str = "us-east-1"
|
|
AWS_BEDROCK_REGION: str = "us-east-1"
|
|
EMBEDDING_DIMENSIONS: int = 1024
|
|
APP_URL: str = "https://quiz.danvics.com"
|
|
|
|
CHROMA_PERSIST_DIR: str = "./chroma_data"
|
|
|
|
MAIL_USERNAME: str = ""
|
|
MAIL_PASSWORD: str = ""
|
|
MAIL_FROM: str = ""
|
|
MAIL_PORT: int = 587
|
|
MAIL_SERVER: str = "smtp.gmail.com"
|
|
MAIL_STARTTLS: bool = True
|
|
MAIL_SSL_TLS: bool = False
|
|
|
|
UPLOAD_DIR: str = "./uploads"
|
|
# local | s3. Reads fall back to the volume either way, so existing uploads
|
|
# keep working and files can migrate gradually.
|
|
STORAGE_BACKEND: str = "local"
|
|
S3_ENDPOINT_URL: str = "http://minio:9000"
|
|
S3_ACCESS_KEY: str = ""
|
|
S3_SECRET_KEY: str = ""
|
|
S3_BUCKET: str = "pedshub-media"
|
|
S3_REGION: str = "us-east-1"
|
|
MAX_UPLOAD_SIZE: int = 524288000 # 500MB
|
|
|
|
# hCaptcha. Leave the secret blank to disable the challenge entirely.
|
|
# Cap, self-hosted beside us. The secret verifies a solve and never leaves
|
|
# the server; the site key is public and names the widget's endpoint.
|
|
CAP_API_URL: str = "http://cap:3000"
|
|
CAP_SECRET_KEY: str = ""
|
|
# The browser gets its own copy of the site key from the frontend
|
|
# container, which is a separate image with a separate .env. This one is
|
|
# here so an operator can keep both halves of the pair together and see
|
|
# at a glance which widget the secret belongs to.
|
|
CAP_SITE_KEY: str = ""
|
|
ADMIN_EMAIL: str = "" # Where contact form submissions are emailed
|
|
|
|
DEFAULT_ADMIN_EMAIL: str = "" # Optional explicit bootstrap admin email
|
|
DEFAULT_ADMIN_PASSWORD: str = "" # Optional explicit bootstrap admin password
|
|
|
|
BBB_SERVER_URL: str = "" # BigBlueButton server URL (e.g. https://bbb.example.com/bigbluebutton)
|
|
BBB_SECRET: str = "" # BigBlueButton shared secret
|
|
|
|
# OIDC / SSO — leave blank to disable
|
|
OIDC_PROVIDER_URL: str = "" # e.g. https://accounts.google.com, https://login.microsoftonline.com/{tenant}/v2.0
|
|
OIDC_CLIENT_ID: str = ""
|
|
OIDC_CLIENT_SECRET: str = ""
|
|
OIDC_SCOPES: str = "openid email profile" # space-separated
|
|
OIDC_PROVIDER_NAME: str = "SSO" # Display name on login button
|
|
|
|
LOG_LEVEL: str = "INFO" # DEBUG, INFO, WARNING, ERROR
|
|
|
|
# Clinical library index (Milvus on the ped-ai stack), read-only. Articles
|
|
# are grounded in what it retrieves; nothing here writes to it.
|
|
CLINICAL_MILVUS_URI: str = ""
|
|
CLINICAL_MILVUS_TOKEN: str = ""
|
|
CLINICAL_MILVUS_COLLECTION: str = "mcp_bge_m3_1024"
|
|
|
|
|
|
settings = Settings()
|
|
|