Spaces:
Sleeping
Sleeping
| Setup | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| LLM | |
| from langchain_ollama import ChatOllama | |
| local_llm = "llama2:7b" | |
| llm = ChatOllama(model=local_llm, temperature=0, base_url="http://localhost:11434") | |
| llm_json_mode = ChatOllama(model=local_llm, temperature=0, format="json", base_url="http://localhost:11434") | |
| Retriever | |
| from langchain_pinecone import PineconeVectorStore | |
| from langchain_huggingface import HuggingFaceEmbeddings | |
| index_name= "tactical-edge-rag-index" | |
| embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2") | |
| vectorstore = PineconeVectorStore(index_name=index_name, embedding=embeddings) | |
| retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) | |
| Router | |
| from langchain_core.prompts import ChatPromptTemplate | |
| system = """You are an expert at routing a user question to a vectorstore or web search. | |
| The vectorstore contains documents related to 2022 Annual report of Mobily and Operation and | |
| Maintenance Manual of Caterpillar. | |
| Use the vectorstore for questions on these topics. For all else, and especially for current events, use web-search. | |
| Return JSON with single key, datasource, that is 'websearch' or 'vectorstore' depending on the question.""" | |
| human_msg = """ User question: {question}""" | |
| route_prompt = ChatPromptTemplate.from_messages( | |
| [ | |
| ("system", system), | |
| ("human", human_msg), | |
| ]) | |
| question_router = route_prompt | llm_json_mode | |
| Documents Grader | |
| system = """You are a grader assessing relevance of a retrieved document to a user question. If the document contains keyword(s) or semantic meaning related to the question, grade it as relevant. | |
| It does not need to be a stringent test. The goal is to filter out erroneous retrievals. | |
| Return JSON with single key, binary_score, that is 'yes' or 'no' score to indicate whether the document contains at least some information that is relevant to the question.""" | |
| human_msg = """ Retrieved documents: {documents} User question: {question}""" | |
| grade_prompt = ChatPromptTemplate.from_messages( | |
| [ | |
| ("system", system), | |
| ("human", human_msg), | |
| ]) | |
| retrieval_grader = grade_prompt | llm_json_mode | |
| Generate Answer | |
| from langchain import hub | |
| from langchain_core.output_parsers import StrOutputParser | |
| prompt = hub.pull("rlm/rag-prompt") #prompt has context and question parameter | |
| Chain | |
| rag_chain = prompt | llm | StrOutputParser() | |
| Hallucination Grader | |
| system = """You are a teacher grading a quiz. You will be given FACTS and a STUDENT ANSWER. Here is the grade criteria to follow: | |
| (1) Ensure the STUDENT ANSWER is grounded in the FACTS. (2) Ensure the STUDENT ANSWER does not contain "hallucinated" information outside the scope of the FACTS. | |
| Score: | |
| A score of yes means that the student's answer meets all of the criteria. This is the highest (best) score. A score of no means that the student's answer does not meet all of the criteria. This is the lowest possible score you can give. | |
| Explain your reasoning in a step-by-step manner to ensure your reasoning and conclusion are correct. Avoid simply stating the correct answer at the outset. | |
| Return JSON with two two keys, binary_score is 'yes' or 'no' score to indicate whether the STUDENT ANSWER is grounded in the FACTS. And a key, explanation, that contains an explanation of the score.""" | |
| human_msg = """ Set of facts: {documents} Student answer: {generation}""" | |
| hallucination_prompt = ChatPromptTemplate.from_messages( | |
| [ | |
| ("system", system), | |
| ("human", human_msg), | |
| ]) | |
| hallucination_grader = hallucination_prompt | llm_json_mode | |
| Answer Grader | |
| system = """"You are a teacher grading a quiz. You will be given a QUESTION and a STUDENT ANSWER. Here is the grade criteria to follow: | |
| (1) The STUDENT ANSWER helps to answer the QUESTION | |
| Score: | |
| A score of yes means that the student's answer meets all of the criteria. This is the highest (best) score. The student can receive a score of yes if the answer contains extra information that is not explicitly asked for in the question. | |
| A score of no means that the student's answer does not meet all of the criteria. This is the lowest possible score you can give. | |
| Explain your reasoning in a step-by-step manner to ensure your reasoning and conclusion are correct. Avoid simply stating the correct answer at the outset. | |
| Return JSON with two two keys, binary_score is 'yes' or 'no' score to indicate whether the STUDENT ANSWER meets the criteria. And a key, explanation, that contains an explanation of the score.""" | |
| human_msg = """ User question: {question} LLM generation: {generation}""" | |
| answer_prompt = ChatPromptTemplate.from_messages( | |
| [ | |
| ("system", system), | |
| ("human", human_msg), | |
| ]) | |
| answer_grader = answer_prompt | llm_json_mode | |
| Question Re-writer | |
| system = """You a question re-writer that converts an input question to a better version that is optimized for vectorstore retrieval. Look at the input and try to reason about the underlying semantic intent / meaning. | |
| Return only the rewritten question without any explanation.""" | |
| human_msg = """ Here is the initial question: {question} Formulate an improved question.""" | |
| re_write_prompt = ChatPromptTemplate.from_messages( | |
| [ | |
| ("system", system), | |
| ("human", human_msg) | |
| ]) | |
| question_rewriter = re_write_prompt | llm | StrOutputParser() | |
| Web Search Tool | |
| from langchain_community.tools.tavily_search import TavilySearchResults | |
| web_search_tool = TavilySearchResults(max_results=3) | |
| Graph State | |
| from typing import List | |
| from typing_extensions import TypedDict | |
| from langchain.schema import Document | |
| import json | |
| class State(TypedDict): | |
| """ | |
| Represents the state of our graph. | |
| Attributes: | |
| question: question | |
| generation: LLM generation | |
| max_retries: Max number of retries for answer generation | |
| loop_step: number of loops for answer generation | |
| documents: list of documents | |
| """ | |
| question: str | |
| generation: str | |
| max_retries: int | |
| loop_step: int | |
| documents: List[Document] | |
| Retriever Node< | |
| def retrieve(state): | |
| """ | |
| Retrieve documents | |
| Args: | |
| state (dict): The current graph state | |
| Returns: | |
| state (dict): New key added to state, documents, that contains retrieved documents | |
| """ | |
| print("---RETRIEVE---") | |
| question = state["question"] | |
| # Retrieval | |
| documents = retriever.invoke(question) | |
| return {"documents": documents} | |
| Generate Node | |
| def generate(state): | |
| """ | |
| Generate answer | |
| Args: | |
| state (dict): The current graph state | |
| Returns: | |
| state (dict): New key added to state, generation, that contains LLM generation | |
| """ | |
| print("---GENERATE---") | |
| question = state["question"] | |
| documents = state["documents"] | |
| loop_step = state.get("loop_step", 0) | |
| # RAG generation | |
| generation = rag_chain.invoke({"context": documents, "question": question}) | |
| return {"generation": generation, "loop_step": loop_step + 1} | |
| Documents Grader Node | |
| If any docs are relevant, we can proceed with generating answer | |
| def grade(state): | |
| """ | |
| Determines whether the retrieved documents are relevant to the question. | |
| Args: | |
| state (dict): The current graph state | |
| Returns: | |
| state (dict): Updates documents key with only filtered relevant documents | |
| """ | |
| print("---CHECK DOCUMENT RELEVANCE TO QUESTION---") | |
| question = state["question"] | |
| documents = state["documents"] | |
| # Score each doc | |
| filtered_docs = [] | |
| for doc in documents: | |
| score = retrieval_grader.invoke( | |
| {"question": question, "documents": doc} | |
| ) | |
| grade = json.loads(score.content)["binary_score"] | |
| if grade.lower() == "yes": | |
| print("---GRADE: DOCUMENT RELEVANT---") | |
| filtered_docs.append(doc) | |
| else: | |
| print("---GRADE: DOCUMENT NOT RELEVANT---") | |
| return {"documents": filtered_docs} | |
| Question Re-writer Node | |
| def rewrite(state): | |
| """ | |
| Transform the query to produce a better question. | |
| Args: | |
| state (dict): The current graph state | |
| Returns: | |
| state (dict): Updates question key with a re-phrased question | |
| """ | |
| print("---Rewrite---") | |
| question = state["question"] | |
| # Re-write question | |
| better_question = question_rewriter.invoke({"question": question}) | |
| return {"question": better_question} | |
| Web Search Node | |
| def search(state): | |
| """ | |
| Web search based on the re-phrased question. | |
| Args: | |
| state (dict): The current graph state | |
| Returns: | |
| state (dict): Updates documents key with appended web results | |
| """ | |
| print("---WEB SEARCH---") | |
| question = state["question"] | |
| # Web search | |
| docs = web_search_tool.invoke(question) | |
| web_results = "\n".join([doc["content"] for doc in docs]) | |
| documents = Document(page_content=web_results) | |
| return {"documents": documents} | |
| Conditional Edge | |
| from typing import Literal | |
| def route_question(state) -> Literal["vectorstore", "web_search"]: | |
| """ | |
| Route question to web search or RAG. | |
| Args: | |
| state (dict): The current graph state | |
| Returns: | |
| str: Next node to call | |
| """ | |
| print("---ROUTE QUESTION---") | |
| question = state["question"] | |
| answer = question_router.invoke({"question": question}) | |
| source = json.loads(answer.content)["datasource"] | |
| if source == "web_search": | |
| print("---ROUTE QUESTION TO WEB SEARCH---") | |
| return "web_search" | |
| elif source == "vectorstore": | |
| print("---ROUTE QUESTION TO RAG---") | |
| return "vectorstore" | |
| def decide_to_generate(state) -> Literal["generate", "rewrite"]: | |
| """ | |
| Determines whether to generate an answer, or rewrite a question. | |
| Args: | |
| state (dict): The current graph state | |
| Returns: | |
| str: Binary decision for next node to call | |
| """ | |
| print("---ASSESS GRADED DOCUMENTS---") | |
| filtered_documents = state["documents"] | |
| if not filtered_documents: | |
| # All documents have been filtered check_relevance | |
| # We will re-generate a new query | |
| print( | |
| "---DECISION: ALL DOCUMENTS ARE NOT RELEVANT TO QUESTION, REWRITE QUESTION---" | |
| ) | |
| return "rewrite" | |
| else: | |
| # We have relevant documents, so generate answer | |
| print("---DECISION: GENERATE---") | |
| return "generate" | |
| def decide_to_answer(state) -> Literal["useful", "not useful", "not supported", "max retries"]: | |
| """ | |
| Determines whether the generation is grounded in the document and answers question. | |
| Args: | |
| state (dict): The current graph state | |
| Returns: | |
| str: Decision for next node to call | |
| """ | |
| print("---CHECK HALLUCINATIONS---") | |
| question = state["question"] | |
| documents = state["documents"] | |
| generation = state["generation"] | |
| max_retries = state.get("max_retries", 3) | |
| hallucination_score = hallucination_grader.invoke( | |
| {"documents": documents, "generation": generation} | |
| ) | |
| hallucination_grade = json.loads(hallucination_score.content)["binary_score"] | |
| # Check hallucination | |
| if hallucination_grade.lower() == "yes": | |
| print("---DECISION: GENERATION IS GROUNDED IN DOCUMENTS---") | |
| # Check question-answering | |
| print("---GRADE GENERATION vs QUESTION---") | |
| answer_score = answer_grader.invoke({"question": question, "generation": generation}) | |
| answer_grade = json.loads(answer_score.content)["binary_score"] | |
| if answer_grade.lower() == "yes": | |
| print("---DECISION: GENERATION ADDRESSES QUESTION---") | |
| return "useful" | |
| elif state["loop_step"] <= max_retries: | |
| print("---DECISION: GENERATION DOES NOT ADDRESS QUESTION---") | |
| return "not useful" | |
| else: | |
| print("---DECISION: MAX RETRIES REACHED---") | |
| return "max retries" | |
| elif state["loop_step"] <= max_retries: | |
| print("---DECISION: GENERATION IS NOT GROUNDED IN DOCUMENTS, RE-TRY---") | |
| return "not supported" | |
| else: | |
| print("---DECISION: MAX RETRIES REACHED---") | |
| return "max retries" | |
| Compile Graph | |
| from IPython.display import Image, display | |
| from langgraph.graph import StateGraph, START, END | |
| workflow = StateGraph(State) | |
| Define the nodes | |
| workflow.add_node("retrieve", retrieve) # retrieve | |
| workflow.add_node("grade", grade) # grade | |
| workflow.add_node("generate", generate) # generatae | |
| workflow.add_node("rewrite", rewrite) # rewrite | |
| workflow.add_node("search", search) # web search | |
| Build graph | |
| workflow.add_conditional_edges( | |
| START, | |
| route_question, | |
| { | |
| "web_search": "search", | |
| "vectorstore": "retrieve", | |
| }, | |
| ) | |
| workflow.add_edge("search", "generate") | |
| workflow.add_edge("retrieve", "grade") | |
| workflow.add_conditional_edges( | |
| "grade", | |
| decide_to_generate, | |
| { | |
| "rewrite": "rewrite", | |
| "generate": "generate", | |
| }, | |
| ) | |
| workflow.add_edge("rewrite", "retrieve") | |
| workflow.add_conditional_edges( | |
| "generate", | |
| decide_to_answer, | |
| { | |
| "useful": END, | |
| "not useful": "rewrite", | |
| "not supported": "generate", | |
| "max retries": END, | |
| }, | |
| ) | |
| Compile | |
| graph = workflow.compile() |