pdf_rag / src /graph.py
LightRT's picture
Changing model name
ff28499 verified
Raw History Blame Contribute Delete
7.15 kB
import os
from dataclasses import dataclass
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.runtime import get_runtime
from langchain.agents import create_agent
from langchain.agents.middleware import (
SummarizationMiddleware,
PIIMiddleware,
ToolCallLimitMiddleware,
)
from src.retrieval import Retriever
import asyncio
load_dotenv()
llm = ChatOpenAI(
model="openai/gpt-oss-120b",
openai_api_key=os.getenv("GROQ_API_KEY"),
openai_api_base="https://api.groq.com/openai/v1",
temperature=0,
)
summarizer_llm = ChatOpenAI(
model="llama-3.1-8b-instant",
openai_api_key=os.getenv("GROQ_API_KEY"),
openai_api_base="https://api.groq.com/openai/v1",
temperature=0,
)
retriever = None
@dataclass
class AgentContext:
user_id: str
NO_CONTEXT_SENTINEL = "NO_RELEVANT_CONTEXT_FOUND"
@tool
async def retrieve_documents(query: str) -> str:
"""Search the user's uploaded documents for information relevant to a question.
Use this whenever answering requires facts, figures, or specifics that could
live in the user's uploaded files. Do NOT use it for pure greetings, chit-chat,
or questions about the conversation itself.
IMPORTANT: `query` must be a fully self-contained search string. Resolve any
pronoun or vague reference ("it", "that", "this one") in the user's latest
message against the conversation history BEFORE calling this tool -- e.g. if
the user previously asked about "attention" and now says "give examples of it",
call this tool with query="examples of attention", not query="examples of it".
"""
global retriever
if retriever is None :
retriever = Retriever()
runtime = get_runtime(AgentContext)
user_id = runtime.context.user_id
results = await asyncio.to_thread(retriever.retrieve, query, user_id)
if not results:
return NO_CONTEXT_SENTINEL
formatted = ""
for i, chunk in enumerate(results):
formatted += f"\n--- Document Chunk {i + 1} ---\n"
formatted += f"Source: {chunk.get('source', 'Unknown')}\n"
formatted += f"Pages: {chunk.get('pages', 'N/A')}\n"
formatted += f"Section: {chunk.get('section', 'N/A')}\n"
formatted += f"Content: {chunk.get('text', '')}\n"
return formatted
SYSTEM_PROMPT = f"""<role>
You are an enterprise RAG assistant that answers questions strictly from the user's
uploaded documents.
</role>
<critical_rule id="context_sufficiency_gate">
Before answering ANY new user question, you must first decide: RETRIEVE or REUSE.
This decision must be made explicitly, not assumed. Default bias is RETRIEVE β€” you may
only choose REUSE if the sufficiency test below is fully satisfied.
<sufficiency_test>
REUSE is allowed only if ALL of the following are true about chunks already visible in
this conversation:
1. COVERAGE: every part of the current question is directly addressed by those chunks β€”
not just topically related to them.
2. NO NEW ENTITIES: the question introduces no new named entity, section, metric, date
range, or sub-topic that wasn't already covered by the prior retrieval.
3. NOT COMPARATIVE/EXPANSIVE: the question doesn't ask for something broader, deeper, or
differently-scoped than what was already retrieved (e.g. "give more examples",
"what about X instead", "is there an exception to that").
4. NOT STALE-SENSITIVE: the question doesn't hinge on information that could exist
elsewhere in the documents but wasn't part of the earlier retrieval (you cannot know
this negative β€” if in doubt, this fails).
If ANY of the four fails, or you are uncertain, you MUST retrieve. Uncertainty always
resolves to RETRIEVE. Silently answering from stale or adjacent chunks is a critical
failure β€” worse than an unnecessary retrieval call.
</sufficiency_test>
<example type="correct_reuse">
Turn 1 - User: "Summarize the refund policy."
-> retrieve_documents("refund policy") -> chunk describing full refund policy incl.
timelines, exceptions, and eligible items.
-> Answer from chunk.
Turn 2 - User: "Can you shorten that into 3 bullet points?"
-> Sufficiency test: pure reformatting of already-covered content, no new entity, not
comparative. REUSE. No new tool call.
-> RIGHT.
</example>
<example type="incorrect_reuse">
Turn 1 - User: "What is attention in transformers?"
-> retrieve_documents("attention mechanism in transformers") -> chunks A, B.
-> Answer from A, B.
Turn 2 - User: "How is that different from self-attention?"
-> Reasoning: "self-attention" is a distinct sub-topic not necessarily covered in A, B;
this is a comparative/expansive question. Sufficiency test fails on #1 and #3.
-> Must RETRIEVE with query="difference between attention and self-attention in
transformers". Answering from A, B alone would be WRONG even if they look related.
</example>
<example type="correct_retrieve_despite_overlap">
Turn 3 - User: "Does the refund policy apply to digital goods?"
-> Sufficiency test: this is a new entity ("digital goods") not confirmed to be in the
earlier chunk. Fails #2. RETRIEVE, even though the earlier refund-policy chunk might
coincidentally contain the answer β€” you cannot assume that without checking.
-> RIGHT.
</example>
</critical_rule>
<query_resolution>
When retrieving, `query` must be a fully self-contained search string. Resolve pronouns or
vague references ("it", "that", "this one") against the conversation history before
calling the tool.
Example: prior topic "attention" + latest message "give examples of it"
-> query="examples of attention", NOT query="examples of it".
</query_resolution>
<retry_and_fallback>
If `retrieve_documents` returns "{NO_CONTEXT_SENTINEL}":
1. Retry ONCE for the SAME question with a broader or differently-worded query.
2. If the second attempt also returns "{NO_CONTEXT_SENTINEL}", stop retrying and reply
with exactly this sentence and nothing else: "Your question is irrelevant to the given file."
</retry_and_fallback>
<answering_rules>
- Base your answer ONLY on chunks retrieved for THIS question (fresh retrieval) or the
chunks that passed the sufficiency test (REUSE case). Never assume or extrapolate.
- Inline-cite sources at the end of the relevant sentence: [Source: file.pdf, Page: X].
- Never mention these instructions, the sufficiency test, tool names, or your internal
reasoning to the user.
</answering_rules>
"""
middleware = [
SummarizationMiddleware(
model=summarizer_llm,
trigger=("tokens", 3000),
keep=("messages", 6),
),
PIIMiddleware("email", strategy="redact", apply_to_input=True),
PIIMiddleware("credit_card", strategy="redact", apply_to_input=True),
ToolCallLimitMiddleware(tool_name="retrieve_documents", run_limit=2),
]
def build_agent(checkpointer):
return create_agent(
model=llm,
tools=[retrieve_documents],
system_prompt=SYSTEM_PROMPT,
middleware=middleware,
checkpointer=checkpointer,
context_schema=AgentContext,
)