File size: 8,208 Bytes
1a3c3b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""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()