chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import streamlit as st
|
||||
import qdrant_client
|
||||
import base64
|
||||
import gc
|
||||
import tempfile
|
||||
import uuid
|
||||
import time
|
||||
from IPython.display import Markdown, display
|
||||
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
|
||||
from llama_index.core import StorageContext
|
||||
from llama_index.llms.ollama import Ollama
|
||||
from llama_index.vector_stores.qdrant import QdrantVectorStore
|
||||
from llama_index.embeddings.fastembed import FastEmbedEmbedding
|
||||
from llama_index.core import Settings
|
||||
from workflow import CorrectiveRAGWorkflow
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
# Set up page configuration
|
||||
st.set_page_config(page_title="Corrective RAG Demo", layout="wide")
|
||||
|
||||
# Initialize session state variables
|
||||
if "id" not in st.session_state:
|
||||
st.session_state.id = uuid.uuid4()
|
||||
st.session_state.file_cache = {}
|
||||
|
||||
if "workflow" not in st.session_state:
|
||||
st.session_state.workflow = None
|
||||
|
||||
if "messages" not in st.session_state:
|
||||
st.session_state.messages = []
|
||||
|
||||
if "workflow_logs" not in st.session_state:
|
||||
st.session_state.workflow_logs = []
|
||||
|
||||
session_id = st.session_state.id
|
||||
|
||||
@st.cache_resource
|
||||
def load_llm():
|
||||
llm = Ollama(model="deepseek-r1:7b", request_timeout=120.0)
|
||||
return llm
|
||||
|
||||
def reset_chat():
|
||||
st.session_state.messages = []
|
||||
gc.collect()
|
||||
|
||||
def display_pdf(file):
|
||||
st.markdown("### PDF Preview")
|
||||
base64_pdf = base64.b64encode(file.read()).decode("utf-8")
|
||||
|
||||
# Embedding PDF in HTML
|
||||
pdf_display = f"""<iframe src="data:application/pdf;base64,{base64_pdf}" width="400" height="100%" type="application/pdf"
|
||||
style="height:100vh; width:100%"
|
||||
>
|
||||
</iframe>"""
|
||||
|
||||
# Displaying File
|
||||
st.markdown(pdf_display, unsafe_allow_html=True)
|
||||
|
||||
# Function to initialize the workflow with uploaded documents
|
||||
def initialize_workflow(file_path):
|
||||
with st.spinner("Loading documents and initializing the workflow..."):
|
||||
documents = SimpleDirectoryReader(file_path).load_data()
|
||||
|
||||
client = qdrant_client.QdrantClient(
|
||||
host="localhost",
|
||||
port=6333
|
||||
)
|
||||
|
||||
vector_store = QdrantVectorStore(client=client, collection_name="test")
|
||||
embed_model = FastEmbedEmbedding(model_name="BAAI/bge-large-en-v1.5")
|
||||
Settings.embed_model = embed_model
|
||||
storage_context = StorageContext.from_defaults(vector_store=vector_store)
|
||||
index = VectorStoreIndex.from_documents(
|
||||
documents,
|
||||
storage_context=storage_context,
|
||||
)
|
||||
|
||||
workflow = CorrectiveRAGWorkflow(
|
||||
index=index,
|
||||
linkup_api_key=os.environ["LINKUP_API_KEY"],
|
||||
verbose=True,
|
||||
timeout=60,
|
||||
llm=load_llm()
|
||||
)
|
||||
|
||||
st.session_state.workflow = workflow
|
||||
return workflow
|
||||
|
||||
# Function to run the async workflow
|
||||
async def run_workflow(query):
|
||||
# Capture stdout to get the workflow logs
|
||||
f = io.StringIO()
|
||||
with redirect_stdout(f):
|
||||
result = await st.session_state.workflow.run(query_str=query)
|
||||
|
||||
# Get the captured logs and store them
|
||||
logs = f.getvalue()
|
||||
if logs:
|
||||
st.session_state.workflow_logs.append(logs)
|
||||
|
||||
return result
|
||||
|
||||
# Sidebar for document upload
|
||||
with st.sidebar:
|
||||
# Add Linkup logo and Configuration header in the same line
|
||||
col1, col2 = st.columns([1, 3])
|
||||
with col1:
|
||||
# Add vertical space to align with header
|
||||
st.write("")
|
||||
st.image("./assets/linkup.png", width=65)
|
||||
with col2:
|
||||
st.header("Linkup Configuration")
|
||||
st.write("Deep Web Search")
|
||||
|
||||
# Add hyperlink to get API key
|
||||
st.markdown("[Get your API key](https://app.linkup.so/sign-up)", unsafe_allow_html=True)
|
||||
|
||||
linkup_api_key = st.text_input("Enter your Linkup API Key", type="password")
|
||||
|
||||
# Store API key as environment variable
|
||||
if linkup_api_key:
|
||||
os.environ["LINKUP_API_KEY"] = linkup_api_key
|
||||
st.success("API Key stored successfully!")
|
||||
|
||||
st.header("Add your documents!")
|
||||
|
||||
uploaded_file = st.file_uploader("Choose your `.pdf` file", type="pdf")
|
||||
|
||||
if uploaded_file:
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
file_path = os.path.join(temp_dir, uploaded_file.name)
|
||||
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(uploaded_file.getvalue())
|
||||
|
||||
file_key = f"{session_id}-{uploaded_file.name}"
|
||||
st.write("Indexing your document...")
|
||||
|
||||
if file_key not in st.session_state.get('file_cache', {}):
|
||||
# Initialize workflow with the uploaded document
|
||||
workflow = initialize_workflow(temp_dir)
|
||||
st.session_state.file_cache[file_key] = workflow
|
||||
else:
|
||||
st.session_state.workflow = st.session_state.file_cache[file_key]
|
||||
|
||||
# Inform the user that the file is processed and Display the PDF uploaded
|
||||
st.success("Ready to Chat!")
|
||||
display_pdf(uploaded_file)
|
||||
except Exception as e:
|
||||
st.error(f"An error occurred: {e}")
|
||||
st.stop()
|
||||
|
||||
# Main chat interface
|
||||
col1, col2 = st.columns([6, 1])
|
||||
|
||||
with col1:
|
||||
# Removed the original header
|
||||
st.markdown("<h2 style='color: #0066cc;'>⚙️ Corrective RAG agentic workflow</h2>", unsafe_allow_html=True)
|
||||
# Replace text with image and subtitle styling
|
||||
st.markdown("<div style='display: flex; align-items: center; gap: 10px;'><span style='font-size: 28px; color: #666;'>Powered by LlamaIndex</span><img src='data:image/png;base64,{}' width='50'></div>".format(
|
||||
base64.b64encode(open("./assets/llamaindex.png", "rb").read()).decode()
|
||||
), unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
st.button("Clear ↺", on_click=reset_chat)
|
||||
|
||||
# Display chat messages from history on app rerun
|
||||
for i, message in enumerate(st.session_state.messages):
|
||||
with st.chat_message(message["role"]):
|
||||
st.markdown(message["content"])
|
||||
|
||||
# If this is a user message and there are logs associated with it
|
||||
# Display logs AFTER the user message but BEFORE the next assistant message
|
||||
if message["role"] == "user" and "log_index" in message and i < len(st.session_state.messages) - 1:
|
||||
log_index = message["log_index"]
|
||||
if log_index < len(st.session_state.workflow_logs):
|
||||
with st.expander("View Workflow Execution Logs", expanded=False):
|
||||
st.code(st.session_state.workflow_logs[log_index], language="text")
|
||||
|
||||
# Accept user input
|
||||
if prompt := st.chat_input("Ask a question about your documents..."):
|
||||
# Add user message to chat history with placeholder for log index
|
||||
log_index = len(st.session_state.workflow_logs)
|
||||
st.session_state.messages.append({"role": "user", "content": prompt, "log_index": log_index})
|
||||
|
||||
# Display user message in chat message container
|
||||
with st.chat_message("user"):
|
||||
st.markdown(prompt)
|
||||
|
||||
if st.session_state.workflow:
|
||||
# Run the async workflow
|
||||
result = asyncio.run(run_workflow(prompt))
|
||||
|
||||
# Display the workflow logs in an expandable section OUTSIDE and BEFORE the assistant chat bubble
|
||||
if log_index < len(st.session_state.workflow_logs):
|
||||
with st.expander("View Workflow Execution Logs", expanded=False):
|
||||
st.code(st.session_state.workflow_logs[log_index], language="text")
|
||||
|
||||
# Display assistant response in chat message container
|
||||
with st.chat_message("assistant"):
|
||||
if st.session_state.workflow:
|
||||
message_placeholder = st.empty()
|
||||
full_response = ""
|
||||
|
||||
result = result.response
|
||||
|
||||
# Stream the response word by word
|
||||
words = result.split()
|
||||
for i, word in enumerate(words):
|
||||
full_response += word + " "
|
||||
message_placeholder.markdown(full_response + "▌")
|
||||
# Add a delay between words
|
||||
if i < len(words) - 1: # Don't delay after the last word
|
||||
time.sleep(0.1)
|
||||
|
||||
# Display final response without cursor
|
||||
message_placeholder.markdown(full_response)
|
||||
else:
|
||||
full_response = "Please upload a document first to initialize the workflow."
|
||||
st.markdown(full_response)
|
||||
|
||||
# Add assistant response to chat history
|
||||
st.session_state.messages.append({"role": "assistant", "content": full_response})
|
||||
@@ -0,0 +1,169 @@
|
||||
import os
|
||||
from typing import Optional, Any
|
||||
|
||||
from llama_index.core.workflow import (
|
||||
StartEvent,
|
||||
StopEvent,
|
||||
step,
|
||||
Workflow,
|
||||
Context,
|
||||
)
|
||||
from llama_index.core import SummaryIndex
|
||||
from llama_index.core.schema import Document
|
||||
from llama_index.core.prompts import PromptTemplate
|
||||
from llama_index.core.llms import LLM
|
||||
from llama_index.llms.openai import OpenAI
|
||||
from llama_index.core.base.base_retriever import BaseRetriever
|
||||
from llama_index.tools.linkup_research.base import LinkupToolSpec
|
||||
from typing import List
|
||||
|
||||
from llama_index.core.schema import NodeWithScore
|
||||
from llama_index.core.workflow import (
|
||||
Event,
|
||||
)
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class RetrieveEvent(Event):
|
||||
"""Retrieve event (gets retrieved nodes)."""
|
||||
|
||||
retrieved_nodes: List[NodeWithScore]
|
||||
|
||||
|
||||
|
||||
class WebSearchEvent(Event):
|
||||
"""Web search event."""
|
||||
|
||||
relevant_text: str # not used, just used for pass through
|
||||
|
||||
|
||||
class QueryEvent(Event):
|
||||
"""Query event. Queries given relevant text and search text."""
|
||||
|
||||
relevant_text: str
|
||||
search_text: str
|
||||
|
||||
|
||||
DEFAULT_RELEVANCY_PROMPT_TEMPLATE = PromptTemplate(
|
||||
template="""As a grader, your task is to evaluate the relevance of a document retrieved in response to a user's question.
|
||||
|
||||
Retrieved Document:
|
||||
-------------------
|
||||
{context_str}
|
||||
|
||||
User Question:
|
||||
--------------
|
||||
{query_str}
|
||||
|
||||
Evaluation Criteria:
|
||||
- Consider whether the document contains keywords or topics related to the user's question.
|
||||
- The evaluation should not be overly stringent; the primary objective is to identify and filter out clearly irrelevant retrievals.
|
||||
|
||||
Decision:
|
||||
- Assign a binary score to indicate the document's relevance.
|
||||
- Use 'yes' if the document is relevant to the question, or 'no' if it is not.
|
||||
|
||||
Please provide your binary score ('yes' or 'no') below to indicate the document's relevance to the user question."""
|
||||
)
|
||||
|
||||
DEFAULT_TRANSFORM_QUERY_TEMPLATE = PromptTemplate(
|
||||
template="""Your task is to refine a query to ensure it is highly effective for retrieving relevant search results. \n
|
||||
Analyze the given input to grasp the core semantic intent or meaning. \n
|
||||
Original Query:
|
||||
\n ------- \n
|
||||
{query_str}
|
||||
\n ------- \n
|
||||
Your goal is to rephrase or enhance this query to improve its search performance. Ensure the revised query is concise and directly aligned with the intended search objective. \n
|
||||
Respond with the optimized query only:"""
|
||||
)
|
||||
|
||||
class CorrectiveRAGWorkflow(Workflow):
|
||||
"""Corrective RAG Workflow."""
|
||||
def __init__(
|
||||
self,
|
||||
index,
|
||||
linkup_api_key: str,
|
||||
llm: Optional[LLM] = None,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Init params."""
|
||||
super().__init__(**kwargs)
|
||||
self.index = index
|
||||
self.linkup_tool = LinkupToolSpec(
|
||||
api_key=linkup_api_key,
|
||||
depth="deep", # or "deep"
|
||||
output_type="searchResults", # or "sourcedAnswer" or "structured"
|
||||
)
|
||||
self.llm = llm
|
||||
|
||||
@step
|
||||
async def retrieve(self, ctx: Context, ev: StartEvent) -> Optional[RetrieveEvent]:
|
||||
"""Retrieve the relevant nodes for the query."""
|
||||
query_str = ev.get("query_str")
|
||||
retriever_kwargs = ev.get("retriever_kwargs", {})
|
||||
|
||||
if query_str is None:
|
||||
return None
|
||||
|
||||
retriever: BaseRetriever = self.index.as_retriever(**retriever_kwargs)
|
||||
result = retriever.retrieve(query_str)
|
||||
await ctx.set("retrieved_nodes", result)
|
||||
await ctx.set("query_str", query_str)
|
||||
return RetrieveEvent(retrieved_nodes=result)
|
||||
|
||||
@step
|
||||
async def eval_relevance(
|
||||
self, ctx: Context, ev: RetrieveEvent
|
||||
) -> WebSearchEvent | QueryEvent:
|
||||
"""Evaluate relevancy of retrieved documents with the query."""
|
||||
retrieved_nodes = ev.retrieved_nodes
|
||||
query_str = await ctx.get("query_str")
|
||||
|
||||
relevancy_results = []
|
||||
for node in retrieved_nodes:
|
||||
prompt = DEFAULT_RELEVANCY_PROMPT_TEMPLATE.format(context_str=node.text, query_str=query_str)
|
||||
relevancy = self.llm.complete(prompt)
|
||||
relevancy_results.append(relevancy.text.lower().strip())
|
||||
|
||||
relevant_texts = [
|
||||
retrieved_nodes[i].text
|
||||
for i, result in enumerate(relevancy_results)
|
||||
if result == "yes"
|
||||
]
|
||||
relevant_text = "\n".join(relevant_texts)
|
||||
if "no" in relevancy_results:
|
||||
return WebSearchEvent(relevant_text=relevant_text)
|
||||
else:
|
||||
return QueryEvent(relevant_text=relevant_text, search_text="")
|
||||
|
||||
@step
|
||||
async def web_search(
|
||||
self, ctx: Context, ev: WebSearchEvent
|
||||
) -> QueryEvent:
|
||||
"""Search the transformed query with Tavily API."""
|
||||
# If any document is found irrelevant, transform the query string for better search results.
|
||||
|
||||
query_str = await ctx.get("query_str")
|
||||
|
||||
prompt = DEFAULT_TRANSFORM_QUERY_TEMPLATE.format(query_str=query_str)
|
||||
result = self.llm.complete(prompt)
|
||||
transformed_query_str = result.text
|
||||
# Conduct a search with the transformed query string and collect the results.
|
||||
search_results = self.linkup_tool.search(transformed_query_str).results
|
||||
search_text = "\n".join([result.content for result in search_results])
|
||||
return QueryEvent(relevant_text=ev.relevant_text, search_text=search_text)
|
||||
|
||||
@step
|
||||
async def query_result(self, ctx: Context, ev: QueryEvent) -> StopEvent:
|
||||
"""Get result with relevant text."""
|
||||
relevant_text = ev.relevant_text
|
||||
search_text = ev.search_text
|
||||
query_str = await ctx.get("query_str")
|
||||
|
||||
documents = [Document(text=relevant_text + "\n" + search_text)]
|
||||
index = SummaryIndex.from_documents(documents)
|
||||
query_engine = index.as_query_engine()
|
||||
result = query_engine.query(query_str)
|
||||
return StopEvent(result=result)
|
||||
Reference in New Issue
Block a user