Sangeethaa007's picture
Create app.py
a473599 verified
Raw
History Blame Contribute Delete
6.26 kB
"""Milestone 5 — Grounded generation + Gradio interface.
Final stage of the pipeline (see planning.md architecture diagram):
Ingestion -> Chunking -> Embedding + Vector Store -> Retrieval -> [Generation -> UI]
^^^^^^^^^^^^^^^^^^
this module
Flow per question:
1. retrieve() pulls the top-k chunks from ChromaDB (Milestone 4).
2. Those chunks — and only those — are passed to the LLM as context.
3. A grounding system prompt forces the model to answer ONLY from that
context and to say so when the answer isn't present.
4. The response is returned as: an answer, followed by a Sources list of
the documents the answer was drawn from.
Run the UI: python src/app.py
Ask in code: from app import answer_question; answer_question("...")
Requires GROQ_API_KEY in .env (see .env.example).
"""
from __future__ import annotations
import os
import gradio as gr
from dotenv import load_dotenv
from groq import Groq
from vector_store import TOP_K, retrieve
load_dotenv()
# Groq's hosted Llama model; override with GROQ_MODEL in .env if you like.
GROQ_MODEL = os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
# --- Grounding ---------------------------------------------------------------
SYSTEM_PROMPT = """\
You are "The Unofficial Guide" to Brooklyn College's Computer Science program.
You answer questions about CS courses and professors using ONLY the numbered
context passages provided with each question.
Rules:
- Use only facts found in the context. Do not use outside knowledge.
- If the context does not contain the answer, reply exactly:
"I don't have enough information in my sources to answer that."
- Do not guess, infer beyond the text, or invent ratings, names, or numbers.
- When you use a fact, attribute it to its source in-line, e.g. (Sokol.txt).
- Be concise and answer the question directly."""
NO_ANSWER = "I don't have enough information in my sources to answer that."
def format_context(hits: list[dict]) -> str:
"""Render retrieved chunks as a numbered, source-labeled context block."""
blocks = []
for i, hit in enumerate(hits, 1):
blocks.append(f"[{i}] (source: {hit['source']})\n{hit['text']}")
return "\n\n".join(blocks)
def build_messages(query: str, hits: list[dict]) -> list[dict]:
"""Assemble the chat messages: grounding system prompt + context + question."""
context = format_context(hits)
user_content = (
f"Context passages:\n\n{context}\n\n"
f"Question: {query}\n\n"
"Answer using only the context above."
)
return [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
]
# --- Generation --------------------------------------------------------------
_client: Groq | None = None
def get_client() -> Groq:
"""Lazily create the Groq client; fail loudly if the key is missing."""
global _client
if _client is None:
if not os.getenv("GROQ_API_KEY"):
raise RuntimeError(
"GROQ_API_KEY is not set. Copy .env.example to .env and add your key."
)
_client = Groq()
return _client
def answer_question(query: str, top_k: int = TOP_K) -> dict:
"""Retrieve, generate a grounded answer, and collect source attribution.
Returns {"answer": str, "sources": list[str], "hits": list[dict]}.
Sources are the distinct documents that were actually fed to the model,
in retrieval-rank order.
"""
query = (query or "").strip()
if not query:
return {"answer": "Please enter a question.", "sources": [], "hits": []}
hits = retrieve(query, top_k=top_k)
if not hits:
return {"answer": NO_ANSWER, "sources": [], "hits": []}
response = get_client().chat.completions.create(
model=GROQ_MODEL,
messages=build_messages(query, hits),
temperature=0, # deterministic, grounded answers
)
answer = response.choices[0].message.content.strip()
# Distinct sources in rank order (de-duplicated, preserving first-seen order).
sources = list(dict.fromkeys(h["source"] for h in hits))
return {"answer": answer, "sources": sources, "hits": hits}
def format_response(result: dict) -> str:
"""Format as: answer + a Markdown Sources list (the output format spec)."""
answer = result["answer"]
if not result["sources"] or answer == NO_ANSWER:
return answer
source_lines = "\n".join(f"- {s}" for s in result["sources"])
return f"{answer}\n\n**Sources:**\n{source_lines}"
# --- Gradio interface --------------------------------------------------------
EXAMPLE_QUESTIONS = [
"What is Prof. Dina Sokol's overall rating?",
"What is Prof. Deborah Elefant's level of difficulty?",
"How does Brooklyn College's CS program compare to NYU and Columbia?",
]
def _ui_handler(query: str) -> str:
"""Gradio callback: question string -> formatted answer + sources."""
return format_response(answer_question(query))
def build_interface() -> gr.Blocks:
"""Construct the Gradio app skeleton (no network calls at build time)."""
with gr.Blocks(title="The Unofficial Guide") as demo:
gr.Markdown(
"# The Unofficial College Guide — Brooklyn College CS\n"
"Ask about CS courses and professors. Answers come **only** from the "
"retrieved source documents, with attribution."
)
with gr.Row():
query_box = gr.Textbox(
label="Your question",
placeholder="e.g. What is Prof. Sokol's overall rating?",
lines=2,
scale=4,
)
ask_btn = gr.Button("Ask", variant="primary", scale=1)
answer_box = gr.Markdown(label="Answer")
gr.Examples(examples=EXAMPLE_QUESTIONS, inputs=query_box)
ask_btn.click(fn=_ui_handler, inputs=query_box, outputs=answer_box)
query_box.submit(fn=_ui_handler, inputs=query_box, outputs=answer_box)
return demo
def main() -> None:
build_interface().launch(share=True)
if __name__ == "__main__":
main()