Spaces:
Sleeping
Sleeping
| """ | |
| CodeWeaver LangGraph ์ํฌํ๋ก์ฐ ๊ตฌ์ฑ. | |
| LangGraph 6๊ฐ์ง ํต์ฌ ๊ธฐ๋ฅ ์๋ฒฝ ๊ตฌํ: | |
| โ Conditional Edges: ์ง๋ฌธ ์ ํ, ์บ์ ์ฌ๋ถ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ | |
| โ Send API: 3๊ฐ ๊ฒ์ ๋ ธ๋ ๋ณ๋ ฌ ์คํ (fan-out/fan-in) | |
| โ Subgraph: ๊ฒ์ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ | |
| โ Map-Reduce: Send API๋ก ๋ณ๋ ฌ ๊ฒ์ โ ๊ฒฐ๊ณผ ๋จธ์ง | |
| โ Checkpointing: MemorySaver๋ก ๋ํ ์ํ ์ ์ฅ | |
| โ Pydantic Typed State: ํ์ ์์ ์ฑ ๋ณด์ฅ | |
| """ | |
| import logging | |
| from typing import Literal | |
| from langgraph.checkpoint.memory import MemorySaver | |
| from langgraph.graph import StateGraph, START, END | |
| from langgraph.types import Send | |
| from src.agent.state import AgentState | |
| from src.agent.nodes import ( | |
| analyze_question_node, | |
| check_cache_node, | |
| create_plan_node, | |
| classify_intent_node, | |
| search_stackoverflow_node, | |
| search_github_node, | |
| search_official_docs_node, | |
| collect_results_node, | |
| evaluate_results_node, | |
| refine_search_node, | |
| filter_and_score_node, | |
| summarize_results_node, | |
| generate_answer_node, | |
| return_cached_answer_node, | |
| generate_with_history_node, | |
| handle_too_many_questions_node, | |
| initiate_dynamic_search_node, | |
| combine_answers_node, | |
| fanout_multi_questions, | |
| run_single_question_worker_node, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| def build_search_subgraph() -> StateGraph: | |
| """ | |
| ๊ฒ์ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ ์๋ธ๊ทธ๋ํ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค. | |
| ํ๋ฆ: filter_and_score โ summarize_results | |
| ์ด ์๋ธ๊ทธ๋ํ๋ ๋ฉ์ธ ๊ทธ๋ํ์์ ํ๋์ ๋ ธ๋์ฒ๋ผ ๋์ํ๋ฉฐ, | |
| ๊ฒ์ ๊ฒฐ๊ณผ์ ํํฐ๋ง๊ณผ ์์ฝ์ ๋ด๋นํฉ๋๋ค. | |
| Returns: | |
| ์ปดํ์ผ๋ ์๋ธ๊ทธ๋ํ | |
| """ | |
| # ์๋ธ๊ทธ๋ํ ์์ฑ (AgentState ์ฌ์ฉ) | |
| subgraph = StateGraph(AgentState) | |
| # ๋ ธ๋ ์ถ๊ฐ | |
| subgraph.add_node("filter_and_score", filter_and_score_node) | |
| subgraph.add_node("summarize_results", summarize_results_node) | |
| # ์๋ธ๊ทธ๋ํ ๋ด๋ถ ํ๋ฆ ์ ์ | |
| # START โ filter_and_score โ summarize_results โ END | |
| 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 route_after_analysis(state: AgentState) -> Literal["generate_with_history", "check_cache"]: | |
| """ | |
| ์ง๋ฌธ ๋ถ์ ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ค์ ๋ ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. | |
| Phase 2: New Routing Structure | |
| Args: | |
| state: ํ์ฌ ์์ด์ ํธ ์ํ | |
| Returns: | |
| - "generate_with_history": ํ์ ์ง๋ฌธ โ ๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ | |
| - "check_cache": ๋ ๋ฆฝ ์ง๋ฌธ โ ์บ์ ํ์ธ | |
| """ | |
| # NOTE: ๊ณผ๊ฑฐ ์ฒดํฌํฌ์ธํธ/๊ตฌ๋ฒ์ ์ํ๊ฐ ํธํ์ ์ํด ๊ตฌ๊ฐ๋ ๋งคํ ์ฒ๋ฆฌ | |
| raw_qtype = state.question_type or "independent" | |
| legacy_map = { | |
| "followup": "clarification", | |
| "cache_candidate": "independent", | |
| "new_search": "independent", | |
| } | |
| question_type = legacy_map.get(raw_qtype, raw_qtype) | |
| if question_type == "clarification": | |
| return "generate_with_history" | |
| # new_topic / independent ๋ ๋ชจ๋ ์บ์ ํ์ธ(ํํธ๋ฉด ๊ฒ์ ์๋ต, ๋ฏธ์ค๋ฉด ๊ฒ์) | |
| return "check_cache" | |
| def route_after_plan(state: AgentState) -> Literal["analyze_question", "initiate_dynamic_search", "handle_too_many_questions"]: | |
| """ | |
| create_plan ๊ฒฐ๊ณผ์ ๋ฐ๋ผ ๋ค์ ๋ ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. | |
| Phase 4: Dynamic Parallel Search | |
| Args: | |
| state: ํ์ฌ ์์ด์ ํธ ์ํ | |
| Returns: | |
| - "analyze_question": ๋จ์ผ ์ฃผ์ โ ๊ธฐ์กด ๊ทธ๋ํ ์คํ | |
| - "initiate_dynamic_search": ๋ค์ค ์ง๋ฌธ (2๊ฐ) โ Send API๋ก ๊ทธ๋ํ 2ํ ์คํ | |
| - "handle_too_many_questions": ์ง๋ฌธ 3๊ฐ ์ด์ โ ์๋ฌ ๋ฉ์์ง | |
| """ | |
| plan = state.plan or {} | |
| case = plan.get("case", "single_topic") | |
| if case == "too_many": | |
| return "handle_too_many_questions" | |
| elif case == "multiple_questions": | |
| return "initiate_dynamic_search" | |
| else: | |
| return "analyze_question" | |
| def route_after_cache(state: AgentState) -> Literal["return_cached_answer", "classify_intent"]: | |
| """ | |
| ์บ์ ํํธ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ค์ ๋ ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. | |
| Phase 3 โ Phase 4: create_plan ์ ๊ฑฐ๋จ (์ด๋ฏธ START์์ ์คํ) | |
| Args: | |
| state: ํ์ฌ ์์ด์ ํธ ์ํ | |
| Returns: | |
| - "return_cached_answer": ์บ์ ํํธ ์ ์ฆ์ ๋ต๋ณ ๋ฐํ | |
| - "classify_intent": ์บ์ ๋ฏธ์ค ์ ์๋ ๋ถ๋ฅ | |
| """ | |
| if state.cached_result: | |
| return "return_cached_answer" | |
| else: | |
| return "classify_intent" | |
| def route_after_generate(state: AgentState) -> Literal["combine_answers", END]: | |
| """ | |
| generate_answer ํ ๋ค์ ๋ ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. | |
| Phase 4: Dynamic Parallel Search | |
| Args: | |
| state: ํ์ฌ ์์ด์ ํธ ์ํ | |
| Returns: | |
| - "combine_answers": ๋ค์ค ์ง๋ฌธ โ ๋ต๋ณ ํตํฉ | |
| - END: ๋จ์ผ ์ง๋ฌธ โ ์ข ๋ฃ | |
| """ | |
| if state.is_multi_question: | |
| return "combine_answers" | |
| return END | |
| def route_after_evaluation(state: AgentState) -> Literal["refine_search", "search_subgraph"]: | |
| """ | |
| ๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ ํ ๋ค์ ๋ ธ๋๋ฅผ ๊ฒฐ์ ํฉ๋๋ค. | |
| Phase 3: Open Deep Research ํจํด - ์ฟผ๋ฆฌ ๊ฐ์ ๋ฃจํ | |
| Args: | |
| state: ํ์ฌ ์์ด์ ํธ ์ํ | |
| Returns: | |
| - "refine_search": ๊ฒฐ๊ณผ ๋ถ์กฑ & ๊ฐ์ ํ์ 0ํ โ ์ฟผ๋ฆฌ ๊ฐ์ | |
| - "search_subgraph": ๊ฒฐ๊ณผ ์ถฉ๋ถ or ๊ฐ์ ํ์ 1ํ โ ํํฐ๋ง ์งํ | |
| """ | |
| needs_refinement = state.needs_refinement | |
| refinement_count = state.refinement_count | |
| # ์์ ์ฅ์น: ์ต๋ 1ํ๋ง ๊ฐ์ | |
| if needs_refinement and refinement_count < 1: | |
| return "refine_search" | |
| else: | |
| return "search_subgraph" | |
| def initiate_parallel_search(state: AgentState): | |
| """ | |
| Send API๋ฅผ ์ฌ์ฉํ์ฌ 3๊ฐ์ ๊ฒ์ ๋ ธ๋๋ฅผ ๋ณ๋ ฌ๋ก ์คํํฉ๋๋ค. | |
| LangGraph Send API (Map-Reduce ํจํด): | |
| - ๊ฐ ๊ฒ์ ๋ ธ๋์ ๋์ผํ state๋ฅผ ์ ์ก | |
| - ๋ชจ๋ ๋ ธ๋๊ฐ ๋ณ๋ ฌ๋ก ์คํ๋จ | |
| - ๊ฒฐ๊ณผ๋ ์๋์ผ๋ก ๋จธ์ง๋จ | |
| Args: | |
| state: ํ์ฌ ์์ด์ ํธ ์ํ | |
| Returns: | |
| Send ๊ฐ์ฒด ๋ฆฌ์คํธ (fan-out) | |
| """ | |
| # Send API๋ฅผ ์ฌ์ฉํ fan-out | |
| # 3๊ฐ์ ๊ฒ์ ๋ ธ๋๊ฐ ๋์์ ์คํ๋จ | |
| return [ | |
| Send("search_stackoverflow", state), | |
| Send("search_github", state), | |
| Send("search_official_docs", state), | |
| ] | |
| def build_agent_graph() -> StateGraph: | |
| """ | |
| CodeWeaver ์์ด์ ํธ์ ๋ฉ์ธ ๊ทธ๋ํ๋ฅผ ๊ตฌ์ฑํฉ๋๋ค. | |
| Phase 4: Dynamic Parallel Search for Multiple Questions | |
| ์ ์ฒด ํ๋ฆ: | |
| 1. START โ create_plan (์ง๋ฌธ ์ ํ ๋ฐ ๊ฐ์ ํ๋จ) | |
| 2. ์ง๋ฌธ ์ ํ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ: | |
| - single_topic: analyze_question โ ๊ธฐ์กด ๊ทธ๋ํ | |
| - multiple_questions: initiate_dynamic_search โ Send API (๊ฐ ์ง๋ฌธ๋ง๋ค ๊ธฐ์กด ๊ทธ๋ํ ๋ ๋ฆฝ ์คํ) | |
| - too_many: handle_too_many_questions โ END | |
| 3. analyze_question โ ์ง๋ฌธ ๋ถ์ | |
| - clarification: generate_with_history โ END | |
| - new_topic/independent: check_cache | |
| 4. ์บ์ ํ์ธ: | |
| - ํํธ: return_cached_answer โ END | |
| - ๋ฏธ์ค: classify_intent | |
| 5. Send API (๋ณ๋ ฌ ๊ฒ์ fan-out): | |
| - classify_intent โ 3๊ฐ ๊ฒ์ ๋ ธ๋ ๋ณ๋ ฌ ์คํ | |
| 6. collect_results (fan-in) โ evaluate_results | |
| 7. ๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ: | |
| - ๋ถ์กฑ & refinement_count < 1: refine_search โ classify_intent (๋ฃจํ) | |
| - ์ถฉ๋ถ or refinement_count >= 1: search_subgraph | |
| 8. search_subgraph (filter โ summarize) | |
| 9. search_subgraph โ generate_answer | |
| 10. generate_answer ํ ๋ถ๊ธฐ: | |
| - is_multi_question: combine_answers โ END | |
| - ๋จ์ผ ์ง๋ฌธ: END | |
| ํต์ฌ ๊ฐ์ ์ฌํญ (Phase 4): | |
| - โ create_plan์ START๋ก ์ด๋ (์ง๋ฌธ ๊ฐ์ ๋จผ์ ๊ฐ์ง) | |
| - โ Send API๋ก ๊ธฐ์กด ๊ทธ๋ํ ์ฌ์ฌ์ฉ (์ฝ๋ ์ค๋ณต ์์) | |
| - โ ์ง๋ฌธ 3๊ฐ ์ด์ ์ ์น์ ํ ์๋ฌ ๋ฉ์์ง | |
| - โ Reducer ํจํด์ผ๋ก ์๋ fan-in | |
| Returns: | |
| ๊ตฌ์ฑ๋ StateGraph (์ปดํ์ผ ์ ) | |
| """ | |
| # ๋ฉ์ธ ๊ทธ๋ํ ์์ฑ | |
| graph = StateGraph(AgentState) | |
| # Phase 4: ๊ณํ ์๋ฆฝ (START ์งํ) | |
| graph.add_node("create_plan", create_plan_node) | |
| graph.add_node("handle_too_many_questions", handle_too_many_questions_node) | |
| graph.add_node("initiate_dynamic_search", initiate_dynamic_search_node) | |
| # Phase 2: ์ง๋ฌธ ๋ถ์ & ๋ํ ํ์คํ ๋ฆฌ ์ฒ๋ฆฌ | |
| graph.add_node("analyze_question", analyze_question_node) | |
| graph.add_node("generate_with_history", generate_with_history_node) | |
| # ์บ์ ๊ด๋ จ | |
| graph.add_node("check_cache", check_cache_node) | |
| graph.add_node("return_cached_answer", return_cached_answer_node) | |
| # ์๋ ๋ถ๋ฅ | |
| graph.add_node("classify_intent", classify_intent_node) | |
| # Send API๋ฅผ ์ํ ๋ณ๋ ฌ ๊ฒ์ ๋ ธ๋ | |
| graph.add_node("search_stackoverflow", search_stackoverflow_node) | |
| graph.add_node("search_github", search_github_node) | |
| graph.add_node("search_official_docs", search_official_docs_node) | |
| # Phase 3: ๊ฒฐ๊ณผ ์์ง ๋ฐ ํ๊ฐ | |
| graph.add_node("collect_results", collect_results_node) | |
| graph.add_node("evaluate_results", evaluate_results_node) | |
| graph.add_node("refine_search", refine_search_node) | |
| # ์ต์ข ๋ต๋ณ ์์ฑ | |
| graph.add_node("generate_answer", generate_answer_node) | |
| # Phase 4: ๋ค์ค ์ง๋ฌธ ๋ต๋ณ ํตํฉ | |
| graph.add_node("combine_answers", combine_answers_node) | |
| graph.add_node("run_single_question_worker", run_single_question_worker_node) | |
| # ์๋ธ๊ทธ๋ํ (ํํฐ๋ง & ์์ฝ) | |
| search_subgraph = build_search_subgraph() | |
| graph.add_node("search_subgraph", search_subgraph) | |
| # ===== ์ฃ์ง ๊ตฌ์ฑ ===== | |
| # 1. START โ create_plan (Phase 4: ์ง์ ์ ๋ณ๊ฒฝ) | |
| graph.add_edge(START, "create_plan") | |
| # 2. create_plan โ ๋ถ๊ธฐ (Phase 4: ์ง๋ฌธ ์ ํ๋ณ ๋ถ๊ธฐ) | |
| graph.add_conditional_edges( | |
| "create_plan", | |
| route_after_plan, | |
| { | |
| "analyze_question": "analyze_question", | |
| "initiate_dynamic_search": "initiate_dynamic_search", | |
| "handle_too_many_questions": "handle_too_many_questions", | |
| } | |
| ) | |
| # 3. handle_too_many_questions โ END | |
| graph.add_edge("handle_too_many_questions", END) | |
| # 4. initiate_dynamic_search๋ Send ๋ฆฌํด (๊ฐ Send๊ฐ analyze_question์ผ๋ก) | |
| # ์ค์ fan-out์ conditional edge ํจ์์์ ์ํํด์ผ ํจ | |
| graph.add_conditional_edges( | |
| "initiate_dynamic_search", | |
| fanout_multi_questions, | |
| ) | |
| # multi-question worker๋ค์ด ๋๋๋ฉด reducer(multi_answers)์ ๋ชจ์ธ ๊ฒฐ๊ณผ๋ฅผ ํฉ์นฉ๋๋ค. | |
| # Fan-in: ๋ worker๊ฐ ๋ชจ๋ ์ด edge๋ก ๋ค์ด์ค๋ฉด combine_answers๋ 1ํ ์คํ๋ฉ๋๋ค. | |
| graph.add_edge("run_single_question_worker", "combine_answers") | |
| # 5. ์ง๋ฌธ ๋ถ์ ๊ฒฐ๊ณผ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ | |
| graph.add_conditional_edges( | |
| "analyze_question", | |
| route_after_analysis, | |
| { | |
| "generate_with_history": "generate_with_history", | |
| "check_cache": "check_cache", | |
| } | |
| ) | |
| # 6. ๋ํ ํ์คํ ๋ฆฌ ๊ธฐ๋ฐ ๋ต๋ณ โ END | |
| graph.add_edge("generate_with_history", END) | |
| # 7. ์บ์ ํ์ธ ๊ฒฐ๊ณผ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ (Phase 4: create_plan ์ ๊ฑฐ๋จ) | |
| graph.add_conditional_edges( | |
| "check_cache", | |
| route_after_cache, | |
| { | |
| "return_cached_answer": "return_cached_answer", | |
| "classify_intent": "classify_intent", | |
| } | |
| ) | |
| # 8. ์บ์ ํํธ ์ ์ฆ์ ์ข ๋ฃ | |
| graph.add_edge("return_cached_answer", END) | |
| # 9. Send API๋ฅผ ์ฌ์ฉํ ๋ณ๋ ฌ ๊ฒ์ (fan-out) | |
| graph.add_conditional_edges( | |
| "classify_intent", | |
| initiate_parallel_search, | |
| ) | |
| # 10. ๋ชจ๋ ๊ฒ์ ๋ ธ๋ โ collect_results (fan-in) | |
| graph.add_edge("search_stackoverflow", "collect_results") | |
| graph.add_edge("search_github", "collect_results") | |
| graph.add_edge("search_official_docs", "collect_results") | |
| # 11. collect_results โ evaluate_results | |
| graph.add_edge("collect_results", "evaluate_results") | |
| # 12. ๊ฒ์ ๊ฒฐ๊ณผ ํ๊ฐ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ (Phase 3: refine_search ์ถ๊ฐ) | |
| graph.add_conditional_edges( | |
| "evaluate_results", | |
| route_after_evaluation, | |
| { | |
| "refine_search": "refine_search", | |
| "search_subgraph": "search_subgraph", | |
| } | |
| ) | |
| # 13. ์ฟผ๋ฆฌ ๊ฐ์ โ ์๋ ๋ถ๋ฅ (๋ฃจํ) | |
| graph.add_edge("refine_search", "classify_intent") | |
| # 14. ์๋ธ๊ทธ๋ํ โ ์ต์ข ๋ต๋ณ ์์ฑ | |
| graph.add_edge("search_subgraph", "generate_answer") | |
| # 15. ์ต์ข ๋ต๋ณ ํ ๋ถ๊ธฐ (Phase 4: ๋ค์ค ์ง๋ฌธ ์ฒ๋ฆฌ) | |
| graph.add_conditional_edges( | |
| "generate_answer", | |
| route_after_generate, | |
| { | |
| "combine_answers": "combine_answers", | |
| END: END | |
| } | |
| ) | |
| # 16. combine_answers โ ์ข ๋ฃ | |
| graph.add_edge("combine_answers", END) | |
| return graph | |
| def create_agent(enable_checkpointing: bool = True): | |
| """ | |
| CodeWeaver ์์ด์ ํธ๋ฅผ ์์ฑํ๊ณ ์ปดํ์ผํฉ๋๋ค. | |
| Args: | |
| enable_checkpointing: ์ฒดํฌํฌ์ธํธ ํ์ฑํ ์ฌ๋ถ | |
| - True: MemorySaver ์ฌ์ฉ (๊ฐ๋ฐ/ํ ์คํธ์ฉ) | |
| - False: ์ฒดํฌํฌ์ธํธ ์์ด ์คํ (์ํ ์ ์ฅ ๋ถ๊ฐ) | |
| Returns: | |
| ์ปดํ์ผ๋ ์คํ ๊ฐ๋ฅํ ๊ทธ๋ํ | |
| Note: | |
| ํ๋ก๋์ ํ๊ฒฝ์์๋ MemorySaver ๋์ | |
| PostgresSaver, SqliteSaver ๋ฑ ์๊ตฌ ์ ์ฅ์ ์ฌ์ฉ ๊ถ์ฅ | |
| """ | |
| graph = build_agent_graph() | |
| if enable_checkpointing: | |
| # ๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ ์ฒดํฌํฌ์ธํฐ (ํ๋ก๋์ ์์๋ DB ์ฌ์ฉ ๊ถ์ฅ) | |
| memory = MemorySaver() | |
| return graph.compile(checkpointer=memory) | |
| else: | |
| return graph.compile() | |
| # ์์ด์ ํธ ์ธ์คํด์ค ์์ฑ (๋ชจ๋ ์ํฌํธ ์ ์๋ ์์ฑ) | |
| agent = create_agent(enable_checkpointing=True) | |