jackf857 commited on
Commit
caddfc2
Β·
verified Β·
1 Parent(s): 9e36b7d

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +22 -0
  2. agentic_rag_streamlit.py +317 -0
  3. requirements.txt +0 -0
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official lightweight Python image
2
+ FROM python:3.12.9-slim
3
+
4
+ # Set environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+
8
+ # Set work directory
9
+ WORKDIR /app
10
+
11
+ # Install dependencies
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy the rest of your code
16
+ COPY . .
17
+
18
+ # Expose the port Streamlit uses
19
+ EXPOSE 8501
20
+
21
+ # Command to run the Streamlit app
22
+ CMD ["streamlit", "run", "agentic_rag_streamlit.py", "--server.port=8501", "--server.address=0.0.0.0"]
agentic_rag_streamlit.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import basics
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ # import streamlit
6
+ import streamlit as st
7
+ from PIL import Image
8
+ import json
9
+
10
+ # import langchain
11
+ from langchain.agents import AgentExecutor
12
+ from langchain_openai import ChatOpenAI
13
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
14
+ from langchain.chat_models import init_chat_model
15
+ from langchain_core.messages import SystemMessage, AIMessage, HumanMessage
16
+ from langchain.agents import create_tool_calling_agent
17
+ from langchain import hub
18
+ from langchain_core.prompts import PromptTemplate
19
+ from langchain_community.vectorstores import SupabaseVectorStore
20
+ from langchain_openai import OpenAIEmbeddings
21
+ from langchain_core.tools import tool
22
+ from langchain.callbacks.tracers.langchain import LangChainTracer
23
+ from langchain.callbacks.tracers.schemas import Run
24
+ from langchain_community.document_loaders import PyPDFLoader, TextLoader
25
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
26
+ from langchain_community.document_loaders import UnstructuredMarkdownLoader
27
+
28
+ # import supabase db
29
+ from supabase.client import Client, create_client
30
+
31
+ # load environment variables
32
+ load_dotenv()
33
+
34
+ # initiating supabase
35
+ supabase_url = os.environ.get("SUPABASE_URL")
36
+ supabase_key = os.environ.get("SUPABASE_SERVICE_KEY")
37
+ supabase: Client = create_client(supabase_url, supabase_key)
38
+
39
+ # initiating embeddings model
40
+ embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
41
+
42
+ # initiating vector store
43
+ vector_store = SupabaseVectorStore(
44
+ embedding=embeddings,
45
+ client=supabase,
46
+ table_name="documents",
47
+ query_name="match_documents",
48
+ )
49
+
50
+ # initiating llm
51
+ llm = ChatOpenAI(model="gpt-4.1",temperature=1)
52
+
53
+ # pulling prompt from hub
54
+ prompt = hub.pull("jackfengrag/myrag")
55
+
56
+
57
+ # Store for captured documents
58
+ if "retrieved_documents" not in st.session_state:
59
+ st.session_state.retrieved_documents = {}
60
+
61
+ # Custom callback handler to capture retrieved documents
62
+ class DocumentCaptureHandler:
63
+ def __init__(self):
64
+ self.captured_docs = []
65
+
66
+ def capture_docs(self, docs):
67
+ self.captured_docs.extend(docs)
68
+
69
+ document_handler = DocumentCaptureHandler()
70
+
71
+ # creating the retriever tool
72
+ @tool(response_format="content_and_artifact")
73
+ def retrieve(query: str):
74
+ """Retrieve information related to a query."""
75
+ retrieved_docs = vector_store.similarity_search(query, k=5)
76
+
77
+ # Capture the documents for display
78
+ document_handler.capture_docs(retrieved_docs)
79
+
80
+ serialized = "\n\n".join(
81
+ (f"Source: {doc.metadata}\n" f"Content: {doc.page_content}")
82
+ for doc in retrieved_docs
83
+ )
84
+ return serialized, retrieved_docs
85
+
86
+ # combining all tools
87
+ tools = [retrieve]
88
+
89
+ # initiating the agent
90
+ agent = create_tool_calling_agent(llm, tools, prompt)
91
+
92
+ # create the agent executor
93
+ agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
94
+
95
+ # Function to format document for display
96
+ def format_source_document(doc, index):
97
+ source = doc.metadata.get("source", "Unknown source")
98
+ # Extract filename from source path
99
+ if isinstance(source, str) and "/" in source:
100
+ source = source.split("/")[-1]
101
+
102
+ # Format source document for display with everything in black color
103
+ return f"""
104
+ <div style="padding: 10px; margin-bottom: 10px; border-radius: 5px; background-color: #f5f5f5; color: #000000;">
105
+ <p><strong style="color: #000000;">Source {index+1}: {source}</strong></p>
106
+ <p style="font-size: 0.9em; color: #000000;">{doc.page_content[:300]}...</p>
107
+ </div>
108
+ """
109
+
110
+ # initiating streamlit app with a new logo
111
+ st.set_page_config(
112
+ page_title="LangChain RAG Assistant",
113
+ page_icon="🧠",
114
+ layout="wide",
115
+ initial_sidebar_state="expanded"
116
+ )
117
+
118
+ # Custom styling for the app
119
+ st.markdown("""
120
+ <style>
121
+ .main-header {
122
+ font-size: 2.5rem;
123
+ color: #4CAF50;
124
+ text-align: center;
125
+ margin-bottom: 1rem;
126
+ }
127
+ .subheader {
128
+ font-size: 1.2rem;
129
+ color: #555;
130
+ text-align: center;
131
+ margin-bottom: 2rem;
132
+ }
133
+ .source-title {
134
+ font-weight: bold;
135
+ margin-bottom: 5px;
136
+ }
137
+ .source-content {
138
+ font-size: 0.9em;
139
+ color: #333;
140
+ padding-left: 10px;
141
+ border-left: 2px solid #4CAF50;
142
+ }
143
+ </style>
144
+ """, unsafe_allow_html=True)
145
+
146
+ # Create sidebar for settings
147
+ with st.sidebar:
148
+ st.markdown("## Settings")
149
+ show_sources = st.checkbox("Show source documents", value=True)
150
+ st.markdown("---")
151
+ st.markdown("## About")
152
+ st.markdown("This assistant uses Agentic RAG (Retrieval-Augmented Generation) to provide information about LangChain by default, With any technical document you upload.")
153
+ st.markdown("It retrieves relevant documents from a vector database and uses them to generate responses.")
154
+
155
+ # Display custom header with new logo
156
+ st.markdown("<h1 class='main-header'>🧠 Technical Document Knowledge Assistant</h1>", unsafe_allow_html=True)
157
+ st.markdown("<p class='subheader'>Powered by Agentic RAG Technology</p>", unsafe_allow_html=True)
158
+
159
+ # Add a horizontal line
160
+ st.markdown("---")
161
+
162
+ # initialize chat history
163
+ if "messages" not in st.session_state:
164
+ st.session_state.messages = []
165
+
166
+ # initialize sources history
167
+ if "sources_history" not in st.session_state:
168
+ st.session_state.sources_history = []
169
+
170
+ # display chat messages from history on app rerun
171
+ for i, message in enumerate(st.session_state.messages):
172
+ if isinstance(message, HumanMessage):
173
+ with st.chat_message("user"):
174
+ st.markdown(message.content)
175
+ elif isinstance(message, AIMessage):
176
+ with st.chat_message("assistant"):
177
+ st.markdown(message.content)
178
+
179
+ # Display sources if available and option is enabled
180
+ if show_sources and i//2 < len(st.session_state.sources_history):
181
+ sources = st.session_state.sources_history[i//2]
182
+ if sources:
183
+ with st.expander("πŸ“š View Source Documents", expanded=False):
184
+ for j, doc in enumerate(sources):
185
+ st.markdown(format_source_document(doc, j), unsafe_allow_html=True)
186
+
187
+
188
+ # --- Document Upload and Ingestion UI ---
189
+ st.markdown("## πŸ“„ Upload and Ingest Documents")
190
+ uploaded_files = st.file_uploader(
191
+ "Upload PDF, TXT, or Markdown (MD) files to ingest into the knowledge base:",
192
+ type=["pdf", "txt", "md"],
193
+ accept_multiple_files=True,
194
+ key="file_uploader"
195
+ )
196
+
197
+ if uploaded_files:
198
+ for uploaded_file in uploaded_files:
199
+ file_name = uploaded_file.name
200
+ file_path = os.path.join("documents", file_name)
201
+ # Save uploaded file to disk
202
+ with open(file_path, "wb") as f:
203
+ f.write(uploaded_file.getbuffer())
204
+ # Load and split document
205
+ if file_name.lower().endswith(".pdf"):
206
+ loader = PyPDFLoader(file_path)
207
+ elif file_name.lower().endswith(".txt"):
208
+ loader = TextLoader(file_path)
209
+ elif file_name.lower().endswith(".md"):
210
+ loader = UnstructuredMarkdownLoader(file_path)
211
+ else:
212
+ st.warning(f"Unsupported file type: {file_name}")
213
+ continue
214
+ documents = loader.load()
215
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
216
+ docs = text_splitter.split_documents(documents)
217
+ # Ingest into vector store in batches of 10
218
+ batch_size = 3
219
+ for doc in docs:
220
+ doc.page_content = doc.page_content.replace('\u0000', '')
221
+ cleaned_docs = docs
222
+ num_batches = (len(cleaned_docs) + batch_size - 1) // batch_size
223
+ for batch_idx in range(num_batches):
224
+ batch_docs = cleaned_docs[batch_idx*batch_size:(batch_idx+1)*batch_size]
225
+ retry_count = 0
226
+ while retry_count < 3:
227
+ try:
228
+ SupabaseVectorStore.from_documents(
229
+ batch_docs,
230
+ embeddings,
231
+ client=supabase,
232
+ table_name="documents",
233
+ query_name="rag_query",
234
+ chunk_size=100,
235
+ )
236
+ if retry_count > 0:
237
+ st.info(f"Batch {batch_idx+1} for {file_name} succeeded after {retry_count} retries.")
238
+ break # Success, exit retry loop
239
+ except Exception as e:
240
+ error_message = str(e)
241
+ # Retry on SSL errors
242
+ if any(kw in error_message.lower() for kw in ["ssl", "tls", "certificate", "handshake", "bad record"]):
243
+ retry_count += 1
244
+ st.warning(f"SSL error on batch {batch_idx+1} for {file_name}, retrying ({retry_count}/3)...")
245
+ time.sleep(1)
246
+ continue
247
+ # Skip on duplicate errors
248
+ if any(kw in error_message.lower() for kw in ["duplicate", "already exists", "unique constraint", "unique violation", "conflict"]):
249
+ st.warning(f"Duplicate detected in batch {batch_idx+1} for {file_name}, skipping batch: {error_message}")
250
+ break
251
+ # Other errors: show and skip batch
252
+ st.error(f"Error in batch {batch_idx+1} for {file_name}: {error_message}")
253
+ break
254
+ else:
255
+ st.error(f"Failed to ingest batch {batch_idx+1} for {file_name} after 3 SSL retries.")
256
+ st.success(f"Ingested {file_name} in {num_batches} batches!")
257
+
258
+ # create the bar where we can type messages
259
+ user_question = st.chat_input("Ask me anything about LangChain...")
260
+
261
+ # did the user submit a prompt?
262
+ if user_question:
263
+ # Reset document handler for new query
264
+ document_handler.captured_docs = []
265
+
266
+ # add the message from the user (prompt) to the screen with streamlit
267
+ with st.chat_message("user"):
268
+ st.markdown(user_question)
269
+ st.session_state.messages.append(HumanMessage(user_question))
270
+
271
+ # Show spinner while agent is generating a response
272
+ with st.spinner("Thinking... Generating response..."):
273
+ # invoking the agent
274
+ result = agent_executor.invoke({"input": user_question, "chat_history":st.session_state.messages})
275
+ ai_message = result["output"]
276
+
277
+ # Store the captured documents for this response
278
+ st.session_state.sources_history.append(document_handler.captured_docs)
279
+
280
+ # adding the response from the llm to the screen (and chat)
281
+ with st.chat_message("assistant"):
282
+ import re
283
+ def render_markdown_with_codeblocks(text):
284
+ code_block_pattern = r"```([\w\+\-]*)\n([\s\S]*?)```"
285
+ related_code_pattern = r"<related_code>([\s\S]*?)</related_code>"
286
+ last_end = 0
287
+ # Find all code blocks (triple backtick and related_code) in order
288
+ matches = []
289
+ for m in re.finditer(code_block_pattern, text):
290
+ matches.append((m.start(), m.end(), 'backtick', m))
291
+ for m in re.finditer(related_code_pattern, text):
292
+ matches.append((m.start(), m.end(), 'related_code', m))
293
+ matches.sort() # sort by start position
294
+ for match in matches:
295
+ start, end, kind, m = match
296
+ if start > last_end:
297
+ st.markdown(text[last_end:start])
298
+ if kind == 'backtick':
299
+ code_lang = m.group(1) or None
300
+ code_content = m.group(2)
301
+ st.code(code_content, language=code_lang)
302
+ elif kind == 'related_code':
303
+ code_content = m.group(1)
304
+ st.code(code_content)
305
+ last_end = end
306
+ if last_end < len(text):
307
+ st.markdown(text[last_end:])
308
+
309
+ render_markdown_with_codeblocks(ai_message)
310
+ st.session_state.messages.append(AIMessage(ai_message))
311
+
312
+ # Display sources if option is enabled
313
+ if show_sources and document_handler.captured_docs:
314
+ with st.expander("πŸ“š View Source Documents", expanded=True):
315
+ for i, doc in enumerate(document_handler.captured_docs):
316
+ st.markdown(format_source_document(doc, i), unsafe_allow_html=True)
317
+
requirements.txt ADDED
Binary file (3.58 kB). View file