Quincy Hsieh commited on
Commit
035ac6d
Β·
1 Parent(s): b818d09

Add token count and explanation

Browse files
Files changed (2) hide show
  1. app.py +55 -11
  2. 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) -> 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
- return data["choices"][0]["message"]["content"].strip()
 
 
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
- "query": query,
 
 
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
- answer = call_llm(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "query": query,
 
 
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
- - query (str): The original query
 
 
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
- return f"{result['answer']}\n\nπŸ“š Sources:\n{sources_text}" if result["sources"] else result["answer"]
 
 
 
 
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
- query_button.click(fn=gradio_query, inputs=query_input, outputs=query_output)
 
 
 
 
 
 
 
 
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