Spaces:
Sleeping
Sleeping
| """ | |
| Streamlit + Groq API - 8็จฎ RAG ็ญ็ฅ PDF ๅ็ญ็ณป็ตฑ | |
| ๅฎ่ฃไพ่ณด: pip install streamlit groq pypdf sentence-transformers numpy faiss-cpu scikit-learn | |
| ๅท่กๆนๅผ: streamlit run rag_streamlit.py | |
| """ | |
| import streamlit as st | |
| from groq import Groq | |
| import numpy as np | |
| from sentence_transformers import SentenceTransformer | |
| import faiss | |
| from pypdf import PdfReader | |
| import re | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| import io | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # RAG ๆ ธๅฟ้กๅฅ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| class MultiStrategyRAG: | |
| def __init__(self, api_key: str): | |
| self.client = Groq(api_key=api_key) | |
| self.embedding_model = SentenceTransformer( | |
| "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" | |
| ) | |
| self.chunks: list[str] = [] | |
| self.embeddings = None | |
| self.index = None | |
| self.tfidf_vectorizer = None | |
| self.tfidf_matrix = None | |
| # โโ ๆไปถ่ผๅ ฅ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def load_pdf(self, pdf_bytes: bytes) -> str: | |
| try: | |
| reader = PdfReader(io.BytesIO(pdf_bytes)) | |
| full_text = "".join( | |
| (page.extract_text() or "") + "\n" for page in reader.pages | |
| ) | |
| self.chunks = self._split_text(full_text, chunk_size=800, overlap=150) | |
| self.embeddings = self.embedding_model.encode( | |
| self.chunks, convert_to_numpy=True | |
| ) | |
| dim = self.embeddings.shape[1] | |
| self.index = faiss.IndexFlatL2(dim) | |
| self.index.add(self.embeddings.astype("float32")) | |
| self.tfidf_vectorizer = TfidfVectorizer(max_features=1000) | |
| self.tfidf_matrix = self.tfidf_vectorizer.fit_transform(self.chunks) | |
| return f"โ ๆๅ่ผๅ ฅ PDF๏ผๅ ฑ {len(reader.pages)} ้ ๏ผๅๅฒ็บ {len(self.chunks)} ๅ็ๆฎต" | |
| except Exception as e: | |
| return f"โ ่ผๅ ฅๅคฑๆ๏ผ{e}" | |
| def _split_text(self, text: str, chunk_size: int, overlap: int) -> list[str]: | |
| chunks, start = [], 0 | |
| while start < len(text): | |
| chunk = re.sub(r"\s+", " ", text[start : start + chunk_size]).strip() | |
| if chunk: | |
| chunks.append(chunk) | |
| start += chunk_size - overlap | |
| return chunks | |
| # โโ 8 ็จฎ RAG ็ญ็ฅ โโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def strategy_1_basic_similarity(self, query: str, top_k: int = 3) -> list[str]: | |
| """็ญ็ฅ1๏ผๅบ็ค่ชๆ็ธไผผๅบฆๆๅฐ""" | |
| vec = self.embedding_model.encode([query]).astype("float32") | |
| _, indices = self.index.search(vec, top_k) | |
| return [self.chunks[i] for i in indices[0]] | |
| def strategy_2_tfidf(self, query: str, top_k: int = 3) -> list[str]: | |
| """็ญ็ฅ2๏ผTF-IDF ้้ต่ฉๆๅฐ""" | |
| qvec = self.tfidf_vectorizer.transform([query]) | |
| scores = (self.tfidf_matrix * qvec.T).toarray().flatten() | |
| top_idx = scores.argsort()[-top_k:][::-1] | |
| return [self.chunks[i] for i in top_idx] | |
| def strategy_3_hybrid(self, query: str, top_k: int = 3) -> list[str]: | |
| """็ญ็ฅ3๏ผๆททๅๆๅฐ๏ผ่ชๆ + TF-IDF๏ผ""" | |
| vec = self.embedding_model.encode([query]).astype("float32") | |
| _, sem_idx = self.index.search(vec, top_k * 2) | |
| qvec = self.tfidf_vectorizer.transform([query]) | |
| tfidf_scores = (self.tfidf_matrix * qvec.T).toarray().flatten() | |
| tfidf_idx = tfidf_scores.argsort()[-top_k * 2 :][::-1] | |
| combined = list(dict.fromkeys(sem_idx[0].tolist() + tfidf_idx.tolist())) | |
| return [self.chunks[i] for i in combined[:top_k]] | |
| def strategy_4_reranking(self, query: str, top_k: int = 3) -> list[str]: | |
| """็ญ็ฅ4๏ผ้ๆฐๆๅบ๏ผLLM ่ฉๅ้ๆ๏ผ""" | |
| candidates = self.strategy_1_basic_similarity(query, top_k=top_k * 2) | |
| reranked = [] | |
| for chunk in candidates: | |
| prompt = ( | |
| f"ๅ้ก๏ผ{query}\n\nๆๆฌ๏ผ{chunk[:200]}...\n\n" | |
| "้ๆฎตๆๆฌ่ๅ้ก็็ธ้ๅบฆ(0-10)๏ผ" | |
| ) | |
| try: | |
| resp = self.client.chat.completions.create( | |
| model="llama-3.1-8b-instant", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=10, | |
| temperature=0, | |
| ) | |
| raw = resp.choices[0].message.content.strip() | |
| nums = re.findall(r"\d+", raw) | |
| score = float(nums[0]) if nums else 0 | |
| except Exception: | |
| score = 0 | |
| reranked.append((chunk, score)) | |
| reranked.sort(key=lambda x: x[1], reverse=True) | |
| return [c for c, _ in reranked[:top_k]] | |
| def strategy_5_multi_query(self, query: str, top_k: int = 3) -> list[str]: | |
| """็ญ็ฅ5๏ผๅคๆฅ่ฉขๆดๅฑ""" | |
| prompt = f"ๅฐไปฅไธๅ้กๆนๅฏซๆ3ๅ็ธ้ไฝไธๅ่งๅบฆ็ๅ้ก๏ผ็จๆ่กๅ้๏ผ\n{query}" | |
| try: | |
| resp = self.client.chat.completions.create( | |
| model="llama-3.1-8b-instant", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=200, | |
| temperature=0.7, | |
| ) | |
| queries = [query] + resp.choices[0].message.content.strip().split("\n")[:3] | |
| except Exception: | |
| queries = [query] | |
| all_chunks: list[str] = [] | |
| for q in queries: | |
| all_chunks.extend(self.strategy_1_basic_similarity(q, top_k=2)) | |
| return list(dict.fromkeys(all_chunks))[:top_k] | |
| def strategy_6_contextual_compression(self, query: str, top_k: int = 3) -> list[str]: | |
| """็ญ็ฅ6๏ผไธไธๆๅฃ็ธฎ""" | |
| chunks = self.strategy_1_basic_similarity(query, top_k=top_k) | |
| compressed = [] | |
| for chunk in chunks: | |
| prompt = ( | |
| f"ๅพไปฅไธๆๆฌไธญๆๅ่ๅ้กใ{query}ใๆ็ธ้็1-2ๅฅ่ฉฑ๏ผ\n\n{chunk}" | |
| ) | |
| try: | |
| resp = self.client.chat.completions.create( | |
| model="llama-3.1-8b-instant", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=150, | |
| temperature=0, | |
| ) | |
| compressed.append(resp.choices[0].message.content.strip()) | |
| except Exception: | |
| compressed.append(chunk[:300]) | |
| return compressed | |
| def strategy_7_parent_child(self, query: str, top_k: int = 3) -> list[str]: | |
| """็ญ็ฅ7๏ผ็ถๅญๆๆช๏ผๅฐ็ๆฎตๅฐๆๅคงไธไธๆ๏ผ""" | |
| small_chunks = self._split_text(" ".join(self.chunks), chunk_size=300, overlap=50) | |
| small_emb = self.embedding_model.encode(small_chunks, convert_to_numpy=True) | |
| small_index = faiss.IndexFlatL2(small_emb.shape[1]) | |
| small_index.add(small_emb.astype("float32")) | |
| vec = self.embedding_model.encode([query]).astype("float32") | |
| _, indices = small_index.search(vec, top_k) | |
| results = [] | |
| for idx in indices[0]: | |
| for big in self.chunks: | |
| if small_chunks[idx] in big: | |
| results.append(big) | |
| break | |
| return list(dict.fromkeys(results))[:top_k] | |
| def strategy_8_hypothetical_answer(self, query: str, top_k: int = 3) -> list[str]: | |
| """็ญ็ฅ8๏ผๅ่จญๆง็ญๆก๏ผHyDE๏ผ""" | |
| prompt = f"่ซๅฐไปฅไธๅ้ก็ตฆๅบไธๅๅ่จญๆง็็ญๆก๏ผๅณไฝฟไธ็ขบๅฎ๏ผ๏ผ\n{query}" | |
| try: | |
| resp = self.client.chat.completions.create( | |
| model="llama-3.1-8b-instant", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=200, | |
| temperature=0.7, | |
| ) | |
| hypo = resp.choices[0].message.content | |
| except Exception: | |
| hypo = query | |
| vec = self.embedding_model.encode([hypo]).astype("float32") | |
| _, indices = self.index.search(vec, top_k) | |
| return [self.chunks[i] for i in indices[0]] | |
| # โโ ็ญๆก็ๆ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def generate_answer( | |
| self, query: str, strategy: str, top_k: int = 3 | |
| ) -> tuple[str, str]: | |
| if not self.chunks: | |
| return "โ ่ซๅ ไธๅณ PDF ๆชๆก๏ผ", "" | |
| strategy_map = { | |
| "1. ๅบ็ค่ชๆๆๅฐ": self.strategy_1_basic_similarity, | |
| "2. TF-IDF ้้ต่ฉ": self.strategy_2_tfidf, | |
| "3. ๆททๅๆๅฐ": self.strategy_3_hybrid, | |
| "4. ้ๆฐๆๅบ": self.strategy_4_reranking, | |
| "5. ๅคๆฅ่ฉขๆดๅฑ": self.strategy_5_multi_query, | |
| "6. ไธไธๆๅฃ็ธฎ": self.strategy_6_contextual_compression, | |
| "7. ็ถๅญๆๆช": self.strategy_7_parent_child, | |
| "8. ๅ่จญๆง็ญๆก (HyDE)": self.strategy_8_hypothetical_answer, | |
| } | |
| retrieval_fn = strategy_map.get(strategy, self.strategy_1_basic_similarity) | |
| relevant_chunks = retrieval_fn(query, top_k) | |
| context = "\n\n---\n\n".join(relevant_chunks) | |
| prompt = ( | |
| "่ซๆ นๆไปฅไธไธไธๆๅ็ญๅ้กใๅฆๆไธไธๆไธญๆฒๆ็ธ้่ณ่จ๏ผ่ซ่ชชๆ็กๆณๅ็ญใ\n\n" | |
| f"ไธไธๆ๏ผ\n{context}\n\nๅ้ก๏ผ{query}\n\n่ซ็จ็น้ซไธญๆ่ฉณ็ดฐๅ็ญ๏ผ" | |
| ) | |
| try: | |
| resp = self.client.chat.completions.create( | |
| model="llama-3.1-8b-instant", | |
| messages=[ | |
| {"role": "system", "content": "ไฝ ๆฏๅฐๆฅญ็ๆไปถๅๆๅฉๆใ"}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| max_tokens=1024, | |
| temperature=0.3, | |
| ) | |
| answer = resp.choices[0].message.content | |
| source_info = ( | |
| f"๐ ไฝฟ็จ็ญ็ฅ๏ผ{strategy}\n" | |
| f"๐ ๆชข็ดข็ๆฎตๆธ๏ผ{len(relevant_chunks)}\n\n" | |
| + "=" * 50 + "\n็ธ้ๆๆฌ็ๆฎต๏ผ\n" + "=" * 50 | |
| + f"\n\n{context}" | |
| ) | |
| return answer, source_info | |
| except Exception as e: | |
| return f"โ ็ๆ็ญๆกๅคฑๆ๏ผ{e}", "" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Streamlit ้ ้ข | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| STRATEGY_DESCRIPTIONS = { | |
| "1. ๅบ็ค่ชๆๆๅฐ": "ไฝฟ็จๅ้้คๅผฆ็ธไผผๅบฆ๏ผๅฐๅ้ก่ๆไปถ็ๆฎตๅฐๆฏ๏ผๆพๅบ่ชๆๆๆฅ่ฟ็ๆฎต่ฝใ", | |
| "2. TF-IDF ้้ต่ฉ": "ๅบๆผ่ฉ้ ป-้ๆๆช้ ป็๏ผTF-IDF๏ผ็ตฑ่จ๏ผ้ฉๅ็ฒพ็ขบ้้ตๅญๅน้ ๅ ดๆฏใ", | |
| "3. ๆททๅๆๅฐ": "ๅๆๅท่ก่ชๆๆๅฐ่ TF-IDF๏ผ่ๅๅ ฉ่ ็ตๆ๏ผๅ ผ้กง่ชๆ่้้ตๅญใ", | |
| "4. ้ๆฐๆๅบ": "ๅ ็จ่ชๆๆๅฐๅฌๅๅ้ธ็ๆฎต๏ผๅ่ฎ LLM ็บๆฏๆฎตๆๅ้ๆฐๆๅบใ", | |
| "5. ๅคๆฅ่ฉขๆดๅฑ": "่ฎ LLM ๅฐๅ้กๆนๅฏซ็บๅคๅ่งๅบฆ็ๅ้ก๏ผๅๅๅฅๆๅฐๅไฝต็ตๆใ", | |
| "6. ไธไธๆๅฃ็ธฎ": "ๅ ่ชๆๆๅฐ๏ผๅ่ซ LLM ๅพๆฏๆฎตไธญ่ๅ่ๅ้กๆ็ธ้็ 1-2 ๅฅใ", | |
| "7. ็ถๅญๆๆช": "ไปฅๆดๅฐ็ๅญ็ๆฎตๆๅฐ๏ผไฝๅๅณๅ ๅซ่ฉฒๅญ็ๆฎต็ๅๅงๅคงๆฎตๆๆฌใ", | |
| "8. ๅ่จญๆง็ญๆก (HyDE)": "ๅ ่ฎ LLM ็ๆไธๅๅ่จญ็ญๆก๏ผ็จๆญคๅ่จญ็ญๆก็ๅ้ๆๅฐๆไปถใ", | |
| } | |
| EXAMPLE_QUESTIONS = [ | |
| "้ไปฝๆไปถ็ไธป่ฆๅ งๅฎนๆฏไป้บผ๏ผ", | |
| "ๆไปถไธญๆๅฐๅชไบ้่ฆๆฆๅฟต๏ผ", | |
| "ๆๅชไบ้้ตๆธๆๆ็ตฑ่จ่ณๆ๏ผ", | |
| "ๆไปถ็็ต่ซๆฏไป้บผ๏ผ", | |
| ] | |
| def get_rag(api_key: str) -> MultiStrategyRAG: | |
| """ๅจ session_state ไธญๅฟซๅ RAG ๅฏฆไพ๏ผ้ฟๅ ้่ค่ผๅ ฅๆจกๅ๏ผ""" | |
| if "rag" not in st.session_state: | |
| st.session_state.rag = MultiStrategyRAG(api_key=api_key) | |
| return st.session_state.rag | |
| def main(): | |
| st.set_page_config( | |
| page_title="ๅค็ญ็ฅ RAG PDF ๅ็ญ็ณป็ตฑ", | |
| page_icon="๐ค", | |
| layout="wide", | |
| ) | |
| # โโ ๆจ้ก โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| st.title("๐ค ๅค็ญ็ฅ RAG PDF ๅ็ญ็ณป็ตฑ") | |
| st.markdown( | |
| "ๆก็จ **8 ็จฎไธๅ็ RAG ็ญ็ฅ**๏ผ็บๆจ็ PDF ๆไปถๆไพๆบ่ฝๅ็ญๆๅ๏ผ" | |
| ) | |
| st.divider() | |
| # โโ API ้้ฐ๏ผๅด้ๆฌ๏ผ โโโโโโโโโโโโโโโโโโโโ | |
| with st.sidebar: | |
| st.header("โ๏ธ ่จญๅฎ") | |
| api_key = st.text_input( | |
| "Groq API Key", | |
| value="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", | |
| type="password", | |
| help="่ซ่ผธๅ ฅๆจ็ Groq API ้้ฐ", | |
| ) | |
| st.divider() | |
| st.subheader("๐ ็ญ็ฅ่ชชๆ") | |
| for name, desc in STRATEGY_DESCRIPTIONS.items(): | |
| with st.expander(name): | |
| st.write(desc) | |
| # โโ ๅๅงๅ RAG โโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if not api_key: | |
| st.warning("โ ๏ธ ่ซๅจๅด้ๆฌ่ผธๅ ฅ Groq API Key ๅพ็นผ็บใ") | |
| st.stop() | |
| rag = get_rag(api_key) | |
| # โโ ๆญฅ้ฉ 1๏ผไธๅณ PDF โโโโโโโโโโโโโโโโโโโโโโ | |
| st.subheader("๐ค ๆญฅ้ฉ 1๏ผไธๅณ PDF") | |
| uploaded_file = st.file_uploader("้ธๆ PDF ๆชๆก", type=["pdf"]) | |
| if uploaded_file is not None: | |
| # ๅชๅจๆชๅๆน่ฎๆ้ๆฐ่ผๅ ฅ | |
| if st.session_state.get("loaded_filename") != uploaded_file.name: | |
| with st.spinner("๐ ๆญฃๅจ่ผๅ ฅไธฆๅปบ็ซ็ดขๅผ๏ผ่ซ็จๅโฆ"): | |
| status = rag.load_pdf(uploaded_file.read()) | |
| st.session_state["loaded_filename"] = uploaded_file.name | |
| st.session_state["load_status"] = status | |
| status_msg = st.session_state.get("load_status", "") | |
| if "โ " in status_msg: | |
| st.success(status_msg) | |
| else: | |
| st.error(status_msg) | |
| st.divider() | |
| # โโ ๆญฅ้ฉ 2 & 3๏ผ็ญ็ฅ้ธๆ + ๆๅ โโโโโโโโโโ | |
| col_left, col_right = st.columns([1, 2]) | |
| with col_left: | |
| st.subheader("โ๏ธ ๆญฅ้ฉ 2๏ผRAG ็ญ็ฅ") | |
| strategy = st.selectbox( | |
| "้ธๆ็ญ็ฅ", | |
| list(STRATEGY_DESCRIPTIONS.keys()), | |
| index=0, | |
| label_visibility="collapsed", | |
| ) | |
| st.caption(STRATEGY_DESCRIPTIONS[strategy]) | |
| top_k = st.slider( | |
| "ๆชข็ดข็ๆฎตๆธ้๏ผTop-K๏ผ", | |
| min_value=1, | |
| max_value=10, | |
| value=3, | |
| step=1, | |
| ) | |
| with col_right: | |
| st.subheader("๐ฌ ๆญฅ้ฉ 3๏ผๆๅ") | |
| # ็ฏไพๅ้กๅฟซ้ๅกซๅ ฅ | |
| st.caption("๐ก ๅฟซ้ๅกซๅ ฅ็ฏไพๅ้ก๏ผ") | |
| example_cols = st.columns(len(EXAMPLE_QUESTIONS)) | |
| for col, q in zip(example_cols, EXAMPLE_QUESTIONS): | |
| if col.button(q[:10] + "โฆ", key=f"ex_{q}", use_container_width=True, help=q): | |
| st.session_state["question_input"] = q | |
| question = st.text_area( | |
| "่ผธๅ ฅๆจ็ๅ้ก", | |
| value=st.session_state.get("question_input", ""), | |
| placeholder="ไพๅฆ๏ผ้ไปฝๆไปถ็ไธป่ฆๅ งๅฎนๆฏไป้บผ๏ผ", | |
| height=100, | |
| key="question_input", | |
| ) | |
| ask_clicked = st.button( | |
| "๐ ๆๅ", | |
| type="primary", | |
| use_container_width=True, | |
| disabled=(not rag.chunks), | |
| ) | |
| if not rag.chunks: | |
| st.info("โน๏ธ ่ซๅ ไธๅณ PDF ๆไปถ๏ผๆ่ฝ้ๅงๆๅใ") | |
| # โโ ็ญๆก่ผธๅบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if ask_clicked: | |
| if not question.strip(): | |
| st.warning("โ ๏ธ ่ซ่ผธๅ ฅๅ้กๅพๅ้ๅบใ") | |
| else: | |
| with st.spinner("๐ง ๆญฃๅจๆ่ไธญ๏ผ่ซ็จๅโฆ"): | |
| answer, source_info = rag.generate_answer(question, strategy, top_k) | |
| st.divider() | |
| st.subheader("๐ก AI ๅ็ญ") | |
| if answer.startswith("โ"): | |
| st.error(answer) | |
| else: | |
| st.markdown(answer) | |
| if source_info: | |
| with st.expander("๐ ๆฅ็ๆชข็ดขๅฐ็ๆๆฌ็ๆฎต"): | |
| st.text(source_info) | |
| if __name__ == "__main__": | |
| main() | |