Spaces:
Sleeping
Sleeping
Quincy Hsieh commited on
Commit Β·
035ac6d
1
Parent(s): b818d09
Add token count and explanation
Browse files- app.py +55 -11
- prompts/rag_prompt.txt +6 -0
app.py
CHANGED
|
@@ -18,6 +18,7 @@ Architecture:
|
|
| 18 |
import os
|
| 19 |
import json
|
| 20 |
import logging
|
|
|
|
| 21 |
from pathlib import Path
|
| 22 |
from typing import Optional
|
| 23 |
|
|
@@ -256,12 +257,14 @@ def retrieve_relevant_context(query: str, top_k: int = TOP_K_RESULTS) -> list[di
|
|
| 256 |
return contexts
|
| 257 |
|
| 258 |
|
| 259 |
-
def call_llm(prompt: str) ->
|
| 260 |
"""
|
| 261 |
Make a call to the LLM via Azure Foundry (GPT-5).
|
| 262 |
|
| 263 |
Calls the Azure OpenAI-compatible chat/completions endpoint.
|
| 264 |
Endpoint URL is loaded from config.json; API key from AZURE_API_KEY env var.
|
|
|
|
|
|
|
| 265 |
"""
|
| 266 |
headers = {
|
| 267 |
"api-key": AZURE_API_KEY,
|
|
@@ -278,7 +281,9 @@ def call_llm(prompt: str) -> str:
|
|
| 278 |
resp = http_requests.post(LLM_ENDPOINT_URL, headers=headers, json=payload, timeout=60)
|
| 279 |
resp.raise_for_status()
|
| 280 |
data = resp.json()
|
| 281 |
-
|
|
|
|
|
|
|
| 282 |
except http_requests.exceptions.HTTPError as e:
|
| 283 |
logger.error(f"LLM API call failed: {e} β {resp.text}")
|
| 284 |
raise HTTPException(status_code=503, detail=f"LLM service unavailable: {str(e)}")
|
|
@@ -318,29 +323,54 @@ def rag_query(query: str, top_k: int = TOP_K_RESULTS) -> dict:
|
|
| 318 |
2. Retrieve relevant context from vector store
|
| 319 |
3. Build augmented prompt with context
|
| 320 |
4. Call LLM to generate answer
|
| 321 |
-
5. Return answer with source metadata
|
| 322 |
"""
|
|
|
|
|
|
|
| 323 |
# Step 1: Retrieve relevant document chunks
|
| 324 |
contexts = retrieve_relevant_context(query, top_k=top_k)
|
| 325 |
|
| 326 |
if not contexts:
|
|
|
|
| 327 |
return {
|
| 328 |
"answer": "No documents have been ingested yet. Please upload documents first.",
|
| 329 |
"sources": [],
|
| 330 |
-
"
|
|
|
|
|
|
|
| 331 |
}
|
| 332 |
|
| 333 |
# Step 2: Build the augmented prompt
|
| 334 |
prompt = build_rag_prompt(query, contexts)
|
| 335 |
|
| 336 |
# Step 3: Generate answer from LLM
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
# Step 4: Return structured response
|
| 340 |
return {
|
| 341 |
"answer": answer,
|
| 342 |
"sources": [{"source": ctx["source"], "score": ctx["similarity_score"]} for ctx in contexts],
|
| 343 |
-
"
|
|
|
|
|
|
|
| 344 |
}
|
| 345 |
|
| 346 |
|
|
@@ -421,7 +451,9 @@ async def query_endpoint(request: QueryRequest):
|
|
| 421 |
Returns:
|
| 422 |
- answer (str): The generated answer
|
| 423 |
- sources (list): Source documents used for context
|
| 424 |
-
-
|
|
|
|
|
|
|
| 425 |
"""
|
| 426 |
logger.info(f"Query received: {request.query}")
|
| 427 |
result = rag_query(request.query, top_k=request.top_k)
|
|
@@ -463,15 +495,19 @@ async def health_check():
|
|
| 463 |
# Step 6: Gradio UI for Interactive Demo
|
| 464 |
# ---------------------------------------------------------------------------
|
| 465 |
|
| 466 |
-
def gradio_query(question: str) -> str:
|
| 467 |
"""Handle queries from the Gradio chat interface."""
|
| 468 |
if not question.strip():
|
| 469 |
-
return "Please enter a question."
|
| 470 |
result = rag_query(question)
|
| 471 |
sources_text = "\n".join(
|
| 472 |
f" - {s['source']} (relevance: {s['score']:.2f})" for s in result["sources"]
|
| 473 |
)
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
| 476 |
|
| 477 |
def gradio_ingest(text: str, source_name: str) -> str:
|
|
@@ -504,7 +540,15 @@ with gr.Blocks(title="RAG Chat API - Gustave Eiffel Hackathon") as demo:
|
|
| 504 |
)
|
| 505 |
query_button = gr.Button("Ask", variant="primary")
|
| 506 |
query_output = gr.Textbox(label="Answer", lines=8, interactive=False)
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
with gr.Tab("π Ingest Documents"):
|
| 510 |
gr.Markdown("Add new documents to the knowledge base.")
|
|
|
|
| 18 |
import os
|
| 19 |
import json
|
| 20 |
import logging
|
| 21 |
+
import time
|
| 22 |
from pathlib import Path
|
| 23 |
from typing import Optional
|
| 24 |
|
|
|
|
| 257 |
return contexts
|
| 258 |
|
| 259 |
|
| 260 |
+
def call_llm(prompt: str) -> dict:
|
| 261 |
"""
|
| 262 |
Make a call to the LLM via Azure Foundry (GPT-5).
|
| 263 |
|
| 264 |
Calls the Azure OpenAI-compatible chat/completions endpoint.
|
| 265 |
Endpoint URL is loaded from config.json; API key from AZURE_API_KEY env var.
|
| 266 |
+
|
| 267 |
+
Returns a dict with 'content' (str) and 'total_tokens' (int).
|
| 268 |
"""
|
| 269 |
headers = {
|
| 270 |
"api-key": AZURE_API_KEY,
|
|
|
|
| 281 |
resp = http_requests.post(LLM_ENDPOINT_URL, headers=headers, json=payload, timeout=60)
|
| 282 |
resp.raise_for_status()
|
| 283 |
data = resp.json()
|
| 284 |
+
content = data["choices"][0]["message"]["content"].strip()
|
| 285 |
+
total_tokens = data.get("usage", {}).get("total_tokens", 0)
|
| 286 |
+
return {"content": content, "total_tokens": total_tokens}
|
| 287 |
except http_requests.exceptions.HTTPError as e:
|
| 288 |
logger.error(f"LLM API call failed: {e} β {resp.text}")
|
| 289 |
raise HTTPException(status_code=503, detail=f"LLM service unavailable: {str(e)}")
|
|
|
|
| 323 |
2. Retrieve relevant context from vector store
|
| 324 |
3. Build augmented prompt with context
|
| 325 |
4. Call LLM to generate answer
|
| 326 |
+
5. Return answer with source metadata, explanation, token count, and timing
|
| 327 |
"""
|
| 328 |
+
start_time = time.perf_counter()
|
| 329 |
+
|
| 330 |
# Step 1: Retrieve relevant document chunks
|
| 331 |
contexts = retrieve_relevant_context(query, top_k=top_k)
|
| 332 |
|
| 333 |
if not contexts:
|
| 334 |
+
elapsed_ms = round((time.perf_counter() - start_time) * 1000, 2)
|
| 335 |
return {
|
| 336 |
"answer": "No documents have been ingested yet. Please upload documents first.",
|
| 337 |
"sources": [],
|
| 338 |
+
"explanation": "No documents found in the vector store to retrieve context from.",
|
| 339 |
+
"total_token": 0,
|
| 340 |
+
"run_time_in_ms": elapsed_ms,
|
| 341 |
}
|
| 342 |
|
| 343 |
# Step 2: Build the augmented prompt
|
| 344 |
prompt = build_rag_prompt(query, contexts)
|
| 345 |
|
| 346 |
# Step 3: Generate answer from LLM
|
| 347 |
+
llm_result = call_llm(prompt)
|
| 348 |
+
raw_content = llm_result["content"]
|
| 349 |
+
total_token = llm_result["total_tokens"]
|
| 350 |
+
|
| 351 |
+
# Parse structured JSON response from LLM (handle markdown code fences)
|
| 352 |
+
json_str = raw_content.strip()
|
| 353 |
+
if json_str.startswith("```"):
|
| 354 |
+
json_str = json_str.split("\n", 1)[-1]
|
| 355 |
+
json_str = json_str.rsplit("```", 1)[0].strip()
|
| 356 |
+
|
| 357 |
+
try:
|
| 358 |
+
parsed = json.loads(json_str)
|
| 359 |
+
answer = parsed["answer"]
|
| 360 |
+
explanation = parsed["explanation"]
|
| 361 |
+
except (json.JSONDecodeError, KeyError):
|
| 362 |
+
answer = raw_content
|
| 363 |
+
explanation = "LLM did not return a structured explanation."
|
| 364 |
+
|
| 365 |
+
elapsed_ms = round((time.perf_counter() - start_time) * 1000, 2)
|
| 366 |
|
| 367 |
# Step 4: Return structured response
|
| 368 |
return {
|
| 369 |
"answer": answer,
|
| 370 |
"sources": [{"source": ctx["source"], "score": ctx["similarity_score"]} for ctx in contexts],
|
| 371 |
+
"explanation": explanation,
|
| 372 |
+
"total_token": total_token,
|
| 373 |
+
"run_time_in_ms": elapsed_ms,
|
| 374 |
}
|
| 375 |
|
| 376 |
|
|
|
|
| 451 |
Returns:
|
| 452 |
- answer (str): The generated answer
|
| 453 |
- sources (list): Source documents used for context
|
| 454 |
+
- explanation (str): Explanation of the retrieval and answer logic
|
| 455 |
+
- total_token (int): Total token count from the LLM call
|
| 456 |
+
- run_time_in_ms (float): Pipeline execution time in milliseconds
|
| 457 |
"""
|
| 458 |
logger.info(f"Query received: {request.query}")
|
| 459 |
result = rag_query(request.query, top_k=request.top_k)
|
|
|
|
| 495 |
# Step 6: Gradio UI for Interactive Demo
|
| 496 |
# ---------------------------------------------------------------------------
|
| 497 |
|
| 498 |
+
def gradio_query(question: str) -> tuple[str, str, str, str]:
|
| 499 |
"""Handle queries from the Gradio chat interface."""
|
| 500 |
if not question.strip():
|
| 501 |
+
return "Please enter a question.", "", "", ""
|
| 502 |
result = rag_query(question)
|
| 503 |
sources_text = "\n".join(
|
| 504 |
f" - {s['source']} (relevance: {s['score']:.2f})" for s in result["sources"]
|
| 505 |
)
|
| 506 |
+
answer = f"{result['answer']}\n\nπ Sources:\n{sources_text}" if result["sources"] else result["answer"]
|
| 507 |
+
explanation = result.get("explanation", "")
|
| 508 |
+
token_info = str(result.get("total_token", 0))
|
| 509 |
+
run_time = f"{result.get('run_time_in_ms', 0)} ms"
|
| 510 |
+
return answer, explanation, token_info, run_time
|
| 511 |
|
| 512 |
|
| 513 |
def gradio_ingest(text: str, source_name: str) -> str:
|
|
|
|
| 540 |
)
|
| 541 |
query_button = gr.Button("Ask", variant="primary")
|
| 542 |
query_output = gr.Textbox(label="Answer", lines=8, interactive=False)
|
| 543 |
+
query_explanation = gr.Textbox(label="Explanation", lines=3, interactive=False)
|
| 544 |
+
with gr.Row():
|
| 545 |
+
query_tokens = gr.Textbox(label="Total Tokens", interactive=False)
|
| 546 |
+
query_runtime = gr.Textbox(label="Run Time", interactive=False)
|
| 547 |
+
query_button.click(
|
| 548 |
+
fn=gradio_query,
|
| 549 |
+
inputs=query_input,
|
| 550 |
+
outputs=[query_output, query_explanation, query_tokens, query_runtime],
|
| 551 |
+
)
|
| 552 |
|
| 553 |
with gr.Tab("π Ingest Documents"):
|
| 554 |
gr.Markdown("Add new documents to the knowledge base.")
|
prompts/rag_prompt.txt
CHANGED
|
@@ -2,6 +2,12 @@ You are a helpful assistant. Answer the user's question based ONLY on the provid
|
|
| 2 |
If the context does not contain enough information to answer, say "I don't have enough information to answer this question."
|
| 3 |
Always be concise and factual.
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
Context:
|
| 6 |
{context}
|
| 7 |
|
|
|
|
| 2 |
If the context does not contain enough information to answer, say "I don't have enough information to answer this question."
|
| 3 |
Always be concise and factual.
|
| 4 |
|
| 5 |
+
You MUST respond in valid JSON with exactly two fields:
|
| 6 |
+
- "answer": your concise factual answer to the question
|
| 7 |
+
- "explanation": a brief explanation of your reasoning, describing which parts of the context you used and why they led to your answer
|
| 8 |
+
|
| 9 |
+
Do NOT include any text outside the JSON object.
|
| 10 |
+
|
| 11 |
Context:
|
| 12 |
{context}
|
| 13 |
|