File size: 11,004 Bytes
2604671 0b87551 38b974d 2604671 38b974d 41e79c8 0b87551 79e127c 41e79c8 8f6bb93 41e79c8 8f6bb93 2b63102 25ec7ce 41e79c8 25ec7ce 2b63102 41e79c8 8f6bb93 2b63102 8f6bb93 79e127c 8f6bb93 2b63102 8f6bb93 2604671 8f6bb93 2b63102 8f6bb93 2b63102 8f6bb93 2b63102 8f6bb93 0b87551 8f6bb93 41e79c8 8f6bb93 2604671 0b87551 8f6bb93 41e79c8 8f6bb93 0b87551 41e79c8 8f6bb93 0b87551 38b974d 0b87551 8f6bb93 2b63102 8f6bb93 38b974d 41e79c8 38b974d 41e79c8 8f6bb93 41e79c8 2604671 0b87551 8f6bb93 2604671 41e79c8 2ca006c 2b63102 0b87551 41e79c8 8f6bb93 0b87551 8f6bb93 41e79c8 8f6bb93 2b63102 8f6bb93 2b63102 8f6bb93 41e79c8 8f6bb93 41e79c8 8f6bb93 41e79c8 2604671 8f6bb93 2604671 8f6bb93 41e79c8 8f6bb93 41e79c8 8f6bb93 41e79c8 8f6bb93 2b63102 8f6bb93 41e79c8 0b87551 41e79c8 2604671 0b87551 8f6bb93 41e79c8 2604671 0b87551 8f6bb93 41e79c8 0b87551 2604671 8f6bb93 | 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 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | import os
import uuid
import gradio as gr
import requests
os.environ["TOKENIZERS_PARALLELISM"] = "false"
FASTAPI_URL = os.getenv("FASTAPI_URL", "http://127.0.0.1:8000")
CHAT_ENDPOINT = f"{FASTAPI_URL}/chat"
UPLOAD_ENDPOINT = f"{FASTAPI_URL}/upload"
RESET_ENDPOINT = f"{FASTAPI_URL}/reset"
CLEAR_MEMORY_ENDPOINT = f"{FASTAPI_URL}/clear_memory"
SUPPORTED_TYPES = [".pdf", ".docx", ".txt", ".md", ".csv"]
CSS = """
#submit-btn { margin-top: auto; margin-bottom: auto; height: 80px; }
#input-row { align-items: center; }
"""
SOURCE_LABELS = {
"rag": "Uploaded Documents (RAG)",
"web": "Web Search",
"rag+web": "Documents + Web Search",
"unknown": "Unknown",
}
def upload_files(files) -> str:
if not files:
return "No files selected."
try:
multipart = []
for f in files:
path = f if isinstance(f, str) else f.name
with open(path, "rb") as fp:
multipart.append(("files", (os.path.basename(path), fp.read(), "application/octet-stream")))
# Large PDFs can take minutes to embed on CPU, so allow a generous timeout.
resp = requests.post(UPLOAD_ENDPOINT, files=multipart, timeout=900)
resp.raise_for_status()
return resp.json().get("status", "Files processed.")
except requests.exceptions.ReadTimeout:
return ("Still indexing. This file is large and CPU embedding is slow; "
"wait a moment and try your question, it may already be indexed.")
except requests.exceptions.RequestException as e:
return f"Upload failed: {e}"
def reset_documents() -> str:
try:
resp = requests.post(RESET_ENDPOINT, timeout=10)
resp.raise_for_status()
return resp.json().get("status", "Documents cleared.")
except requests.exceptions.RequestException as e:
return f"Reset failed: {e}"
def _bar(value: float, width: int = 12) -> str:
filled = round(value * width)
return "█" * filled + "░" * (width - filled)
def _fmt(label: str, value: float, hint: str) -> str:
return f"**{label}** \n`{_bar(value)} {value:.2f} / 1.0` \n_{hint}_\n"
def _faith_hint(score: float, source: str) -> str:
what = "web results" if source == "web" else "retrieved documents"
if score >= 0.75:
return f"Answer is well-grounded in the {what}"
if score >= 0.50:
return f"Answer is mostly grounded in the {what}, minor unsupported details possible"
return f"Low grounding: answer may contain content not present in the {what}"
def _relevance_hint(score: float) -> str:
if score >= 0.70:
return "Answer directly addresses the question"
if score >= 0.45:
return "Answer is related but may not fully address the question"
return "Answer may be off-topic or incomplete"
def _accuracy_hint(score: float) -> str:
if score >= 0.75:
return "Strong match with your reference answer"
if score >= 0.40:
return "Partial match: some key points differ from your reference"
return "Low overlap with your reference answer"
def _format_metrics(source: str, faithfulness, answer_relevance, accuracy) -> str:
if faithfulness is None and answer_relevance is None:
return ""
src_label = SOURCE_LABELS.get(source, source)
lines = [f"**Answer source: {src_label}**", "---"]
if faithfulness is not None:
faith_label = "Faithfulness (grounded in documents)" if source != "web" else "Faithfulness (grounded in web results)"
lines.append(_fmt(faith_label, faithfulness, _faith_hint(faithfulness, source)))
if answer_relevance is not None:
lines.append(_fmt("Answer Relevance (addresses the question)", answer_relevance, _relevance_hint(answer_relevance)))
if accuracy is not None:
lines.append(_fmt("Accuracy (matches your reference)", accuracy, _accuracy_hint(accuracy)))
else:
lines.append("_Accuracy: provide a reference answer above to see this score._")
return "\n".join(lines)
def process_query(message: str, api_key: str, reference: str, session_id: str, chat_history: list) -> tuple[list, str, str]:
if not api_key.strip():
return chat_history, "Enter your Google Gemini API key above to start.", ""
if not message.strip():
return chat_history, "Please enter a question.", ""
try:
payload = {"query": message, "api_key": api_key.strip(), "session_id": session_id}
if reference.strip():
payload["reference"] = reference.strip()
resp = requests.post(CHAT_ENDPOINT, json=payload, timeout=180)
if resp.status_code in (400, 401, 429):
detail = resp.json().get("detail", "Request could not be completed.")
chat_history.append({"role": "user", "content": message})
chat_history.append({"role": "assistant", "content": f"Sorry, I can't respond right now:\n\n{detail}"})
return chat_history, detail, ""
resp.raise_for_status()
data = resp.json()
response_text = data.get("response", "")
source = data.get("source", "unknown")
citations = data.get("citations", [])
src_label = SOURCE_LABELS.get(source, source)
footer = f"\n\n--- Source: {src_label} ---"
if citations:
footer += "\n📎 Sources used: " + "; ".join(citations)
chat_history.append({"role": "user", "content": message})
chat_history.append({"role": "assistant", "content": f"{response_text}{footer}"})
metrics_md = _format_metrics(
source,
data.get("faithfulness"),
data.get("answer_relevance"),
data.get("accuracy"),
)
return chat_history, f"Done. Answered via {src_label}", metrics_md
except requests.exceptions.RequestException as e:
error = f"Request failed: {e}"
chat_history.append({"role": "user", "content": message})
chat_history.append({"role": "assistant", "content": f"ERROR: {error}"})
return chat_history, error, ""
def clear_chat(session_id: str) -> tuple[list, str, str]:
"""Clear the UI and the server-side conversation memory for this session."""
try:
requests.post(CLEAR_MEMORY_ENDPOINT, params={"session_id": session_id}, timeout=10)
except requests.exceptions.RequestException:
pass
return [], "", ""
with gr.Blocks(title="Agentic RAG Knowledge Search") as demo:
session_id = gr.State() # per-browser id for server-side memory
gr.Markdown("# Agentic RAG Knowledge Search")
gr.Markdown(
"Upload your own documents and ask questions. "
"The agent searches your files via RAG first, then falls back to web search if needed. "
"It remembers the conversation, so follow-up questions work. "
"**No files uploaded?** The agent uses a built-in legal/policy document as the default knowledge base."
)
with gr.Group():
gr.Markdown(
"### Your Gemini API Key (required)\n"
"This app uses **your own** Google Gemini key; it is sent only with your requests and never stored. "
"Get a free key at [Google AI Studio](https://aistudio.google.com/apikey)."
)
api_key_input = gr.Textbox(
label="Google Gemini API Key",
placeholder="Paste your API key here (starts with AIza...)",
type="password",
lines=1,
)
with gr.Group():
gr.Markdown(
f"### Upload Documents\n"
f"Supported: **{', '.join(SUPPORTED_TYPES)}**. Multiple files allowed; new uploads add to the index. \n"
"_Large PDFs (100s of pages) can take a few minutes to index on the free CPU, so please be patient._"
)
file_input = gr.File(label="Select Files", file_count="multiple", file_types=SUPPORTED_TYPES)
with gr.Row():
upload_btn = gr.Button("Process & Index Files", variant="primary")
reset_btn = gr.Button("Clear Uploaded Documents", variant="secondary")
upload_status = gr.Textbox(label="Upload Status", interactive=False, lines=2,
placeholder="Upload status will appear here...")
gr.Markdown("---")
with gr.Group():
gr.Markdown(
"### Ask a Question\n"
"> Press **Enter** or click **Submit**. The agent decides whether to search your documents, the web, or both."
)
chatbot = gr.Chatbot(label="Conversation", height=420)
with gr.Row(elem_id="input-row"):
user_input = gr.Textbox(
placeholder="Ask anything about your documents or the web...",
lines=2, scale=5, show_label=False, container=False,
)
submit_btn = gr.Button("Submit", variant="primary", scale=1, elem_id="submit-btn")
reference_input = gr.Textbox(
label="Reference Answer (optional)",
placeholder="Paste an expected correct answer here to also see an Accuracy score...",
lines=2,
)
with gr.Row():
clear_btn = gr.Button("Clear Chat", variant="secondary", scale=1)
status_output = gr.Textbox(label="Status", interactive=False, lines=1,
placeholder="Status will appear here...")
gr.Markdown("---")
with gr.Group():
gr.Markdown(
"### Evaluation Metrics\n"
"Computed automatically after every response, with **no extra API calls and no reference needed** for the first two.\n\n"
"| Metric | Always shown? | What it measures |\n"
"|---|---|---|\n"
"| **Faithfulness** | Yes | Is the answer grounded in the source it used? (docs or web results) |\n"
"| **Answer Relevance** | Yes | Does the answer actually address your question? |\n"
"| **Accuracy** | Only with reference | Does the answer match an expected correct answer? |"
)
metrics_output = gr.Markdown()
demo.load(fn=lambda: str(uuid.uuid4()), inputs=[], outputs=[session_id])
upload_btn.click(fn=upload_files, inputs=[file_input], outputs=[upload_status])
reset_btn.click(fn=reset_documents, inputs=[], outputs=[upload_status])
submit_btn.click(
fn=process_query,
inputs=[user_input, api_key_input, reference_input, session_id, chatbot],
outputs=[chatbot, status_output, metrics_output],
).then(fn=lambda: "", inputs=[], outputs=[user_input])
user_input.submit(
fn=process_query,
inputs=[user_input, api_key_input, reference_input, session_id, chatbot],
outputs=[chatbot, status_output, metrics_output],
).then(fn=lambda: "", inputs=[], outputs=[user_input])
clear_btn.click(fn=clear_chat, inputs=[session_id], outputs=[chatbot, status_output, metrics_output])
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, share=False, theme=gr.themes.Soft(), css=CSS)
|