{requirements}\n{evaluation_prompt}"
messages = [
{"role": "user", "content": question},
{"role": "assistant", "content": answer},
{"role": "user", "content": req_turn},
]
return messages, "requirement-check", None, 20
def _format_requirement(raw):
try:
result = json.loads(raw)
score = result.get("score", raw)
satisfied = score == "yes"
label = "Satisfied" if satisfied else "Not satisfied"
color = "#22c55e" if satisfied else "#ef4444"
return (
f''
f"{_badge('Requirement Check')} {label} ({score})
"
)
except json.JSONDecodeError:
return f"{_badge('Requirement Check')} {html.escape(raw)}"
# ── Pipeline output assembly ──────────────────────────────────────────────
def _render_context_viewer(context_log):
"""Render context log as collapsible HTML JSON viewer."""
if not context_log:
return 'No context generated.
'
items = []
for i, entry in enumerate(context_log):
adapter = html.escape(entry["adapter"])
prompt = html.escape(entry["prompt"])
items.append(
f''
f''
f'[{i}] {adapter}
'
f'{prompt}'
f' '
)
return (
f''
f'
'
f'{len(context_log)} adapter call(s) — click to expand
'
+ "".join(items) + '
'
)
def _format_retrieved_docs_badge(docs):
return f"{_badge('Retrieval')} Retrieved **{len(docs)}** documents (see Context panel)"
def _render_retrieved_docs_viewer(docs):
"""Render retrieved docs as collapsible HTML viewer (for context panel)."""
if not docs:
return ""
items = []
for i, doc in enumerate(docs):
title = html.escape(doc.get("title", f"Document {i+1}"))
text = html.escape(doc["text"][:600])
items.append(
f''
f''
f'[{i}] {title}
'
f'{text}'
f' '
)
return (
f''
f'
'
f'Retrieved Documents ({len(docs)})
'
+ "".join(items) + '
'
)
def _assemble_output(answer, sections):
parts = []
validations = [s for stage, s in sections if stage == "validation"]
if validations:
parts.append("".join(validations))
for stage, content in sections:
if stage == "pre_retrieval":
parts.append(content)
for stage, content in sections:
if stage == "retrieval":
parts.append(content)
for stage, content in sections:
if stage == "post_retrieval":
parts.append(content)
if answer:
parts.append(f"\n\n{answer}\n\n")
else:
blocked_msgs = [s for stage, s in sections if stage == "blocked"]
reason = blocked_msgs[0] if blocked_msgs else "Pipeline halted by adapter"
parts.append(
f''
f'Generation skipped: {html.escape(reason)}
'
)
post_gen = [s for stage, s in sections if stage == "post_generation"]
if post_gen:
inner = "".join(post_gen)
parts.append(
f'Adapter Analysis
'
f'{inner}
'
)
skipped = [s for stage, s in sections if stage == "skipped"]
if skipped:
parts.append(
f''
+ " | ".join(skipped) + '
'
)
return "\n".join(parts)
# ── Pipeline orchestrator ─────────────────────────────────────────────────
def run_pipeline(
user_message, history, enabled_adapters, adapter_config, max_tokens
):
_get_model()
_context_log.clear()
sections = []
docs_parsed = None
docs_raw_texts = None
retrieved_docs_full = None
blocked = False
block_reason = ""
# --- Stage 1: User Validation ---
if "guardian-core" in enabled_adapters:
msgs, adapter, docs, mt = _build_guardian(
user_message,
adapter_config.get("guardian_criteria", "harm"),
adapter_config.get("guardian_custom_criteria", ""),
)
raw = _generate_raw(msgs, adapter, docs, mt)
guardian_result = _format_guardian(raw, adapter_config.get("guardian_criteria", "harm"))
if guardian_result:
sections.append(("validation", guardian_result))
if adapter_config.get("exit_on_guardian", True):
blocked = True
block_reason = "Guardian flagged this message"
if "policy-guardrails" in enabled_adapters and not blocked:
policy_text = adapter_config.get("policy_text", "") or ""
if policy_text.strip():
msgs, adapter, docs, mt = _build_policy(user_message, policy_text)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("validation", _format_policy(raw)))
try:
policy_label = json.loads(raw).get("label", "")
if policy_label == "No" and adapter_config.get("exit_on_policy", True):
blocked = True
block_reason = "Policy non-compliance detected"
except (json.JSONDecodeError, AttributeError):
pass
# --- Stage 2: Pre-Retrieval ---
search_query = user_message
if "query_rewrite" in enabled_adapters and not blocked:
msgs, adapter, docs, mt = _build_query_rewrite(user_message)
raw = _generate_raw(msgs, adapter, docs, mt)
try:
parsed = json.loads(raw)
search_query = parsed.get("rewritten_question", parsed.get("question", raw))
except (json.JSONDecodeError, AttributeError):
search_query = raw
sections.append(("pre_retrieval", _format_query_rewrite(search_query, user_message)))
# --- Stage 3: Retrieval ---
if not blocked:
retrieval_enabled = "retrieval" in enabled_adapters
if retrieval_enabled and db_manager.is_ready:
db_results = db_manager.query(search_query, n_results=5)
if db_results:
docs_parsed = [{"text": d["text"]} for d in db_results]
docs_raw_texts = [d["text"] for d in db_results]
retrieved_docs_full = db_results
sections.append(("retrieval", _format_retrieved_docs_badge(db_results)))
elif retrieval_enabled and not db_manager.is_ready:
sections.append(("skipped", "Retrieval skipped (Vector DB not ready)"))
if docs_parsed is None:
fallback_docs_text = adapter_config.get("context_documents", "") or ""
fallback_parsed = _parse_docs(fallback_docs_text)
if fallback_parsed:
docs_parsed = fallback_parsed
docs_raw_texts = [d["text"] for d in fallback_parsed]
# --- Stage 4: Post-Retrieval ---
if "answerability" in enabled_adapters and not blocked:
if docs_parsed:
msgs, adapter, docs, mt = _build_answerability(user_message, docs_parsed)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("post_retrieval", _format_answerability(raw)))
if _is_unanswerable(raw) and adapter_config.get("exit_on_answerability", True):
blocked = True
block_reason = "Question not answerable from available documents"
else:
sections.append(("skipped", "Answerability skipped (no documents)"))
# --- Stage 5: Generation ---
if blocked:
answer = None
sections.append(("blocked", block_reason))
else:
model_messages = [
{"role": m["role"], "content": m["content"]}
for m in history
if m["role"] in ("user", "assistant") and "metadata" not in m
]
model_messages.append({"role": "user", "content": user_message})
answer = _generate_raw(model_messages, adapter=None, documents=docs_parsed, max_new_tokens=max_tokens)
# --- Stage 6: Post-Generation (skipped if blocked) ---
if not blocked:
if "citations" in enabled_adapters:
if docs_parsed:
msgs, adapter, docs, mt = _build_citations(user_message, answer, docs_parsed)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("post_generation", _format_citations(raw)))
else:
sections.append(("skipped", "Citations skipped (no documents)"))
if "hallucination_detection" in enabled_adapters:
if docs_parsed:
msgs, adapter, docs, mt = _build_hallucination(user_message, answer, docs_parsed)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("post_generation", _format_hallucination(raw)))
else:
sections.append(("skipped", "Hallucination Detection skipped (no documents)"))
if "factuality-detection" in enabled_adapters:
if docs_parsed:
msgs, adapter, docs, mt = _build_factuality_det(answer, docs_parsed)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("post_generation", _format_factuality_det(raw)))
else:
sections.append(("skipped", "Factuality Detection skipped (no documents)"))
if "factuality-correction" in enabled_adapters:
if docs_parsed:
msgs, adapter, docs, mt = _build_factuality_cor(answer, docs_parsed)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("post_generation", _format_factuality_cor(raw, answer)))
else:
sections.append(("skipped", "Factuality Correction skipped (no documents)"))
if "context-attribution" in enabled_adapters:
if docs_raw_texts:
msgs, adapter, docs, mt = _build_context_attr(user_message, answer, docs_raw_texts)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("post_generation", _format_context_attr(raw)))
else:
sections.append(("skipped", "Context Attribution skipped (no documents)"))
if "uncertainty" in enabled_adapters:
conv_text = f"User: {user_message}\nAssistant: {answer}"
msgs, adapter, docs, mt = _build_uncertainty(conv_text)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("post_generation", _format_uncertainty(raw)))
if "requirement-check" in enabled_adapters:
req_text = adapter_config.get("requirements_text", "") or ""
if req_text.strip():
msgs, adapter, docs, mt = _build_requirement(user_message, answer, req_text)
raw = _generate_raw(msgs, adapter, docs, mt)
sections.append(("post_generation", _format_requirement(raw)))
# --- Assemble ---
assistant_html = _assemble_output(answer, sections)
new_history = list(history) + [
{"role": "user", "content": user_message},
{"role": "assistant", "content": assistant_html, "metadata": {"pipeline": True}},
]
context_html = _render_context_viewer(_context_log)
docs_html = _render_retrieved_docs_viewer(retrieved_docs_full) if retrieved_docs_full else ""
return new_history, new_history, "", docs_html, context_html
# ── Gradio UI ─────────────────────────────────────────────────────────────
CSS = """
#db-status { font-size: 0.85em; padding: 4px 0; }
.compact-cb label { font-size: 0.9em !important; }
details summary { cursor: pointer; font-weight: 600; }
@keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.4; } }
#main-chatbot .message { text-align: left !important; }
#main-chatbot .user, #main-chatbot .bot { justify-content: flex-start !important; }
#main-chatbot .message-row { justify-content: flex-start !important; }
"""
def get_db_status():
s = db_manager._status
if s == "ready":
count = db_manager._collection.count() if db_manager._collection else "?"
return (
f''
f''
f'Vector DB: Ready ({count:,} docs)
'
)
if s == "loading":
return (
f''
f''
f'Vector DB: Loading...
'
)
if s == "error":
return (
f''
f''
f'Vector DB: Error
'
)
return (
f''
f''
f'Vector DB: Not started
'
)
@spaces.GPU
def handle_submit(
message, history,
cb_guardian, cb_policy, cb_qr, cb_retrieval, cb_answerability,
cb_citations, cb_hallucination, cb_fact_det, cb_fact_cor,
cb_context_attr, cb_uncertainty, cb_requirement,
cfg_guardian_criteria, cfg_guardian_custom, cfg_policy_text,
cfg_requirements, cfg_context_docs,
cfg_exit_guardian, cfg_exit_policy, cfg_exit_answerability,
max_tokens,
):
if not message:
return history, history, "", "", ""
message = message.strip()
if not message:
return history, history, "", "", ""
enabled = []
for val, key in [
(cb_guardian, "guardian-core"),
(cb_policy, "policy-guardrails"),
(cb_qr, "query_rewrite"),
(cb_retrieval, "retrieval"),
(cb_answerability, "answerability"),
(cb_citations, "citations"),
(cb_hallucination, "hallucination_detection"),
(cb_fact_det, "factuality-detection"),
(cb_fact_cor, "factuality-correction"),
(cb_context_attr, "context-attribution"),
(cb_uncertainty, "uncertainty"),
(cb_requirement, "requirement-check"),
]:
if val:
enabled.append(key)
config = {
"guardian_criteria": cfg_guardian_criteria,
"guardian_custom_criteria": cfg_guardian_custom,
"policy_text": cfg_policy_text,
"requirements_text": cfg_requirements,
"context_documents": cfg_context_docs,
"exit_on_guardian": cfg_exit_guardian,
"exit_on_policy": cfg_exit_policy,
"exit_on_answerability": cfg_exit_answerability,
}
try:
return run_pipeline(message, history, enabled, config, max_tokens)
except Exception as e:
err_msg = str(e)
if "CUDA" in err_msg or "GPU" in err_msg or "No GPU" in err_msg:
error_html = (
f''
f'GPU temporarily unavailable — ZeroGPU could not allocate '
f'a GPU for this request. Please try again in a few seconds.
'
)
else:
error_html = (
f''
f'Error: {html.escape(err_msg)}
'
)
new_history = list(history) + [
{"role": "user", "content": message},
{"role": "assistant", "content": error_html},
]
return new_history, new_history, "", "", f"Error: {err_msg}"
@spaces.GPU
def run_scenario(scenario_name):
scenario = SCENARIOS[scenario_name]
_get_model()
if "retrieval" in scenario["adapters"]:
import time as _time
deadline = _time.time() + 60
while not db_manager.is_ready and _time.time() < deadline:
_time.sleep(1)
cb_values = [k in scenario["adapters"] for k in ADAPTER_KEYS]
config = {
"guardian_criteria": scenario["settings"].get("guardian_criteria", "off_scope"),
"guardian_custom_criteria": "",
"policy_text": "",
"requirements_text": "",
"context_documents": "",
"exit_on_guardian": scenario["settings"].get("exit_on_guardian", True),
"exit_on_policy": scenario["settings"].get("exit_on_policy", True),
"exit_on_answerability": scenario["settings"].get("exit_on_answerability", True),
}
history = []
enabled = list(scenario["adapters"].keys())
docs_html = ""
ctx_html = ""
for query in scenario["queries"]:
history, _, _, docs_html, ctx_html = run_pipeline(query, history, enabled, config, 128)
return (
*cb_values,
scenario["settings"].get("guardian_criteria", "off_scope"),
scenario["settings"].get("exit_on_guardian", True),
scenario["settings"].get("exit_on_policy", True),
scenario["settings"].get("exit_on_answerability", True),
history, history, "", docs_html, ctx_html,
)
with gr.Blocks(title="Granite Switch 4.1 3B Playground") as demo:
gr.Markdown(
"# Granite Switch 4.1 3B Playground\n\n"
"[Granite Switch](https://github.com/generative-computing/granite-switch) "
"embeds multiple LoRA adapters inside a single Granite checkpoint and "
"activates them on demand via control tokens. This playground runs "
"**Granite Switch 4.1 3B** with 11 adapters organized in a "
"RAG pipeline:\n\n"
"1. **User Validation** — Guardian & Policy Guardrails screen the input\n"
"2. **Pre-Retrieval** — Query Rewrite optimizes the search query\n"
"3. **Retrieval** — Vector DB searches ~2k NASA passages\n"
"4. **Post-Retrieval** — Answerability checks whether the docs can answer the question\n"
"5. **Generation** — Base model produces an answer grounded in retrieved context\n"
"6. **Post-Generation** — Citations, Hallucination Detection, Factuality, "
"Context Attribution, Uncertainty, and Requirement Check analyze the response\n\n"
"Enable adapters with the checkboxes on the left, then ask a question about "
"NASA missions, Earth observation, or space science."
)
db_status_html = gr.HTML("", elem_id="db-status")
gr.Markdown("**Demo Scenarios:**")
with gr.Row():
btn_scenario_1 = gr.Button("1: User Validation", variant="secondary", size="sm")
btn_scenario_2 = gr.Button("2: Basic RAG", variant="secondary", size="sm")
btn_scenario_3 = gr.Button("3: Full Pipeline", variant="secondary", size="sm")
chat_state = gr.State([])
with gr.Row(equal_height=True):
# ── Sidebar ───────────────────────────────────────────────
with gr.Column(scale=1, min_width=220):
gr.Markdown("**Pipeline Adapters**")
with gr.Accordion("User Validation", open=True):
cb_guardian = gr.Checkbox(label="Guardian", value=True, elem_classes=["compact-cb"])
cb_policy = gr.Checkbox(label="Policy Guardrails", value=False, elem_classes=["compact-cb"])
with gr.Accordion("Pre-Retrieval", open=True):
cb_qr = gr.Checkbox(label="Query Rewrite", value=False, elem_classes=["compact-cb"])
with gr.Accordion("Retrieval", open=True):
cb_retrieval = gr.Checkbox(label="Vector DB Search", value=True, elem_classes=["compact-cb"])
with gr.Accordion("Post-Retrieval", open=True):
cb_answerability = gr.Checkbox(label="Answerability", value=False, elem_classes=["compact-cb"])
with gr.Accordion("Post-Generation", open=True):
cb_citations = gr.Checkbox(label="Citations", value=False, elem_classes=["compact-cb"])
cb_hallucination = gr.Checkbox(label="Hallucination Detection", value=False, elem_classes=["compact-cb"])
cb_fact_det = gr.Checkbox(label="Factuality Detection", value=False, elem_classes=["compact-cb"])
cb_fact_cor = gr.Checkbox(label="Factuality Correction", value=False, elem_classes=["compact-cb"])
cb_context_attr = gr.Checkbox(label="Context Attribution", value=False, elem_classes=["compact-cb"])
cb_uncertainty = gr.Checkbox(label="Uncertainty", value=False, elem_classes=["compact-cb"])
cb_requirement = gr.Checkbox(label="Requirement Check", value=False, elem_classes=["compact-cb"])
with gr.Accordion("Adapter Settings", open=False):
cfg_guardian_criteria = gr.Dropdown(
choices=list(GUARDIAN_CRITERIA_BANK.keys()) + ["Custom"],
value="off_scope", label="Guardian Criteria",
)
cfg_guardian_desc = gr.Textbox(
label="Criteria Description (read-only for presets, editable for Custom)",
value=GUARDIAN_CRITERIA_BANK["off_scope"],
lines=3, interactive=False,
)
cfg_guardian_custom = gr.Textbox(
label="Custom Criteria (used when 'Custom' is selected)",
lines=2, visible=False,
)
cfg_policy_text = gr.Textbox(
label="Policy Text", lines=2,
placeholder="e.g., No investment advice.",
)
cfg_requirements = gr.Textbox(
label="Requirements", lines=2,
placeholder="e.g., Formal tone, under 100 words.",
)
cfg_context_docs = gr.Textbox(
label="Context Documents (fallback when retrieval is off)",
lines=4,
placeholder="Paste documents separated by ---",
)
gr.Markdown("**Exit on warning**")
cfg_exit_guardian = gr.Checkbox(
label="Guardian blocks generation", value=True, elem_classes=["compact-cb"]
)
cfg_exit_policy = gr.Checkbox(
label="Policy blocks generation", value=True, elem_classes=["compact-cb"]
)
cfg_exit_answerability = gr.Checkbox(
label="Answerability blocks generation", value=True, elem_classes=["compact-cb"]
)
max_tokens_slider = gr.Slider(16, 512, value=128, step=16, label="Max tokens")
# ── Main chat area ────────────────────────────────────────
with gr.Column(scale=3):
chatbot = gr.Chatbot(sanitize_html=False, height=520, elem_id="main-chatbot")
with gr.Row():
msg_input = gr.Textbox(
show_label=False, lines=1, scale=4,
placeholder="Type a message... (Enter to send, Shift+Enter for newline)",
)
with gr.Column(scale=1, min_width=80):
send_btn = gr.Button("Send", variant="primary")
clear_btn = gr.Button("Clear", variant="secondary")
# ── Context panel ─────────────────────────────────────────
with gr.Column(scale=2):
with gr.Accordion("Retrieved Documents", open=True):
docs_display = gr.HTML(
value='Documents will appear here after retrieval...
',
)
with gr.Accordion("Full Context (prompts)", open=False):
context_display = gr.HTML(
value='Context will appear here after sending a message...
',
)
all_inputs = [
msg_input, chat_state,
cb_guardian, cb_policy, cb_qr, cb_retrieval, cb_answerability,
cb_citations, cb_hallucination, cb_fact_det, cb_fact_cor,
cb_context_attr, cb_uncertainty, cb_requirement,
cfg_guardian_criteria, cfg_guardian_custom, cfg_policy_text,
cfg_requirements, cfg_context_docs,
cfg_exit_guardian, cfg_exit_policy, cfg_exit_answerability,
max_tokens_slider,
]
all_outputs = [chatbot, chat_state, msg_input, docs_display, context_display]
def _update_guardian_desc(choice):
if choice == "Custom":
return gr.update(visible=False), gr.update(visible=True)
text = GUARDIAN_CRITERIA_BANK.get(choice, "")
return gr.update(value=text, visible=True), gr.update(visible=False)
cfg_guardian_criteria.change(
_update_guardian_desc,
inputs=[cfg_guardian_criteria],
outputs=[cfg_guardian_desc, cfg_guardian_custom],
)
send_btn.click(handle_submit, inputs=all_inputs, outputs=all_outputs)
msg_input.submit(handle_submit, inputs=all_inputs, outputs=all_outputs)
clear_btn.click(
lambda: (
[], [],
'Documents will appear here after retrieval...
',
'Context will appear here after sending a message...
',
),
outputs=[chatbot, chat_state, docs_display, context_display],
)
scenario_outputs = [
cb_guardian, cb_policy, cb_qr, cb_retrieval, cb_answerability,
cb_citations, cb_hallucination, cb_fact_det, cb_fact_cor,
cb_context_attr, cb_uncertainty, cb_requirement,
cfg_guardian_criteria,
cfg_exit_guardian, cfg_exit_policy, cfg_exit_answerability,
chatbot, chat_state, msg_input, docs_display, context_display,
]
btn_scenario_1.click(lambda: run_scenario("User Validation"), outputs=scenario_outputs)
btn_scenario_2.click(lambda: run_scenario("Basic RAG"), outputs=scenario_outputs)
btn_scenario_3.click(lambda: run_scenario("Full Pipeline"), outputs=scenario_outputs)
demo.load(get_db_status, outputs=db_status_html)
timer = gr.Timer(5)
timer.tick(get_db_status, outputs=db_status_html)
# ── Startup ───────────────────────────────────────────────────────────────
db_manager.start_loading()
if __name__ == "__main__":
print(f"[INFO] Inference backend: {_inference_backend_summary()}", flush=True)
demo.launch(
server_name="0.0.0.0",
server_port=int(os.getenv("PORT", "7860")),
css=CSS,
ssr_mode=False,
)