BRAG / app.py
Angshul's picture
Upload 8 files
1a3c3b3 verified
Raw History Blame Contribute Delete
8.21 kB
"""BRAG - Bayesian RAG: Gradio Space (live mode) with a programmatic API."""
import json, os, hashlib, threading
from functools import lru_cache
import gradio as gr
from brag import BRAG, Corpus, hf_llm
HERE = os.path.dirname(os.path.abspath(__file__))
CORPORA = {'HotpotQA': 'hotpotqa_corpus.json', '2WikiMultihopQA': '2wikimultihopqa_corpus.json',
'MuSiQue': 'musique_corpus.json'}
DEFAULT_MODEL = os.getenv('BRAG_MODEL', 'Qwen/Qwen2.5-7B-Instruct')
CAL_PATH = os.path.join(HERE, 'data', 'calibration.json')
DEFAULT_CAL = json.load(open(CAL_PATH)) if os.path.exists(CAL_PATH) else None
_lock = threading.Lock()
@lru_cache(maxsize=len(CORPORA))
def builtin_corpus(name):
return Corpus.from_json(os.path.join(HERE, 'data', CORPORA[name]))
@lru_cache(maxsize=32)
def custom_corpus(digest, text):
text = text.strip()
if text.startswith('['):
rows = json.loads(text)
ps = [r if isinstance(r, str) else f"{r.get('title', '')}: {r.get('text', '')}" for r in rows]
else:
ps = [l.strip() for l in text.splitlines() if l.strip()]
return Corpus(ps)
def get_corpus(corpus, documents):
if documents and documents.strip():
return custom_corpus(hashlib.sha1(documents.encode()).hexdigest(), documents)
if corpus not in CORPORA:
raise gr.Error(f'corpus must be one of {list(CORPORA)} or documents must be given')
with _lock:
return builtin_corpus(corpus)
def get_llm(model, hf_token, provider):
if os.getenv('BRAG_MOCK') == '1': # local testing only
from mock_llm import mock_llm
return mock_llm
token = (hf_token or '').strip() or os.getenv('HF_TOKEN')
if not token:
raise gr.Error('No Hugging Face token: pass hf_token, or set the HF_TOKEN secret on the Space.')
return hf_llm((model or DEFAULT_MODEL).strip(), token, (provider or 'auto').strip())
def parse_cal(calibration):
if calibration and str(calibration).strip():
try:
return json.loads(calibration) if isinstance(calibration, str) else calibration
except json.JSONDecodeError as e:
raise gr.Error(f'calibration is not valid JSON: {e}')
return DEFAULT_CAL
def brag(question: str, corpus: str = 'HotpotQA', documents: str = '', lam: float = 0.05,
model: str = DEFAULT_MODEL, hf_token: str = '', provider: str = 'auto', calibration: str = ''):
"""Answer a question with BRAG (Bayesian RAG).
Args:
question: the question to answer.
corpus: built-in corpus to search: HotpotQA, 2WikiMultihopQA or MuSiQue (ignored if documents given).
documents: optional custom corpus: one passage per line, or a JSON list of strings / {title, text}.
lam: cost of one LLM call in units of answer error (higher = cheaper, earlier stopping).
model: Hugging Face model id, or URL of an OpenAI-compatible endpoint (dedicated Inference Endpoint, TGI, vLLM).
hf_token: optional Hugging Face token (defaults to the Space's HF_TOKEN secret).
provider: Hugging Face inference provider ('auto' picks one that serves the model).
calibration: optional JSON calibration (output of the calibrate endpoint) for this model.
Returns:
answer, and a JSON report with the per-step posterior, information rates, actions and costs.
"""
if not question or not question.strip():
raise gr.Error('question is empty')
engine = BRAG(get_llm(model, hf_token, provider), parse_cal(calibration), lam=float(lam))
try:
res = engine.run(question.strip(), get_corpus(corpus, documents))
except gr.Error:
raise
except Exception as e:
raise gr.Error(f'LLM/provider error: {type(e).__name__}: {e}')
rep = res.to_dict(); rep['model'] = model; rep['lambda'] = float(lam)
return res.answer or '', rep
def calibrate(labelled_json: str, corpus: str = 'HotpotQA', documents: str = '',
model: str = DEFAULT_MODEL, hf_token: str = '', provider: str = 'auto'):
"""Calibrate BRAG's hyperpriors for a model/corpus from labelled questions.
Args:
labelled_json: JSON list of {"question": ..., "gold": [aliases]} (30+ recommended, max 200).
corpus / documents / model / hf_token / provider: as in the brag endpoint.
Returns:
calibration JSON to pass as `calibration` to the brag endpoint.
"""
try:
rows = json.loads(labelled_json)
assert isinstance(rows, list) and rows and all('question' in r and 'gold' in r for r in rows)
except Exception:
raise gr.Error('labelled_json must be a JSON list of {"question": ..., "gold": [...]}')
if len(rows) > 200:
raise gr.Error('at most 200 labelled questions per call')
engine = BRAG(get_llm(model, hf_token, provider), DEFAULT_CAL)
try:
return engine.calibrate(rows, get_corpus(corpus, documents))
except Exception as e:
raise gr.Error(f'LLM/provider error: {type(e).__name__}: {e}')
API_DOC = """
### Programmatic use
```python
from gradio_client import Client
c = Client("Angshul/BRAG") # or Client("Angshul/BRAG", hf_token="hf_...")
answer, report = c.predict(
question="Friedrich Ratzel coined the phrase Lebensraum; which political movement made it notorious?",
corpus="HotpotQA", documents="", lam=0.05,
model="Qwen/Qwen2.5-7B-Instruct", hf_token="", provider="auto", calibration="",
api_name="/brag")
```
Own documents: pass `documents` (one passage per line, or a JSON list). Different model: run
`api_name="/calibrate"` once on 30+ labelled questions and pass the returned JSON as `calibration`.
REST: `POST /gradio_api/call/brag` with `{"data": [question, corpus, documents, lam, model, hf_token, provider, calibration]}`.
"""
with gr.Blocks(title='BRAG - Bayesian RAG') as demo:
gr.Markdown('# BRAG — Bayesian RAG\nClosed-form Bayesian orchestration of retrieval-augmented QA. '
'The orchestrator makes **no LLM calls**: each decision is arithmetic on conjugate posteriors '
'(microseconds). It chooses between reading retrieved passages, a second retrieval hop and '
'closed-book sampling, and stops when the expected error reduction per call falls below λ.')
with gr.Tab('Ask'):
q = gr.Textbox(label='Question', lines=2)
with gr.Row():
corpus = gr.Dropdown(list(CORPORA), value='HotpotQA', label='Corpus')
lam = gr.Slider(0.005, 0.3, value=0.05, step=0.005, label='λ (cost per LLM call)')
docs = gr.Textbox(label='Own documents (optional): one passage per line, or JSON list', lines=3)
with gr.Accordion('Model', open=False):
model = gr.Textbox(value=DEFAULT_MODEL, label='HF model id or OpenAI-compatible endpoint URL')
provider = gr.Textbox(value='auto', label='Inference provider')
tok = gr.Textbox(label='HF token (optional; default: Space secret)', type='password')
cal = gr.Textbox(label='Calibration JSON (optional)', lines=2)
go = gr.Button('Answer', variant='primary')
ans = gr.Textbox(label='Answer'); rep = gr.JSON(label='Trace: posterior, information rates, actions, costs')
go.click(brag, [q, corpus, docs, lam, model, tok, provider, cal], [ans, rep], api_name='brag')
with gr.Tab('Calibrate'):
lab = gr.Textbox(label='Labelled questions JSON: [{"question": ..., "gold": [...]}, ...]', lines=6)
with gr.Row():
corpus2 = gr.Dropdown(list(CORPORA), value='HotpotQA', label='Corpus')
model2 = gr.Textbox(value=DEFAULT_MODEL, label='HF model id')
docs2 = gr.Textbox(label='Own documents (optional)', lines=2)
tok2 = gr.Textbox(label='HF token (optional)', type='password'); prov2 = gr.Textbox(value='auto', label='Provider')
go2 = gr.Button('Calibrate'); out2 = gr.JSON(label='Calibration (pass to the brag endpoint)')
go2.click(calibrate, [lab, corpus2, docs2, model2, tok2, prov2], out2, api_name='calibrate')
gr.Markdown(API_DOC)
demo.queue(default_concurrency_limit=int(os.getenv('BRAG_CONCURRENCY', '16')), max_size=256)
if __name__ == '__main__':
demo.launch()