Spaces:
Running
Running
| """ | |
| # Simple RAG with Filtering + Query Transformations | |
| ## μ μ© κΈ°λ² | |
| - **Metadata Filtering** : category_large / category_medium κΈ°λ° 2λ¨κ³ μ₯λ₯΄ νν° | |
| - **Query Transformations** : Sub-query Decomposition β μλΈμΏΌλ¦¬λ³ κ²μκ²°ν© | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import pandas as pd | |
| from dotenv import load_dotenv | |
| from langchain_core.messages import HumanMessage, AIMessage | |
| from langchain_core.prompts import ChatPromptTemplate | |
| from langchain_openai import ChatOpenAI | |
| from langgraph.graph import StateGraph, START, END | |
| 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, Phase, UserProfile | |
| load_dotenv() | |
| # ## 0. μ€μΉ λ° μ΄κΈ°ν | |
| # μΉ΄ν κ³ λ¦¬ νΈλ¦¬ λ‘λ | |
| _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. μ₯λ₯΄ μΆμΆ λ Έλ β 2λ¨κ³ κ³μΈ΅ μΆμΆ (λλΆλ₯ β μ€λΆλ₯) | |
| top_genre_prompt = ChatPromptTemplate.from_template(""" | |
| μ¬μ©μ νλ‘νμΌμ λ³΄κ³ μλ λλΆλ₯ λͺ©λ‘μμ μ ν©ν κ²μ μ΅λ 2κ° μ ννμΈμ. | |
| λͺ©λ‘μ μλ κ°μ μ λ λ°ννμ§ λ§μΈμ. | |
| λλΆλ₯ λͺ©λ‘: {large_list} | |
| μ¬μ©μ νλ‘νμΌ: {summary} | |
| JSONμΌλ‘λ§ λ°ν: {{"categories": ["μμ€/μ/ν¬κ³‘"]}} | |
| """) | |
| medium_genre_prompt = ChatPromptTemplate.from_template(""" | |
| μ¬μ©μ νλ‘νμΌμ λ³΄κ³ μλ μ€λΆλ₯ λͺ©λ‘μμ μ ν©ν κ²μ μ΅λ 3κ° μ ννμΈμ. | |
| λͺ©λ‘μ μλ κ°μ μ λ λ°ννμ§ λ§μΈμ. | |
| μ€λΆλ₯ λͺ©λ‘: {medium_list} | |
| μ¬μ©μ νλ‘νμΌ: {summary} | |
| JSONμΌλ‘λ§ λ°ν: {{"categories": ["νκ΅μμ€"]}} | |
| """) | |
| def extract_genre_node(state: GraphState) -> dict: | |
| summary = state.get("summary", "") | |
| # ββ 1λ¨κ³: λλΆλ₯ (30κ°) ββββββββββββββββββββββββββββββββββ | |
| top_resp = (top_genre_prompt | 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] λλΆλ₯ μΆμΆ μ€ν¨ β νν° μμ") | |
| return {"genre_filter": [], "genre_level": "none"} | |
| # ββ 2λ¨κ³: μ€λΆλ₯ (ν΄λΉ λλΆλ₯ νμλ§) βββββββββββββββββββ | |
| medium_candidates = [] | |
| for cat in top_cats: | |
| medium_candidates.extend(CATEGORY_TREE.get(cat, [])) | |
| if not medium_candidates: | |
| print(f"[Genre] λλΆλ₯ fallback: {top_cats}") | |
| return {"genre_filter": top_cats, "genre_level": "large"} | |
| medium_resp = (medium_genre_prompt | 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] μ€λΆλ₯ μΆμΆ μ€ν¨ β λλΆλ₯ fallback: {top_cats}") | |
| return {"genre_filter": top_cats, "genre_level": "large"} | |
| print(f"[Genre] λλΆλ₯: {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: | |
| """ | |
| μ¬μ©μ νλ‘νμΌ β μΆμνλ λ μ λͺ©μ | |
| Args: | |
| summary : μλ³Έ μ¬μ©μ νλ‘νμΌ | |
| llm : LLM μΈμ€ν΄μ€ | |
| Returns: | |
| str: μΆμνλ λ μ λͺ©μ | |
| """ | |
| chain = step_back_prompt | llm | |
| return chain.invoke({"summary": summary}).content.strip() | |
| # ## 3. Query Rewriting | |
| # **μ λ ₯**: summary κ²°κ³Ό (μΆμνλ λ μ λͺ©μ ) | |
| # **μΆλ ₯**: κ²μμ μ΅μ νλ ꡬ체μ μΈ μΏΌλ¦¬ | |
| # **λͺ©μ **: μΆμνλ μλλ₯Ό λμ κ²μμ λ μ ν©ν ννμΌλ‘ ꡬ체ν | |
| rewrite_prompt = ChatPromptTemplate.from_template(""" | |
| λΉμ μ λμ μΆμ² μμ€ν μ κ²μ 쿼리 μ λ¬Έκ°μ λλ€. | |
| μλ μ¬μ©μ νλ‘νμΌμ λ°νμΌλ‘, λ²‘ν° κ²μμ μ ν©ν λμ κ²μ 쿼리λ₯Ό μμ±νμΈμ. | |
| [κ·μΉ] | |
| - λμ μκ°κΈ(book_intro)μ΄λ μΆνμ¬ μνμ μ€μ λ‘ λ±μ₯ν λ²ν μ΄νμ ννμ μ¬μ©νμΈμ. | |
| - μ₯λ₯΄, μ£Όμ μμ, μμ λ°©μ, λμ λ μμΈ΅ λ± λμ λ©νλ°μ΄ν°μ λ§€μΉλ μ μλ 쑰건μ ν¬ν¨νμΈμ. | |
| - μ¬μ©μκ° μΈκΈν κΈ°μ‘΄ λμκ° μλ€λ©΄, κ·Έ λμμ ν΅μ¬ νΉμ±(μμ λ°©μ, μ£Όμ λ²μ)μ λ°μνμΈμ. | |
| - 3λ¬Έμ₯ μ΄λ΄λ‘ μμ±νμΈμ. | |
| λ μ λͺ©μ : {summary} | |
| μ¬μμ±λ κ²μ 쿼리 (λ λ¬Έμ₯ μ΄λ΄λ‘): | |
| """) | |
| def rewrite_query(summary: str, llm) -> str: | |
| """ | |
| μΆμνλ λ μ λͺ©μ β κ²μμ μ΅μ νλ ꡬ체μ 쿼리 | |
| Args: | |
| summary : step_back_query()μ κ²°κ³Ό | |
| llm : LLM μΈμ€ν΄μ€ | |
| Returns: | |
| str: μ¬μμ±λ κ²μ 쿼리 | |
| """ | |
| chain = rewrite_prompt | llm | |
| return chain.invoke({"summary": summary}).content.strip() | |
| # ## 4. Sub-query Decomposition | |
| # **μ λ ₯**: rewritten κ²°κ³Ό (ꡬ체νλ 쿼리) | |
| # **μΆλ ₯**: 쑰건 λ¨μλ‘ λΆν΄λ μλΈμΏΌλ¦¬ 리μ€νΈ | |
| # **λͺ©μ **: λ€μ€ 쑰건μ λΆν΄νμ¬ κ°κ° κ²μ β RRF κ²°ν©μΌλ‘ 쑰건 λ§μ‘±λ₯ ν₯μ | |
| decompose_prompt = ChatPromptTemplate.from_template(""" | |
| λΉμ μ λμ μΆμ² μμ€ν μ κ²μ 쿼리 μ λ¬Έκ°μ λλ€. | |
| μλ κ²μ 쿼리λ₯Ό 2~4κ°μ μλΈμΏΌλ¦¬λ‘ λΆν΄νμΈμ. | |
| [ν΅μ¬ μμΉ] | |
| - κ° μλΈμΏΌλ¦¬λ μλ‘ λ€λ₯Έ λ 립μ κ²μ μΈ‘λ©΄μ λ€λ€μΌ ν©λλ€. | |
| λμΌν μλ―Έλ₯Ό λ€λ₯Έ ννμΌλ‘ λ°λ³΅νλ κ²μ μλΈμΏΌλ¦¬κ° μλλλ€. | |
| - λΆν΄ κΈ°μ€ μμ: μ£Όμ /μ₯λ₯΄ μΈ‘λ©΄, μμ λ°©μ/ꡬ쑰 μΈ‘λ©΄, μ μ¬ λμ νΉμ± μΈ‘λ©΄, λμ λ μ μν© μΈ‘λ©΄ | |
| - κ° μλΈμΏΌλ¦¬λ λ 립μ μΌλ‘ κ²μνμ λ μλ‘ λ€λ₯Έ ν보 λμκ΅°μ λ°νν μ μμ΄μΌ ν©λλ€. | |
| [μμ± κ·μΉ] | |
| - λμ μκ°κΈ(book_intro)μ λ±μ₯ν λ²ν μ΄νλ₯Ό μ¬μ©νμΈμ. | |
| - "μ΄ μ€μμ", "κ·Έ μ€μμ" κ°μ μ°Έμ‘° ννμ μ¬μ©νμ§ λ§μΈμ. | |
| - "μΆμ²ν΄μ£ΌμΈμ", "μκ³ μΆμ΅λλ€" κ°μ μμ²ν μ’ κ²°μ μ¬μ©νμ§ λ§μΈμ. | |
| - 리뷰, νμ λ± λμ μκ°κΈ μΈμ μ 보λ₯Ό μμ²νμ§ λ§μΈμ. | |
| κ²μ 쿼리: {rewritten} | |
| μΆλ ₯ νμ (λ²νΈμ ν μ€νΈλ§, λ€λ₯Έ ν μ€νΈ μμ΄): | |
| 1. [μλΈμΏΌλ¦¬ 1] | |
| 2. [μλΈμΏΌλ¦¬ 2] | |
| 3. [μλΈμΏΌλ¦¬ 3] | |
| """) | |
| def decompose_query(rewritten: str, llm) -> list: | |
| """ | |
| ꡬ체νλ 쿼리 β μλΈμΏΌλ¦¬ 리μ€νΈ | |
| Args: | |
| rewritten : rewrite_query()μ κ²°κ³Ό | |
| llm : LLM μΈμ€ν΄μ€ | |
| Returns: | |
| List[str]: μλΈμΏΌλ¦¬ 리μ€νΈ | |
| """ | |
| chain = decompose_prompt | llm | |
| response = chain.invoke({"rewritten": rewritten}).content | |
| sub_queries = [ | |
| q.strip().lstrip("1234567890. ") | |
| for q in response.split("\n") | |
| if q.strip() and q.strip()[0].isdigit() | |
| ] | |
| return sub_queries | |
| # ## 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: | |
| """ | |
| μΈ κ°μ§ λ³νμ μμλλ‘ μ΄μ΄μ μ€ν | |
| νλ¦: summary β step_back | |
| νλ¦: summary β rewritten β sub_queries | |
| Args: | |
| summary : μλ³Έ μ¬μ©μ νλ‘νμΌ | |
| llm : LLM μΈμ€ν΄μ€ | |
| use_step_back : Step-back Prompting μ¬μ© μ¬λΆ (κΈ°λ³Έ True) | |
| use_rewrite : Query Rewriting μ¬μ© μ¬λΆ (κΈ°λ³Έ True) | |
| use_decompose : Sub-query Decomposition μ¬μ© μ¬λΆ (κΈ°λ³Έ True) | |
| Returns: | |
| { | |
| 'step_back' : str, # Step-back κ²°κ³Ό | |
| 'rewritten' : str, # Rewriting κ²°κ³Ό | |
| 'sub_queries': List[str], # Decomposition κ²°κ³Ό | |
| 'all' : List[str], # RRFμ λκΈΈ μ 체 쿼리 λͺ©λ‘ | |
| } | |
| """ | |
| # Step-back β μλ μΆμν | |
| step_back = step_back_query(user_profile_query, llm) if use_step_back else user_profile_query | |
| print(f" [Step-back] : {step_back}") | |
| # Rewriting β μΆμνλ μλλ₯Ό ꡬ체μ κ²μμ΄λ‘ | |
| rewritten = rewrite_query(user_profile_query, llm) if use_rewrite else user_profile_query | |
| print(f" [Rewritten] : {rewritten}") | |
| # Decomposition β ꡬ체μ 쿼리λ₯Ό 쑰건 λ¨μλ‘ λΆν΄ | |
| sub_queries = decompose_query(rewritten, llm) if use_decompose else [] | |
| print(f" [Sub-queries]: {sub_queries}") | |
| # μ 체 쿼리 λͺ©λ‘ (RRF μ λ ₯μ©) | |
| all_queries = [step_back, rewritten] + sub_queries | |
| return { | |
| "step_back": step_back, | |
| "rewritten": rewritten, | |
| "sub_queries": sub_queries, | |
| "all": all_queries, | |
| } | |
| # ## 8. RRF ν¨μ μ μ | |
| def reciprocal_rank_fusion(results_list: list, k: int = 60) -> list: | |
| """μ¬λ¬ κ²μ κ²°κ³Όλ₯Ό RRFλ‘ κ²°ν©. Returns: payload λμ λ리 리μ€νΈ (μ μ λ΄λ¦Όμ°¨μ)""" | |
| 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)] | |
| # ## 9. 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("---------------------") | |
| # summary + reflection κ²°ν© | |
| user_profile_query = " ".join(filter(None, [summary, reflection])) | |
| # ββ 1. Query Transformations ββββββββββββββββββββββββββββ | |
| print("\n[Query Transformations]") | |
| queries = get_chained_queries(user_profile_query, llm) | |
| all_queries = queries["all"] | |
| # ββ 2. μ₯λ₯΄ νν° κ΅¬μ± ββββββββββββββββββββββββββββββββββββ | |
| 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))] | |
| ) | |
| # ββ 3. μΏΌλ¦¬λ³ κ²μ ββββββββββββββββββββββββββββββββββββββ | |
| 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=5, threshold=0.5, | |
| ) | |
| else: | |
| results = db.search(QDRANT_COLLECTION_NAME, query_vector, limit=5, threshold=0.5) | |
| all_results.append(results) | |
| # print(query) | |
| # for r in results: | |
| # print(f"score: {r.score}") | |
| # print(f"title: {r.payload.get('title')}") | |
| # print(f"book_intro: {r.payload.get('book_intro')}") | |
| # print("---------------") | |
| # ββ 4. RRF κ²°ν© | |
| merged_payloads = reciprocal_rank_fusion(all_results) | |
| # ββ 5. Reranker Model μ μ© | |
| reranked_payloads = reranker.rerank(query=user_profile_query, books=merged_payloads) | |
| # ββ 6. κ²°κ³Ό μ 리 | |
| 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[:3] | |
| ] | |
| # print(f"\nμ΅μ’ κ²μ κ²°κ³Ό: {len(retrieved_books)}κΆ") | |
| return {"retrieved_books": retrieved_books} | |
| # ## 10. Explain λ Έλ | |
| # κ° κ²μ λμμ λν΄ `summary + reflection + book_intro` κΈ°λ°μΌλ‘ μ¬μ λΆμμ μμ±. | |
| # `rag_llm_node`κ° μ΄ λΆμμ κ·Όκ±°λ‘ μΆμ² μ΄μ λ₯Ό μμ±νλλ‘ ν¨. | |
| explain_prompt = ChatPromptTemplate.from_template(""" | |
| λΉμ μ λμ μΆμ² μμ€ν μ AI μ΄μμ€ν΄νΈμ λλ€. | |
| μλ μ¬μ©μ νλ‘νμΌκ³Ό μ± μκ°λ₯Ό μ½κ³ , | |
| μ΄ μ± μ΄ μ΄ μ¬μ©μμκ² μ μ ν©νμ§ λλ μ ν©νμ§ μμμ§ 2λ¬Έμ₯μΌλ‘ λΆμνμΈμ. | |
| [μ£Όμ] | |
| - μ± μκ°μ μλ λ΄μ©μ μ λ μ§μ΄λ΄μ§ λ§μΈμ. | |
| - μ¬μ©μ νλ‘νμΌμ λ μ λͺ©μ , μ νΈ μ₯λ₯΄, λ μ μ€νμΌκ³Ό μ°κ²°ν΄μ μμ±νμΈμ. | |
| [μ¬μ©μ νλ‘νμΌ] | |
| {summary} | |
| [μ± μκ°] | |
| {book_intro} | |
| λΆμ: | |
| """) | |
| explain_chain = explain_prompt | llm | |
| def explain_node(state: GraphState) -> dict: | |
| summary = state.get("summary", "") | |
| reflection = state.get("reflection", "") | |
| books = state["retrieved_books"] | |
| user_profile_query = " ".join(filter(None, [summary, reflection])) | |
| for book in books: | |
| analysis = explain_chain.invoke({ | |
| "summary": user_profile_query, | |
| "book_intro": book.get("book_intro", ""), | |
| }).content | |
| book["analysis"] = analysis | |
| print(f" [λΆμ] {book.get('title')} β {analysis[:60]}...") | |
| return {"retrieved_books": books} | |
| # ## 11. LLM λ Έλ | |
| def rag_llm_node(state: GraphState) -> dict: | |
| summary = state.get("summary", "") | |
| reflection = state.get("reflection", "") | |
| books = state["retrieved_books"] | |
| user_profile_query = " ".join(filter(None, [summary, reflection])) | |
| context = "\n\n".join([ | |
| f"ISBN: {b['isbn']}\n" | |
| f"μ λͺ©: {b['title']}\n" | |
| f"μ μ: {b['author']}\n" | |
| f"μ₯λ₯΄: {b.get('category_medium') or b.get('category_large', '')}\n" | |
| f"νμ§URL: {b.get('cover_url', '')}\n" | |
| f"μκ°: {b['book_intro'][:300]}\n" | |
| f"μ¬μ λΆμ: {b.get('analysis', '')}" | |
| for b in books | |
| ]) | |
| rag_prompt = ChatPromptTemplate.from_template(""" | |
| λΉμ μ λμκ΄ νλ μ΄ν° AIμ λλ€. | |
| [κ·μΉ] | |
| - [κ²μλ λμ λͺ©λ‘]μ 3κΆ μ λΆμ λν΄ μΆμ² μ΄μ λ₯Ό μμ±νμΈμ. μ μΈνκ±°λ λ€λ₯Έ μ± μΌλ‘ λ체νμ§ λ§μΈμ. | |
| - λ°λμ JSON νμμΌλ‘λ§ λ΅νμΈμ. λ€λ₯Έ ν μ€νΈλ μ λ ν¬ν¨νμ§ λ§μΈμ. | |
| - μ₯λ₯΄λ λ°λμ [κ²μλ λμ λͺ©λ‘]μ μ₯λ₯΄ κ°μ κ·Έλλ‘ μ¬μ©νμΈμ. | |
| [μΆμ² μ΄μ μμ± κ·μΉ] | |
| - λ°λμ [μ¬μ λΆμ]κ³Ό [μκ°]μ λμ¨ κ΅¬μ²΄μ μΈ λ΄μ©μ κ·Όκ±°λ‘ μμ±νμΈμ. | |
| - μ¬μ©μ νλ‘νμΌμ μ΄λ€ λΆλΆ(λ μ λͺ©μ , μ νΈ μ₯λ₯΄, λ μ μ€νμΌ)κ³Ό μ°κ²°λλμ§ λͺ μνμΈμ. | |
| - [μκ°]μ [μ¬μ λΆμ]μ μλ λ΄μ©μ μμλ‘ μ§μ΄λ΄μ§ λ§μΈμ. | |
| - 2~3λ¬Έμ₯μΌλ‘ μμ±νμΈμ. | |
| [μ¬μ©μ νλ‘νμΌ] | |
| {summary} | |
| [κ²μλ λμ λͺ©λ‘] | |
| {context} | |
| [μΆλ ₯ νμ] | |
| [ | |
| {{"title": "μ± μ λͺ©", "author": "μ μ", "isbn": "ISBNλ²νΈ", "cover_url": "νμ§URL", "book_intro": "μ± μκ°", "category_medium": "μ₯λ₯΄", "reason": "μΆμ² μ΄μ 2~3λ¬Έμ₯"}}, | |
| {{"title": "μ± μ λͺ©", "author": "μ μ", "isbn": "ISBNλ²νΈ", "cover_url": "νμ§URL", "book_intro": "μ± μκ°", "category_medium": "μ₯λ₯΄", "reason": "μΆμ² μ΄μ 2~3λ¬Έμ₯"}}, | |
| {{"title": "μ± μ λͺ©", "author": "μ μ", "isbn": "ISBNλ²νΈ", "cover_url": "νμ§URL", "book_intro": "μ± μκ°", "category_medium": "μ₯λ₯΄", "reason": "μΆμ² μ΄μ 2~3λ¬Έμ₯"}} | |
| ] | |
| """) | |
| chain = rag_prompt | llm | |
| response = chain.invoke({"context": context, "summary": user_profile_query}) | |
| try: | |
| recommendations = json.loads(response.content) | |
| except json.JSONDecodeError: | |
| recommendations = response.content | |
| return { | |
| "messages": [AIMessage(content=response.content)], | |
| "recommendations": recommendations, | |
| } | |