Spaces:
Running
Running
| """ | |
| # Simple RAG with Filtering + Query Transformations (v6) | |
| v5 ๋๋น ๋ณ๊ฒฝ: | |
| - extract_genre_node_v2 ๋์ : ๋๋ถ๋ฅ 2๊ฐ, ์ค๋ถ๋ฅ 3๊ฐ๋ฅผ "๋ฐ๋์" ์ ํํ๋๋ก ๊ฐ์ | |
| (v5์ "์ต๋ N๊ฐ" โ "๋ฐ๋์ N๊ฐ"๋ก ํ๋กฌํํธ ๋ณ๊ฒฝ) | |
| ์ฅ๋ฅด ํํฐ๊ฐ ํญ์ ์ค๋ถ๋ฅ ๋ ๋ฒจ์์ ํ์ ๋๋ฏ๋ก fallback ๋ถ๊ธฐ ๋ฐ์ ๋น๋ ๊ฐ์ | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import pandas as pd | |
| from dotenv import load_dotenv | |
| from langchain_core.prompts import ChatPromptTemplate | |
| from langchain_openai import ChatOpenAI | |
| from qdrant_client.models import Filter, FieldCondition, MatchAny | |
| from app.config import QDRANT_COLLECTION_NAME | |
| from app.db.qdrant import QdrantDB | |
| from app.embedding.embedder import LocalEmbedder | |
| from app.reranking.reranker import LocalReranker | |
| from app.state.state_v3 import GraphState | |
| load_dotenv() | |
| # โโ Query Transformation ํ๋๊ทธ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| USE_STEP_BACK = True | |
| USE_REWRITE = True | |
| USE_DECOMPOSE = True | |
| # โโ ๊ฒ์ ๊ฒฐ๊ณผ ํฌ๊ธฐ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| SEARCH_LIMIT = 10 # ์ฟผ๋ฆฌ๋น Qdrant ๊ฒ์ ๊ฒฐ๊ณผ ์ | |
| RETRIEVE_TOP_N = 10 # ๋ฆฌ๋ญํน ํ ์ต์ข ๋ฐํ ์ | |
| # โโ ์ด๊ธฐํ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| _csv_path = os.path.join( | |
| os.path.dirname(__file__), | |
| "../../../research/src/rag/query_transformations/aladin_category.csv", | |
| ) | |
| _df = pd.read_csv(_csv_path) | |
| CATEGORY_TREE = ( | |
| _df.groupby("category_large")["category_medium"] | |
| .apply(lambda x: sorted(x.unique().tolist())) | |
| .to_dict() | |
| ) | |
| CATEGORY_LARGE_LIST = sorted(CATEGORY_TREE.keys()) | |
| embedder = LocalEmbedder("BAAI/bge-m3") | |
| db = QdrantDB(vector_size=1024) | |
| llm = ChatOpenAI(model="gpt-4o-mini") | |
| reranker = LocalReranker("BAAI/bge-reranker-v2-m3") | |
| # ## 1. ์ฅ๋ฅด ์ถ์ถ ๋ ธ๋ v2 โ ํ์ ์ ํ | |
| top_genre_prompt_v2 = ChatPromptTemplate.from_template(""" | |
| ์ฌ์ฉ์ ํ๋กํ์ผ์ ๋ณด๊ณ ์๋ ๋๋ถ๋ฅ ๋ชฉ๋ก์์ ๊ฐ์ฅ ์ ํฉํ ๊ฒ์ ๋ฐ๋์ 3๊ฐ ์ ํํ์ธ์. | |
| ๋ชฉ๋ก์ ์๋ ๊ฐ์ ์ ๋ ๋ฐํํ์ง ๋ง์ธ์. | |
| ๋๋ถ๋ฅ ๋ชฉ๋ก: {large_list} | |
| ์ฌ์ฉ์ ํ๋กํ์ผ: {summary} | |
| JSON์ผ๋ก๋ง ๋ฐํ: {{"categories": ["์์ค/์/ํฌ๊ณก", "๊ฒฝ์ ๊ฒฝ์", "์๊ธฐ๊ณ๋ฐ"]}} | |
| """) | |
| medium_genre_prompt_v2 = ChatPromptTemplate.from_template(""" | |
| ์ฌ์ฉ์ ํ๋กํ์ผ์ ๋ณด๊ณ ์๋ ์ค๋ถ๋ฅ ๋ชฉ๋ก์์ ๊ฐ์ฅ ์ ํฉํ ๊ฒ์ ๋ฐ๋์ 5๊ฐ ์ ํํ์ธ์. | |
| ๋ชฉ๋ก์ ์๋ ๊ฐ์ ์ ๋ ๋ฐํํ์ง ๋ง์ธ์. | |
| ์ค๋ถ๋ฅ ๋ชฉ๋ก: {medium_list} | |
| ์ฌ์ฉ์ ํ๋กํ์ผ: {summary} | |
| JSON์ผ๋ก๋ง ๋ฐํ: {{"categories": ["ํ๊ตญ์์ค", "์ธ๊ตญ์์ค", "๊ฒฝ์ ์ผ๋ฐ", "์๊ธฐ๊ณ๋ฐ", "ํ๊ตญ์์ธ์ด"]}} | |
| """) | |
| def extract_genre_node_v2(state: GraphState) -> dict: | |
| summary = state.get("summary", "") | |
| top_resp = (top_genre_prompt_v2 | llm).invoke({ | |
| "large_list": CATEGORY_LARGE_LIST, | |
| "summary": summary, | |
| }) | |
| try: | |
| top_cats = json.loads(top_resp.content)["categories"] | |
| except (json.JSONDecodeError, KeyError): | |
| top_cats = [] | |
| if not top_cats: | |
| print("[Genre v2] ๋๋ถ๋ฅ ์ถ์ถ ์คํจ โ ํํฐ ์์") | |
| return {"genre_filter": [], "genre_level": "none"} | |
| medium_candidates = [] | |
| for cat in top_cats: | |
| medium_candidates.extend(CATEGORY_TREE.get(cat, [])) | |
| if not medium_candidates: | |
| print(f"[Genre v2] ๋๋ถ๋ฅ fallback: {top_cats}") | |
| return {"genre_filter": top_cats, "genre_level": "large"} | |
| medium_resp = (medium_genre_prompt_v2 | llm).invoke({ | |
| "medium_list": medium_candidates, | |
| "summary": summary, | |
| }) | |
| try: | |
| medium_cats = json.loads(medium_resp.content)["categories"] | |
| except (json.JSONDecodeError, KeyError): | |
| medium_cats = [] | |
| if not medium_cats: | |
| print(f"[Genre v2] ์ค๋ถ๋ฅ ์ถ์ถ ์คํจ โ ๋๋ถ๋ฅ fallback: {top_cats}") | |
| return {"genre_filter": top_cats, "genre_level": "large"} | |
| print(f"[Genre v2] ๋๋ถ๋ฅ: {top_cats} โ ์ค๋ถ๋ฅ: {medium_cats}") | |
| return {"genre_filter": medium_cats, "genre_level": "medium"} | |
| # ## 2. Step-back Prompting | |
| step_back_prompt = ChatPromptTemplate.from_template(""" | |
| ๋น์ ์ ๋์ ์ถ์ฒ ์์คํ ์ ๊ฒ์ ์ฟผ๋ฆฌ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| ์๋ ์ฌ์ฉ์์ ์๋ณธ ์ง๋ฌธ์์ ํ ๋จ๊ณ ๋ฌผ๋ฌ๋, | |
| ์ด ์ฌ์ฉ์๊ฐ ๊ทผ๋ณธ์ ์ผ๋ก ์ด๋ค ์ข ๋ฅ์ ๋ ์ ๊ฒฝํ์ ์ํ๋์ง๋ฅผ ํฌ์ฐฉํ๋ ์์ ์ง๋ฌธ์ ์์ฑํ์ธ์. | |
| [๊ท์น] | |
| - ์ฌ์ฉ์๊ฐ ์ธ๊ธํ ๊ตฌ์ฒด์ ์ธ ์ฅ๋ฅด๋ช , ์ฑ ์ ๋ชฉ, ์กฐ๊ฑด์ ๊ทธ๋๋ก ๋ฐ๋ณตํ์ง ๋ง์ธ์. | |
| - ๋์ , ๊ทธ ์กฐ๊ฑด๋ค์ด ๊ฐ๋ฆฌํค๋ ๋ ๋์ ๋ ์ ์๊ตฌ๋ ๋์ ์ ํ์ ๋ณธ์ง์ ํน์ฑ์ ํํํ์ธ์. | |
| - ๋์ ์๊ฐ๊ธ(book_intro)์ ์ค์ ๋ก ๋ฑ์ฅํ ๋ฒํ ์์ ํํ์ ์ฌ์ฉํ์ธ์. | |
| - 3๋ฌธ์ฅ ์ด๋ด๋ก ์์ฑํ์ธ์. | |
| ์ฌ์ฉ์ ํ๋กํ์ผ: {summary} | |
| ์ถ๋ ฅ: | |
| """) | |
| def step_back_query(summary: str, llm) -> str: | |
| return (step_back_prompt | llm).invoke({"summary": summary}).content.strip() | |
| # ## 3. Query Rewriting | |
| rewrite_prompt = ChatPromptTemplate.from_template(""" | |
| ๋น์ ์ ๋์ ์ถ์ฒ ์์คํ ์ ๊ฒ์ ์ฟผ๋ฆฌ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| ์๋ ์ฌ์ฉ์ ํ๋กํ์ผ์ ๋ฐํ์ผ๋ก, ๋ฒกํฐ ๊ฒ์์ ์ ํฉํ ๋์ ๊ฒ์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ์ธ์. | |
| [๊ท์น] | |
| - ๋์ ์๊ฐ๊ธ(book_intro)์ด๋ ์ถํ์ฌ ์ํ์ ์ค์ ๋ก ๋ฑ์ฅํ ๋ฒํ ์ดํ์ ํํ์ ์ฌ์ฉํ์ธ์. | |
| - ์ฅ๋ฅด, ์ฃผ์ ์์ญ, ์์ ๋ฐฉ์, ๋์ ๋ ์์ธต ๋ฑ ๋์ ๋ฉํ๋ฐ์ดํฐ์ ๋งค์นญ๋ ์ ์๋ ์กฐ๊ฑด์ ํฌํจํ์ธ์. | |
| - ์ฌ์ฉ์๊ฐ ์ธ๊ธํ ๊ธฐ์กด ๋์๊ฐ ์๋ค๋ฉด, ๊ทธ ๋์์ ํต์ฌ ํน์ฑ(์์ ๋ฐฉ์, ์ฃผ์ ๋ฒ์)์ ๋ฐ์ํ์ธ์. | |
| - 3๋ฌธ์ฅ ์ด๋ด๋ก ์์ฑํ์ธ์. | |
| ๋ ์ ๋ชฉ์ : {summary} | |
| ์ฌ์์ฑ๋ ๊ฒ์ ์ฟผ๋ฆฌ (๋ ๋ฌธ์ฅ ์ด๋ด๋ก): | |
| """) | |
| def rewrite_query(summary: str, llm) -> str: | |
| return (rewrite_prompt | llm).invoke({"summary": summary}).content.strip() | |
| # ## 4. Sub-query Decomposition | |
| decompose_prompt = ChatPromptTemplate.from_template(""" | |
| ๋น์ ์ ๋์ ์ถ์ฒ ์์คํ ์ ๊ฒ์ ์ฟผ๋ฆฌ ์ ๋ฌธ๊ฐ์ ๋๋ค. | |
| ์๋ ๊ฒ์ ์ฟผ๋ฆฌ๋ฅผ 2~4๊ฐ์ ์๋ธ์ฟผ๋ฆฌ๋ก ๋ถํดํ์ธ์. | |
| [ํต์ฌ ์์น] | |
| - ๊ฐ ์๋ธ์ฟผ๋ฆฌ๋ ์๋ก ๋ค๋ฅธ ๋ ๋ฆฝ์ ๊ฒ์ ์ธก๋ฉด์ ๋ค๋ค์ผ ํฉ๋๋ค. | |
| ๋์ผํ ์๋ฏธ๋ฅผ ๋ค๋ฅธ ํํ์ผ๋ก ๋ฐ๋ณตํ๋ ๊ฒ์ ์๋ธ์ฟผ๋ฆฌ๊ฐ ์๋๋๋ค. | |
| - ๋ถํด ๊ธฐ์ค ์์: ์ฃผ์ /์ฅ๋ฅด ์ธก๋ฉด, ์์ ๋ฐฉ์/๊ตฌ์กฐ ์ธก๋ฉด, ์ ์ฌ ๋์ ํน์ฑ ์ธก๋ฉด, ๋์ ๋ ์ ์ํฉ ์ธก๋ฉด | |
| - ๊ฐ ์๋ธ์ฟผ๋ฆฌ๋ ๋ ๋ฆฝ์ ์ผ๋ก ๊ฒ์ํ์ ๋ ์๋ก ๋ค๋ฅธ ํ๋ณด ๋์๊ตฐ์ ๋ฐํํ ์ ์์ด์ผ ํฉ๋๋ค. | |
| [์์ฑ ๊ท์น] | |
| - ๋์ ์๊ฐ๊ธ(book_intro)์ ๋ฑ์ฅํ ๋ฒํ ์ดํ๋ฅผ ์ฌ์ฉํ์ธ์. | |
| - "์ด ์ค์์", "๊ทธ ์ค์์" ๊ฐ์ ์ฐธ์กฐ ํํ์ ์ฌ์ฉํ์ง ๋ง์ธ์. | |
| - "์ถ์ฒํด์ฃผ์ธ์", "์๊ณ ์ถ์ต๋๋ค" ๊ฐ์ ์์ฒญํ ์ข ๊ฒฐ์ ์ฌ์ฉํ์ง ๋ง์ธ์. | |
| - ๋ฆฌ๋ทฐ, ํ์ ๋ฑ ๋์ ์๊ฐ๊ธ ์ธ์ ์ ๋ณด๋ฅผ ์์ฒญํ์ง ๋ง์ธ์. | |
| ๊ฒ์ ์ฟผ๋ฆฌ: {rewritten} | |
| ์ถ๋ ฅ ํ์ (๋ฒํธ์ ํ ์คํธ๋ง, ๋ค๋ฅธ ํ ์คํธ ์์ด): | |
| 1. [์๋ธ์ฟผ๋ฆฌ 1] | |
| 2. [์๋ธ์ฟผ๋ฆฌ 2] | |
| 3. [์๋ธ์ฟผ๋ฆฌ 3] | |
| """) | |
| def decompose_query(rewritten: str, llm) -> list: | |
| response = (decompose_prompt | llm).invoke({"rewritten": rewritten}).content | |
| return [ | |
| q.strip().lstrip("1234567890. ") | |
| for q in response.split("\n") | |
| if q.strip() and q.strip()[0].isdigit() | |
| ] | |
| # ## 5. Chained Pipeline | |
| def get_chained_queries(user_profile_query: str, llm, | |
| use_step_back: bool = True, | |
| use_rewrite: bool = True, | |
| use_decompose: bool = True) -> dict: | |
| all_queries = [] | |
| step_back = step_back_query(user_profile_query, llm) if use_step_back else user_profile_query | |
| print(f" [Step-back] : {step_back}") | |
| if use_step_back: | |
| all_queries.append(step_back) | |
| rewritten = rewrite_query(user_profile_query, llm) if use_rewrite else user_profile_query | |
| print(f" [Rewritten] : {rewritten}") | |
| if use_rewrite: | |
| all_queries.append(rewritten) | |
| sub_queries = decompose_query(rewritten, llm) if use_decompose else [] | |
| print(f" [Sub-queries]: {sub_queries}") | |
| all_queries.extend(sub_queries) | |
| return { | |
| "step_back": step_back, | |
| "rewritten": rewritten, | |
| "sub_queries": sub_queries, | |
| "all": all_queries, | |
| } | |
| # ## 6. RRF | |
| def reciprocal_rank_fusion(results_list: list, k: int = 60) -> list: | |
| scores, payloads = {}, {} | |
| for results in results_list: | |
| for rank, r in enumerate(results): | |
| isbn = r.payload.get("isbn", "") | |
| if isbn: | |
| scores[isbn] = scores.get(isbn, 0) + 1 / (k + rank + 1) | |
| payloads[isbn] = r.payload | |
| return [payloads[isbn] for isbn, _ in sorted(scores.items(), key=lambda x: x[1], reverse=True)] | |
| # ## 7. Query Transform RAG ๋ ธ๋ | |
| def query_transform_rag_node(state: GraphState) -> dict: | |
| summary = state.get("summary", "") | |
| reflection = state.get("reflection", "") | |
| categories = state.get("genre_filter", []) | |
| genre_level = state.get("genre_level", "none") | |
| print("---------------------") | |
| print("summary:", summary) | |
| print("reflection:", reflection) | |
| print("---------------------") | |
| user_profile_query = " ".join(filter(None, [summary, reflection])) | |
| print("\n[Query Transformations]") | |
| if not (USE_STEP_BACK or USE_REWRITE or USE_DECOMPOSE): | |
| print(" [๋ณํ ์์] ์๋ณธ ์ฟผ๋ฆฌ๋ง ์ฌ์ฉ") | |
| all_queries = [user_profile_query] | |
| else: | |
| queries = get_chained_queries( | |
| user_profile_query, llm, | |
| use_step_back=USE_STEP_BACK, | |
| use_rewrite=USE_REWRITE, | |
| use_decompose=USE_DECOMPOSE, | |
| ) | |
| all_queries = queries["all"] | |
| field_map = {"large": "category_large", "medium": "category_medium"} | |
| query_filter = None | |
| if categories and genre_level in field_map: | |
| query_filter = Filter( | |
| must=[FieldCondition(key=field_map[genre_level], match=MatchAny(any=categories))] | |
| ) | |
| all_results = [] | |
| for query in all_queries: | |
| query_vector = embedder.embed(query) | |
| if query_filter: | |
| results = db.search_with_filter( | |
| QDRANT_COLLECTION_NAME, query_vector, | |
| query_filter=query_filter, limit=SEARCH_LIMIT, threshold=0.5, | |
| ) | |
| else: | |
| results = db.search(QDRANT_COLLECTION_NAME, query_vector, limit=SEARCH_LIMIT, threshold=0.5) | |
| all_results.append(results) | |
| merged_payloads = reciprocal_rank_fusion(all_results) | |
| reranked_payloads = reranker.rerank(query=user_profile_query, books=merged_payloads) | |
| retrieved_books = [ | |
| { | |
| "isbn": p.get("isbn"), | |
| "title": p.get("title"), | |
| "author": p.get("author"), | |
| "book_intro": p.get("book_intro"), | |
| "category_large": p.get("category_large"), | |
| "category_medium": p.get("category_medium"), | |
| "cover_url": p.get("cover_url", ""), | |
| } | |
| for p in reranked_payloads[:RETRIEVE_TOP_N] | |
| ] | |
| print(f"\n[RAG] ์ต์ข ๊ฒ์ ๊ฒฐ๊ณผ: {len(retrieved_books)}๊ถ") | |
| return {"retrieved_books": retrieved_books} | |