Spaces:
Sleeping
Sleeping
Download graph.py from aryan195a/LangGraph-RAG-Chatbot: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/spaces/aryan195a/LangGraph-RAG-Chatbot/resolve/main/graph.py
- Command line
-
hf download hf://spaces/aryan195a/LangGraph-RAG-Chatbot/graph.py
-
curl -L -o graph.py https://huggingface.co/spaces/aryan195a/LangGraph-RAG-Chatbot/resolve/main/graph.py
10.1 kB
| import datetime | |
| import os | |
| import re | |
| from typing import TypedDict, Optional, List | |
| from llama_index.core.schema import Document | |
| from langchain_google_genai import ChatGoogleGenerativeAI | |
| from langchain_openai import ChatOpenAI | |
| from llama_index.core.retrievers import BaseRetriever | |
| from langgraph.graph import StateGraph, END | |
| class GraphState(TypedDict): | |
| query: str | |
| response: Optional[str] | |
| retrieved_docs: Optional[List[Document]] | |
| context: Optional[str] | |
| history: list | |
| retriever: Optional[BaseRetriever] | |
| llm: any | |
| def _select_chunks_by_relevance_and_position(docs: List[Document], query: str, target_count: int) -> List[Document]: | |
| """Select chunks based on semantic relevance + positional importance without summarization.""" | |
| if len(docs) <= target_count: | |
| return docs | |
| selected = [] | |
| if len(docs) >= 2: | |
| selected = [docs[0], docs[-1]] | |
| remaining_slots = target_count - 2 | |
| middle_docs = docs[1:-1] | |
| else: | |
| remaining_slots = target_count | |
| middle_docs = docs[:] | |
| if remaining_slots > 0 and middle_docs: | |
| query_words = set(query.lower().split()) | |
| scored_docs = [] | |
| for doc in middle_docs: | |
| content = doc.text.lower() | |
| relevance_score = sum(content.count(word) for word in query_words) | |
| length_bonus = len(doc.text) / 1000 | |
| total_score = relevance_score + length_bonus | |
| scored_docs.append((total_score, doc)) | |
| scored_docs.sort(key=lambda x: x[0], reverse=True) | |
| selected.extend([doc for _, doc in scored_docs[:remaining_slots]]) | |
| return selected[:target_count] | |
| def _create_context_with_priorities(docs: List[Document], max_chars: int) -> str: | |
| """Create context by prioritizing important chunks without summarization.""" | |
| if not docs: | |
| return "" | |
| contexts = [] | |
| total_chars = 0 | |
| priority_docs = [] | |
| if len(docs) >= 2: | |
| priority_docs.append(("BOUNDARY", docs[0])) | |
| priority_docs.append(("BOUNDARY", docs[-1])) | |
| middle_docs = docs[1:-1] if len(docs) > 2 else [] | |
| middle_docs.sort(key=lambda d: len(d.text), reverse=True) | |
| priority_docs.extend([("CONTENT", doc) for doc in middle_docs]) | |
| else: | |
| priority_docs = [("CONTENT", doc) for doc in docs] | |
| for priority_type, doc in priority_docs: | |
| content = doc.text | |
| if total_chars + len(content) > max_chars: | |
| remaining_chars = max_chars - total_chars | |
| if remaining_chars > 200: | |
| truncated = content[:remaining_chars] | |
| last_period = truncated.rfind('.') | |
| if last_period > remaining_chars * 0.8: | |
| truncated = truncated[:last_period + 1] | |
| truncated += "...[truncated]" | |
| contexts.append(f"[{priority_type}] {truncated}") | |
| break | |
| contexts.append(f"[{priority_type}] {content}") | |
| total_chars += len(content) | |
| return "\n\n---\n\n".join(contexts) | |
| def router_node(state: GraphState) -> GraphState: | |
| """ | |
| Router that determines the next step based on available retriever. | |
| """ | |
| print("---NODE: ROUTER---") | |
| return state | |
| def general_chat_node(state: GraphState) -> GraphState: | |
| """ | |
| Generates a response for general conversation using the LLM. | |
| """ | |
| print("---NODE: GENERAL CHAT---") | |
| llm = state["llm"] | |
| query = state["query"] | |
| history = state["history"] | |
| history_context = "\n".join([f"Human: {q}\nAI: {a}" for q, a in history]) | |
| current_time = datetime.datetime.now().strftime("%Y-%m-%d %I:%M %p") | |
| prompt = f"""You are Sarathi, a friendly and knowledgeable AI assistant. | |
| Instructions: | |
| - The current time is {current_time}. | |
| - Be conversational, helpful, and concise. | |
| - If you don't know the answer to a question, admit it honestly. | |
| - Do not make up information. | |
| **Conversation History:** | |
| {history_context} | |
| **Current User Query:** | |
| Human: {query} | |
| AI:""" | |
| try: | |
| response_obj = llm.invoke(prompt) | |
| response_text = getattr(response_obj, "content", str(response_obj)) | |
| except Exception as e: | |
| response_text = f"Model inference failed: {str(e)}" | |
| return {"response": response_text.strip()} | |
| def retrieve_node(state: GraphState) -> GraphState: | |
| """ | |
| Retrieves relevant documents and creates intelligent context without summarization. | |
| """ | |
| print("---NODE: RETRIEVE---") | |
| query = state["query"] | |
| retriever = state["retriever"] | |
| history = state["history"] | |
| context = "" | |
| retrieved_docs = [] | |
| try: | |
| q_len = len(query.split()) | |
| if q_len < 5: | |
| top_k = 5 | |
| elif q_len < 15: | |
| top_k = 8 | |
| else: | |
| top_k = 12 | |
| retrieved_docs = retriever.retrieve(query) | |
| print(f"Retrieved {len(retrieved_docs)} documents") | |
| if retrieved_docs: | |
| max_chunks = min(len(retrieved_docs), top_k) | |
| selected_docs = _select_chunks_by_relevance_and_position( | |
| retrieved_docs, query, max_chunks | |
| ) | |
| print(f"Selected {len(selected_docs)} chunks for context") | |
| MAX_CONTEXT_CHARS = 6000 | |
| doc_context = _create_context_with_priorities(selected_docs, MAX_CONTEXT_CHARS) | |
| if history and len(doc_context) < MAX_CONTEXT_CHARS * 0.8: | |
| history_context = "\n\n".join([f"Human: {q}\nAI: {a}" for q, a in history[-3:]]) # Last 3 exchanges | |
| remaining_chars = MAX_CONTEXT_CHARS - len(doc_context) | |
| if len(history_context) <= remaining_chars: | |
| context = f"{doc_context}\n\n--- Recent Chat History ---\n{history_context}" | |
| else: | |
| context = doc_context | |
| else: | |
| context = doc_context | |
| else: | |
| context = "No relevant content found in the document." | |
| except Exception as e: | |
| print(f"Error in retrieve_node: {e}") | |
| context = f"Retriever failed: {str(e)}" | |
| retrieved_docs = [] | |
| return {"retrieved_docs": retrieved_docs, "context": context} | |
| def generate_rag_node(state: GraphState) -> GraphState: | |
| """ | |
| Generates an answer using the retrieved context from the PDF. | |
| """ | |
| print("---NODE: GENERATE RAG RESPONSE---") | |
| llm = state["llm"] | |
| query = state["query"] | |
| context = state.get("context", "") | |
| if not context.strip(): | |
| return {"response": "I don't have enough context from the document to answer this question."} | |
| prompt = f"""You are Sarathi, an intelligent document analysis assistant. Use the provided context to answer the user's question thoroughly and accurately. | |
| Context from document: | |
| {context} | |
| Question: {query} | |
| Instructions: | |
| - Provide detailed, well-structured answers | |
| - If the question has multiple parts, address each part separately | |
| - Cite specific information from the context when possible | |
| - If the context doesn't fully cover the question, clearly state what's missing | |
| - Use a conversational but professional tone | |
| Answer:""" | |
| try: | |
| response_obj = llm.invoke(prompt) | |
| response_text = getattr(response_obj, "content", str(response_obj)) | |
| except Exception as e: | |
| response_text = f"Model inference failed: {str(e)}" | |
| return {"response": response_text.strip()} | |
| def route_query(state: GraphState) -> str: | |
| """ | |
| Checks if a retriever is available in the state to decide the next step. | |
| """ | |
| if state.get("retriever"): | |
| print("---ROUTING: PDF available, routing to retrieve_node---") | |
| return "retrieve" | |
| else: | |
| print("---ROUTING: No PDF, routing to general_chat_node---") | |
| return "general_chat" | |
| def build_graph(model_type: str = "groq", retriever=None): | |
| """ | |
| Builds the workflow graph with LLM and retriever. | |
| """ | |
| def make_llm(temp: float): | |
| if model_type == "groq": | |
| api_key = os.getenv("OPENROUTER_API_KEY", "").strip() | |
| if not api_key: | |
| raise ValueError("OPENROUTER_API_KEY environment variable not set.") | |
| return ChatOpenAI( | |
| model="x-ai/grok-4-fast", | |
| base_url="https://openrouter.ai/api/v1", | |
| api_key=api_key, | |
| temperature=temp, | |
| ) | |
| elif model_type == "gemini": | |
| api_key = os.getenv("GEMINI_API_KEY", "").strip() | |
| if not api_key: | |
| raise ValueError("GEMINI_API_KEY environment variable not set.") | |
| return ChatGoogleGenerativeAI( | |
| model="gemini-2.0-flash", | |
| api_key=api_key, | |
| temperature=temp, | |
| ) | |
| else: | |
| raise ValueError("Invalid model_type. Choose 'groq' or 'gemini'.") | |
| workflow = StateGraph(GraphState) | |
| workflow.add_node("router", router_node) | |
| workflow.add_node("general_chat", general_chat_node) | |
| workflow.add_node("retrieve", retrieve_node) | |
| workflow.add_node("generate", generate_rag_node) | |
| workflow.set_entry_point("router") | |
| workflow.add_conditional_edges( | |
| "router", | |
| route_query, | |
| { | |
| "retrieve": "retrieve", | |
| "general_chat": "general_chat", | |
| }, | |
| ) | |
| workflow.add_edge("retrieve", "generate") | |
| workflow.add_edge("generate", END) | |
| workflow.add_edge("general_chat", END) | |
| compiled_graph = workflow.compile() | |
| def graph_wrapper(query: str, temperature: float = 0.7, raw_text: str = None, history=None, retriever_override=None): | |
| active_retriever = retriever_override or retriever | |
| llm = make_llm(temperature) | |
| return compiled_graph.invoke({ | |
| "query": query, | |
| "retriever": active_retriever.as_retriever() if active_retriever else None, | |
| "history": history or [], | |
| "llm": llm, | |
| "response": None, | |
| "retrieved_docs": None, | |
| "context": None, | |
| }) | |
| return graph_wrapper |