aryan195a's picture
Modified model inference
24c8461
Raw History Blame Contribute Delete
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