from typing import TypedDict, List, Optional import google.generativeai as genai from langgraph.graph import StateGraph, END from rag_store import search_knowledge from eval_logger import log_eval MODEL_NAME = "gemini-2.5-flash" # =============================== # STATE # =============================== class AgentState(TypedDict): query: str decision: str retrieved_chunks: List[dict] answer: Optional[str] confidence: float answer_known: bool # =============================== # DECISION NODE # =============================== def agent_decision_node(state: AgentState) -> AgentState: q = state["query"].lower() rag_keywords = [ "summarize", "summary", "fee", "fees", "refund", "tuition", "document", "policy", "offer", "scholarship" ] decision = "use_rag" if any(k in q for k in rag_keywords) else "no_rag" return {**state, "decision": decision} # =============================== # RETRIEVAL NODE (TOOL) # =============================== def retrieve_node(state: AgentState) -> AgentState: chunks = search_knowledge(state["query"]) return {**state, "retrieved_chunks": chunks} # =============================== # ANSWER WITH RAG # =============================== def answer_with_rag_node(state: AgentState) -> AgentState: if not state["retrieved_chunks"]: return no_answer_node(state) context = "\n\n".join(c["text"] for c in state["retrieved_chunks"]) prompt = f""" Answer using ONLY the context below. If the answer is not present, say "I don't know". Context: {context} Question: {state["query"]} """ model = genai.GenerativeModel(MODEL_NAME) resp = model.generate_content(prompt) answer_text = resp.text confidence = min(1.0, len(state["retrieved_chunks"]) / 5) answer_known = "i don't know" not in answer_text.lower() log_eval( query=state["query"], retrieved_count=len(state["retrieved_chunks"]), confidence=confidence, answer_known=answer_known ) return { **state, "answer": answer_text, "confidence": confidence, "answer_known": answer_known } # =============================== # ANSWER WITHOUT RAG # =============================== def answer_direct_node(state: AgentState) -> AgentState: prompt = f"Answer the following question concisely:\n\n{state['query']}" model = genai.GenerativeModel(MODEL_NAME) resp = model.generate_content(prompt) log_eval( query=state["query"], retrieved_count=0, confidence=0.3, answer_known=True ) return { **state, "answer": resp.text, "confidence": 0.3, "answer_known": True } # =============================== # NO ANSWER # =============================== def no_answer_node(state: AgentState) -> AgentState: log_eval( query=state["query"], retrieved_count=0, confidence=0.0, answer_known=False ) return { **state, "answer": "I don't know based on the provided documents.", "confidence": 0.0, "answer_known": False } # =============================== # GRAPH BUILDER # =============================== def build_agentic_rag_graph(): graph = StateGraph(AgentState) graph.add_node("decide", agent_decision_node) graph.add_node("retrieve", retrieve_node) graph.add_node("answer_rag", answer_with_rag_node) graph.add_node("answer_direct", answer_direct_node) graph.add_node("no_answer", no_answer_node) graph.set_entry_point("decide") graph.add_conditional_edges( "decide", lambda s: s["decision"], { "use_rag": "retrieve", "no_rag": "answer_direct" } ) graph.add_edge("retrieve", "answer_rag") graph.add_edge("answer_rag", END) graph.add_edge("answer_direct", END) graph.add_edge("no_answer", END) return graph.compile()