Spaces:
Sleeping
Sleeping
| """ | |
| CodeWeaver LangGraph ๋ ธ๋ ๊ตฌํ. | |
| ๊ฐ ๋ ธ๋๋ AgentState๋ฅผ ๋ฐ์ ์ฒ๋ฆฌํ๊ณ ์ ๋ฐ์ดํธ๋ ์ํ๋ฅผ ๋ฐํํฉ๋๋ค. | |
| ๋ชจ๋ ๋ ธ๋๋ LangSmith๋ฅผ ํตํด ์๋์ผ๋ก ์ถ์ ๋ฉ๋๋ค. | |
| """ | |
| import asyncio | |
| import logging | |
| import os | |
| from typing import List, Literal, Optional | |
| from langchain_core.messages import HumanMessage, SystemMessage | |
| from langchain_google_genai import ChatGoogleGenerativeAI | |
| from langgraph.graph import StateGraph, START, END | |
| from langgraph.types import Send | |
| from src.agent.state import AgentState, SearchResult | |
| from src.agent.state import _MULTI_ANS_RESET_TOKEN # reset token for multi_answers reducer | |
| from src.tools.search_tools import ( | |
| search_github, | |
| search_official_docs, | |
| search_stackoverflow, | |
| ) | |
| from src.utils.tracing import trace_node | |
| from src.vector_db.qdrant_client import QdrantManager | |
| logger = logging.getLogger(__name__) | |
| # LLM ์ด๊ธฐํ (Gemini 2.5 Flash) | |
| llm = ChatGoogleGenerativeAI( | |
| model="gemini-2.5-flash-lite", | |
| temperature=0.7, | |
| ) | |
| # Qdrant ๋งค๋์ ์ด๊ธฐํ | |
| qdrant_manager = QdrantManager() | |
| async def analyze_question_node(state: AgentState) -> dict: | |
| """ | |
| ์ง๋ฌธ์ ๋ถ์ํ์ฌ ์ ํ์ ๋ถ๋ฅํ๊ณ ์บ์ ์ ๊ฒฉ์ฑ์ ํ๋จํฉ๋๋ค. | |
| Phase 2: Question Analysis & Cache Eligibility Decision | |
| ๋ถ๋ฅ: | |
| - followup: ์ด์ ๋ํ์ ์์กดํ๋ ํ์ ์ง๋ฌธ | |
| - cache_candidate: ๋ ๋ฆฝ์ ์ด๊ณ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ง๋ฌธ | |
| - new_search: ๋ ๋ฆฝ์ ์ด์ง๋ง ์บ์ํ์ง ์์ ์ง๋ฌธ (์๊ฐ ๋ฏผ๊ฐ ๋ฑ) | |
| """ | |
| user_question = state.user_question | |
| messages = state.messages | |
| # ๋ํ ๋งฅ๋ฝ ๊ตฌ์ฑ | |
| has_history = messages and len(messages) > 1 | |
| context_info = "" | |
| if has_history: | |
| context_info = "\n์ด์ ๋ํ ๋งฅ๋ฝ:\n" | |
| for msg in messages[-4:-1]: # ํ์ฌ ์ง๋ฌธ ์ ์ธ ์ต๊ทผ 3๊ฐ | |
| if hasattr(msg, 'type') and hasattr(msg, 'content'): | |
| role = "์ฌ์ฉ์" if msg.type == "human" else "AI" | |
| context_info += f"{role}: {msg.content[:100]}\n" | |
| analysis_prompt = f"""์ง๋ฌธ์ ๋ถ์ํ์ฌ ์ ํ์ ๋ถ๋ฅํ๊ณ , ์บ์ ์ ๊ฒฉ์ฑ์ ํ๋จํ์ธ์. | |
| {context_info} | |
| ํ์ฌ ์ง๋ฌธ: {user_question} | |
| ๋ถ๋ฅ ๊ธฐ์ค: | |
| 1. **clarification** (๋ณด์ถฉ/ํ์ ๋ณ๊ฒฝ ์์ฒญ) | |
| - ์ด์ ๋ต๋ณ/๋ํ ๋ด์ฉ์ ๋ฐํ์ผ๋ก "์ค๋ช ๋ฐฉ์"์ ๋ฐ๊พธ๊ฑฐ๋ ๋ณด์ถฉ์ ์์ฒญ | |
| - ์: "์ข ๋ ์ฝ๊ฒ ์ค๋ช ํด์ค", "์์ ์ฝ๋๋ก ๋ณด์ฌ์ค", "ํ ์ค๋ก ์์ฝํด์ค", "๋ค์ ์ค๋ช ํด์ค" | |
| - ์์น: ๊ฒ์/์บ์๊ฐ ์๋๋ผ ๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ | |
| - should_cache = false, canonical_question = null | |
| 2. **new_topic** (๋ํ ์ค ์ ๊ฐ๋ ์ง๋ฌธ) | |
| - ๋ํ๊ฐ ์ด์ด์ง๋ ์ค์ด์ง๋ง, ์ง๋ฌธ ์์ฒด๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ์ฑ๋ฆฝํ๋ '์ ๊ฐ๋ /์ ์/๋น๊ต/์ฌ์ฉ๋ฒ' ์ง๋ฌธ | |
| - ์: (React ์ด์ผ๊ธฐ ์ค) "Event Listener๋ ๋ญ์ผ?", "CORS๊ฐ ๋ญ์ผ?" | |
| - ์์น: ๊ฒ์ + ์บ์ ์ ์ฅ ๊ฐ์น๊ฐ ํผ | |
| - should_cache = true (๊ธฐ๋ณธ), canonical_question ์์ฑ | |
| 3. **independent** (์์ ๋ ๋ฆฝ ์ง๋ฌธ) | |
| - ์ด์ ๋ํ ์์ด๋ ์ดํด ๊ฐ๋ฅํ ์ผ๋ฐ ์ง๋ฌธ | |
| - ์: "Spring Security๊ฐ ๋ญ์ผ?", "Docker Compose ์ฌ์ฉ๋ฒ์?" | |
| - ์์น: ๊ฒ์ + ์บ์ ์ ์ฅ ๊ฐ์น๊ฐ ํผ | |
| - should_cache = true (๊ธฐ๋ณธ), canonical_question ์์ฑ | |
| ๋ค์ JSON ํ์์ผ๋ก๋ง ๋ต๋ณํ์ธ์: | |
| {{ | |
| "question_type": "clarification|new_topic|independent", | |
| "should_cache": true|false, | |
| "reasoning": "๋ถ๋ฅ ์ด์ 1-2๋ฌธ์ฅ", | |
| "canonical_question": "์บ์ํ ์ ๊ทํ๋ ์ง๋ฌธ (should_cache๊ฐ true์ธ ๊ฒฝ์ฐ์๋ง, ์๋๋ฉด null)" | |
| }} | |
| JSON ์ธ์ ๋ค๋ฅธ ํ ์คํธ๋ ํฌํจํ์ง ๋ง์ธ์.""" | |
| try: | |
| messages_to_llm = [HumanMessage(content=analysis_prompt)] | |
| response = llm.invoke(messages_to_llm) | |
| # JSON ํ์ฑ | |
| import json | |
| response_text = response.content.strip() | |
| # JSON ๋ธ๋ก ์ถ์ถ (๋งํฌ๋ค์ด ์ฝ๋ ๋ธ๋ก ์ ๊ฑฐ) | |
| if "```json" in response_text: | |
| response_text = response_text.split("```json")[1].split("```")[0].strip() | |
| elif "```" in response_text: | |
| response_text = response_text.split("```")[1].split("```")[0].strip() | |
| analysis = json.loads(response_text) | |
| question_type = analysis.get("question_type", "independent") | |
| should_cache = analysis.get("should_cache", False) | |
| reasoning = analysis.get("reasoning", "") | |
| canonical_question = analysis.get("canonical_question", user_question) | |
| # ์ ํจ์ฑ ๊ฒ์ฆ | |
| if question_type not in ["clarification", "new_topic", "independent"]: | |
| question_type = "independent" | |
| # 1์ฐจ ์ ์ฑ ๋ณด์ : clarification์ ์บ์ ๊ธ์ง | |
| if question_type == "clarification": | |
| should_cache = False | |
| canonical_question = None | |
| else: | |
| # new_topic/independent๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์บ์ ๊ฐ๋ฅ | |
| if canonical_question is None or (isinstance(canonical_question, str) and not canonical_question.strip()): | |
| canonical_question = user_question | |
| # ์คํ(run) ์์๋ง๋ค step ๋ก๊ทธ๋ฅผ ๋ฆฌ์ ํ๊ณ , ์ด๋ฒ ์คํ์ step๋ง ๋์ ๋๊ฒ ํจ | |
| steps_delta = [ | |
| "__RESET_STEPS__", | |
| f"๐ ์ง๋ฌธ ๋ถ์: {question_type} (์บ์ ์ฌ๋ถ: {should_cache})", | |
| ] | |
| return { | |
| "question_type": question_type, | |
| "should_cache": should_cache, | |
| "analysis_reasoning": reasoning, | |
| "canonical_question": canonical_question if should_cache else None, | |
| "intermediate_steps": steps_delta | |
| } | |
| except Exception as e: | |
| logger.error("์ง๋ฌธ ๋ถ์ ์คํจ: %s", e, exc_info=True) | |
| # ๊ธฐ๋ณธ๊ฐ: ๋ ๋ฆฝ ์ง๋ฌธ์ผ๋ก ๊ฐ์ฃผ | |
| steps_delta = [ | |
| "__RESET_STEPS__", | |
| "โ ๏ธ ์ง๋ฌธ ๋ถ์ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ: independent", | |
| ] | |
| return { | |
| "question_type": "independent", | |
| "should_cache": True, | |
| "analysis_reasoning": "๋ถ์ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ", | |
| "canonical_question": user_question, | |
| "intermediate_steps": steps_delta | |
| } | |
| async def check_cache_node(state: AgentState) -> dict: | |
| """ | |
| ๋ฒกํฐ DB ์บ์์์ ์ ์ฌํ ์ง๋ฌธ์ ๊ฒ์ํฉ๋๋ค. | |
| threshold 0.85 ์ด์์ธ ๊ฒฝ์ฐ ์บ์ ํํธ๋ก ํ๋จํฉ๋๋ค. | |
| """ | |
| question_for_lookup = state.canonical_question or state.user_question | |
| logger.info("์บ์ ํ์ธ ์ค: %s", question_for_lookup[:50]) | |
| try: | |
| cached_result = await qdrant_manager.search_cache( | |
| question=question_for_lookup, | |
| threshold=0.85 | |
| ) | |
| updates = {} | |
| steps_delta: List[str] = [] | |
| if cached_result: | |
| updates["cached_result"] = cached_result | |
| steps_delta.append(f"โ ์บ์ ํํธ (๋ต๋ณ ๊ธธ์ด: {len(cached_result)}์)") | |
| logger.info("์บ์ ํํธ") | |
| else: | |
| updates["cached_result"] = None | |
| steps_delta.append("โ ์บ์ ๋ฏธ์ค: ์๋ก์ด ๊ฒ์ ํ์") | |
| logger.info("์บ์ ๋ฏธ์ค") | |
| except Exception as e: | |
| logger.error("์บ์ ํ์ธ ์คํจ: %s", e, exc_info=True) | |
| updates["cached_result"] = None | |
| steps_delta.append(f"โ ๏ธ ์บ์ ํ์ธ ์ค๋ฅ: {str(e)}") | |
| updates["intermediate_steps"] = steps_delta | |
| return updates | |
| def create_plan_node(state: AgentState) -> dict: | |
| """ | |
| ์ง๋ฌธ์ ๋ถ์ํ์ฌ ์ ํ๊ณผ ๊ฐ์๋ฅผ ํ๋จํฉ๋๋ค. | |
| Phase 4: Dynamic Parallel Search | |
| - single_topic: ํ๋์ ์ฃผ์ (๊ธฐ์กด ๊ทธ๋ํ ์คํ) | |
| - multiple_questions: ๋ ๋ฆฝ ์ง๋ฌธ 2๊ฐ (Send API๋ก ๊ทธ๋ํ 2ํ ์คํ) | |
| - too_many: ๋ ๋ฆฝ ์ง๋ฌธ 3๊ฐ ์ด์ (์๋ฌ ๋ฉ์์ง) | |
| LangGraph ๊ณต์ ๊ฐ์ด๋๋ผ์ธ: ๋ ธ๋๋ ํ ๊ฐ์ง ์ผ๋ง ์ํ (๊ณํ ์๋ฆฝ) | |
| """ | |
| user_question = state.user_question | |
| logger.info("์ง๋ฌธ ๋ถ์ ๋ฐ ๊ณํ ์๋ฆฝ ์ค: %s", user_question[:50]) | |
| def _extract_question_candidates(text: str) -> List[str]: | |
| """์ ๋ ฅ ๋ฌธ์์ด์์ '์ง๋ฌธ ํ๋ณด'๋ฅผ ์ต๋ํ ๋ณด์์ ์ผ๋ก ์ถ์ถํฉ๋๋ค(3๊ฐ ์ด์ ๊ฐ์ง์ฉ).""" | |
| import re | |
| if not text: | |
| return [] | |
| t = text.strip() | |
| # 1) ๋ฌผ์ํ ๊ธฐ๋ฐ ๋ถ๋ฆฌ (๊ฐ์ฅ ์ ๋ขฐ๋ ๋์) | |
| parts = re.split(r"[?๏ผ]+", t) | |
| candidates = [p.strip() for p in parts if p.strip()] | |
| if len(candidates) >= 2 and re.search(r"[?๏ผ]", t): | |
| # ๋ฌผ์ํ๊ฐ ์กด์ฌํ ๋๋ง ์ด ๊ท์น์ ์ ๋ขฐ | |
| return candidates | |
| # 2) ์ค๋ฐ๊ฟ/๋ฒํธ ๋งค๊ธฐ๊ธฐ ๊ธฐ๋ฐ (๋ค์ค ์ง๋ฌธ ์ ๋ ฅ ํจํด) | |
| lines = [ln.strip() for ln in re.split(r"[\r\n]+", t) if ln.strip()] | |
| numbered = [] | |
| for ln in lines: | |
| if re.match(r"^\s*(\d+[\.\)]|[-*])\s+", ln): | |
| numbered.append(re.sub(r"^\s*(\d+[\.\)]|[-*])\s+", "", ln).strip()) | |
| if len(numbered) >= 2: | |
| return numbered | |
| # 3) ๊ตฌ๋ถ์ ๊ธฐ๋ฐ(์ธ๋ฏธ์ฝ๋ก ) โ ๋ณด์กฐ | |
| semi = [p.strip() for p in t.split(";") if p.strip()] | |
| if len(semi) >= 2: | |
| return semi | |
| return [t] | |
| def _hard_guard_too_many(text: str) -> Optional[dict]: | |
| """ | |
| ํ๋ ๊ฐ๋: ์ฌ์ฉ์๊ฐ '์ง๋ฌธ 3๊ฐ ์ด์'์ ํ ๋ฒ์ ๋์ง ๊ฒ์ผ๋ก ํ์คํ ๊ฒฝ์ฐ, | |
| LLM ๋ถ๋ฅ์ ๋ฌด๊ดํ๊ฒ too_many๋ก ๊ฐ์ ํฉ๋๋ค. | |
| """ | |
| import re | |
| if not text: | |
| return None | |
| # ๊ฐ์ฅ ํ์คํ ๊ธฐ์ค: ๋ฌผ์ํ๊ฐ 3๊ฐ ์ด์ | |
| qmarks = len(re.findall(r"[?๏ผ]", text)) | |
| if qmarks >= 3: | |
| candidates = _extract_question_candidates(text) | |
| msg = "์ฃ์กํฉ๋๋ค. ์ง๋ฌธ์ ํ ๋ฒ์ ์ต๋ 2๊ฐ๊น์ง ๊ฐ๋ฅํฉ๋๋ค. ๊ฐ์ฅ ์ค์ํ 2๊ฐ๋ง ๊ณจ๋ผ์ ๋ค์ ์ง๋ฌธํด ์ฃผ์ธ์." | |
| return { | |
| "case": "too_many", | |
| "sub_questions": candidates, | |
| "reasoning": f"๋ฌผ์ํ๊ฐ {qmarks}๊ฐ๋ก, 3๊ฐ ์ด์์ ๋ ๋ฆฝ ์ง๋ฌธ์ผ๋ก ํ๋จํ์ต๋๋ค.", | |
| "error_message": msg, | |
| "steps_note": f"โ ๏ธ ์ง๋ฌธ ์ ์ด๊ณผ ๊ฐ์ง(๋ฌผ์ํ {qmarks}๊ฐ) โ too_many๋ก ๊ฐ์ ", | |
| } | |
| # ๋ฒํธ ๋งค๊ธฐ๊ธฐ/๋ฆฌ์คํธ๋ก 3๊ฐ ์ด์ | |
| candidates = _extract_question_candidates(text) | |
| if len(candidates) >= 3: | |
| msg = "์ฃ์กํฉ๋๋ค. ์ง๋ฌธ์ ํ ๋ฒ์ ์ต๋ 2๊ฐ๊น์ง ๊ฐ๋ฅํฉ๋๋ค. ๊ฐ์ฅ ์ค์ํ 2๊ฐ๋ง ๊ณจ๋ผ์ ๋ค์ ์ง๋ฌธํด ์ฃผ์ธ์." | |
| return { | |
| "case": "too_many", | |
| "sub_questions": candidates, | |
| "reasoning": f"์ง๋ฌธ ํ๋ณด๊ฐ {len(candidates)}๊ฐ๋ก ๊ฐ์ง๋์ด 3๊ฐ ์ด์ ์ง๋ฌธ์ผ๋ก ํ๋จํ์ต๋๋ค.", | |
| "error_message": msg, | |
| "steps_note": f"โ ๏ธ ์ง๋ฌธ ์ ์ด๊ณผ ๊ฐ์ง(ํ๋ณด {len(candidates)}๊ฐ) โ too_many๋ก ๊ฐ์ ", | |
| } | |
| return None | |
| # ํ๋ ๊ฐ๋(๊ฒฐ์ ๋ก ์ ) โ LLM์ด ์๋ชป ๋ถ๋ฅํ๋๋ผ๋ 3๊ฐ ์ด์์ด๋ฉด ๋ฌด์กฐ๊ฑด ์ฐจ๋จ | |
| hard = _hard_guard_too_many(user_question) | |
| if hard: | |
| steps_delta = [ | |
| f"๐ ๊ณํ ํ์ : {hard['case']}", | |
| f" ์๋ธ์ง๋ฌธ: {len(hard['sub_questions'])}๊ฐ", | |
| f" ์ด์ : {hard['reasoning']}", | |
| hard["steps_note"], | |
| ] | |
| logger.info("๊ณํ ์๋ฆฝ ์๋ฃ(ํ๋ ๊ฐ๋): too_many, %d๊ฐ ์๋ธ์ง๋ฌธ", len(hard["sub_questions"])) | |
| return { | |
| "plan": { | |
| "case": hard["case"], | |
| "sub_questions": hard["sub_questions"], | |
| "reasoning": hard["reasoning"], | |
| "error_message": hard["error_message"], | |
| }, | |
| "is_multi_question": False, | |
| "sub_question_index": 0, | |
| "sub_question_text": None, | |
| "original_multi_question": None, | |
| "multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}], | |
| "intermediate_steps": steps_delta, | |
| } | |
| plan_prompt = f"""์ง๋ฌธ์ ๋ถ์ํ์ฌ ์ ํ๊ณผ ๊ฐ์๋ฅผ ํ๋จํ์ธ์. | |
| ์ง๋ฌธ: {user_question} | |
| **์ค์**: sub_questions์ ์ฉ๋๋ case์ ๋ฐ๋ผ ๋ค๋ฆ ๋๋ค! | |
| **Case 1: single_topic** (ํ๋์ ์ฃผ์ ) | |
| - ์: "Spring Security JWT ์ธ์ฆ ๊ตฌํ" | |
| โ sub_questions: ["๊ฐ๋ ", "๊ตฌํ", "์์ "] | |
| โ ์ฉ๋: ๋ต๋ณ ์น์ ๊ตฌ์กฐ (๊ฒ์์ ์๋ณธ ์ง๋ฌธ์ผ๋ก 1ํ๋ง) | |
| โ ๊ฒ์: "Spring Security JWT ์ธ์ฆ ๊ตฌํ" | |
| - ์: "React hooks ์๋ฒฝ ๊ฐ์ด๋" | |
| โ sub_questions: ["hooks๋", "์ฃผ์ hooks", "์ค๋ฌด ํจํด"] | |
| โ ์ฉ๋: ๋ต๋ณ ์น์ ๊ตฌ์กฐ | |
| โ ๊ฒ์: "React hooks ์๋ฒฝ ๊ฐ์ด๋" | |
| **Case 2: multiple_questions** (์ฌ๋ฌ ๋ ๋ฆฝ ์ง๋ฌธ, ์ต๋ 2๊ฐ) | |
| - ์: "JWT๊ฐ ๋ญ์ผ? CORS๋?" | |
| โ sub_questions: ["JWT๊ฐ ๋ญ์ผ?", "CORS๋?"] | |
| โ ์ฉ๋: ๊ฐ ์ง๋ฌธ๋ง๋ค ๋ณ๋ ๊ฒ์ | |
| โ ๊ฒ์: "JWT๊ฐ ๋ญ์ผ?" (1ํ), "CORS๋?" (1ํ) | |
| - ์: "Docker ์ฌ์ฉ๋ฒ์? Redis ์ค์น๋?" | |
| โ sub_questions: ["Docker ์ฌ์ฉ๋ฒ์?", "Redis ์ค์น๋?"] | |
| โ ์ฉ๋: ๊ฐ ์ง๋ฌธ๋ง๋ค ๋ณ๋ ๊ฒ์ | |
| **Case 3: too_many** (3๊ฐ ์ด์ ์ง๋ฌธ) | |
| - ์: "JWT? CORS? Docker?" | |
| โ ๋๋ฌด ๋ง์์ ์ฒ๋ฆฌ ๋ถ๊ฐ | |
| โ error_message ์ ๊ณต | |
| ๊ท์น: | |
| - single_topic: sub_questions๋ ์งง์ ํค์๋/๊ตฌ์ (1-5๊ฐ) | |
| - multiple_questions: sub_questions๋ ์์ ํ ๋ฌธ์ฅ (์ ํํ 2๊ฐ๋ง) | |
| - too_many: 3๊ฐ ์ด์์ด๋ฉด ์ด ์ผ์ด์ค๋ก ๋ถ๋ฅ | |
| ๋ค์ JSON ํ์์ผ๋ก๋ง ๋ต๋ณํ์ธ์: | |
| {{ | |
| "case": "single_topic|multiple_questions|too_many", | |
| "sub_questions": [...], | |
| "reasoning": "์ด ์ผ์ด์ค๋ก ํ๋จํ ์ด์ ", | |
| "error_message": "..." (too_many์ธ ๊ฒฝ์ฐ๋ง, ๊ทธ ์ธ๋ ๋น ๋ฌธ์์ด) | |
| }} | |
| JSON ์ธ์ ๋ค๋ฅธ ํ ์คํธ๋ ํฌํจํ์ง ๋ง์ธ์.""" | |
| try: | |
| import json | |
| messages_to_llm = [HumanMessage(content=plan_prompt)] | |
| response = llm.invoke(messages_to_llm) | |
| # JSON ํ์ฑ | |
| response_text = response.content.strip() | |
| # JSON ๋ธ๋ก ์ถ์ถ (๋งํฌ๋ค์ด ์ฝ๋ ๋ธ๋ก ์ ๊ฑฐ) | |
| if "```json" in response_text: | |
| response_text = response_text.split("```json")[1].split("```")[0].strip() | |
| elif "```" in response_text: | |
| response_text = response_text.split("```")[1].split("```")[0].strip() | |
| plan_data = json.loads(response_text) | |
| case = plan_data.get("case", "single_topic") | |
| sub_questions = plan_data.get("sub_questions", [user_question]) | |
| reasoning = plan_data.get("reasoning", "") | |
| error_message = plan_data.get("error_message", "") | |
| # LLM ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ๋ค์๋ ํ ๋ฒ ๋ ํ๋ ๊ฐ๋ ์ ์ฉ (์์ ์ฅ์น) | |
| hard2 = _hard_guard_too_many(user_question) | |
| if hard2: | |
| case = hard2["case"] | |
| sub_questions = hard2["sub_questions"] | |
| reasoning = hard2["reasoning"] | |
| error_message = hard2["error_message"] | |
| # ์ ํจ์ฑ ๊ฒ์ฆ | |
| if not sub_questions or len(sub_questions) == 0: | |
| sub_questions = [user_question] | |
| case = "single_topic" | |
| # multiple_questions์ผ ๋ 2๊ฐ ์ ํ ๊ฐ์ (๋จ, 3๊ฐ ์ด์์ ์ ํ๋ ๊ฐ๋์์ too_many๋ก ์ฒ๋ฆฌ๋จ) | |
| if case == "multiple_questions" and len(sub_questions) > 2: | |
| sub_questions = sub_questions[:2] | |
| reasoning += " (์ง๋ฌธ ์ ์ ํ: ์ต๋ 2๊ฐ)" | |
| steps_delta = [ | |
| f"๐ ๊ณํ ํ์ : {case}", | |
| f" ์๋ธ์ง๋ฌธ: {len(sub_questions)}๊ฐ", | |
| f" ์ด์ : {reasoning}" | |
| ] | |
| logger.info("๊ณํ ์๋ฆฝ ์๋ฃ: %s, %d๊ฐ ์๋ธ์ง๋ฌธ", case, len(sub_questions)) | |
| # NOTE: ์ด ๊ทธ๋ํ๋ ์ฒดํฌํฌ์ธํ /์ค๋ ๋ ์ ์ง๊ฐ ๊ฐ๋ฅํ๋ฏ๋ก, | |
| # multi_answers๋ ๋งค ์คํ(run) ์์ ์ ๋ฆฌ์ ํด์ผ ์ด์ ํด ๋์ ์ด ๋ฐ์ํ์ง ์์ต๋๋ค. | |
| return { | |
| "plan": { | |
| "case": case, | |
| "sub_questions": sub_questions, | |
| "reasoning": reasoning, | |
| "error_message": error_message | |
| }, | |
| "is_multi_question": False, | |
| "sub_question_index": 0, | |
| "sub_question_text": None, | |
| "original_multi_question": None, | |
| "multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}], | |
| "intermediate_steps": steps_delta | |
| } | |
| except Exception as e: | |
| logger.error("๊ณํ ์๋ฆฝ ์คํจ: %s", e, exc_info=True) | |
| # ๊ธฐ๋ณธ๊ฐ: ์๋ณธ ์ง๋ฌธ ๊ทธ๋๋ก ์ฌ์ฉ | |
| steps_delta = [ | |
| "โ ๏ธ ๊ณํ ์๋ฆฝ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ: single_topic" | |
| ] | |
| return { | |
| "plan": { | |
| "case": "single_topic", | |
| "sub_questions": [user_question], | |
| "reasoning": "๊ณํ ์๋ฆฝ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ", | |
| "error_message": "" | |
| }, | |
| "is_multi_question": False, | |
| "sub_question_index": 0, | |
| "sub_question_text": None, | |
| "original_multi_question": None, | |
| "multi_answers": [{"__token__": _MULTI_ANS_RESET_TOKEN}], | |
| "intermediate_steps": steps_delta | |
| } | |
| def classify_intent_node(state: AgentState) -> dict: | |
| """ | |
| LLM์ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์ ์ง๋ฌธ์ ์๋๋ฅผ ๋ถ๋ฅํฉ๋๋ค. | |
| ๋ถ๋ฅ ์นดํ ๊ณ ๋ฆฌ: | |
| - debugging: ์๋ฌ ํด๊ฒฐ, ๋ฒ๊ทธ ์์ | |
| - learning: ๊ฐ๋ ํ์ต, ์๋ฆฌ ์ดํด | |
| - code_review: ์ฝ๋ ๊ฐ์ , ๋ฆฌํฉํ ๋ง | |
| """ | |
| logger.info("์๋ ๋ถ๋ฅ ์ค: %s", state.user_question[:50]) | |
| classification_prompt = f"""์ง๋ฌธ์ ๋ค์ ์ธ ๊ฐ์ง ์๋ ์ค ํ๋๋ก ๋ถ๋ฅํ์ธ์: | |
| 1. debugging: ์๋ฌ ํด๊ฒฐ, ๋ฒ๊ทธ ์์ , ๋ฌธ์ ํด๊ฒฐ | |
| ์: "ImportError๊ฐ ๋ฐ์ํด์", "์ด ์ฝ๋๊ฐ ์๋ํ์ง ์์์" | |
| 2. learning: ๊ฐ๋ ํ์ต, ์๋ฆฌ ์ดํด, ํํ ๋ฆฌ์ผ | |
| ์: "async/await๊ฐ ๋ญ๊ฐ์?", "JPA ๋์ ์๋ฆฌ๋?" | |
| 3. code_review: ์ฝ๋ ๊ฐ์ , ๋ฆฌํฉํ ๋ง, ๋ฒ ์คํธ ํ๋ํฐ์ค | |
| ์: "์ด ์ฝ๋๋ฅผ ๊ฐ์ ํ ๋ฐฉ๋ฒ์?", "๋ ๋์ ์ค๊ณ๋?" | |
| ์ง๋ฌธ: {state.user_question} | |
| ๋ฐ๋์ debugging, learning, code_review ์ค ํ๋๋ง ๋ตํ์ธ์.""" | |
| updates = {} | |
| steps_delta: List[str] = [] | |
| try: | |
| messages = [ | |
| SystemMessage(content="๋น์ ์ ๊ฐ๋ฐ์ ์ง๋ฌธ์ ๋ถ๋ฅํ๋ ์ ๋ฌธ๊ฐ์ ๋๋ค."), | |
| HumanMessage(content=classification_prompt) | |
| ] | |
| response = llm.invoke(messages) | |
| intent_raw = response.content.strip().lower() | |
| # ์ ํจํ ์๋๋ก ์ ๊ทํ | |
| valid_intents = ["debugging", "learning", "code_review"] | |
| intent = next((i for i in valid_intents if i in intent_raw), "learning") | |
| updates["detected_intent"] = intent | |
| steps_delta.append(f"๐ฏ ์๋ ๋ถ๋ฅ: {intent}") | |
| logger.info("์๋ ๋ถ๋ฅ ์๋ฃ: %s", intent) | |
| except Exception as e: | |
| logger.error("์๋ ๋ถ๋ฅ ์คํจ: %s", e, exc_info=True) | |
| updates["detected_intent"] = "learning" | |
| steps_delta.append("โ ๏ธ ์๋ ๋ถ๋ฅ ์คํจ, ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ: learning") | |
| updates["intermediate_steps"] = steps_delta | |
| return updates | |
| def search_stackoverflow_node(state: AgentState) -> dict: | |
| """ | |
| Stack Overflow์์ ๊ฒ์์ ์ํํฉ๋๋ค. | |
| Send API๋ฅผ ํตํ ๋ณ๋ ฌ ๊ฒ์์ ์ผ๋ถ๋ก ์คํ๋ฉ๋๋ค. | |
| search_results์ intermediate_steps๋ Annotated[List, add]๋ก | |
| ์ ์๋์ด ์์ด ์๋์ผ๋ก ๋จธ์ง๋ฉ๋๋ค. | |
| """ | |
| intent = state.detected_intent or "learning" | |
| count = 5 if intent == "debugging" else 3 | |
| logger.info("Stack Overflow ๊ฒ์ ์์: %d๊ฐ", count) | |
| try: | |
| results = search_stackoverflow(state.user_question, count) | |
| logger.info("Stack Overflow์์ %d๊ฐ ๊ฒฐ๊ณผ ์์ง", len(results)) | |
| # reducer๊ฐ ์๋์ผ๋ก ๋จธ์งํ๋ฏ๋ก ์ ๊ฒฐ๊ณผ๋ง ๋ฐํ | |
| return { | |
| "search_results": results, | |
| "intermediate_steps": [f"๐ Stack Overflow: {len(results)}๊ฐ ๊ฒฐ๊ณผ"] | |
| } | |
| except Exception as e: | |
| logger.error("Stack Overflow ๊ฒ์ ์คํจ: %s", e) | |
| return { | |
| "intermediate_steps": [f"โ ๏ธ Stack Overflow ๊ฒ์ ์คํจ: {str(e)}"] | |
| } | |
| def search_github_node(state: AgentState) -> dict: | |
| """ | |
| GitHub Issues/Discussions์์ ๊ฒ์์ ์ํํฉ๋๋ค. | |
| Send API๋ฅผ ํตํ ๋ณ๋ ฌ ๊ฒ์์ ์ผ๋ถ๋ก ์คํ๋ฉ๋๋ค. | |
| """ | |
| intent = state.detected_intent or "learning" | |
| count = 5 if intent == "code_review" else 3 if intent == "learning" else 2 | |
| logger.info("GitHub ๊ฒ์ ์์: %d๊ฐ", count) | |
| try: | |
| results = search_github(state.user_question, count) | |
| logger.info("GitHub์์ %d๊ฐ ๊ฒฐ๊ณผ ์์ง", len(results)) | |
| # reducer๊ฐ ์๋์ผ๋ก ๋จธ์ง | |
| return { | |
| "search_results": results, | |
| "intermediate_steps": [f"๐ GitHub: {len(results)}๊ฐ ๊ฒฐ๊ณผ"] | |
| } | |
| except Exception as e: | |
| logger.error("GitHub ๊ฒ์ ์คํจ: %s", e) | |
| return { | |
| "intermediate_steps": [f"โ ๏ธ GitHub ๊ฒ์ ์คํจ: {str(e)}"] | |
| } | |
| def search_official_docs_node(state: AgentState) -> dict: | |
| """ | |
| ๊ณต์ ๋ฌธ์/Tavily์์ ๊ฒ์์ ์ํํฉ๋๋ค. | |
| Send API๋ฅผ ํตํ ๋ณ๋ ฌ ๊ฒ์์ ์ผ๋ถ๋ก ์คํ๋ฉ๋๋ค. | |
| """ | |
| intent = state.detected_intent or "learning" | |
| count = 5 if intent == "learning" else 2 | |
| logger.info("๊ณต์ ๋ฌธ์ ๊ฒ์ ์์: %d๊ฐ", count) | |
| try: | |
| results = search_official_docs(state.user_question, count) | |
| logger.info("๊ณต์ ๋ฌธ์์์ %d๊ฐ ๊ฒฐ๊ณผ ์์ง", len(results)) | |
| # reducer๊ฐ ์๋์ผ๋ก ๋จธ์ง | |
| return { | |
| "search_results": results, | |
| "intermediate_steps": [f"๐ ๊ณต์ ๋ฌธ์: {len(results)}๊ฐ ๊ฒฐ๊ณผ"] | |
| } | |
| except Exception as e: | |
| logger.error("๊ณต์ ๋ฌธ์ ๊ฒ์ ์คํจ: %s", e) | |
| return { | |
| "intermediate_steps": [f"โ ๏ธ ๊ณต์ ๋ฌธ์ ๊ฒ์ ์คํจ: {str(e)}"] | |
| } | |
| def collect_results_node(state: AgentState) -> dict: | |
| """ | |
| ๋ณ๋ ฌ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์์งํ๊ณ ์นด์ดํธํฉ๋๋ค. | |
| Fan-in ํฌ์ธํธ: 3๊ฐ์ ๋ณ๋ ฌ ๊ฒ์ ๋ ธ๋๊ฐ ๋ชจ๋ ์๋ฃ๋ ํ ์คํ๋ฉ๋๋ค. | |
| LangGraph ๊ณต์ ๊ฐ์ด๋๋ผ์ธ: Send API์ fan-in ์ง์ ์์ ๊ฒฐ๊ณผ ์ง๊ณ | |
| """ | |
| total_results = len(state.search_results) | |
| logger.info("๊ฒ์ ๊ฒฐ๊ณผ ์์ง ์๋ฃ: %d๊ฐ", total_results) | |
| steps_delta = [ | |
| f"๐ ๊ฒ์ ๊ฒฐ๊ณผ ์์ง: ์ด {total_results}๊ฐ" | |
| ] | |
| return { | |
| "intermediate_steps": steps_delta | |
| } | |
| def evaluate_results_node(state: AgentState) -> dict: | |
| """ | |
| ๊ฒ์ ๊ฒฐ๊ณผ์ ๊ฐ์์ ํ์ง์ ๋ชจ๋ ํ๊ฐํฉ๋๋ค. | |
| ํ๊ฐ ๊ธฐ์ค: | |
| 1. ๊ฐ์: ์ต์ 2๊ฐ ์ด์ | |
| 2. ํ์ง: ํ๊ท relevance_score >= 0.6 | |
| """ | |
| search_results = state.search_results # ์ง์ ์ฌ์ฉ (๋ ์์ ) | |
| refinement_count = state.refinement_count | |
| result_count = len(search_results) | |
| logger.info("๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ: %d๊ฐ (๊ฐ์ ํ์: %d)", result_count, refinement_count) | |
| # ์์ ์ฅ์น: ์ด๋ฏธ 1ํ ๊ฐ์ ํ์ผ๋ฉด ๋ ์ด์ ๊ฐ์ ํ์ง ์์ | |
| if refinement_count >= 1: | |
| steps_delta = [ | |
| f"โ ๏ธ ์ต๋ ๊ฐ์ ํ์ ๋๋ฌ ({refinement_count}ํ), ํ์ฌ ๊ฒฐ๊ณผ๋ก ์งํ" | |
| ] | |
| return { | |
| "needs_refinement": False, | |
| "intermediate_steps": steps_delta | |
| } | |
| # 1์ฐจ ํ๊ฐ: ๊ฐ์ | |
| if result_count < 2: | |
| steps_delta = [ | |
| f"โ ๏ธ ๊ฒ์ ๊ฒฐ๊ณผ ๋ถ์กฑ ({result_count}๊ฐ < 2๊ฐ), ์ฟผ๋ฆฌ ๊ฐ์ ํ์" | |
| ] | |
| return { | |
| "needs_refinement": True, | |
| "intermediate_steps": steps_delta | |
| } | |
| # 2์ฐจ ํ๊ฐ: ํ์ง (relevance_score๊ฐ ์๋ ๊ฒฝ์ฐ๋ง) | |
| scored_results = [r for r in search_results if r.relevance_score is not None] | |
| if scored_results: | |
| avg_score = sum(r.relevance_score for r in scored_results) / len(scored_results) | |
| # ํ๊ท ์ ์๊ฐ 0.5 ๋ฏธ๋ง์ด๋ฉด ํ์ง ๋ถ์กฑ | |
| if avg_score < 0.5: | |
| steps_delta = [ | |
| f"โ ๏ธ ๊ฒ์ ๊ฒฐ๊ณผ ํ์ง ๋ถ์กฑ (ํ๊ท ์ ์: {avg_score:.2f} < 0.5), ์ฟผ๋ฆฌ ๊ฐ์ ํ์" | |
| ] | |
| return { | |
| "needs_refinement": True, | |
| "intermediate_steps": steps_delta | |
| } | |
| steps_delta = [ | |
| f"โ ๊ฒ์ ๊ฒฐ๊ณผ ์ถฉ๋ถ ({result_count}๊ฐ, ํ๊ท ์ ์: {avg_score:.2f}), ํํฐ๋ง ๋จ๊ณ๋ก ์งํ" | |
| ] | |
| else: | |
| # relevance_score๊ฐ ์์ง ์์ผ๋ฉด ๊ฐ์๋ง์ผ๋ก ํ๋จ | |
| steps_delta = [ | |
| f"โ ๊ฒ์ ๊ฒฐ๊ณผ ์ถฉ๋ถ ({result_count}๊ฐ), ํํฐ๋ง ๋จ๊ณ๋ก ์งํ" | |
| ] | |
| return { | |
| "needs_refinement": False, | |
| "intermediate_steps": steps_delta | |
| } | |
| def refine_search_node(state: AgentState) -> dict: | |
| """ | |
| ๊ฒ์ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ ํฉ๋๋ค. | |
| Open Deep Research ํจํด: | |
| - LLM์ด ์ ๋ต์ ์ ํ (๊ตฌ์ฒดํ/์ผ๋ฐํ/๋ฒ์ญ) | |
| - ์๋ณธ ์ง๋ฌธ ๋ณด์กด (์ต์ข ๋ต๋ณ ์์ฑ ์ ์ฌ์ฉ) | |
| LangGraph ๊ณต์ ๊ฐ์ด๋๋ผ์ธ: | |
| - ์ํ์ ์์ ๋ฐ์ดํฐ ์ ์ฅ (์ ๋ต ์ ๋ณด ํฌํจ) | |
| - ํ๋กฌํํธ๋ ๋ ธ๋ ๋ด์์ ๋์ ์์ฑ | |
| """ | |
| user_question = state.user_question | |
| original_question = state.original_question or user_question | |
| result_count = len(state.search_results) | |
| logger.info("๊ฒ์ ์ฟผ๋ฆฌ ๊ฐ์ ์ค: %s (%d๊ฐ ๊ฒฐ๊ณผ)", user_question[:50], result_count) | |
| refinement_prompt = f"""๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ๋ถ์กฑํฉ๋๋ค. ๊ฒ์ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ ํ์ธ์. | |
| ์๋ณธ ์ง๋ฌธ: {user_question} | |
| ํ์ฌ ๊ฒฐ๊ณผ ์: {result_count}๊ฐ (๋ชฉํ: 2๊ฐ ์ด์) | |
| ๊ฐ์ ์ ๋ต (ํ๋ ์ ํ): | |
| 1. MORE_SPECIFIC: ๊ธฐ์ ์ ์ธ๋ถ์ฌํญ ์ถ๊ฐ | |
| ์: "React hooks" โ "React useEffect cleanup function dependencies" | |
| 2. MORE_GENERAL: ๋ ๋์ ์ฉ์ด ์ฌ์ฉ | |
| ์: "Spring Cloud Sleuth 2.x trace" โ "distributed tracing Spring Boot" | |
| 3. TRANSLATE: ์ธ์ด ๋ณํ | |
| ์: "JWT ์ธ์ฆ ๊ตฌํ" โ "JWT authentication implementation" | |
| ์: "WebSocket connection" โ "WebSocket ์ฐ๊ฒฐ ๋ฐฉ๋ฒ" | |
| ๋ค์ JSON ํ์์ผ๋ก๋ง ๋ต๋ณํ์ธ์: | |
| {{ | |
| "new_query": "๊ฐ์ ๋ ๊ฒ์ ์ฟผ๋ฆฌ", | |
| "strategy": "MORE_SPECIFIC|MORE_GENERAL|TRANSLATE", | |
| "reasoning": "์ด ์ ๋ต์ ์ ํํ ์ด์ 1-2๋ฌธ์ฅ" | |
| }} | |
| JSON ์ธ์ ๋ค๋ฅธ ํ ์คํธ๋ ํฌํจํ์ง ๋ง์ธ์.""" | |
| try: | |
| import json | |
| messages_to_llm = [HumanMessage(content=refinement_prompt)] | |
| response = llm.invoke(messages_to_llm) | |
| # JSON ํ์ฑ | |
| response_text = response.content.strip() | |
| if "```json" in response_text: | |
| response_text = response_text.split("```json")[1].split("```")[0].strip() | |
| elif "```" in response_text: | |
| response_text = response_text.split("```")[1].split("```")[0].strip() | |
| refinement_data = json.loads(response_text) | |
| new_query = refinement_data.get("new_query", user_question) | |
| strategy = refinement_data.get("strategy", "MORE_GENERAL") | |
| reasoning = refinement_data.get("reasoning", "") | |
| steps_delta = [ | |
| f"๐ ์ฟผ๋ฆฌ ๊ฐ์ : {strategy}", | |
| f" ์ด์ : {user_question[:50]}...", | |
| f" ์ดํ: {new_query[:50]}...", | |
| f" ์ด์ : {reasoning}" | |
| ] | |
| logger.info("์ฟผ๋ฆฌ ๊ฐ์ ์๋ฃ: %s โ %s", user_question[:30], new_query[:30]) | |
| return { | |
| "user_question": new_query, | |
| "original_question": original_question, | |
| "refinement_count": state.refinement_count + 1, | |
| "search_results": [], # CRITICAL: ์ด์ ๊ฒ์ ๊ฒฐ๊ณผ ์ ๊ฑฐ ํ ์ฌ๊ฒ์ | |
| "intermediate_steps": steps_delta | |
| } | |
| except Exception as e: | |
| logger.error("์ฟผ๋ฆฌ ๊ฐ์ ์คํจ: %s", e, exc_info=True) | |
| # ๊ธฐ๋ณธ ์ ๋ต: ์๋ฌธ ํค์๋ ์ถ์ถ (๊ฐ๋จํ fallback) | |
| fallback_query = user_question + " tutorial example" | |
| steps_delta = [ | |
| f"โ ๏ธ ์ฟผ๋ฆฌ ๊ฐ์ ์คํจ, ๊ธฐ๋ณธ ์ ๋ต ์ฌ์ฉ", | |
| f" ์ดํ: {fallback_query}" | |
| ] | |
| return { | |
| "user_question": fallback_query, | |
| "original_question": original_question, | |
| "refinement_count": state.refinement_count + 1, | |
| "search_results": [], # CRITICAL: ์คํจ ์์๋ ์ด์ ๊ฒ์ ๊ฒฐ๊ณผ ์ ๊ฑฐ | |
| "intermediate_steps": steps_delta | |
| } | |
| def filter_and_score_node(state: AgentState) -> dict: | |
| """ | |
| ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ํํฐ๋งํ๊ณ ๊ด๋ จ๋ ์ ์๋ฅผ ๋งค๊น๋๋ค. | |
| - ์ต์ ๊ธธ์ด 50์ ์ด์, URL ์กด์ฌํ๋ ๊ฒฐ๊ณผ๋ง ์ ์ง | |
| - ์์ 5๊ฐ ๊ฒฐ๊ณผ์ ๋ํด LLM์ผ๋ก ๊ด๋ จ๋ ํ๊ฐ | |
| - ๊ด๋ จ๋ ์์ผ๋ก ์ ๋ ฌํ์ฌ ์์ 10๊ฐ ์ ํ | |
| """ | |
| search_results = state.search_results | |
| logger.info("๊ฒ์ ๊ฒฐ๊ณผ ํํฐ๋ง ์ค: %d๊ฐ", len(search_results)) | |
| # ๊ธฐ๋ณธ ํํฐ๋ง | |
| filtered = [ | |
| r for r in search_results | |
| if r.content and len(r.content) >= 50 and r.url | |
| ] | |
| logger.info("๊ธฐ๋ณธ ํํฐ๋ง ํ: %d๊ฐ ๊ฒฐ๊ณผ", len(filtered)) | |
| # ์์ 5๊ฐ ๊ฒฐ๊ณผ๋ง LLM์ผ๋ก ์ ์ ๋งค๊ธฐ๊ธฐ (๋น์ฉ ์ ๊ฐ) | |
| for result in filtered[:5]: | |
| if result.relevance_score is None: | |
| try: | |
| scoring_prompt = f"""์ง๋ฌธ: {state.user_question} | |
| ๊ฒ์ ๊ฒฐ๊ณผ: {result.content[:500]} | |
| ์ด ๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์ง๋ฌธ์ ์ผ๋ง๋ ๊ด๋ จ์ด ์๋์ง 0.0์์ 1.0 ์ฌ์ด์ ์ ์๋ก ํ๊ฐํ์ธ์. | |
| ์ ์๋ง ์ซ์๋ก ๋ตํ์ธ์. (์: 0.8)""" | |
| response = llm.invoke([HumanMessage(content=scoring_prompt)]) | |
| score_str = response.content.strip() | |
| result.relevance_score = float(score_str) | |
| except Exception as e: | |
| logger.warning("์ ์ ๋งค๊ธฐ๊ธฐ ์คํจ: %s", e) | |
| result.relevance_score = 0.5 | |
| # ๊ด๋ จ๋ ์์ผ๋ก ์ ๋ ฌ | |
| filtered.sort(key=lambda r: r.relevance_score or 0, reverse=True) | |
| # ์์ 5๊ฐ๋ง ์ ์ง | |
| top_results = filtered[:5] | |
| subtask_results = dict(state.subtask_results) | |
| subtask_results["filtered_results"] = [r.model_dump() for r in top_results] | |
| steps_delta = [f"โ๏ธ ํํฐ๋ง ์๋ฃ: {len(top_results)}๊ฐ ๊ฒฐ๊ณผ ์ ํ"] | |
| logger.info("ํํฐ๋ง ์๋ฃ: %d๊ฐ ๊ฒฐ๊ณผ", len(top_results)) | |
| return { | |
| "subtask_results": subtask_results, | |
| "intermediate_steps": steps_delta | |
| } | |
| def summarize_results_node(state: AgentState) -> dict: | |
| """ | |
| ํํฐ๋ง๋ ๊ฐ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ณด ๊ฐ๋ฐ์๊ฐ ์ดํดํ๊ธฐ ์ฝ๊ฒ ์์ฝํฉ๋๋ค. | |
| ๊ฐ ๊ฒฐ๊ณผ๋ฅผ 2-3๋ฌธ์ฅ์ผ๋ก ํต์ฌ ๋ด์ฉ๋ง ์ถ์ถํฉ๋๋ค. | |
| """ | |
| subtask_results = state.subtask_results | |
| filtered_results = subtask_results.get("filtered_results", []) | |
| logger.info("๊ฒ์ ๊ฒฐ๊ณผ ์์ฝ ์ค: %d๊ฐ", len(filtered_results)) | |
| summaries = [] | |
| for result_dict in filtered_results: | |
| try: | |
| summary_prompt = f"""๋ค์ ๊ฒ์ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ณด ๊ฐ๋ฐ์๊ฐ ์ดํดํ๊ธฐ ์ฝ๊ฒ 2-3๋ฌธ์ฅ์ผ๋ก ์์ฝํ์ธ์: | |
| ์ถ์ฒ: {result_dict['source']} | |
| ๋ด์ฉ: {result_dict['content'][:1000]} | |
| ํต์ฌ ๋ด์ฉ๋ง ๊ฐ๋จ๋ช ๋ฃํ๊ฒ ์์ฝํ์ธ์.""" | |
| response = llm.invoke([HumanMessage(content=summary_prompt)]) | |
| summaries.append({ | |
| "source": result_dict['source'], | |
| "url": result_dict['url'], | |
| "summary": response.content.strip(), | |
| "relevance": result_dict.get('relevance_score', 0.5) | |
| }) | |
| except Exception as e: | |
| logger.error("์์ฝ ์คํจ: %s", e) | |
| updated_subtask_results = dict(subtask_results) | |
| updated_subtask_results["summaries"] = summaries | |
| steps_delta = [f"๐ ์์ฝ ์๋ฃ: {len(summaries)}๊ฐ ๊ฒฐ๊ณผ"] | |
| logger.info("์์ฝ ์๋ฃ: %d๊ฐ", len(summaries)) | |
| return { | |
| "subtask_results": updated_subtask_results, | |
| "intermediate_steps": steps_delta | |
| } | |
| async def generate_answer_node(state: AgentState) -> dict: | |
| """ | |
| ์์ฝ๋ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ต์ข ๋ต๋ณ์ ์์ฑํฉ๋๋ค. | |
| ์๋๋ณ๋ก ๋ค๋ฅธ ๋ต๋ณ ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ๋ฉฐ, ์์ฑ๋ ๋ต๋ณ์ ์บ์์ ์ ์ฅ๋ฉ๋๋ค. | |
| """ | |
| subtask_results = state.subtask_results | |
| summaries = subtask_results.get("summaries", []) | |
| intent = state.detected_intent or "learning" | |
| logger.info("์ต์ข ๋ต๋ณ ์์ฑ ์ค: %s", intent) | |
| # ์๋๋ณ ํ๋กฌํํธ ํ ํ๋ฆฟ | |
| templates = { | |
| "debugging": """๋ค์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ๋๋ฒ๊น ์ง๋ฌธ์ ๋ต๋ณํ์ธ์: | |
| ์ง๋ฌธ: {question} | |
| ์์ง๋ ์ ๋ณด: | |
| {summaries} | |
| ๋ต๋ณ ๊ตฌ์กฐ: | |
| 1. ๋ฌธ์ ์ ์ | |
| 2. ๋ฐ์ ์์ธ | |
| 3. ํด๊ฒฐ ๋ฐฉ๋ฒ (์ฝ๋ ์์ ํฌํจ) | |
| 4. ์ฃผ์์ฌํญ | |
| 5. ์ฐธ๊ณ ์๋ฃ | |
| ์ด๋ณด ๊ฐ๋ฐ์๋ ์ดํดํ ์ ์๊ฒ Markdown ํ์์ผ๋ก ์์ฑํ์ธ์.""", | |
| "learning": """๋ค์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ํ์ต ์ง๋ฌธ์ ๋ต๋ณํ์ธ์: | |
| ์ง๋ฌธ: {question} | |
| ์์ง๋ ์ ๋ณด: | |
| {summaries} | |
| ๋ต๋ณ ๊ตฌ์กฐ: | |
| 1. ๊ฐ๋ ์ค๋ช (๊ฐ๋จ๋ช ๋ฃ) | |
| 2. ๋์ ์๋ฆฌ | |
| 3. ์์ ์ฝ๋ (์ฃผ์ ํฌํจ) | |
| 4. ์ค๋ฌด ํ์ฉ ํ | |
| 5. ์ถ๊ฐ ํ์ต ์๋ฃ | |
| ์ด๋ณด ๊ฐ๋ฐ์๋ ์ดํดํ ์ ์๊ฒ Markdown ํ์์ผ๋ก ์์ฑํ์ธ์.""", | |
| "code_review": """๋ค์ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก ์ฝ๋ ๋ฆฌ๋ทฐ ์ง๋ฌธ์ ๋ต๋ณํ์ธ์: | |
| ์ง๋ฌธ: {question} | |
| ์์ง๋ ์ ๋ณด: | |
| {summaries} | |
| ๋ต๋ณ ๊ตฌ์กฐ: | |
| 1. ํ์ฌ ์ ๊ทผ ๋ฐฉ์ ๋ถ์ | |
| 2. ๊ฐ์ ํฌ์ธํธ | |
| 3. ๋ฆฌํฉํ ๋ง ์์ | |
| 4. ๋ฒ ์คํธ ํ๋ํฐ์ค | |
| 5. ์ฐธ๊ณ ํจํด | |
| ์ด๋ณด ๊ฐ๋ฐ์๋ ์ดํดํ ์ ์๊ฒ Markdown ํ์์ผ๋ก ์์ฑํ์ธ์.""" | |
| } | |
| template = templates.get(intent, templates["learning"]) | |
| # ์์ฝ ํ ์คํธ ํฌ๋งทํ | |
| summaries_text = "\n\n".join([ | |
| f"์ถ์ฒ: {s['source']} ({s['url']})\n์์ฝ: {s['summary']}" | |
| for s in summaries | |
| ]) | |
| # ์ด์ ๋ํ ๋งฅ๋ฝ ์ถ๊ฐ (messages ์ฌ์ฉ) | |
| context_prefix = "" | |
| messages_history = state.messages | |
| if messages_history and len(messages_history) > 1: | |
| context_prefix = "์ด์ ๋ํ ๋งฅ๋ฝ:\n" | |
| # ์ต๊ทผ 6๊ฐ ๋ฉ์์ง (3ํด) ์ฌ์ฉ | |
| for msg in messages_history[-6:]: | |
| if hasattr(msg, 'type'): | |
| if msg.type == "human": | |
| context_prefix += f"์ฌ์ฉ์: {msg.content}\n" | |
| elif msg.type == "ai": | |
| context_prefix += f"AI: {msg.content[:200]}...\n\n" | |
| context_prefix += "---\nํ์ฌ ์ง๋ฌธ:\n" | |
| final_prompt = (context_prefix + template).format( | |
| question=(state.original_question or state.user_question), | |
| summaries=summaries_text | |
| ) | |
| updates = {} | |
| steps_delta: List[str] = [] | |
| try: | |
| response = llm.invoke([HumanMessage(content=final_prompt)]) | |
| final_answer = response.content.strip() | |
| updates["final_answer"] = final_answer | |
| # Phase 3: ์กฐ๊ฑด๋ถ ์บ์ ์ ์ฅ | |
| # - clarification: ์บ์ ๊ธ์ง (๊ทธ๋ํ ์ generate_with_history๋ก ๋น ์ง์ง๋ง, ๋ฐฉ์ด์ ์ผ๋ก ํ ๋ฒ ๋ ์ฒดํฌ) | |
| # - new_topic/independent: ์บ์ ๊ฐ๋ฅ(should_cache๊ฐ True์ผ ๋) | |
| should_cache = state.should_cache if state.should_cache is not None else True | |
| canonical_question = state.canonical_question | |
| qtype = state.question_type or "independent" | |
| if should_cache and qtype in ["new_topic", "independent"]: | |
| # ์บ์ํ ์ง๋ฌธ: canonical_question ์ฐ์ , ์์ผ๋ฉด ์๋ณธ ์ง๋ฌธ | |
| question_to_cache = canonical_question or state.user_question | |
| await qdrant_manager.save_to_cache( | |
| question=question_to_cache, | |
| answer=final_answer | |
| ) | |
| steps_delta.append(f"โ ์ต์ข ๋ต๋ณ ์์ฑ ์๋ฃ (๊ธธ์ด: {len(final_answer)}์)") | |
| steps_delta.append(f"๐พ ์บ์ ์ ์ฅ ์๋ฃ (์ง๋ฌธ: {question_to_cache[:50]}...)") | |
| logger.info("์ต์ข ๋ต๋ณ ์์ฑ ๋ฐ ์บ์ ์ ์ฅ ์๋ฃ: %s", question_to_cache[:50]) | |
| else: | |
| steps_delta.append(f"โ ์ต์ข ๋ต๋ณ ์์ฑ ์๋ฃ (๊ธธ์ด: {len(final_answer)}์)") | |
| steps_delta.append("โ ๏ธ ์บ์ ์ ์ฅ ์๋ต (๋ ๋ฆฝ์ ์ด์ง ์๊ฑฐ๋ ์ผํ์ฑ ์ง๋ฌธ)") | |
| logger.info("์ต์ข ๋ต๋ณ ์์ฑ ์๋ฃ (์บ์ ์ ์ฅ ์๋ต)") | |
| except Exception as e: | |
| logger.error("๋ต๋ณ ์์ฑ ์คํจ: %s", e, exc_info=True) | |
| updates["final_answer"] = "๋ต๋ณ ์์ฑ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์." | |
| steps_delta.append(f"โ ๋ต๋ณ ์์ฑ ์คํจ: {str(e)}") | |
| updates["intermediate_steps"] = steps_delta | |
| # Phase 4: Multi-question handling | |
| # NOTE: AgentState๋ Pydantic(BaseModel)์ด๋ฏ๋ก dict-style state.get(...) ์ฌ์ฉ ๊ธ์ง | |
| if state.is_multi_question: | |
| answer_text = updates.get("final_answer") | |
| if answer_text: | |
| # Append to multi_answers (reducer will auto-merge) | |
| updates["multi_answers"] = [{ | |
| "index": state.sub_question_index, | |
| "question": state.sub_question_text or state.user_question, | |
| "answer": answer_text | |
| }] | |
| logger.info("๋ค์ค ์ง๋ฌธ ๋ต๋ณ ์ถ๊ฐ: Q%d", state.sub_question_index) | |
| return updates | |
| def return_cached_answer_node(state: AgentState) -> dict: | |
| """ | |
| ์บ์ ํํธ ์ ์ ์ฅ๋ ๋ต๋ณ์ ๋ฐํํฉ๋๋ค. | |
| ๊ฒ์ ๋ฐ ์์ฑ ๊ณผ์ ์ ๊ฑด๋๋ฐ๊ณ ์ฆ์ ๋ต๋ณ์ ์ ๊ณตํฉ๋๋ค. | |
| """ | |
| logger.info("์บ์๋ ๋ต๋ณ ๋ฐํ") | |
| steps_delta = ["๐พ ์บ์๋ ๋ต๋ณ ๋ฐํ (๊ฒ์ ์๋ต)"] | |
| return { | |
| "final_answer": state.cached_result, | |
| "intermediate_steps": steps_delta | |
| } | |
| def handle_too_many_questions_node(state: AgentState) -> dict: | |
| """ | |
| 3๊ฐ ์ด์ ์ง๋ฌธ ์ ์๋ด ๋ฉ์์ง๋ฅผ ๋ฐํํฉ๋๋ค. | |
| ๋ํ๋ฅผ ์ข ๋ฃํ์ง ์๊ณ , ์ฌ์ฉ์๊ฐ ๋ค์ ์ง๋ฌธํ ์ ์๋๋ก ํฉ๋๋ค. | |
| """ | |
| plan = state.plan or {} | |
| error_message = plan.get("error_message", "") | |
| sub_questions = plan.get("sub_questions", []) | |
| logger.info("์ง๋ฌธ ์ ์ด๊ณผ: %d๊ฐ", len(sub_questions)) | |
| default_message = """์ฃ์กํฉ๋๋ค. ํ ๋ฒ์ ์ต๋ 2๊ฐ์ ์ง๋ฌธ๊น์ง๋ง ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. | |
| ๋ค์ ์ค ํ๋๋ฅผ ์ ํํด์ ๋ค์ ์ง๋ฌธํด ์ฃผ์ธ์: | |
| 1. **ํ๋์ ์ฃผ์ ๋ก ํตํฉํด์ ์ง๋ฌธ** | |
| ์: "JWT ์ธ์ฆ๊ณผ CORS ์ค์ ์ ํจ๊ป ๊ตฌํํ๋ ๋ฐฉ๋ฒ" | |
| 2. **๊ฐ์ฅ ์ค์ํ 2๊ฐ ์ง๋ฌธ๋ง ์ ํ** | |
| ์: "JWT๊ฐ ๋ญ์ผ? ๋ด ์ฝ๋์ ์ด๋ป๊ฒ ์ ์ฉํด?" | |
| 3. **์ง๋ฌธ์ ๋๋ ์ ์์ฐจ์ ์ผ๋ก ์ง๋ฌธ** | |
| ์: ๋จผ์ "JWT๊ฐ ๋ญ์ผ?" ์ง๋ฌธ โ ๋ต๋ณ ํ์ธ โ ๋ค์ ์ง๋ฌธ | |
| ์ด๋ป๊ฒ ๋์๋๋ฆด๊น์?""" | |
| final_message = error_message if error_message else default_message | |
| steps_delta = [ | |
| f"โ ๏ธ ์ง๋ฌธ ์ ์ด๊ณผ: {len(sub_questions)}๊ฐ", | |
| "๐ฌ ์๋ด ๋ฉ์์ง ์ ๊ณต (๋ํ ๊ณ์ ๊ฐ๋ฅ)" | |
| ] | |
| return { | |
| "final_answer": final_message, | |
| "intermediate_steps": steps_delta | |
| } | |
| def initiate_dynamic_search_node(state: AgentState) -> dict: | |
| """ | |
| ๋ค์ค ์ง๋ฌธ ์ฒ๋ฆฌ์ ์ง์ ๋ ธ๋. | |
| IMPORTANT: | |
| - LangGraph์์ `List[Send]`๋ **๋ ธ๋ ๋ฐํ๊ฐ**์ด ์๋๋ผ, | |
| `add_conditional_edges(...)`์ ์ ๋ฌํ๋ **edge ํจ์ ๋ฐํ๊ฐ**์ผ๋ก๋ง ์ฌ์ฉํด์ผ ํฉ๋๋ค. | |
| - ๋ฐ๋ผ์ ์ด ๋ ธ๋๋ dict ์ ๋ฐ์ดํธ๋ง ๋ฐํํ๊ณ , | |
| ์ค์ fan-out์ ๋ณ๋ edge ํจ์(`fanout_multi_questions`)๊ฐ ๋ด๋นํฉ๋๋ค. | |
| """ | |
| plan = state.plan or {} | |
| sub_questions = plan.get("sub_questions", []) | |
| logger.info("๋์ ๋ณต์ ์ค๋น: %d๊ฐ ์ง๋ฌธ", len(sub_questions)) | |
| return { | |
| "intermediate_steps": [f"๐ ๋ค์ค ์ง๋ฌธ fan-out ์ค๋น: {len(sub_questions)}๊ฐ"] | |
| } | |
| def fanout_multi_questions(state: AgentState): | |
| """ | |
| ๋ค์ค ์ง๋ฌธ์ Send API๋ก fan-out ํฉ๋๋ค. | |
| ๋ฐํ๊ฐ(List[Send])์ conditional edge ํจ์์์๋ง ํ์ฉ๋ฉ๋๋ค. | |
| """ | |
| from langgraph.types import Send | |
| plan = state.plan or {} | |
| sub_questions = plan.get("sub_questions", []) | |
| original_question = state.user_question | |
| messages = state.messages | |
| logger.info("๋์ ๋ณต์ : %d๊ฐ ์ง๋ฌธ์ ๊ฐ๊ฐ ์ ์ฒด ๊ทธ๋ํ๋ก ์คํ", len(sub_questions)) | |
| sends = [] | |
| for i, sq in enumerate(sub_questions): | |
| # IMPORTANT: ์ด ํ๋ก์ ํธ๋ AgentState(BaseModel)๋ฅผ ๋ ธ๋ ์ ๋ ฅ์ผ๋ก ์ฌ์ฉํ๋ฏ๋ก, | |
| # Send arg๋ dict๊ฐ ์๋๋ผ AgentState ์ธ์คํด์ค๋ก ๋ณด๋ด์ผ ํฉ๋๋ค. | |
| child = state.model_copy(deep=True) | |
| # ์ง๋ฌธ ๊ต์ฒด + ๋ค์ค ์ง๋ฌธ ๋ฉํ๋ฐ์ดํฐ | |
| child.user_question = sq | |
| child.is_multi_question = True | |
| child.sub_question_index = i | |
| child.sub_question_text = sq | |
| child.original_multi_question = original_question | |
| # ๊ณตํต ์ ์ง ํ๋ | |
| child.messages = messages | |
| child.plan = plan | |
| # ๊ธฐ์กด ๊ทธ๋ํ๊ฐ ๋ค์ ์ฑ์ธ ํ๋๋ค์ ์ด๊ธฐํ | |
| child.question_type = None | |
| child.should_cache = None | |
| child.canonical_question = None | |
| child.analysis_reasoning = None | |
| child.cached_result = None | |
| child.detected_intent = None | |
| child.search_results = [] | |
| child.subtask_results = {} | |
| child.refinement_count = 0 | |
| child.needs_refinement = False | |
| child.original_question = None | |
| child.final_answer = None | |
| child.multi_answers = [] | |
| child.intermediate_steps = [f"๐ ์ง๋ฌธ {i+1}/{len(sub_questions)}: {sq[:50]}"] | |
| # ๋ค์ค ์ง๋ฌธ์ outer graph์์ ๊ธฐ์กด ํ์ดํ๋ผ์ธ ์ ์ฒด๋ฅผ ๋ณ๋ ฌ๋ก ๋๋ฆฌ๋ฉด | |
| # scalar state ์ฑ๋(question_type ๋ฑ)์์ concurrent update ์ถฉ๋์ด ๋ฉ๋๋ค. | |
| # ๋ฐ๋ผ์ worker ๋ ธ๋ ์์์ '๋จ์ผ ์ง๋ฌธ ๊ทธ๋ํ'๋ฅผ ๋ณ๋๋ก ์คํํ ๋ค, | |
| # outer state์๋ multi_answers(reducer)๋ง ์ ๋ฐ์ดํธํฉ๋๋ค. | |
| sends.append(Send("run_single_question_worker", child)) | |
| return sends | |
| def combine_answers_node(state: AgentState) -> dict: | |
| """ | |
| Fan-in: ๋ชจ๋ Send๊ฐ ์๋ฃ๋๋ฉด multi_answers๋ฅผ ์กฐํฉํฉ๋๋ค. | |
| Reducer (Annotated[List[dict], add])๊ฐ ์๋์ผ๋ก | |
| ๋ชจ๋ parallel Send์ ๊ฒฐ๊ณผ๋ฅผ multi_answers์ ๋ชจ์๋ก๋๋ค. | |
| ์ด ๋ ธ๋๋ ๋จ์ํ ๋ชจ์์ง ๊ฒฐ๊ณผ๋ฅผ ์ฝ์ด์ Markdown์ผ๋ก ์กฐํฉํฉ๋๋ค. | |
| """ | |
| answers = state.multi_answers | |
| original_question = state.original_multi_question or state.user_question | |
| if not answers: | |
| logger.error("๋ค์ค ๋ต๋ณ์ด ๋น์ด์์") | |
| return { | |
| "final_answer": "๋ต๋ณ ์์ฑ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.", | |
| "intermediate_steps": ["โ multi_answers ๋น์ด์์"] | |
| } | |
| # ์ธ๋ฑ์ค ์์ผ๋ก ์ ๋ ฌ | |
| answers.sort(key=lambda x: x["index"]) | |
| # Markdown ํ์์ผ๋ก ์กฐํฉ | |
| combined_parts = [] | |
| for ans in answers: | |
| section = f"""## {ans['index']+1}. {ans['question']} | |
| {ans['answer']}""" | |
| combined_parts.append(section) | |
| combined = "\n\n---\n\n".join(combined_parts) | |
| # ํค๋ ์ถ๊ฐ | |
| header = f"# ๋ค์ค ์ง๋ฌธ ๋ต๋ณ\n\n์๋ณธ ์ง๋ฌธ: {original_question}\n\n---\n\n" | |
| final_combined = header + combined | |
| logger.info("๋ค์ค ๋ต๋ณ ์กฐํฉ ์๋ฃ: %d๊ฐ", len(answers)) | |
| return { | |
| "final_answer": final_combined, | |
| "intermediate_steps": [f"โ {len(answers)}๊ฐ ๋ต๋ณ ์กฐํฉ ์๋ฃ"] | |
| } | |
| def _build_search_subgraph_local() -> StateGraph: | |
| """nodes.py ๋ด๋ถ์์ ๋จ์ผ ์ง๋ฌธ ๊ทธ๋ํ์ฉ ๊ฒ์ ์๋ธ๊ทธ๋ํ๋ฅผ ๊ตฌ์ฑ.""" | |
| subgraph = StateGraph(AgentState) | |
| subgraph.add_node("filter_and_score", filter_and_score_node) | |
| subgraph.add_node("summarize_results", summarize_results_node) | |
| subgraph.add_edge(START, "filter_and_score") | |
| subgraph.add_edge("filter_and_score", "summarize_results") | |
| subgraph.add_edge("summarize_results", END) | |
| return subgraph.compile() | |
| def _get_single_question_agent(): | |
| """ | |
| ๋ค์ค ์ง๋ฌธ worker์์ ์ฌ์ฉํ '๋จ์ผ ์ง๋ฌธ ํ์ดํ๋ผ์ธ' ๊ทธ๋ํ๋ฅผ lazy-compile ํด์ ์บ์ฑํฉ๋๋ค. | |
| (outer state ์ถฉ๋์ ํผํ๊ธฐ ์ํด, worker ๋ด๋ถ์์ ๋ณ๋ ๊ทธ๋ํ๋ฅผ ์คํ) | |
| """ | |
| global _SINGLE_QUESTION_AGENT # type: ignore[name-defined] | |
| try: | |
| return _SINGLE_QUESTION_AGENT # type: ignore[name-defined] | |
| except Exception: | |
| pass | |
| # ---- routing helpers (graph.py ์ ๋จ์ผ ์ง๋ฌธ ํ๋ฆ๊ณผ ๋์ผ) ---- | |
| def _route_after_analysis(s: AgentState) -> Literal["generate_with_history", "check_cache"]: | |
| raw_qtype = s.question_type or "independent" | |
| legacy_map = {"followup": "clarification", "cache_candidate": "independent", "new_search": "independent"} | |
| question_type = legacy_map.get(raw_qtype, raw_qtype) | |
| return "generate_with_history" if question_type == "clarification" else "check_cache" | |
| def _route_after_cache(s: AgentState) -> Literal["return_cached_answer", "classify_intent"]: | |
| return "return_cached_answer" if s.cached_result else "classify_intent" | |
| def _route_after_evaluation(s: AgentState) -> Literal["refine_search", "search_subgraph"]: | |
| if s.needs_refinement and s.refinement_count < 1: | |
| return "refine_search" | |
| return "search_subgraph" | |
| def _initiate_parallel_search(s: AgentState): | |
| return [ | |
| Send("search_stackoverflow", s), | |
| Send("search_github", s), | |
| Send("search_official_docs", s), | |
| ] | |
| # ---- build ---- | |
| g = StateGraph(AgentState) | |
| g.add_node("analyze_question", analyze_question_node) | |
| g.add_node("generate_with_history", generate_with_history_node) | |
| g.add_node("check_cache", check_cache_node) | |
| g.add_node("return_cached_answer", return_cached_answer_node) | |
| g.add_node("classify_intent", classify_intent_node) | |
| g.add_node("search_stackoverflow", search_stackoverflow_node) | |
| g.add_node("search_github", search_github_node) | |
| g.add_node("search_official_docs", search_official_docs_node) | |
| g.add_node("collect_results", collect_results_node) | |
| g.add_node("evaluate_results", evaluate_results_node) | |
| g.add_node("refine_search", refine_search_node) | |
| g.add_node("generate_answer", generate_answer_node) | |
| search_subgraph = _build_search_subgraph_local() | |
| g.add_node("search_subgraph", search_subgraph) | |
| g.add_edge(START, "analyze_question") | |
| g.add_conditional_edges( | |
| "analyze_question", | |
| _route_after_analysis, | |
| {"generate_with_history": "generate_with_history", "check_cache": "check_cache"}, | |
| ) | |
| g.add_edge("generate_with_history", END) | |
| g.add_conditional_edges( | |
| "check_cache", | |
| _route_after_cache, | |
| {"return_cached_answer": "return_cached_answer", "classify_intent": "classify_intent"}, | |
| ) | |
| g.add_edge("return_cached_answer", END) | |
| g.add_conditional_edges("classify_intent", _initiate_parallel_search) | |
| g.add_edge("search_stackoverflow", "collect_results") | |
| g.add_edge("search_github", "collect_results") | |
| g.add_edge("search_official_docs", "collect_results") | |
| g.add_edge("collect_results", "evaluate_results") | |
| g.add_conditional_edges( | |
| "evaluate_results", | |
| _route_after_evaluation, | |
| {"refine_search": "refine_search", "search_subgraph": "search_subgraph"}, | |
| ) | |
| g.add_edge("refine_search", "classify_intent") | |
| g.add_edge("search_subgraph", "generate_answer") | |
| g.add_edge("generate_answer", END) | |
| _SINGLE_QUESTION_AGENT = g.compile() | |
| return _SINGLE_QUESTION_AGENT | |
| async def run_single_question_worker_node(state: AgentState) -> dict: | |
| """ | |
| ๋ค์ค ์ง๋ฌธ์ ๊ฐ ์๋ธ ์ง๋ฌธ์ '๋จ์ผ ์ง๋ฌธ ๊ทธ๋ํ'๋ก ์คํํ ๋ค, | |
| outer graph์๋ reducer ์ฑ๋(multi_answers)๋ง ์ ๋ฐ์ดํธํฉ๋๋ค. | |
| """ | |
| agent = _get_single_question_agent() | |
| # inner ์คํ์ multi-question ํ๋๊ทธ๋ฅผ ๊บผ์(=multi_answers append ๋ฐฉ์ง) | |
| inner = state.model_copy(deep=True) | |
| inner.is_multi_question = False | |
| inner.multi_answers = [] | |
| result = await agent.ainvoke( | |
| { | |
| "user_question": inner.user_question, | |
| "messages": inner.messages, | |
| } | |
| ) | |
| answer_text = result.get("final_answer") or "" | |
| return { | |
| "multi_answers": [ | |
| { | |
| "index": state.sub_question_index, | |
| "question": state.sub_question_text or state.user_question, | |
| "answer": answer_text, | |
| } | |
| ], | |
| "intermediate_steps": [f"โ ์๋ธ ์ง๋ฌธ {state.sub_question_index + 1} ์ฒ๋ฆฌ ์๋ฃ"], | |
| } | |
| async def generate_with_history_node(state: AgentState) -> dict: | |
| """ | |
| ๋ํ ํ์คํ ๋ฆฌ๋ง ์ฌ์ฉํ์ฌ ํ์ ์ง๋ฌธ์ ๋ต๋ณํฉ๋๋ค. | |
| Phase 2: Follow-up Handler | |
| - ์บ์ ๊ฒ์ ์ ํจ | |
| - ์น ๊ฒ์ ์ ํจ | |
| - ์บ์์ ์ ์ฅ ์ ํจ | |
| - messages ํ์คํ ๋ฆฌ๋ง ํ์ฉ | |
| """ | |
| user_question = state.user_question | |
| messages_history = state.messages | |
| logger.info("๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ ์์ฑ: %s", user_question[:50]) | |
| # ๋ํ ๋งฅ๋ฝ ๊ตฌ์ฑ | |
| context_prompt = "์ด์ ๋ํ๋ฅผ ์ฐธ๊ณ ํ์ฌ ํ์ ์ง๋ฌธ์ ๋ต๋ณํ์ธ์.\n\n" | |
| if messages_history: | |
| context_prompt += "๋ํ ๋ด์ญ:\n" | |
| for msg in messages_history[:-1]: # ํ์ฌ ์ง๋ฌธ ์ ์ธ | |
| if hasattr(msg, 'type') and hasattr(msg, 'content'): | |
| role = "์ฌ์ฉ์" if msg.type == "human" else "AI" | |
| context_prompt += f"{role}: {msg.content}\n\n" | |
| context_prompt += f"ํ์ฌ ์ง๋ฌธ: {user_question}\n\n" | |
| context_prompt += "์ด์ ๋ํ ๋งฅ๋ฝ์ ๊ณ ๋ คํ์ฌ ์์ธํ๊ณ ์น์ ํ๊ฒ ๋ต๋ณํ์ธ์." | |
| updates = {} | |
| steps_delta: List[str] = [] | |
| try: | |
| response = llm.invoke([HumanMessage(content=context_prompt)]) | |
| final_answer = response.content.strip() | |
| updates["final_answer"] = final_answer | |
| steps_delta.append(f"๐ฌ ๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ ์์ฑ (๊ธธ์ด: {len(final_answer)}์)") | |
| steps_delta.append("โ ๏ธ ์บ์ ์ ์ฅ ์๋ต (๋ณด์ถฉ ์์ฒญ)") | |
| logger.info("๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ ์์ฑ ์๋ฃ") | |
| except Exception as e: | |
| logger.error("๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ ์์ฑ ์คํจ: %s", e, exc_info=True) | |
| updates["final_answer"] = "๋ต๋ณ ์์ฑ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์." | |
| steps_delta.append(f"โ ๋ต๋ณ ์์ฑ ์คํจ: {str(e)}") | |
| updates["intermediate_steps"] = steps_delta | |
| return updates | |