├── 03-Modules ├── 04-MCP │ ├── server │ │ ├── __init__.py │ │ ├── rag │ │ │ ├── __init__.py │ │ │ ├── utils.py │ │ │ ├── pdf.py │ │ │ └── base.py │ │ ├── data │ │ │ └── SPRI_AI_Brief_2023년12월호_F.pdf │ │ ├── mcp_server_local.py │ │ ├── mcp_server_remote.py │ │ ├── mcp_server_rag.py │ │ └── mcp_rag_server.py │ └── assets │ │ └── mcp-inspector.png ├── 03-Use-Cases │ ├── Chinook.db │ ├── assets │ │ ├── langgraph-crag.png │ │ ├── agent-simulations.png │ │ ├── prompt-generator.png │ │ ├── langgraph-self-rag.png │ │ ├── langgraph-sql-agent.png │ │ ├── langgraph-multi-agent.png │ │ ├── meta-prompt-generator.png │ │ ├── langgraph-storm-concept.png │ │ ├── langgraph-plan-and-execute.png │ │ ├── langgraph-sql-agent-evaluation.png │ │ ├── langgraph-multi-agent-supervisor.png │ │ └── langgraph-multi-agent-team-supervisor.png │ ├── data │ │ └── SPRI_AI_Brief_2023년12월호_F.pdf │ └── rag │ │ ├── utils.py │ │ ├── pdf.py │ │ └── base.py ├── 01-Core-Features │ ├── image │ │ ├── crag.jpeg │ │ ├── langgraph-01.png │ │ ├── langgraph-02.png │ │ ├── langgraph-05.png │ │ ├── langgraph-06.png │ │ ├── self-rag-01.jpeg │ │ ├── self-rag-02.jpeg │ │ ├── langgraph-03.jpeg │ │ ├── langgraph-04.jpeg │ │ ├── langgraph-07.jpeg │ │ ├── langgraph-08.jpeg │ │ ├── langgraph-09.jpeg │ │ ├── langgraph-10.jpeg │ │ ├── langgraph-11.jpeg │ │ ├── langgraph-12.jpeg │ │ ├── langgraph-13.jpeg │ │ ├── langgraph-14.jpeg │ │ ├── langgraph-15.jpeg │ │ ├── langgraph-16.jpeg │ │ ├── langgraph-17.jpeg │ │ ├── langgraph-18.jpeg │ │ ├── langgraph-19.jpeg │ │ ├── langgraph-20.jpeg │ │ ├── langgraph-21.jpeg │ │ ├── langgraph-22.jpeg │ │ ├── langgraph-23.jpeg │ │ ├── langgraph-24.jpeg │ │ ├── langgraph-25.jpeg │ │ ├── tool-message-01.png │ │ ├── tool-message-02.png │ │ ├── retrieve-and-search.jpeg │ │ └── solar-pro-prompt-optimizer.png │ ├── 01-introduction.py │ ├── 02-LangGraph-ChatBot.ipynb │ ├── 14-LangGraph-Subgraph-Transform-State.ipynb │ ├── 01-LangGraph-Introduction.ipynb │ ├── 10-LangGraph-ToolNode.ipynb │ ├── 06-LangGraph-Human-In-the-Loop.ipynb │ └── 04-LangGraph-Agent-With-Memory.ipynb ├── 02-RAG │ ├── assets │ │ ├── langgraph-crag.png │ │ ├── langgraph-agentic-rag.png │ │ ├── langgraph-naive-rag.png │ │ ├── langgraph-web-search.png │ │ ├── langgraph-adaptive-rag.png │ │ ├── langgraph-query-rewrite.png │ │ ├── langgraph-building-graphs.png │ │ └── langgraph-add-relevance-check.png │ ├── data │ │ └── SPRI_AI_Brief_2023년12월호_F.pdf │ ├── rag │ │ ├── utils.py │ │ ├── pdf.py │ │ └── base.py │ ├── 01-LangGraph-Building-Graphs.ipynb │ └── 02-LangGraph-Naive-RAG.ipynb ├── README.md └── 06-Memory │ └── 02-LangGraph-Memory-Postgres.ipynb ├── .gitignore ├── .env.example ├── pyproject.toml ├── 01-QuickStart └── README.md ├── 02-Practice ├── README.md ├── 01-Practice-Chatbot.ipynb ├── 02-Practice-Tool-Integration.ipynb ├── 03-Practice-Memory-Chatbot.ipynb ├── 05-Practice-State-Customization.ipynb └── 04-Practice-Human-in-the-Loop.ipynb ├── LICENSE └── README.md /03-Modules/04-MCP/server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/rag/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/Chinook.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/Chinook.db -------------------------------------------------------------------------------- /03-Modules/04-MCP/assets/mcp-inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/04-MCP/assets/mcp-inspector.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/crag.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/crag.jpeg -------------------------------------------------------------------------------- /03-Modules/02-RAG/assets/langgraph-crag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/assets/langgraph-crag.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-01.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-02.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-05.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-06.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/self-rag-01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/self-rag-01.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/self-rag-02.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/self-rag-02.jpeg -------------------------------------------------------------------------------- /03-Modules/02-RAG/assets/langgraph-agentic-rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/assets/langgraph-agentic-rag.png -------------------------------------------------------------------------------- /03-Modules/02-RAG/assets/langgraph-naive-rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/assets/langgraph-naive-rag.png -------------------------------------------------------------------------------- /03-Modules/02-RAG/assets/langgraph-web-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/assets/langgraph-web-search.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-crag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-crag.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-03.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-03.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-04.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-04.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-07.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-07.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-08.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-08.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-09.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-09.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-10.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-11.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-12.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-12.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-13.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-13.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-14.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-14.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-15.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-15.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-16.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-16.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-17.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-17.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-18.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-18.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-19.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-19.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-20.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-20.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-21.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-21.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-22.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-22.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-23.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-23.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-24.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-24.jpeg -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/langgraph-25.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/langgraph-25.jpeg -------------------------------------------------------------------------------- /03-Modules/02-RAG/assets/langgraph-adaptive-rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/assets/langgraph-adaptive-rag.png -------------------------------------------------------------------------------- /03-Modules/02-RAG/assets/langgraph-query-rewrite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/assets/langgraph-query-rewrite.png -------------------------------------------------------------------------------- /03-Modules/02-RAG/data/SPRI_AI_Brief_2023년12월호_F.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/data/SPRI_AI_Brief_2023년12월호_F.pdf -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/agent-simulations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/agent-simulations.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/prompt-generator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/prompt-generator.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/tool-message-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/tool-message-01.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/tool-message-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/tool-message-02.png -------------------------------------------------------------------------------- /03-Modules/02-RAG/assets/langgraph-building-graphs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/assets/langgraph-building-graphs.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-self-rag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-self-rag.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-sql-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-sql-agent.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-multi-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-multi-agent.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/meta-prompt-generator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/meta-prompt-generator.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/retrieve-and-search.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/retrieve-and-search.jpeg -------------------------------------------------------------------------------- /03-Modules/02-RAG/assets/langgraph-add-relevance-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/02-RAG/assets/langgraph-add-relevance-check.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-storm-concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-storm-concept.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/data/SPRI_AI_Brief_2023년12월호_F.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/data/SPRI_AI_Brief_2023년12월호_F.pdf -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/data/SPRI_AI_Brief_2023년12월호_F.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/04-MCP/server/data/SPRI_AI_Brief_2023년12월호_F.pdf -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-plan-and-execute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-plan-and-execute.png -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/image/solar-pro-prompt-optimizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/01-Core-Features/image/solar-pro-prompt-optimizer.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-sql-agent-evaluation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-sql-agent-evaluation.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-multi-agent-supervisor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-multi-agent-supervisor.png -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/assets/langgraph-multi-agent-team-supervisor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teddynote-lab/langgraph-tutorial/HEAD/03-Modules/03-Use-Cases/assets/langgraph-multi-agent-team-supervisor.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | .cache/ 9 | 10 | .DS_Store 11 | 12 | 02-Solution/ 13 | 99-Templates/ 14 | 15 | # Virtual environments 16 | .venv 17 | uv.lock 18 | 19 | .env 20 | 21 | CLAUDE.md 22 | 23 | *.faiss 24 | *.pkl 25 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | 3 | LANGSMITH_API_KEY= 4 | LANGSMITH_TRACING=true 5 | LANGSMITH_PROJECT=LangGraph-Tutorial 6 | 7 | TAVILY_API_KEY= 8 | 9 | OPENROUTER_API_KEY= 10 | OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 11 | 12 | POSTGRES_USER= 13 | POSTGRES_PASSWORD= 14 | POSTGRES_HOST= 15 | POSTGRES_PORT= 16 | POSTGRES_DB= -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/01-introduction.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | 3 | 4 | class User(TypedDict): 5 | name: str 6 | age: int 7 | email: str 8 | 9 | 10 | def create_user(name: str, age: int, email: str) -> User: 11 | return {"name": name, "age": age, "email": email} 12 | 13 | 14 | if __name__ == "__main__": 15 | # 올바른 사용 16 | user1 = create_user("Alice", 30, "alice@example.com") 17 | 18 | # 타입 오류 (age에 문자열 할당) 19 | user2 = create_user("Bob", "25", "bob@example.com") 20 | 21 | # 타입 오류 (추가 필드 할당) 22 | user3: User = { 23 | "name": "Charlie", 24 | "age": 35, 25 | "email": "charlie@example.com", 26 | "extra": "info", 27 | } 28 | 29 | # 타입 오류 (필수 필드 누락) 30 | user4: User = {"name": "David", "age": 40} 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "LangGraph-Tutorial" 3 | version = "0.1.0" 4 | description = "한국어 버전의 LangGraph 튜토리얼" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "faiss-cpu>=1.11.0.post1", 9 | "fastmcp>=2.11.3", 10 | "grandalf>=0.8", 11 | "jupyter>=1.1.1", 12 | "langchain>=0.3.27", 13 | "langchain-chroma>=0.2.3", 14 | "langchain-community>=0.3.27", 15 | "langchain-experimental>=0.3.4", 16 | "langchain-mcp-adapters>=0.1.9", 17 | "langchain-openai>=0.3.28", 18 | "langchain-tavily>=0.2.11", 19 | "langchain-teddynote>=0.4", 20 | "langgraph>=0.5.4", 21 | "langgraph-checkpoint-postgres>=2.0.23", 22 | "mcp>=1.12.4", 23 | "neo4j-driver>=5.28.2", 24 | "notebook>=7.4.4", 25 | "pandas>=2.3.1", 26 | "pdfplumber>=0.11.7", 27 | "psycopg[binary,pool]>=3.2.9", 28 | "python-dotenv>=1.1.1", 29 | "rank-bm25>=0.2.2", 30 | "unstructured>=0.18.11", 31 | ] 32 | -------------------------------------------------------------------------------- /03-Modules/02-RAG/rag/utils.py: -------------------------------------------------------------------------------- 1 | def format_docs(docs): 2 | return "\n".join( 3 | [ 4 | f"{doc.page_content}{doc.metadata['source']}{int(doc.metadata['page'])+1}" 5 | for doc in docs 6 | ] 7 | ) 8 | 9 | 10 | def format_searched_docs(docs): 11 | return "\n".join( 12 | [ 13 | f"{doc['content']}{doc['url']}" 14 | for doc in docs 15 | ] 16 | ) 17 | 18 | 19 | def format_task(tasks): 20 | # 결과를 저장할 빈 리스트 생성 21 | task_time_pairs = [] 22 | 23 | # 리스트를 순회하면서 각 항목을 처리 24 | for item in tasks: 25 | # 콜론(:) 기준으로 문자열을 분리 26 | task, time_str = item.rsplit(":", 1) 27 | # '시간' 문자열을 제거하고 정수로 변환 28 | time = int(time_str.replace("시간", "").strip()) 29 | # 할 일과 시간을 튜플로 만들어 리스트에 추가 30 | task_time_pairs.append((task, time)) 31 | 32 | # 결과 출력 33 | return task_time_pairs 34 | -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/rag/utils.py: -------------------------------------------------------------------------------- 1 | def format_docs(docs): 2 | return "\n".join( 3 | [ 4 | f"{doc.page_content}{doc.metadata['source']}{int(doc.metadata['page'])+1}" 5 | for doc in docs 6 | ] 7 | ) 8 | 9 | 10 | def format_searched_docs(docs): 11 | return "\n".join( 12 | [ 13 | f"{doc['content']}{doc['url']}" 14 | for doc in docs 15 | ] 16 | ) 17 | 18 | 19 | def format_task(tasks): 20 | # 결과를 저장할 빈 리스트 생성 21 | task_time_pairs = [] 22 | 23 | # 리스트를 순회하면서 각 항목을 처리 24 | for item in tasks: 25 | # 콜론(:) 기준으로 문자열을 분리 26 | task, time_str = item.rsplit(":", 1) 27 | # '시간' 문자열을 제거하고 정수로 변환 28 | time = int(time_str.replace("시간", "").strip()) 29 | # 할 일과 시간을 튜플로 만들어 리스트에 추가 30 | task_time_pairs.append((task, time)) 31 | 32 | # 결과 출력 33 | return task_time_pairs 34 | -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/rag/utils.py: -------------------------------------------------------------------------------- 1 | def format_docs(docs): 2 | return "\n".join( 3 | [ 4 | f"{doc.page_content}{doc.metadata['source']}{int(doc.metadata['page'])+1}" 5 | for doc in docs 6 | ] 7 | ) 8 | 9 | 10 | def format_searched_docs(docs): 11 | return "\n".join( 12 | [ 13 | f"{doc['content']}{doc['url']}" 14 | for doc in docs 15 | ] 16 | ) 17 | 18 | 19 | def format_task(tasks): 20 | # 결과를 저장할 빈 리스트 생성 21 | task_time_pairs = [] 22 | 23 | # 리스트를 순회하면서 각 항목을 처리 24 | for item in tasks: 25 | # 콜론(:) 기준으로 문자열을 분리 26 | task, time_str = item.rsplit(":", 1) 27 | # '시간' 문자열을 제거하고 정수로 변환 28 | time = int(time_str.replace("시간", "").strip()) 29 | # 할 일과 시간을 튜플로 만들어 리스트에 추가 30 | task_time_pairs.append((task, time)) 31 | 32 | # 결과 출력 33 | return task_time_pairs 34 | -------------------------------------------------------------------------------- /01-QuickStart/README.md: -------------------------------------------------------------------------------- 1 | # 🚀 QuickStart - LangGraph 빠른 시작 2 | 3 | ## 📝 개요 4 | 5 | LangGraph를 처음 접하는 개발자들을 위한 빠른 시작 가이드입니다. 기본 개념부터 핵심 기능까지 단계별로 학습할 수 있도록 구성되어 있습니다. 6 | 7 | ## 📋 학습 순서 8 | 9 | 각 노트북을 순서대로 진행하시면 LangGraph의 기본기를 체계적으로 익힐 수 있습니다. 10 | 11 | ### 01. LangGraph 기본 튜토리얼 12 | **파일:** `01-QuickStart-LangGraph-Tutorial.ipynb` 13 | - LangGraph의 기본 개념과 구조 이해 14 | - 간단한 그래프 생성 및 실행 15 | - 기본 워크플로우 구축 방법 16 | 17 | ### 02. Graph API 활용 18 | **파일:** `02-QuickStart-LangGraph-Graph-API.ipynb` 19 | - LangGraph의 핵심 API 사용법 20 | - 노드와 엣지 정의 및 연결 21 | - 그래프 실행 및 결과 처리 22 | 23 | ### 03. 서브그래프 이해 24 | **파일:** `03-QuickStart-LangGraph-Subgraph.ipynb` 25 | - 서브그래프의 개념과 활용 26 | - 복잡한 워크플로우의 모듈화 27 | - 서브그래프 간 데이터 전달 방법 28 | 29 | ## 🎯 학습 목표 30 | 31 | 이 섹션을 완료하면 다음과 같은 능력을 갖게 됩니다: 32 | 33 | - ✅ LangGraph의 기본 구조와 동작 원리 이해 34 | - ✅ 간단한 AI 에이전트 워크플로우 구축 35 | - ✅ Graph API를 활용한 노드 및 엣지 관리 36 | - ✅ 서브그래프를 통한 모듈화된 시스템 설계 37 | 38 | ## 📚 다음 단계 39 | 40 | QuickStart를 완료한 후에는 다음 섹션으로 진행하세요: 41 | - [02-Practice/](../02-Practice/) - 실습을 통한 심화 학습 42 | - [03-Modules/](../03-Modules/) - 모듈별 상세 기능 학습 43 | 44 | --- 45 | **Made by TeddyNote LAB** -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/mcp_server_local.py: -------------------------------------------------------------------------------- 1 | from fastmcp import FastMCP 2 | 3 | # Initialize FastMCP server with configuration 4 | mcp = FastMCP( 5 | "Weather", # Name of the MCP server 6 | instructions="You are a weather assistant that can answer questions about the weather in a given location.", # Instructions for the LLM on how to use this tool 7 | ) 8 | 9 | 10 | @mcp.tool() 11 | async def get_weather(location: str) -> str: 12 | """ 13 | Get current weather information for the specified location. 14 | 15 | This function simulates a weather service by returning a fixed response. 16 | In a production environment, this would connect to a real weather API. 17 | 18 | Args: 19 | location (str): The name of the location (city, region, etc.) to get weather for 20 | 21 | Returns: 22 | str: A string containing the weather information for the specified location 23 | """ 24 | # Return a mock weather response 25 | # In a real implementation, this would call a weather API 26 | return f"It's always Sunny in {location}" 27 | 28 | 29 | if __name__ == "__main__": 30 | # Start the MCP server with stdio transport 31 | # stdio transport allows the server to communicate with clients 32 | # through standard input/output streams, making it suitable for 33 | # local development and testing 34 | mcp.run(transport="stdio") 35 | -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/mcp_server_remote.py: -------------------------------------------------------------------------------- 1 | from fastmcp import FastMCP 2 | from typing import Optional 3 | import pytz 4 | from datetime import datetime 5 | 6 | mcp = FastMCP( 7 | "Current Time", # Name of the MCP server 8 | instructions="Information about the current time in a given timezone", # Instructions for the LLM on how to use this tool 9 | ) 10 | 11 | 12 | @mcp.tool() 13 | async def get_current_time(timezone: Optional[str] = "Asia/Seoul") -> str: 14 | """ 15 | Get current time information for the specified timezone. 16 | 17 | This function returns the current system time for the requested timezone. 18 | 19 | Args: 20 | timezone (str, optional): The timezone to get current time for. Defaults to "Asia/Seoul". 21 | 22 | Returns: 23 | str: A string containing the current time information for the specified timezone 24 | """ 25 | try: 26 | # Get the timezone object 27 | tz = pytz.timezone(timezone) 28 | 29 | # Get current time in the specified timezone 30 | current_time = datetime.now(tz) 31 | 32 | # Format the time as a string 33 | formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S %Z") 34 | 35 | return f"Current time in {timezone} is: {formatted_time}" 36 | except pytz.exceptions.UnknownTimeZoneError: 37 | return f"Error: Unknown timezone '{timezone}'. Please provide a valid timezone." 38 | except Exception as e: 39 | return f"Error getting time: {str(e)}" 40 | 41 | 42 | if __name__ == "__main__": 43 | # Print a message indicating the server is starting 44 | print("mcp remote server is running...") 45 | 46 | # start the server 47 | mcp.run(transport="streamable-http", port=8002) 48 | -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/mcp_server_rag.py: -------------------------------------------------------------------------------- 1 | from langchain_text_splitters import RecursiveCharacterTextSplitter 2 | from langchain_community.document_loaders import PyMuPDFLoader 3 | from langchain_community.vectorstores import FAISS 4 | from langchain_openai import OpenAIEmbeddings 5 | from mcp.server.fastmcp import FastMCP 6 | from dotenv import load_dotenv 7 | from typing import Any 8 | from rag.pdf import PDFRetrievalChain 9 | 10 | 11 | load_dotenv(override=True) 12 | 13 | 14 | def create_retriever() -> Any: 15 | """""" 16 | # PDF 문서를 로드합니다. 17 | import os 18 | current_dir = os.path.dirname(os.path.abspath(__file__)) 19 | pdf_path = os.path.join(current_dir, "data", "SPRI_AI_Brief_2023년12월호_F.pdf") 20 | pdf = PDFRetrievalChain([pdf_path]).create_chain() 21 | 22 | # retriever와 chain을 생성합니다. 23 | pdf_retriever = pdf.retriever 24 | 25 | return pdf_retriever 26 | 27 | 28 | # Initialize FastMCP server with configuration 29 | mcp = FastMCP( 30 | "Retriever", 31 | instructions="A Retriever that can retrieve information from the database.", 32 | ) 33 | 34 | 35 | @mcp.tool() 36 | async def retrieve(query: str) -> str: 37 | """ 38 | Retrieves information from the document database based on the query. 39 | 40 | This function creates a retriever, queries it with the provided input, 41 | and returns the concatenated content of all retrieved documents. 42 | 43 | Args: 44 | query (str): The search query to find relevant information 45 | 46 | Returns: 47 | str: Concatenated text content from all retrieved documents 48 | """ 49 | retriever = create_retriever() 50 | 51 | # Use the invoke() method to get relevant documents based on the query 52 | retrieved_docs = retriever.invoke(query) 53 | 54 | # Join all document contents with newlines and return as a single string 55 | return "\n".join([doc.page_content for doc in retrieved_docs]) 56 | 57 | 58 | if __name__ == "__main__": 59 | # Run the MCP server with stdio transport for integration with MCP clients 60 | mcp.run(transport="stdio") 61 | -------------------------------------------------------------------------------- /02-Practice/README.md: -------------------------------------------------------------------------------- 1 | # 💻 Practice - 실습 과제 2 | 3 | ## 📝 개요 4 | 5 | LangGraph의 핵심 기능들을 실제 시나리오에서 직접 구현해보는 실습 과제 모음입니다. 기본 개념을 익힌 후 실무 능력을 향상시킬 수 있도록 구성되어 있습니다. 6 | 7 | ## 🎯 학습 방식 8 | 9 | 각 실습은 **문제 제시 → 직접 구현 → 해답 확인** 형태로 진행됩니다. 먼저 스스로 구현해본 후 해답지를 참고하여 학습 효과를 극대화하세요. 10 | 11 | ## 📋 실습 과제 목록 12 | 13 | ### 01. 기본 채팅봇 구현 14 | **파일:** `01-Practice-Chatbot.ipynb` 15 | - LangGraph를 활용한 기본 채팅봇 구축 16 | - 메시지 처리 및 응답 생성 워크플로우 17 | - 대화 흐름 관리 실습 18 | 19 | ### 02. 도구 통합 시스템 20 | **파일:** `02-Practice-Tool-Integration.ipynb` 21 | - 외부 도구 및 API 연동 22 | - 도구 호출 및 결과 처리 23 | - 멀티 도구 워크플로우 구성 24 | 25 | ### 03. 메모리 기능 채팅봇 26 | **파일:** `03-Practice-Memory-Chatbot.ipynb` 27 | - 대화 히스토리 관리 28 | - 컨텍스트 유지 및 활용 29 | - 장기 메모리 시스템 구현 30 | 31 | ### 04. 휴먼-인-더-루프 시스템 32 | **파일:** `04-Practice-Human-in-the-Loop.ipynb` 33 | - 인간 개입이 필요한 워크플로우 34 | - 승인/거부 메커니즘 구현 35 | - 인터랙티브 의사결정 과정 36 | 37 | ### 05. 상태 커스터마이징 38 | **파일:** `05-Practice-State-Customization.ipynb` 39 | - 사용자 정의 상태 관리 40 | - 복잡한 데이터 구조 처리 41 | - 상태 전환 로직 구현 42 | 43 | ### 06. 비즈니스 이메일 봇 44 | **파일:** `06-Practice-Business-Email-Bot.ipynb` 45 | - 비즈니스 시나리오 기반 실습 46 | - 이메일 자동 분류 및 응답 47 | - 업무 프로세스 자동화 48 | 49 | ### 07. 멀티 쿼리 RAG 시스템 50 | **파일:** `07-Practice-Send-Multi-Query-RAG.ipynb` 51 | - 다중 쿼리 처리 시스템 52 | - RAG 기반 정보 검색 53 | - 쿼리 분석 및 통합 응답 54 | 55 | ### 08. 명령어 워크플로우 56 | **파일:** `08-Practice-Command-Workflow.ipynb` 57 | - 명령어 기반 자동화 시스템 58 | - 조건부 실행 로직 59 | - 복잡한 작업 체인 구성 60 | 61 | ### 09. 금융 멀티 쿼리 RAG 62 | **파일:** `09-Practice-Financial-Multi-Query-RAG.ipynb` 63 | - 금융 도메인 특화 RAG 시스템 64 | - 전문 데이터 처리 및 분석 65 | - 도메인 지식 활용 실습 66 | 67 | ## 📊 난이도 표시 68 | 69 | - 🟢 **초급** (01-03): LangGraph 기본 기능 활용 70 | - 🟡 **중급** (04-06): 실무 시나리오 적용 71 | - 🔴 **고급** (07-09): 전문 도메인 시스템 구축 72 | 73 | ## 💡 학습 팁 74 | 75 | 1. **단계별 접근**: 순서대로 진행하여 점진적 난이도 상승 경험 76 | 2. **독립적 구현**: 해답 참고 전 스스로 구현 시도 77 | 3. **응용 학습**: 각 실습을 자신의 프로젝트에 적용해보기 78 | 4. **반복 연습**: 어려운 부분은 여러 번 반복 학습 79 | 80 | ## 📚 연관 자료 81 | 82 | - **해답지**: `../02-Solution/` 폴더에서 완성된 코드 확인 가능 83 | - **이론 학습**: `../03-Modules/` 폴더에서 상세 기능 학습 84 | - **기초 개념**: `../01-QuickStart/` 폴더에서 기본 개념 복습 85 | 86 | --- 87 | **Made by TeddyNote LAB** -------------------------------------------------------------------------------- /03-Modules/README.md: -------------------------------------------------------------------------------- 1 | # 🧩 Modules - 심화 학습 모듈 2 | 3 | ## 📝 개요 4 | 5 | LangGraph의 모든 기능을 심도 있게 학습할 수 있는 모듈별 튜토리얼입니다. 각 모듈은 특정 주제에 집중하여 이론부터 실무 활용까지 포괄적으로 다룹니다. 6 | 7 | ## 📚 모듈 구성 8 | 9 | ### 🔧 01-Core-Features - 핵심 기능 10 | **총 15개 튜토리얼 | 난이도: 🟢 초급 ~ 🟡 중급** 11 | 12 | LangGraph의 핵심 기능들을 단계별로 학습합니다. 13 | 14 | - **기본 구조** (01-03): 소개, 채팅봇, 에이전트 기본 구현 15 | - **메모리 & 스트리밍** (04-05): 메모리 관리, 실시간 출력 처리 16 | - **고급 제어** (06-08): 휴먼-인-더-루프, 수동 상태 업데이트, 커스터마이징 17 | - **메시지 관리** (09-10): 메시지 삭제, 도구 노드 활용 18 | - **구조 설계** (11-15): 분기 처리, 대화 요약, 서브그래프, 스트리밍 단계 19 | 20 | ### 🔍 02-RAG - 검색 증강 생성 21 | **총 9개 튜토리얼 | 난이도: 🟡 중급 ~ 🔴 고급** 22 | 23 | RAG(Retrieval-Augmented Generation) 시스템의 모든 것을 학습합니다. 24 | 25 | - **기본 RAG** (01-02): 그래프 구축, 기본 RAG 구현 26 | - **향상된 RAG** (03-05): 근거 검증, 웹 검색, 쿼리 재작성 27 | - **고급 RAG** (06-09): 에이전트형 RAG, CRAG, Self-RAG, Adaptive RAG 28 | 29 | **포함 자료**: 시각화 이미지, 샘플 데이터, RAG 유틸리티 모듈 30 | 31 | ### 🎯 03-Use-Cases - 실무 활용 사례 32 | **총 5개 튜토리얼 | 난이도: 🔴 고급** 33 | 34 | 실제 비즈니스 시나리오에서 활용할 수 있는 고급 사례들을 학습합니다. 35 | 36 | - **시뮬레이션** (01): 에이전트 간 상호작용 시뮬레이션 37 | - **생성 시스템** (02): 프롬프트 자동 생성 시스템 38 | - **작업 관리** (03): 계획 수립 및 실행 프레임워크 39 | - **데이터 분석** (04): SQL 에이전트를 통한 데이터베이스 쿼리 40 | - **연구 도구** (05): 자동화된 연구 어시스턴트 41 | 42 | **포함 자료**: Chinook 데이터베이스, 시각화 에셋, 유틸리티 모듈 43 | 44 | ### 🔌 04-MCP - Model Context Protocol 45 | **총 1개 튜토리얼 | 난이도: 🔴 고급** 46 | 47 | 차세대 AI 통합 프로토콜인 MCP를 활용한 고급 시스템 구축을 학습합니다. 48 | 49 | - **MCP 통합**: 로컬/원격 MCP 서버 구축 및 연동 50 | - **RAG 서버**: MCP 기반 RAG 시스템 구현 51 | - **서버 관리**: 다양한 MCP 서버 유형별 활용법 52 | 53 | **포함 자료**: MCP 서버 구현체, RAG 모듈, 검사 도구 54 | 55 | ### 👥 05-Supervisor - 멀티 에이전트 관리 56 | **총 4개 튜토리얼 | 난이도: 🟡 중급 ~ 🔴 고급** 57 | 58 | 여러 에이전트를 조율하고 관리하는 복잡한 시스템을 구축합니다. 59 | 60 | - **기본 관리** (01): 단일 수퍼바이저 에이전트 시스템 61 | - **협업 시스템** (02): 다중 에이전트 협업 프레임워크 62 | - **계층적 관리** (03): 수퍼바이저 기반 에이전트 관리 63 | - **팀 구조** (04): 계층적 에이전트 팀 구성 64 | 65 | ### 🧠 06-Memory - 메모리 관리 66 | **총 2개 튜토리얼 | 난이도: 🟡 중급** 67 | 68 | 장기 메모리와 상태 지속성을 구현하는 방법을 학습합니다. 69 | 70 | - **기본 메모리** (01): 메모리 시스템 추가 및 관리 71 | - **데이터베이스 연동** (02): PostgreSQL 기반 영구 메모리 시스템 72 | 73 | ## 🎓 학습 권장 순서 74 | 75 | ### 📈 단계별 학습 경로 76 | 77 | 1. **기초 단계**: 01-Core-Features (01-05) 78 | 2. **응용 단계**: 01-Core-Features (06-10) + 06-Memory 79 | 3. **심화 단계**: 01-Core-Features (11-15) + 02-RAG (01-04) 80 | 4. **전문 단계**: 02-RAG (05-09) + 05-Supervisor 81 | 5. **마스터 단계**: 03-Use-Cases + 04-MCP 82 | 83 | ### 🎯 주제별 학습 경로 84 | 85 | - **RAG 시스템**: 02-RAG → 03-Use-Cases (05) → 04-MCP 86 | - **멀티 에이전트**: 01-Core-Features → 05-Supervisor → 03-Use-Cases (01) 87 | - **실무 활용**: 01-Core-Features → 06-Memory → 03-Use-Cases 88 | 89 | ## 💡 학습 팁 90 | 91 | - **순차 학습**: 각 모듈 내에서는 번호 순서대로 진행 92 | - **실습 연계**: Practice 폴더와 병행하여 이론과 실습 균형 93 | - **프로젝트 적용**: 학습한 내용을 개인 프로젝트에 즉시 적용 94 | - **반복 학습**: 복잡한 개념은 여러 번 반복하여 완전히 이해 95 | 96 | --- 97 | **Made by TeddyNote LAB** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LangGraph Tutorial Educational License 2 | 3 | Copyright (c) 2025 TeddyNote LAB 4 | 5 | ============================================================================== 6 | EDUCATIONAL USE ONLY LICENSE 7 | ============================================================================== 8 | 9 | This license governs the use of the LangGraph Tutorial project (the "Software") 10 | and associated documentation files. 11 | 12 | PERMITTED USES: 13 | 1. Educational purposes only, including but not limited to: 14 | - Personal learning and skill development 15 | - Academic research and study 16 | - Teaching in educational institutions 17 | - Non-commercial training programs 18 | 19 | 2. Viewing, studying, and modifying the code for educational purposes 20 | 3. Creating derivative works solely for educational use 21 | 22 | PROHIBITED USES: 23 | 1. Commercial use of any kind, including but not limited to: 24 | - Use in commercial products or services 25 | - Use to generate revenue or profit 26 | - Use in for-profit training programs 27 | - Consulting or professional services based on this software 28 | 29 | 2. Distribution or redistribution of the software, in whole or in part: 30 | - No sharing via public repositories 31 | - No sharing via file sharing platforms 32 | - No inclusion in other software packages 33 | - No sublicensing to third parties 34 | 35 | 3. Public hosting or deployment of applications based on this software 36 | 37 | ATTRIBUTION REQUIREMENTS: 38 | - Any use of this software must include proper attribution to TeddyNote LAB 39 | - The original copyright notice must be preserved in all copies 40 | - Any derivative works must clearly indicate their relationship to the original 41 | 42 | WARRANTY DISCLAIMER: 43 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 44 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 45 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 46 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 47 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 48 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 49 | SOFTWARE. 50 | 51 | TERMINATION: 52 | This license automatically terminates if you violate any of its terms. Upon 53 | termination, you must immediately cease all use of the software and destroy 54 | all copies in your possession. 55 | 56 | GOVERNING LAW: 57 | This license shall be governed by and construed in accordance with the laws 58 | of the Republic of Korea. 59 | 60 | For questions regarding this license, please contact TeddyNote LAB. 61 | 62 | ============================================================================== 63 | © 2025 TeddyNote LAB. All rights reserved. 64 | ============================================================================== -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/mcp_rag_server.py: -------------------------------------------------------------------------------- 1 | 2 | from mcp.server.fastmcp import FastMCP 3 | from langchain_openai import OpenAIEmbeddings 4 | from langchain_community.vectorstores import FAISS 5 | from langchain_community.document_loaders import PyMuPDFLoader 6 | from langchain_text_splitters import RecursiveCharacterTextSplitter 7 | from langchain_tavily import TavilySearch 8 | from langchain_core.documents import Document 9 | from dotenv import load_dotenv 10 | from typing import List, Literal 11 | import os 12 | import pickle 13 | 14 | load_dotenv(override=True) 15 | 16 | # FastMCP 서버 초기화 17 | mcp = FastMCP( 18 | "RAG_Server", 19 | instructions="A RAG server that provides vector search, document addition, and web search capabilities." 20 | ) 21 | 22 | # 전역 변수로 벡터스토어 관리 23 | vector_store = None 24 | embeddings = OpenAIEmbeddings() 25 | 26 | def initialize_vector_store(): 27 | """벡터 스토어를 초기화하고 PDF 문서를 로드합니다.""" 28 | global vector_store 29 | 30 | current_dir = os.path.dirname(os.path.abspath(__file__)) 31 | pdf_path = os.path.join(current_dir, "data", "SPRI_AI_Brief_2023년12월호_F.pdf") 32 | 33 | loader = PyMuPDFLoader(pdf_path) 34 | documents = loader.load() 35 | 36 | text_splitter = RecursiveCharacterTextSplitter( 37 | chunk_size=1000, 38 | chunk_overlap=200 39 | ) 40 | splits = text_splitter.split_documents(documents) 41 | 42 | vector_store = FAISS.from_documents(splits, embeddings) 43 | return vector_store 44 | 45 | @mcp.tool() 46 | async def vector_search( 47 | query: str, 48 | search_type: Literal["semantic", "keyword", "hybrid"] = "semantic", 49 | k: int = 5 50 | ) -> str: 51 | """벡터 스토어에서 문서를 검색합니다.""" 52 | global vector_store 53 | 54 | if vector_store is None: 55 | initialize_vector_store() 56 | 57 | if search_type == "semantic": 58 | results = vector_store.similarity_search(query, k=k) 59 | elif search_type == "keyword": 60 | all_docs = vector_store.similarity_search("", k=100) 61 | results = [doc for doc in all_docs if query.lower() in doc.page_content.lower()][:k] 62 | elif search_type == "hybrid": 63 | semantic_results = vector_store.similarity_search(query, k=k*2) 64 | keyword_results = [doc for doc in semantic_results if query.lower() in doc.page_content.lower()] 65 | results = keyword_results[:k] if keyword_results else semantic_results[:k] 66 | 67 | return "\n\n".join([doc.page_content for doc in results]) 68 | 69 | @mcp.tool() 70 | async def add_document(text: str, metadata: dict = None) -> str: 71 | """사용자 텍스트를 벡터 스토어에 추가합니다.""" 72 | global vector_store 73 | 74 | if vector_store is None: 75 | initialize_vector_store() 76 | 77 | if metadata is None: 78 | metadata = {"source": "user_input"} 79 | 80 | text_splitter = RecursiveCharacterTextSplitter( 81 | chunk_size=1000, 82 | chunk_overlap=200 83 | ) 84 | 85 | documents = [Document(page_content=text, metadata=metadata)] 86 | splits = text_splitter.split_documents(documents) 87 | 88 | vector_store.add_documents(splits) 89 | 90 | return f"문서가 성공적으로 추가되었습니다. 총 {len(text)} 문자, {len(splits)}개 청크로 분할됨" 91 | 92 | @mcp.tool() 93 | async def web_search(query: str, max_results: int = 3) -> str: 94 | """TavilySearch를 사용하여 웹 검색을 수행합니다.""" 95 | tavily = TavilySearch(max_results=max_results) 96 | results = tavily.invoke(query) 97 | 98 | formatted_results = [] 99 | for i, result in enumerate(results, 1): 100 | formatted_results.append( 101 | f"검색 결과 {i}:\n" 102 | f"제목: {result.get('title', 'N/A')}\n" 103 | f"URL: {result.get('url', 'N/A')}\n" 104 | f"내용: {result.get('content', 'N/A')}\n" 105 | ) 106 | 107 | return "\n".join(formatted_results) 108 | 109 | if __name__ == "__main__": 110 | # 서버 초기화 111 | print("RAG MCP 서버를 초기화합니다...") 112 | initialize_vector_store() 113 | print("벡터 스토어 초기화 완료!") 114 | 115 | # MCP 서버 실행 116 | mcp.run(transport="stdio") 117 | -------------------------------------------------------------------------------- /03-Modules/02-RAG/rag/pdf.py: -------------------------------------------------------------------------------- 1 | from rag.base import RetrievalChain 2 | from langchain_community.document_loaders import PDFPlumberLoader 3 | from langchain_text_splitters import RecursiveCharacterTextSplitter 4 | from langchain_core.documents import Document 5 | from typing import List, Annotated 6 | from pathlib import Path 7 | import os 8 | import hashlib 9 | 10 | 11 | class PDFRetrievalChain(RetrievalChain): 12 | def __init__(self, source_uri: Annotated[str, "Source URI"], **kwargs): 13 | super().__init__(**kwargs) 14 | self.source_uri = source_uri 15 | 16 | # PDF 파일 경로 기반으로 고유한 캐시 디렉토리 생성 17 | if isinstance(source_uri, str): 18 | file_hash = hashlib.md5(source_uri.encode()).hexdigest()[:8] 19 | file_name = Path(source_uri).stem 20 | cache_suffix = f"{file_name}_{file_hash}" 21 | 22 | self.cache_dir = Path(f".cache/embeddings/{cache_suffix}") 23 | self.index_dir = Path(f".cache/faiss_index/{cache_suffix}") 24 | print(f"Cache configured for PDF: {file_name}") 25 | print(f"- Embeddings cache: {self.cache_dir}") 26 | print(f"- FAISS index cache: {self.index_dir}") 27 | else: 28 | # 여러 파일의 경우 기본 캐시 사용 29 | self.cache_dir = Path(".cache/embeddings/multi_pdf") 30 | self.index_dir = Path(".cache/faiss_index/multi_pdf") 31 | print("Cache configured for multi-PDF processing") 32 | 33 | def load_documents(self, source_uris: List[str]) -> List[Document]: 34 | docs = [] 35 | successful_files = 0 36 | failed_files = [] 37 | 38 | for source_uri in source_uris: 39 | try: 40 | # 파일 존재 및 권한 확인 41 | file_path = Path(source_uri) 42 | if not file_path.exists(): 43 | print(f"Warning: File not found: {source_uri}") 44 | failed_files.append(source_uri) 45 | continue 46 | 47 | if not file_path.is_file(): 48 | print(f"Warning: Not a file: {source_uri}") 49 | failed_files.append(source_uri) 50 | continue 51 | 52 | if not os.access(source_uri, os.R_OK): 53 | print(f"Warning: No read permission: {source_uri}") 54 | failed_files.append(source_uri) 55 | continue 56 | 57 | # PDF 파일 확장자 확인 58 | if not source_uri.lower().endswith(".pdf"): 59 | print(f"Warning: Not a PDF file: {source_uri}") 60 | failed_files.append(source_uri) 61 | continue 62 | 63 | # PDF 로딩 시도 64 | print(f"Loading PDF: {source_uri}") 65 | loader = PDFPlumberLoader(source_uri) 66 | loaded_docs = loader.load() 67 | 68 | if not loaded_docs: 69 | print(f"Warning: No content loaded from: {source_uri}") 70 | failed_files.append(source_uri) 71 | continue 72 | 73 | docs.extend(loaded_docs) 74 | successful_files += 1 75 | print( 76 | f"Successfully loaded {len(loaded_docs)} pages from: {source_uri}" 77 | ) 78 | 79 | except Exception as e: 80 | print(f"Error loading PDF {source_uri}: {e}") 81 | failed_files.append(source_uri) 82 | continue 83 | 84 | # 결과 요약 출력 85 | print(f"\nLoading Summary:") 86 | print(f"- Successfully loaded: {successful_files} files") 87 | print(f"- Failed to load: {len(failed_files)} files") 88 | if failed_files: 89 | print(f"- Failed files: {failed_files}") 90 | print(f"- Total documents loaded: {len(docs)}") 91 | 92 | if not docs: 93 | raise ValueError( 94 | "No documents were successfully loaded from the provided source URIs" 95 | ) 96 | 97 | return docs 98 | 99 | def create_text_splitter(self) -> RecursiveCharacterTextSplitter: 100 | return RecursiveCharacterTextSplitter( 101 | chunk_size=1200, 102 | chunk_overlap=200, 103 | length_function=len, 104 | is_separator_regex=False, 105 | ) 106 | -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/rag/pdf.py: -------------------------------------------------------------------------------- 1 | from rag.base import RetrievalChain 2 | from langchain_community.document_loaders import PDFPlumberLoader 3 | from langchain_text_splitters import RecursiveCharacterTextSplitter 4 | from langchain_core.documents import Document 5 | from typing import List, Annotated 6 | from pathlib import Path 7 | import os 8 | import hashlib 9 | 10 | 11 | class PDFRetrievalChain(RetrievalChain): 12 | def __init__(self, source_uri: Annotated[str, "Source URI"], **kwargs): 13 | super().__init__(**kwargs) 14 | self.source_uri = source_uri 15 | 16 | # PDF 파일 경로 기반으로 고유한 캐시 디렉토리 생성 17 | if isinstance(source_uri, str): 18 | file_hash = hashlib.md5(source_uri.encode()).hexdigest()[:8] 19 | file_name = Path(source_uri).stem 20 | cache_suffix = f"{file_name}_{file_hash}" 21 | 22 | self.cache_dir = Path(f".cache/embeddings/{cache_suffix}") 23 | self.index_dir = Path(f".cache/faiss_index/{cache_suffix}") 24 | print(f"Cache configured for PDF: {file_name}") 25 | print(f"- Embeddings cache: {self.cache_dir}") 26 | print(f"- FAISS index cache: {self.index_dir}") 27 | else: 28 | # 여러 파일의 경우 기본 캐시 사용 29 | self.cache_dir = Path(".cache/embeddings/multi_pdf") 30 | self.index_dir = Path(".cache/faiss_index/multi_pdf") 31 | print("Cache configured for multi-PDF processing") 32 | 33 | def load_documents(self, source_uris: List[str]) -> List[Document]: 34 | docs = [] 35 | successful_files = 0 36 | failed_files = [] 37 | 38 | for source_uri in source_uris: 39 | try: 40 | # 파일 존재 및 권한 확인 41 | file_path = Path(source_uri) 42 | if not file_path.exists(): 43 | print(f"Warning: File not found: {source_uri}") 44 | failed_files.append(source_uri) 45 | continue 46 | 47 | if not file_path.is_file(): 48 | print(f"Warning: Not a file: {source_uri}") 49 | failed_files.append(source_uri) 50 | continue 51 | 52 | if not os.access(source_uri, os.R_OK): 53 | print(f"Warning: No read permission: {source_uri}") 54 | failed_files.append(source_uri) 55 | continue 56 | 57 | # PDF 파일 확장자 확인 58 | if not source_uri.lower().endswith(".pdf"): 59 | print(f"Warning: Not a PDF file: {source_uri}") 60 | failed_files.append(source_uri) 61 | continue 62 | 63 | # PDF 로딩 시도 64 | print(f"Loading PDF: {source_uri}") 65 | loader = PDFPlumberLoader(source_uri) 66 | loaded_docs = loader.load() 67 | 68 | if not loaded_docs: 69 | print(f"Warning: No content loaded from: {source_uri}") 70 | failed_files.append(source_uri) 71 | continue 72 | 73 | docs.extend(loaded_docs) 74 | successful_files += 1 75 | print( 76 | f"Successfully loaded {len(loaded_docs)} pages from: {source_uri}" 77 | ) 78 | 79 | except Exception as e: 80 | print(f"Error loading PDF {source_uri}: {e}") 81 | failed_files.append(source_uri) 82 | continue 83 | 84 | # 결과 요약 출력 85 | print(f"\nLoading Summary:") 86 | print(f"- Successfully loaded: {successful_files} files") 87 | print(f"- Failed to load: {len(failed_files)} files") 88 | if failed_files: 89 | print(f"- Failed files: {failed_files}") 90 | print(f"- Total documents loaded: {len(docs)}") 91 | 92 | if not docs: 93 | raise ValueError( 94 | "No documents were successfully loaded from the provided source URIs" 95 | ) 96 | 97 | return docs 98 | 99 | def create_text_splitter(self) -> RecursiveCharacterTextSplitter: 100 | return RecursiveCharacterTextSplitter( 101 | chunk_size=1200, 102 | chunk_overlap=200, 103 | length_function=len, 104 | is_separator_regex=False, 105 | ) 106 | -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/rag/pdf.py: -------------------------------------------------------------------------------- 1 | from .base import RetrievalChain 2 | from langchain_community.document_loaders import PDFPlumberLoader 3 | from langchain_text_splitters import RecursiveCharacterTextSplitter 4 | from langchain_core.documents import Document 5 | from typing import List, Annotated 6 | from pathlib import Path 7 | import os 8 | import hashlib 9 | 10 | 11 | class PDFRetrievalChain(RetrievalChain): 12 | def __init__(self, source_uri: Annotated[str, "Source URI"], **kwargs): 13 | super().__init__(**kwargs) 14 | self.source_uri = source_uri 15 | 16 | # PDF 파일 경로 기반으로 고유한 캐시 디렉토리 생성 17 | if isinstance(source_uri, str): 18 | file_hash = hashlib.md5(source_uri.encode()).hexdigest()[:8] 19 | file_name = Path(source_uri).stem 20 | cache_suffix = f"{file_name}_{file_hash}" 21 | 22 | self.cache_dir = Path(f".cache/embeddings/{cache_suffix}") 23 | self.index_dir = Path(f".cache/faiss_index/{cache_suffix}") 24 | print(f"Cache configured for PDF: {file_name}") 25 | print(f"- Embeddings cache: {self.cache_dir}") 26 | print(f"- FAISS index cache: {self.index_dir}") 27 | else: 28 | # 여러 파일의 경우 기본 캐시 사용 29 | self.cache_dir = Path(".cache/embeddings/multi_pdf") 30 | self.index_dir = Path(".cache/faiss_index/multi_pdf") 31 | print("Cache configured for multi-PDF processing") 32 | 33 | def load_documents(self, source_uris: List[str]) -> List[Document]: 34 | docs = [] 35 | successful_files = 0 36 | failed_files = [] 37 | 38 | for source_uri in source_uris: 39 | try: 40 | # 파일 존재 및 권한 확인 41 | file_path = Path(source_uri) 42 | if not file_path.exists(): 43 | print(f"Warning: File not found: {source_uri}") 44 | failed_files.append(source_uri) 45 | continue 46 | 47 | if not file_path.is_file(): 48 | print(f"Warning: Not a file: {source_uri}") 49 | failed_files.append(source_uri) 50 | continue 51 | 52 | if not os.access(source_uri, os.R_OK): 53 | print(f"Warning: No read permission: {source_uri}") 54 | failed_files.append(source_uri) 55 | continue 56 | 57 | # PDF 파일 확장자 확인 58 | if not source_uri.lower().endswith(".pdf"): 59 | print(f"Warning: Not a PDF file: {source_uri}") 60 | failed_files.append(source_uri) 61 | continue 62 | 63 | # PDF 로딩 시도 64 | print(f"Loading PDF: {source_uri}") 65 | loader = PDFPlumberLoader(source_uri) 66 | loaded_docs = loader.load() 67 | 68 | if not loaded_docs: 69 | print(f"Warning: No content loaded from: {source_uri}") 70 | failed_files.append(source_uri) 71 | continue 72 | 73 | docs.extend(loaded_docs) 74 | successful_files += 1 75 | print( 76 | f"Successfully loaded {len(loaded_docs)} pages from: {source_uri}" 77 | ) 78 | 79 | except Exception as e: 80 | print(f"Error loading PDF {source_uri}: {e}") 81 | failed_files.append(source_uri) 82 | continue 83 | 84 | # 결과 요약 출력 85 | print(f"\nLoading Summary:") 86 | print(f"- Successfully loaded: {successful_files} files") 87 | print(f"- Failed to load: {len(failed_files)} files") 88 | if failed_files: 89 | print(f"- Failed files: {failed_files}") 90 | print(f"- Total documents loaded: {len(docs)}") 91 | 92 | if not docs: 93 | raise ValueError( 94 | "No documents were successfully loaded from the provided source URIs" 95 | ) 96 | 97 | return docs 98 | 99 | def create_text_splitter(self) -> RecursiveCharacterTextSplitter: 100 | return RecursiveCharacterTextSplitter( 101 | chunk_size=1200, 102 | chunk_overlap=200, 103 | length_function=len, 104 | is_separator_regex=False, 105 | ) 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📚 LangGraph Tutorial 2 | 3 | > **LangGraph의 모든 것을 마스터하세요** 4 | > 이 종합 가이드는 초급자부터 고급 개발자까지 LangGraph의 핵심 개념부터 실무 활용까지 체계적으로 학습할 수 있도록 설계된 한국어 튜토리얼입니다. 실전 프로젝트와 심화 실습을 통해 복잡한 AI 에이전트 시스템을 구축하는 전문 역량을 키워보세요. 5 | 6 | 7 | 8 | 9 | LangGraph Logo 10 | 11 | 12 |
13 |
14 |
15 | 16 | [![Version](https://img.shields.io/pypi/v/langgraph.svg)](https://pypi.org/project/langgraph/) 17 | [![Downloads](https://static.pepy.tech/badge/langgraph/month)](https://pepy.tech/project/langgraph) 18 | [![Open Issues](https://img.shields.io/github/issues-raw/langchain-ai/langgraph)](https://github.com/langchain-ai/langgraph/issues) 19 | [![Docs](https://img.shields.io/badge/docs-latest-blue)](https://langchain-ai.github.io/langgraph/) 20 | 21 | **LangGraph**는 장기 실행, 상태 유지 에이전트를 구축, 관리 및 배포하기 위한 저수준 오케스트레이션 프레임워크입니다. Klarna, Replit, Elastic 등 에이전트 미래를 선도하는 기업들에서 신뢰받고 있으며, 복잡한 AI 워크플로우를 위한 강력한 도구를 제공합니다. 22 | 23 | ## 🎯 핵심 기능 24 | 25 | - **내구성 있는 실행**: 장애를 견디고 장기간 실행 가능한 에이전트 구축, 중단된 지점에서 자동 재개 26 | - **휴먼-인-더-루프**: 실행 중 언제든지 에이전트 상태를 검사하고 수정하여 인간의 감독을 원활히 통합 27 | - **포괄적인 메모리**: 진행 중인 추론을 위한 단기 작업 메모리와 세션 간 장기 지속 메모리를 갖춘 진정한 상태 유지 에이전트 28 | - **LangSmith 디버깅**: 실행 경로 추적, 상태 전환 캡처, 상세한 런타임 메트릭을 제공하는 시각화 도구 29 | - **프로덕션 배포**: 상태 유지 장기 실행 워크플로우의 고유한 문제를 처리하도록 설계된 확장 가능한 인프라 30 | 31 | ## ⬇️ 프로젝트 다운로드 32 | 33 | 다음 명령어를 사용하여 프로젝트를 다운로드하십시오: 34 | 35 | ```bash 36 | git clone https://github.com/teddynote-lab/LangGraph-Tutorial.git 37 | cd LangGraph-Tutorial 38 | ``` 39 | 40 | ## 🔧 설치 방법 41 | 42 | ### UV 패키지 매니저를 사용한 설치 43 | 44 | 본 프로젝트는 `uv` 패키지 매니저를 사용하여 의존성을 관리합니다. 다음 단계를 따라 설치하십시오. 45 | 46 | #### UV 설치 (사전 요구사항) 47 | 48 | **macOS:** 49 | ```bash 50 | # Homebrew 사용 51 | brew install uv 52 | 53 | # 또는 curl 사용 54 | curl -LsSf https://astral.sh/uv/install.sh | sh 55 | ``` 56 | 57 | **Windows:** 58 | ```powershell 59 | # PowerShell 사용 60 | powershell -c "irm https://astral.sh/uv/install.ps1 | iex" 61 | 62 | # 또는 pip 사용 63 | pip install uv 64 | ``` 65 | 66 | #### 프로젝트 의존성 설치 67 | 68 | UV가 설치되었다면, 다음 명령어로 프로젝트 의존성을 설치하십시오: 69 | 70 | ```bash 71 | uv sync 72 | ``` 73 | 74 | 이 명령어는 가상 환경을 자동으로 생성하고 모든 필요한 의존성을 설치합니다. 75 | 76 | #### 가상 환경 활성화 77 | 78 | ```bash 79 | # 가상 환경 활성화 80 | source .venv/bin/activate # macOS/Linux 81 | 82 | # 또는 83 | .venv\Scripts\activate # Windows 84 | ``` 85 | 86 | ## 📁 프로젝트 폴더 구성 87 | 88 | ``` 89 | langgraph-tutorial/ 90 | ├── 01-QuickStart/ # LangGraph 빠른 시작 가이드 91 | │ ├── 01-QuickStart-LangGraph-Tutorial.ipynb 92 | │ ├── 02-QuickStart-LangGraph-Graph-API.ipynb 93 | │ └── 03-QuickStart-LangGraph-Subgraph.ipynb 94 | ├── 02-Practice/ # 실습 문제 모음 95 | │ ├── 01-Practice-Chatbot.ipynb 96 | │ ├── 02-Practice-Tool-Integration.ipynb 97 | │ ├── 03-Practice-Memory-Chatbot.ipynb 98 | │ ├── 04-Practice-Human-in-the-Loop.ipynb 99 | │ ├── 05-Practice-State-Customization.ipynb 100 | │ ├── 06-Practice-Business-Email-Bot.ipynb 101 | │ ├── 07-Practice-Send-Multi-Query-RAG.ipynb 102 | │ ├── 08-Practice-Command-Workflow.ipynb 103 | │ └── 09-Practice-Financial-Multi-Query-RAG.ipynb 104 | ├── 03-Modules/ # 핵심 모듈별 심화 학습 105 | │ ├── 01-Core-Features/ # LangGraph 핵심 기능 106 | │ ├── 02-RAG/ # Retrieval-Augmented Generation 107 | │ ├── 03-Use-Cases/ # 실제 활용 사례들 108 | │ ├── 04-MCP/ # Model Context Protocol 109 | │ ├── 05-Supervisor/ # 멀티 에이전트 관리 110 | │ └── 06-Memory/ # 메모리 관리 시스템 111 | └── 99-Templates/ # 개발용 템플릿 112 | └── 00-Practice-Template.ipynb 113 | ``` 114 | 115 | **참고** 116 | - 02-Practice/ 폴더에는 실습 문제가 있습니다. 이에 대한 정답지는 별도로 제공될 예정입니다. 117 | 118 | ### 폴더별 상세 설명 119 | 120 | - **01-QuickStart/**: LangGraph의 기본 개념과 사용법을 빠르게 익힐 수 있는 입문 자료 121 | - **02-Practice/**: 단계별 실습 문제를 통해 실무 능력을 향상시킬 수 있는 연습 자료 122 | - **03-Modules/**: 각 기능별로 세분화된 심화 학습 자료 123 | - **01-Core-Features/**: 기본 기능, 챗봇, 에이전트, 메모리, 스트리밍 등 124 | - **02-RAG/**: 문서 검색 및 생성 통합 시스템 구현 125 | - **03-Use-Cases/**: 실제 비즈니스 시나리오 기반 활용 사례 126 | - **04-MCP/**: Model Context Protocol을 활용한 고급 통합 127 | - **05-Supervisor/**: 멀티 에이전트 협업 및 관리 시스템 128 | - **06-Memory/**: 장기 메모리 및 상태 관리 시스템 129 | - **99-Templates/**: 새로운 실습이나 프로젝트 개발을 위한 기본 템플릿 130 | 131 | ## 🔗 참고 링크 132 | 133 | ### 📚 공식 문서 및 리포지토리 134 | - [LangGraph 공식 GitHub](https://github.com/langchain-ai/langgraph) - LangGraph 소스 코드 및 최신 업데이트 135 | - [LangGraph 공식 문서](https://langchain-ai.github.io/langgraph/) - 상세한 API 문서 및 가이드 136 | 137 | ### 🎓 학습 자료 138 | - [테디노트 유튜브 채널](https://www.youtube.com/c/teddynote) - AI/ML 관련 한국어 강의 및 튜토리얼 139 | - [RAG 고급 온라인 강의](https://fastcampus.co.kr/data_online_teddy) - 체계적인 RAG 시스템 구축 강의 140 | 141 | ## 📄 라이센스 142 | 143 | 본 프로젝트의 라이센스 정보는 [LICENSE](./LICENSE) 파일을 참조하십시오. 144 | 145 | ## 🏢 제작자 146 | 147 | **Made by TeddyNote LAB** -------------------------------------------------------------------------------- /03-Modules/02-RAG/rag/base.py: -------------------------------------------------------------------------------- 1 | from langchain_core.output_parsers import StrOutputParser 2 | from langchain_community.vectorstores import FAISS 3 | from langchain_openai import OpenAIEmbeddings, ChatOpenAI 4 | from langchain.embeddings.cache import CacheBackedEmbeddings 5 | from langchain.storage import LocalFileStore 6 | 7 | from abc import ABC, abstractmethod 8 | from operator import itemgetter 9 | from pathlib import Path 10 | import os 11 | import hashlib 12 | from langchain import hub 13 | 14 | 15 | class RetrievalChain(ABC): 16 | def __init__(self): 17 | self.source_uri = None 18 | self.k = 8 19 | self.model_name = "gpt-4.1-mini" 20 | self.temperature = 0 21 | self.prompt = "teddynote/rag-prompt" 22 | self.embeddings = "text-embedding-3-small" 23 | self.cache_dir = Path(".cache/embeddings") 24 | self.index_dir = Path(".cache/faiss_index") 25 | 26 | @abstractmethod 27 | def load_documents(self, source_uris): 28 | """loader를 사용하여 문서를 로드합니다.""" 29 | pass 30 | 31 | @abstractmethod 32 | def create_text_splitter(self): 33 | """text splitter를 생성합니다.""" 34 | pass 35 | 36 | def split_documents(self, docs, text_splitter): 37 | """text splitter를 사용하여 문서를 분할합니다.""" 38 | return text_splitter.split_documents(docs) 39 | 40 | def create_embedding(self): 41 | try: 42 | # 캐시 디렉토리 생성 43 | self.cache_dir.mkdir(parents=True, exist_ok=True) 44 | 45 | # 기본 임베딩 모델 생성 46 | underlying_embeddings = OpenAIEmbeddings(model=self.embeddings) 47 | 48 | # 파일 기반 캐시 스토어 생성 49 | store = LocalFileStore(str(self.cache_dir)) 50 | 51 | # 캐시 기반 임베딩 생성 (SHA-256 사용으로 보안 강화) 52 | cached_embeddings = CacheBackedEmbeddings.from_bytes_store( 53 | underlying_embeddings, 54 | store, 55 | namespace=self.embeddings, 56 | key_encoder="sha256" 57 | ) 58 | 59 | return cached_embeddings 60 | 61 | except Exception as e: 62 | print(f"Warning: Failed to create cached embeddings: {e}") 63 | print("Falling back to basic OpenAI embeddings without caching") 64 | return OpenAIEmbeddings(model=self.embeddings) 65 | 66 | def create_vectorstore(self, split_docs): 67 | try: 68 | # 인덱스 디렉토리 생성 69 | self.index_dir.mkdir(parents=True, exist_ok=True) 70 | 71 | # 문서 내용 기반 해시 계산 72 | doc_contents = "\n".join([doc.page_content for doc in split_docs]) 73 | doc_hash = hashlib.md5(doc_contents.encode()).hexdigest() 74 | 75 | # 해시 파일 경로와 인덱스 파일 경로 76 | hash_file = self.index_dir / "doc_hash.txt" 77 | index_path = str(self.index_dir / "faiss_index") 78 | 79 | # 기존 인덱스가 있고 문서가 변경되지 않았는지 확인 80 | try: 81 | if ( 82 | hash_file.exists() 83 | and Path(index_path + ".faiss").exists() 84 | and hash_file.read_text().strip() == doc_hash 85 | ): 86 | 87 | # 기존 인덱스 로드 시도 88 | vectorstore = FAISS.load_local( 89 | index_path, 90 | self.create_embedding(), 91 | allow_dangerous_deserialization=True, 92 | ) 93 | print("Loaded existing FAISS index from cache") 94 | return vectorstore 95 | 96 | except Exception as e: 97 | print(f"Warning: Failed to load existing index: {e}") 98 | print("Creating new index...") 99 | 100 | # 새로운 인덱스 생성 101 | vectorstore = FAISS.from_documents( 102 | documents=split_docs, embedding=self.create_embedding() 103 | ) 104 | 105 | # 인덱스와 해시 저장 시도 106 | try: 107 | vectorstore.save_local(index_path) 108 | hash_file.write_text(doc_hash) 109 | print("FAISS index saved to cache") 110 | except Exception as e: 111 | print(f"Warning: Failed to save index to cache: {e}") 112 | print("Index will not be cached for next use") 113 | 114 | return vectorstore 115 | 116 | except Exception as e: 117 | print(f"Error: Failed to create vectorstore with caching: {e}") 118 | print("Falling back to basic FAISS creation without caching") 119 | return FAISS.from_documents( 120 | documents=split_docs, embedding=self.create_embedding() 121 | ) 122 | 123 | def create_retriever(self, vectorstore): 124 | # Cosine Similarity 사용하여 검색을 수행하는 retriever를 생성합니다. 125 | dense_retriever = vectorstore.as_retriever( 126 | search_type="similarity", search_kwargs={"k": self.k} 127 | ) 128 | return dense_retriever 129 | 130 | def create_model(self): 131 | return ChatOpenAI(model_name=self.model_name, temperature=self.temperature) 132 | 133 | def create_prompt(self): 134 | return hub.pull(self.prompt) 135 | 136 | def create_chain(self): 137 | docs = self.load_documents(self.source_uri) 138 | text_splitter = self.create_text_splitter() 139 | split_docs = self.split_documents(docs, text_splitter) 140 | self.vectorstore = self.create_vectorstore(split_docs) 141 | self.retriever = self.create_retriever(self.vectorstore) 142 | model = self.create_model() 143 | prompt = self.create_prompt() 144 | self.chain = ( 145 | {"question": itemgetter("question"), "context": itemgetter("context")} 146 | | prompt 147 | | model 148 | | StrOutputParser() 149 | ) 150 | return self 151 | -------------------------------------------------------------------------------- /03-Modules/03-Use-Cases/rag/base.py: -------------------------------------------------------------------------------- 1 | from langchain_core.output_parsers import StrOutputParser 2 | from langchain_community.vectorstores import FAISS 3 | from langchain_openai import OpenAIEmbeddings, ChatOpenAI 4 | from langchain.embeddings.cache import CacheBackedEmbeddings 5 | from langchain.storage import LocalFileStore 6 | 7 | from abc import ABC, abstractmethod 8 | from operator import itemgetter 9 | from pathlib import Path 10 | import os 11 | import hashlib 12 | from langchain import hub 13 | 14 | 15 | class RetrievalChain(ABC): 16 | def __init__(self): 17 | self.source_uri = None 18 | self.k = 8 19 | self.model_name = "gpt-4.1-mini" 20 | self.temperature = 0 21 | self.prompt = "teddynote/rag-prompt" 22 | self.embeddings = "text-embedding-3-small" 23 | self.cache_dir = Path(".cache/embeddings") 24 | self.index_dir = Path(".cache/faiss_index") 25 | 26 | @abstractmethod 27 | def load_documents(self, source_uris): 28 | """loader를 사용하여 문서를 로드합니다.""" 29 | pass 30 | 31 | @abstractmethod 32 | def create_text_splitter(self): 33 | """text splitter를 생성합니다.""" 34 | pass 35 | 36 | def split_documents(self, docs, text_splitter): 37 | """text splitter를 사용하여 문서를 분할합니다.""" 38 | return text_splitter.split_documents(docs) 39 | 40 | def create_embedding(self): 41 | try: 42 | # 캐시 디렉토리 생성 43 | self.cache_dir.mkdir(parents=True, exist_ok=True) 44 | 45 | # 기본 임베딩 모델 생성 46 | underlying_embeddings = OpenAIEmbeddings(model=self.embeddings) 47 | 48 | # 파일 기반 캐시 스토어 생성 49 | store = LocalFileStore(str(self.cache_dir)) 50 | 51 | # 캐시 기반 임베딩 생성 (SHA-256 사용으로 보안 강화) 52 | cached_embeddings = CacheBackedEmbeddings.from_bytes_store( 53 | underlying_embeddings, 54 | store, 55 | namespace=self.embeddings, 56 | key_encoder="sha256" 57 | ) 58 | 59 | return cached_embeddings 60 | 61 | except Exception as e: 62 | print(f"Warning: Failed to create cached embeddings: {e}") 63 | print("Falling back to basic OpenAI embeddings without caching") 64 | return OpenAIEmbeddings(model=self.embeddings) 65 | 66 | def create_vectorstore(self, split_docs): 67 | try: 68 | # 인덱스 디렉토리 생성 69 | self.index_dir.mkdir(parents=True, exist_ok=True) 70 | 71 | # 문서 내용 기반 해시 계산 72 | doc_contents = "\n".join([doc.page_content for doc in split_docs]) 73 | doc_hash = hashlib.md5(doc_contents.encode()).hexdigest() 74 | 75 | # 해시 파일 경로와 인덱스 파일 경로 76 | hash_file = self.index_dir / "doc_hash.txt" 77 | index_path = str(self.index_dir / "faiss_index") 78 | 79 | # 기존 인덱스가 있고 문서가 변경되지 않았는지 확인 80 | try: 81 | if ( 82 | hash_file.exists() 83 | and Path(index_path + ".faiss").exists() 84 | and hash_file.read_text().strip() == doc_hash 85 | ): 86 | 87 | # 기존 인덱스 로드 시도 88 | vectorstore = FAISS.load_local( 89 | index_path, 90 | self.create_embedding(), 91 | allow_dangerous_deserialization=True, 92 | ) 93 | print("Loaded existing FAISS index from cache") 94 | return vectorstore 95 | 96 | except Exception as e: 97 | print(f"Warning: Failed to load existing index: {e}") 98 | print("Creating new index...") 99 | 100 | # 새로운 인덱스 생성 101 | vectorstore = FAISS.from_documents( 102 | documents=split_docs, embedding=self.create_embedding() 103 | ) 104 | 105 | # 인덱스와 해시 저장 시도 106 | try: 107 | vectorstore.save_local(index_path) 108 | hash_file.write_text(doc_hash) 109 | print("FAISS index saved to cache") 110 | except Exception as e: 111 | print(f"Warning: Failed to save index to cache: {e}") 112 | print("Index will not be cached for next use") 113 | 114 | return vectorstore 115 | 116 | except Exception as e: 117 | print(f"Error: Failed to create vectorstore with caching: {e}") 118 | print("Falling back to basic FAISS creation without caching") 119 | return FAISS.from_documents( 120 | documents=split_docs, embedding=self.create_embedding() 121 | ) 122 | 123 | def create_retriever(self, vectorstore): 124 | # Cosine Similarity 사용하여 검색을 수행하는 retriever를 생성합니다. 125 | dense_retriever = vectorstore.as_retriever( 126 | search_type="similarity", search_kwargs={"k": self.k} 127 | ) 128 | return dense_retriever 129 | 130 | def create_model(self): 131 | return ChatOpenAI(model_name=self.model_name, temperature=self.temperature) 132 | 133 | def create_prompt(self): 134 | return hub.pull(self.prompt) 135 | 136 | def create_chain(self): 137 | docs = self.load_documents(self.source_uri) 138 | text_splitter = self.create_text_splitter() 139 | split_docs = self.split_documents(docs, text_splitter) 140 | self.vectorstore = self.create_vectorstore(split_docs) 141 | self.retriever = self.create_retriever(self.vectorstore) 142 | model = self.create_model() 143 | prompt = self.create_prompt() 144 | self.chain = ( 145 | {"question": itemgetter("question"), "context": itemgetter("context")} 146 | | prompt 147 | | model 148 | | StrOutputParser() 149 | ) 150 | return self 151 | -------------------------------------------------------------------------------- /03-Modules/04-MCP/server/rag/base.py: -------------------------------------------------------------------------------- 1 | from langchain_core.output_parsers import StrOutputParser 2 | from langchain_community.vectorstores import FAISS 3 | from langchain_openai import OpenAIEmbeddings, ChatOpenAI 4 | from langchain.embeddings.cache import CacheBackedEmbeddings 5 | from langchain.storage import LocalFileStore 6 | 7 | from abc import ABC, abstractmethod 8 | from operator import itemgetter 9 | from pathlib import Path 10 | import os 11 | import hashlib 12 | from langchain import hub 13 | 14 | 15 | class RetrievalChain(ABC): 16 | def __init__(self): 17 | self.source_uri = None 18 | self.k = 8 19 | self.model_name = "gpt-4.1-mini" 20 | self.temperature = 0 21 | self.prompt = "teddynote/rag-prompt" 22 | self.embeddings = "text-embedding-3-small" 23 | self.cache_dir = Path(".cache/embeddings") 24 | self.index_dir = Path(".cache/faiss_index") 25 | 26 | @abstractmethod 27 | def load_documents(self, source_uris): 28 | """loader를 사용하여 문서를 로드합니다.""" 29 | pass 30 | 31 | @abstractmethod 32 | def create_text_splitter(self): 33 | """text splitter를 생성합니다.""" 34 | pass 35 | 36 | def split_documents(self, docs, text_splitter): 37 | """text splitter를 사용하여 문서를 분할합니다.""" 38 | return text_splitter.split_documents(docs) 39 | 40 | def create_embedding(self): 41 | try: 42 | # 캐시 디렉토리 생성 43 | self.cache_dir.mkdir(parents=True, exist_ok=True) 44 | 45 | # 기본 임베딩 모델 생성 46 | underlying_embeddings = OpenAIEmbeddings(model=self.embeddings) 47 | 48 | # 파일 기반 캐시 스토어 생성 49 | store = LocalFileStore(str(self.cache_dir)) 50 | 51 | # 캐시 기반 임베딩 생성 (SHA-256 사용으로 보안 강화) 52 | cached_embeddings = CacheBackedEmbeddings.from_bytes_store( 53 | underlying_embeddings, 54 | store, 55 | namespace=self.embeddings, 56 | key_encoder="sha256" 57 | ) 58 | 59 | return cached_embeddings 60 | 61 | except Exception as e: 62 | print(f"Warning: Failed to create cached embeddings: {e}") 63 | print("Falling back to basic OpenAI embeddings without caching") 64 | return OpenAIEmbeddings(model=self.embeddings) 65 | 66 | def create_vectorstore(self, split_docs): 67 | try: 68 | # 인덱스 디렉토리 생성 69 | self.index_dir.mkdir(parents=True, exist_ok=True) 70 | 71 | # 문서 내용 기반 해시 계산 72 | doc_contents = "\n".join([doc.page_content for doc in split_docs]) 73 | doc_hash = hashlib.md5(doc_contents.encode()).hexdigest() 74 | 75 | # 해시 파일 경로와 인덱스 파일 경로 76 | hash_file = self.index_dir / "doc_hash.txt" 77 | index_path = str(self.index_dir / "faiss_index") 78 | 79 | # 기존 인덱스가 있고 문서가 변경되지 않았는지 확인 80 | try: 81 | if ( 82 | hash_file.exists() 83 | and Path(index_path + ".faiss").exists() 84 | and hash_file.read_text().strip() == doc_hash 85 | ): 86 | 87 | # 기존 인덱스 로드 시도 88 | vectorstore = FAISS.load_local( 89 | index_path, 90 | self.create_embedding(), 91 | allow_dangerous_deserialization=True, 92 | ) 93 | print("Loaded existing FAISS index from cache") 94 | return vectorstore 95 | 96 | except Exception as e: 97 | print(f"Warning: Failed to load existing index: {e}") 98 | print("Creating new index...") 99 | 100 | # 새로운 인덱스 생성 101 | vectorstore = FAISS.from_documents( 102 | documents=split_docs, embedding=self.create_embedding() 103 | ) 104 | 105 | # 인덱스와 해시 저장 시도 106 | try: 107 | vectorstore.save_local(index_path) 108 | hash_file.write_text(doc_hash) 109 | print("FAISS index saved to cache") 110 | except Exception as e: 111 | print(f"Warning: Failed to save index to cache: {e}") 112 | print("Index will not be cached for next use") 113 | 114 | return vectorstore 115 | 116 | except Exception as e: 117 | print(f"Error: Failed to create vectorstore with caching: {e}") 118 | print("Falling back to basic FAISS creation without caching") 119 | return FAISS.from_documents( 120 | documents=split_docs, embedding=self.create_embedding() 121 | ) 122 | 123 | def create_retriever(self, vectorstore): 124 | # Cosine Similarity 사용하여 검색을 수행하는 retriever를 생성합니다. 125 | dense_retriever = vectorstore.as_retriever( 126 | search_type="similarity", search_kwargs={"k": self.k} 127 | ) 128 | return dense_retriever 129 | 130 | def create_model(self): 131 | return ChatOpenAI(model_name=self.model_name, temperature=self.temperature) 132 | 133 | def create_prompt(self): 134 | return hub.pull(self.prompt) 135 | 136 | def create_chain(self): 137 | docs = self.load_documents(self.source_uri) 138 | text_splitter = self.create_text_splitter() 139 | split_docs = self.split_documents(docs, text_splitter) 140 | self.vectorstore = self.create_vectorstore(split_docs) 141 | self.retriever = self.create_retriever(self.vectorstore) 142 | model = self.create_model() 143 | prompt = self.create_prompt() 144 | self.chain = ( 145 | {"question": itemgetter("question"), "context": itemgetter("context")} 146 | | prompt 147 | | model 148 | | StrOutputParser() 149 | ) 150 | return self 151 | -------------------------------------------------------------------------------- /02-Practice/01-Practice-Chatbot.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "d0fdb214", 6 | "metadata": {}, 7 | "source": [ 8 | "## 기본 챗봇 구축 연습\n", 9 | "\n", 10 | "목표\n", 11 | "- StateGraph와 `messages` 상태를 정의하고, 간단한 챗봇 노드를 만들어 그래프를 실행합니다.\n", 12 | "- START → chatbot → END 경로를 구성합니다.\n", 13 | "\n", 14 | "요구사항\n", 15 | "- `State` TypedDict에 `messages: Annotated[list, add_messages]`를 정의하세요.\n", 16 | "- `ChatOpenAI` 모델(gpt-4.1, temperature=0)로 마지막 메시지를 받아 응답을 반환하는 `chatbot` 노드를 구현하세요.\n", 17 | "- 그래프를 compile 한 뒤, `HumanMessage` 1개로 실행하고 응답을 확인하세요.\n" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "id": "dad0af2b", 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from dotenv import load_dotenv\n", 28 | "from langchain_teddynote import logging\n", 29 | "\n", 30 | "load_dotenv(override=True)\n", 31 | "\n", 32 | "# 프로젝트 이름\n", 33 | "logging.langsmith(\"LangGraph-Exercises\")" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "0098e0ca", 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "# Part 1 준비 코드\n", 44 | "from typing import Annotated\n", 45 | "from typing_extensions import TypedDict\n", 46 | "from dotenv import load_dotenv\n", 47 | "\n", 48 | "from langchain_teddynote import logging\n", 49 | "from langchain_openai import ChatOpenAI\n", 50 | "from langchain_core.messages import HumanMessage\n", 51 | "from langgraph.graph import StateGraph, START, END\n", 52 | "from langgraph.graph.message import add_messages" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "id": "f7f59d67", 58 | "metadata": {}, 59 | "source": [ 60 | "### 1. 모델을 생성해 주세요.\n", 61 | "\n", 62 | "- 사용 모델: `ChatOpenAI`, 모델명: `gpt-4.1`, 온도: `0`" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "82de1d13", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "# 실습 코드\n", 73 | "# TODO: LLM 모델을 정의해 주세요.\n", 74 | "llm = # 코드 입력" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "id": "5b13fee5", 80 | "metadata": {}, 81 | "source": [ 82 | "### 2. State 정의\n", 83 | "\n", 84 | "- 그래프에서 사용할 State를 정의해 주세요." 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "id": "53bc398c", 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "# 실습 코드\n", 95 | "# TODO: 그래프에서 사용할 State를 정의해 주세요.\n", 96 | "\n", 97 | "class State(TypedDict):\n", 98 | " messages: # 코드 입력: add_messages 사용" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "id": "04419820", 104 | "metadata": {}, 105 | "source": [ 106 | "### 3. Chatbot 노드 작성\n", 107 | "\n", 108 | "- Chatbot 노드를 작성해 주세요.\n", 109 | "- 노드에서는 LLM 의 추론이 이루어 집니다.\n", 110 | "- 반환 형식은 `messages` 키에 응답을 배열로 담아 반환해 주세요." 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "id": "6e7f8113", 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "# 실습 코드\n", 121 | "# TODO: state[\"messages\"]를 모델에 전달하고, 응답을 반환\n", 122 | "# 응답 반환시 스키마에 유념하세요.\n", 123 | "\n", 124 | "def chatbot(state: State):\n", 125 | " response = # 코드 입력\n", 126 | " return # 코드 입력" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "id": "58cc1a4c", 132 | "metadata": {}, 133 | "source": [ 134 | "### 4. 그래프 생성\n", 135 | "\n", 136 | "- 그래프는 `StateGraph` 를 사용해 주세요.\n", 137 | "- 그래프는 `START` → `chatbot` → `END` 경로를 가지도록 주세요." 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "id": "ff29a21e", 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "# 실습 코드\n", 148 | "# TODO: 노드를 정의하고 엣지를 구성해 주세요.\n", 149 | "\n", 150 | "builder = # 코드 입력\n", 151 | "builder.# 코드 입력\n", 152 | "builder.# 코드 입력\n", 153 | "builder.# 코드 입력\n", 154 | "\n", 155 | "app = builder.# 코드 입력" 156 | ] 157 | }, 158 | { 159 | "cell_type": "markdown", 160 | "id": "43230cb4", 161 | "metadata": {}, 162 | "source": [ 163 | "### 5. 그래프 시각화\n", 164 | "\n", 165 | "- 컴파일한 그래프를 시각화 하세요." 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": null, 171 | "id": "097a0804", 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [ 175 | "# 실습 코드\n", 176 | "# TODO: 그래프를 시각화 하세요.\n", 177 | "\n", 178 | "from langchain_teddynote.graphs import visualize_graph\n", 179 | "\n", 180 | "# 코드 입력" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "id": "81ca4f71", 186 | "metadata": {}, 187 | "source": [ 188 | "### 6. 그래프 실행\n", 189 | "\n", 190 | "- 메시지를 입력하고 결과를 출력하세요." 191 | ] 192 | }, 193 | { 194 | "cell_type": "code", 195 | "execution_count": null, 196 | "id": "f2e95605", 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "# 실습 코드\n", 201 | "# TODO: 메시지를 입력하고 결과를 출력하세요.\n", 202 | "from langchain_teddynote.messages import stream_graph\n", 203 | "\n", 204 | "inputs = {\n", 205 | " \"messages\": [\n", 206 | " HumanMessage(content=\"이번 주 팀 회의 공지 초안을 4문장으로 작성해줘.\")\n", 207 | " ]\n", 208 | "}\n", 209 | "stream_graph(app, inputs=inputs)" 210 | ] 211 | } 212 | ], 213 | "metadata": { 214 | "kernelspec": { 215 | "display_name": ".venv", 216 | "language": "python", 217 | "name": "python3" 218 | }, 219 | "language_info": { 220 | "codemirror_mode": { 221 | "name": "ipython", 222 | "version": 3 223 | }, 224 | "file_extension": ".py", 225 | "mimetype": "text/x-python", 226 | "name": "python", 227 | "nbconvert_exporter": "python", 228 | "pygments_lexer": "ipython3", 229 | "version": "3.12.8" 230 | } 231 | }, 232 | "nbformat": 4, 233 | "nbformat_minor": 5 234 | } 235 | -------------------------------------------------------------------------------- /02-Practice/02-Practice-Tool-Integration.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 도구 연동 연습 - Tavily 검색 도구를 활용한 제조 도메인 챗봇\n", 8 | "\n", 9 | "목표\n", 10 | "- Tavily Search API를 활용한 검색 도구를 그래프에 통합합니다.\n", 11 | "- 도구 호출 조건부 라우팅을 구현하여 검색이 필요한 경우 자동으로 도구를 사용합니다.\n", 12 | "- 제조업 관련 정보(스마트 팩토리, 산업 자동화 등)를 검색하고 답변하는 챗봇을 구축합니다.\n", 13 | "\n", 14 | "요구사항\n", 15 | "- `TavilySearch` 도구를 설정하고 LLM에 바인딩합니다.\n", 16 | "- 도구 호출이 필요한 경우 `tools` 노드로, 아니면 종료하는 조건부 라우팅을 구현합니다.\n", 17 | "- START → chatbot → 조건부 라우팅 → tools/END 경로를 구성합니다.\n", 18 | "- 제조업 관련 질문으로 테스트하여 검색 결과가 올바르게 반환되는지 확인합니다." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from dotenv import load_dotenv\n", 28 | "from langchain_teddynote import logging\n", 29 | "\n", 30 | "load_dotenv(override=True)\n", 31 | "\n", 32 | "# 프로젝트 이름\n", 33 | "logging.langsmith(\"LangGraph-Tool-Integration\")" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "# 준비 코드\n", 43 | "from typing import Annotated\n", 44 | "from typing_extensions import TypedDict\n", 45 | "\n", 46 | "from langchain_openai import ChatOpenAI\n", 47 | "from langchain_tavily import TavilySearch\n", 48 | "from langchain_core.messages import HumanMessage, AIMessage\n", 49 | "from langgraph.graph import StateGraph, START, END\n", 50 | "from langgraph.graph.message import add_messages\n", 51 | "from langgraph.prebuilt import ToolNode, tools_condition" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "### 1. State 및 LLM 모델 정의\n", 59 | "\n", 60 | "- `State` TypedDict에 `messages: Annotated[list, add_messages]`를 정의하세요.\n", 61 | "- ChatOpenAI 모델을 생성하세요. (model=\"gpt-4o-mini\", temperature=0)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "# 실습 코드\n", 71 | "# TODO: State 클래스를 정의하세요.\n", 72 | "class State(TypedDict):\n", 73 | " messages: # 코드 입력\n", 74 | "\n", 75 | "# TODO: LLM 모델을 생성하세요.\n", 76 | "llm = # 코드 입력" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "### 2. Tavily 검색 도구 설정\n", 84 | "\n", 85 | "- TavilySearch 도구를 생성하세요. (max_results=3)\n", 86 | "- 도구를 리스트로 만들고 LLM에 바인딩하세요.\n", 87 | "- 간단한 테스트로 \"스마트 팩토리 최신 동향\"을 검색해보세요." 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "# 실습 코드\n", 97 | "# TODO: Tavily 검색 도구를 생성하세요.\n", 98 | "tavily_tool = # 코드 입력\n", 99 | "tools = # 코드 입력\n", 100 | "\n", 101 | "# TODO: LLM에 도구를 바인딩하세요.\n", 102 | "llm_with_tools = # 코드 입력\n", 103 | "\n", 104 | "# TODO: 도구 테스트 - \"스마트 팩토리 최신 동향\" 검색\n", 105 | "test_result = # 코드 입력\n", 106 | "print(f\"검색 결과 수: {len(test_result['results'])}개\")" 107 | ] 108 | }, 109 | { 110 | "cell_type": "markdown", 111 | "metadata": {}, 112 | "source": [ 113 | "### 3. Chatbot 노드 구현\n", 114 | "\n", 115 | "- 도구가 바인딩된 LLM을 사용하여 chatbot 노드를 구현하세요.\n", 116 | "- state의 messages를 받아 응답을 반환합니다.\n", 117 | "- 반환 형식은 {\"messages\": [response]} 입니다." 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "# 실습 코드\n", 127 | "# TODO: chatbot 노드를 구현하세요.\n", 128 | "def chatbot(state: State):\n", 129 | " response = # 코드 입력\n", 130 | " return # 코드 입력" 131 | ] 132 | }, 133 | { 134 | "cell_type": "markdown", 135 | "metadata": {}, 136 | "source": [ 137 | "### 4. 그래프 구성 및 조건부 라우팅\n", 138 | "\n", 139 | "- StateGraph를 생성하고 노드들을 추가하세요.\n", 140 | "- chatbot 노드와 ToolNode를 추가합니다.\n", 141 | "- chatbot에서 조건부 라우팅을 설정합니다 (tools_condition 사용).\n", 142 | "- tools 노드는 다시 chatbot으로 연결됩니다." 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "# 실습 코드\n", 152 | "# TODO: 그래프를 구성하세요.\n", 153 | "builder = # 코드 입력\n", 154 | "\n", 155 | "# TODO: 노드들을 추가하세요.\n", 156 | "builder.add_node(# 코드 입력)\n", 157 | "builder.add_node(# 코드 입력)\n", 158 | "\n", 159 | "# TODO: 엣지를 추가하세요.\n", 160 | "builder.add_edge(# 코드 입력)\n", 161 | "builder.add_conditional_edges(# 코드 입력)\n", 162 | "builder.add_edge(# 코드 입력)\n", 163 | "\n", 164 | "# TODO: 그래프를 컴파일하세요.\n", 165 | "app = # 코드 입력" 166 | ] 167 | }, 168 | { 169 | "cell_type": "markdown", 170 | "metadata": {}, 171 | "source": [ 172 | "### 5. 그래프 시각화 및 실행\n", 173 | "\n", 174 | "- 컴파일된 그래프를 시각화하세요.\n", 175 | "- 제조업 관련 질문으로 그래프를 실행하고 결과를 확인하세요.\n", 176 | "- 예시: \"한국의 산업용 로봇 시장 현황과 주요 기업은?\"" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": null, 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "# 실습 코드\n", 186 | "# TODO: 그래프를 시각화하세요.\n", 187 | "from langchain_teddynote.graphs import visualize_graph\n", 188 | "\n", 189 | "# 코드 입력" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "metadata": {}, 196 | "outputs": [], 197 | "source": [ 198 | "# 실습 코드\n", 199 | "# TODO: 제조업 관련 질문으로 실행하세요.\n", 200 | "from langchain_teddynote.messages import stream_graph\n", 201 | "from langchain_core.runnables import RunnableConfig\n", 202 | "\n", 203 | "config = RunnableConfig(recursion_limit=10)\n", 204 | "\n", 205 | "inputs = {\n", 206 | " \"messages\": [\n", 207 | " # 코드 입력: HumanMessage로 질문 작성\n", 208 | " ]\n", 209 | "}\n", 210 | "\n", 211 | "# 코드 입력: stream_graph로 실행" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": {}, 217 | "source": [ 218 | "아래의 코드를 실행하여 결과를 확인하세요" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": null, 224 | "metadata": {}, 225 | "outputs": [], 226 | "source": [ 227 | "# 정답 코드\n", 228 | "# TODO: 제조업 관련 질문으로 실행하세요.\n", 229 | "from langchain_teddynote.messages import stream_graph\n", 230 | "from langchain_core.runnables import RunnableConfig\n", 231 | "\n", 232 | "config = RunnableConfig(recursion_limit=10)\n", 233 | "\n", 234 | "inputs = {\n", 235 | " \"messages\": [HumanMessage(content=\"한국의 산업용 로봇 시장 현황과 주요 기업은?\")]\n", 236 | "}\n", 237 | "\n", 238 | "stream_graph(app, inputs=inputs, config=config)" 239 | ] 240 | }, 241 | { 242 | "cell_type": "markdown", 243 | "metadata": {}, 244 | "source": [ 245 | "### 추가 연습\n", 246 | "\n", 247 | "다음 제조업 관련 질문들로 챗봇을 테스트해보세요:\n", 248 | "- \"스마트 팩토리 구축에 필요한 핵심 기술은?\"\n", 249 | "- \"최신 제조업 자동화 트렌드와 AI 활용 사례는?\"\n", 250 | "- \"글로벌 제조업 디지털 전환 성공 사례를 알려줘\"" 251 | ] 252 | } 253 | ], 254 | "metadata": { 255 | "kernelspec": { 256 | "display_name": ".venv", 257 | "language": "python", 258 | "name": "python3" 259 | }, 260 | "language_info": { 261 | "codemirror_mode": { 262 | "name": "ipython", 263 | "version": 3 264 | }, 265 | "file_extension": ".py", 266 | "mimetype": "text/x-python", 267 | "name": "python", 268 | "nbconvert_exporter": "python", 269 | "pygments_lexer": "ipython3", 270 | "version": "3.12.8" 271 | } 272 | }, 273 | "nbformat": 4, 274 | "nbformat_minor": 4 275 | } 276 | -------------------------------------------------------------------------------- /03-Modules/06-Memory/02-LangGraph-Memory-Postgres.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "e8c45936", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from langgraph.store.postgres import PostgresStore\n", 11 | "from langgraph.checkpoint.postgres import PostgresSaver\n", 12 | "from datetime import datetime\n", 13 | "import uuid\n", 14 | "import os\n", 15 | "\n", 16 | "# 환경변수 설정\n", 17 | "POSTGRES_USER = os.getenv(\"POSTGRES_USER\")\n", 18 | "POSTGRES_PASSWORD = os.getenv(\"POSTGRES_PASSWORD\")\n", 19 | "POSTGRES_HOST = os.getenv(\"POSTGRES_HOST\")\n", 20 | "POSTGRES_PORT = os.getenv(\"POSTGRES_PORT\")\n", 21 | "POSTGRES_DB = os.getenv(\"POSTGRES_DB\")\n", 22 | "\n", 23 | "# 데이터베이스 연결 문자열 생성\n", 24 | "DB_URI = f\"postgres://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}?sslmode=require\"\n", 25 | "\n", 26 | "# 쓰기 설정\n", 27 | "write_config = {\"configurable\": {\"thread_id\": \"1\", \"checkpoint_ns\": \"test1\"}}\n", 28 | "\n", 29 | "# 읽기 설정\n", 30 | "read_config = {\"configurable\": {\"thread_id\": \"1\"}}\n", 31 | "\n", 32 | "# 데이터베이스 연결 설정\n", 33 | "with (\n", 34 | " PostgresStore.from_conn_string(DB_URI) as store,\n", 35 | " PostgresSaver.from_conn_string(DB_URI) as checkpointer,\n", 36 | "):\n", 37 | " checkpointer.setup()\n", 38 | " store.setup()\n", 39 | "\n", 40 | " checkpoint = {\n", 41 | " \"v\": 4,\n", 42 | " \"ts\": datetime.now().isoformat(),\n", 43 | " \"id\": str(uuid.uuid4()),\n", 44 | " \"channel_values\": {\"my_key\": \"teddy\", \"node\": \"node\"},\n", 45 | " \"channel_versions\": {\"__start__\": 2, \"my_key\": 3, \"start:node\": 3, \"node\": 3},\n", 46 | " \"versions_seen\": {\n", 47 | " \"__input__\": {},\n", 48 | " \"__start__\": {\"__start__\": 1},\n", 49 | " \"node\": {\"start:node\": 2},\n", 50 | " },\n", 51 | " }\n", 52 | "\n", 53 | " checkpointer.put(\n", 54 | " write_config,\n", 55 | " checkpoint,\n", 56 | " {\"step\": -1, \"source\": \"input\", \"parents\": {}, \"user_id\": \"1\"},\n", 57 | " {},\n", 58 | " )\n", 59 | "\n", 60 | " # 목록 조회\n", 61 | " print(list(checkpointer.list(read_config)))" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "id": "f95ac651", 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "from langchain_teddynote.memory import create_memory_extractor\n", 72 | "\n", 73 | "memory_extractor = create_memory_extractor(model=\"gpt-4.1\")" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "id": "de6eda32", 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "from langgraph.graph import StateGraph, MessagesState, START\n", 84 | "from langgraph.checkpoint.postgres import PostgresSaver\n", 85 | "from langgraph.store.postgres import PostgresStore\n", 86 | "from langchain_core.runnables import RunnableConfig\n", 87 | "from langgraph.store.base import BaseStore\n", 88 | "from typing import Any\n", 89 | "import uuid\n", 90 | "from langchain_openai import ChatOpenAI\n", 91 | "\n", 92 | "model = ChatOpenAI(model=\"gpt-4.1\", temperature=0)\n", 93 | "\n", 94 | "\n", 95 | "def call_model(\n", 96 | " state: MessagesState,\n", 97 | " config: RunnableConfig,\n", 98 | " *,\n", 99 | " store: BaseStore,\n", 100 | ") -> dict[str, Any]:\n", 101 | " \"\"\"Call the LLM model and manage user memory.\n", 102 | "\n", 103 | " Args:\n", 104 | " state (MessagesState): The current state containing messages.\n", 105 | " config (RunnableConfig): The runnable configuration.\n", 106 | " store (BaseStore): The memory store.\n", 107 | " \"\"\"\n", 108 | " # 마지막 메시지에서 user_id 추출\n", 109 | " user_id = config[\"configurable\"][\"user_id\"]\n", 110 | " namespace = (\"memories\", user_id)\n", 111 | "\n", 112 | " # 유저의 메모리 검색\n", 113 | " memories = store.search(namespace, query=str(state[\"messages\"][-1].content))\n", 114 | " info = \"\\n\".join([f\"{memory.key}: {memory.value}\" for memory in memories])\n", 115 | " system_msg = f\"You are a helpful assistant talking to the user. User info: {info}\"\n", 116 | "\n", 117 | " # 사용자가 기억 요청 시 메모리 저장\n", 118 | " last_message = state[\"messages\"][-1]\n", 119 | " if \"remember\" in last_message.content.lower():\n", 120 | " result = memory_extractor.invoke({\"input\": str(state[\"messages\"][-1].content)})\n", 121 | " for memory in result.memories:\n", 122 | " print(memory)\n", 123 | " print(\"-\" * 100)\n", 124 | " store.put(namespace, str(uuid.uuid4()), {memory.key: memory.value})\n", 125 | "\n", 126 | " # LLM 호출\n", 127 | " response = model.invoke(\n", 128 | " [{\"role\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n", 129 | " )\n", 130 | " return {\"messages\": response}" 131 | ] 132 | }, 133 | { 134 | "cell_type": "code", 135 | "execution_count": null, 136 | "id": "202f4f0e", 137 | "metadata": {}, 138 | "outputs": [], 139 | "source": [ 140 | "from langchain_teddynote.messages import stream_graph\n", 141 | "\n", 142 | "\n", 143 | "with (\n", 144 | " PostgresStore.from_conn_string(DB_URI) as store,\n", 145 | " PostgresSaver.from_conn_string(DB_URI) as checkpointer,\n", 146 | "):\n", 147 | " # 그래프 생성\n", 148 | " builder = StateGraph(MessagesState)\n", 149 | " builder.add_node(\"call_model\", call_model)\n", 150 | " builder.add_edge(START, \"call_model\")\n", 151 | "\n", 152 | " # 그래프 컴파일\n", 153 | " graph_with_memory = builder.compile(\n", 154 | " checkpointer=checkpointer,\n", 155 | " store=store,\n", 156 | " )\n", 157 | "\n", 158 | " def run_graph(\n", 159 | " msg,\n", 160 | " thread_id,\n", 161 | " user_id,\n", 162 | " ):\n", 163 | " config = {\n", 164 | " \"configurable\": {\n", 165 | " \"thread_id\": thread_id,\n", 166 | " \"user_id\": user_id,\n", 167 | " }\n", 168 | " }\n", 169 | " print(f\"\\n[User🙋] {msg}\")\n", 170 | " stream_graph(\n", 171 | " graph_with_memory,\n", 172 | " inputs={\"messages\": [{\"role\": \"user\", \"content\": msg}]},\n", 173 | " config=config,\n", 174 | " )\n", 175 | " print()\n", 176 | "\n", 177 | " run_graph(\"내 이름이 뭐라고?\", \"1\", \"someone\")\n", 178 | "\n", 179 | " run_graph(\"내 이름이 뭐라고?\", \"2\", \"someone\")\n", 180 | "\n", 181 | " run_graph(\"내 이름은 테디야 remember\", \"3\", \"someone\")\n", 182 | "\n", 183 | " run_graph(\"내 이름이 뭐라고?\", \"100\", \"someone\")" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": null, 189 | "id": "ca37f016", 190 | "metadata": {}, 191 | "outputs": [], 192 | "source": [] 193 | } 194 | ], 195 | "metadata": { 196 | "kernelspec": { 197 | "display_name": ".venv", 198 | "language": "python", 199 | "name": "python3" 200 | }, 201 | "language_info": { 202 | "codemirror_mode": { 203 | "name": "ipython", 204 | "version": 3 205 | }, 206 | "file_extension": ".py", 207 | "mimetype": "text/x-python", 208 | "name": "python", 209 | "nbconvert_exporter": "python", 210 | "pygments_lexer": "ipython3", 211 | "version": "3.12.8" 212 | } 213 | }, 214 | "nbformat": 4, 215 | "nbformat_minor": 5 216 | } 217 | -------------------------------------------------------------------------------- /03-Modules/02-RAG/01-LangGraph-Building-Graphs.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# 기본 그래프 생성\n", 8 | "\n", 9 | "이번 튜토리얼에서는 LangGraph를 사용하여 그래프를 생성하는 방법을 배웁니다.\n", 10 | "\n", 11 | "LangGraph 의 그래프를 정의하기 위해서는\n", 12 | "\n", 13 | "1. State 정의\n", 14 | "2. 노드 정의\n", 15 | "3. 그래프 정의\n", 16 | "4. 그래프 컴파일\n", 17 | "5. 그래프 시각화\n", 18 | "\n", 19 | "단계를 거칩니다.\n", 20 | "\n", 21 | "그래프 생성시 조건부 엣지를 사용하는 방법과 다양한 흐름 변경 방법을 알아봅니다.\n", 22 | "\n", 23 | "![langgraph-building-graphs](assets/langgraph-building-graphs.png)" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "## State 정의" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "from typing import TypedDict, Annotated, List\n", 40 | "from langchain_core.documents import Document\n", 41 | "import operator\n", 42 | "\n", 43 | "\n", 44 | "# State 정의\n", 45 | "class GraphState(TypedDict):\n", 46 | " context: Annotated[List[Document], operator.add]\n", 47 | " answer: Annotated[List[Document], operator.add]\n", 48 | " question: Annotated[str, \"user question\"]\n", 49 | " sql_query: Annotated[str, \"sql query\"]\n", 50 | " binary_score: Annotated[str, \"binary score yes or no\"]" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "## 노드 정의" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "def retrieve(state: GraphState) -> GraphState:\n", 67 | " # retrieve: 검색\n", 68 | " documents = \"검색된 문서\"\n", 69 | " return {\"context\": documents}\n", 70 | "\n", 71 | "\n", 72 | "def rewrite_query(state: GraphState) -> GraphState:\n", 73 | " # Query Transform: 쿼리 재작성\n", 74 | " documents = \"검색된 문서\"\n", 75 | " return GraphState(context=documents)\n", 76 | "\n", 77 | "\n", 78 | "def llm_gpt_execute(state: GraphState) -> GraphState:\n", 79 | " # LLM 실행\n", 80 | " answer = \"GPT 생성된 답변\"\n", 81 | " return GraphState(answer=answer)\n", 82 | "\n", 83 | "\n", 84 | "def llm_claude_execute(state: GraphState) -> GraphState:\n", 85 | " # LLM 실행\n", 86 | " answer = \"Claude 의 생성된 답변\"\n", 87 | " return GraphState(answer=answer)\n", 88 | "\n", 89 | "\n", 90 | "def relevance_check(state: GraphState) -> GraphState:\n", 91 | " # Relevance Check: 관련성 확인\n", 92 | " binary_score = \"Relevance Score\"\n", 93 | " return GraphState(binary_score=binary_score)\n", 94 | "\n", 95 | "\n", 96 | "def sum_up(state: GraphState) -> GraphState:\n", 97 | " # sum_up: 결과 종합\n", 98 | " answer = \"종합된 답변\"\n", 99 | " return GraphState(answer=answer)\n", 100 | "\n", 101 | "\n", 102 | "def search_on_web(state: GraphState) -> GraphState:\n", 103 | " # Search on Web: 웹 검색\n", 104 | " documents = state[\"context\"] = \"기존 문서\"\n", 105 | " searched_documents = \"검색된 문서\"\n", 106 | " documents += searched_documents\n", 107 | " return GraphState(context=documents)\n", 108 | "\n", 109 | "\n", 110 | "def get_table_info(state: GraphState) -> GraphState:\n", 111 | " # Get Table Info: 테이블 정보 가져오기\n", 112 | " table_info = \"테이블 정보\"\n", 113 | " return GraphState(context=table_info)\n", 114 | "\n", 115 | "\n", 116 | "def generate_sql_query(state: GraphState) -> GraphState:\n", 117 | " # Make SQL Query: SQL 쿼리 생성\n", 118 | " sql_query = \"SQL 쿼리\"\n", 119 | " return GraphState(sql_query=sql_query)\n", 120 | "\n", 121 | "\n", 122 | "def execute_sql_query(state: GraphState) -> GraphState:\n", 123 | " # Execute SQL Query: SQL 쿼리 실행\n", 124 | " sql_result = \"SQL 결과\"\n", 125 | " return GraphState(context=sql_result)\n", 126 | "\n", 127 | "\n", 128 | "def validate_sql_query(state: GraphState) -> GraphState:\n", 129 | " # Validate SQL Query: SQL 쿼리 검증\n", 130 | " binary_score = \"SQL 쿼리 검증 결과\"\n", 131 | " return GraphState(binary_score=binary_score)\n", 132 | "\n", 133 | "\n", 134 | "def handle_error(state: GraphState) -> GraphState:\n", 135 | " # Error Handling: 에러 처리\n", 136 | " error = \"에러 발생\"\n", 137 | " return GraphState(context=error)\n", 138 | "\n", 139 | "\n", 140 | "def decision(state: GraphState) -> GraphState:\n", 141 | " # 의사결정\n", 142 | " decision = \"결정\"\n", 143 | " # 로직을 추가할 수 가 있고요.\n", 144 | "\n", 145 | " if state[\"binary_score\"] == \"yes\":\n", 146 | " return \"종료\"\n", 147 | " else:\n", 148 | " return \"재검색\"" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "## 그래프 정의" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "from langgraph.graph import END, StateGraph\n", 165 | "from langgraph.checkpoint.memory import MemorySaver\n", 166 | "from langchain_teddynote.graphs import visualize_graph\n", 167 | "\n", 168 | "# (1): Conventional RAG\n", 169 | "# (2): 재검색\n", 170 | "# (3): 멀티 LLM\n", 171 | "# (4): 쿼리 재작성\n", 172 | "\n", 173 | "\n", 174 | "# langgraph.graph에서 StateGraph와 END를 가져옵니다.\n", 175 | "workflow = StateGraph(GraphState)\n", 176 | "\n", 177 | "# 노드를 추가합니다.\n", 178 | "workflow.add_node(\"retrieve\", retrieve)\n", 179 | "\n", 180 | "# workflow.add_node(\"rewrite_query\", rewrite_query) # (4)\n", 181 | "\n", 182 | "workflow.add_node(\"GPT 요청\", llm_gpt_execute)\n", 183 | "# workflow.add_node(\"Claude 요청\", llm_claude_execute) # (3)\n", 184 | "workflow.add_node(\"GPT_relevance_check\", relevance_check)\n", 185 | "# workflow.add_node(\"Claude_relevance_check\", relevance_check) # (3)\n", 186 | "workflow.add_node(\"결과 종합\", sum_up)\n", 187 | "\n", 188 | "# 각 노드들을 연결합니다.\n", 189 | "workflow.add_edge(\"retrieve\", \"GPT 요청\")\n", 190 | "# workflow.add_edge(\"retrieve\", \"Claude 요청\") # (3)\n", 191 | "# workflow.add_edge(\"rewrite_query\", \"retrieve\") # (4)\n", 192 | "workflow.add_edge(\"GPT 요청\", \"GPT_relevance_check\")\n", 193 | "workflow.add_edge(\"GPT_relevance_check\", \"결과 종합\")\n", 194 | "# workflow.add_edge(\"Claude 요청\", \"Claude_relevance_check\") # (3)\n", 195 | "# workflow.add_edge(\"Claude_relevance_check\", \"결과 종합\") # (3)\n", 196 | "\n", 197 | "workflow.add_edge(\"결과 종합\", END) # (2) - off\n", 198 | "\n", 199 | "# 조건부 엣지를 추가합니다. (2), (4)\n", 200 | "# workflow.add_conditional_edges(\n", 201 | "# \"결과 종합\", # 관련성 체크 노드에서 나온 결과를 is_relevant 함수에 전달합니다.\n", 202 | "# decision,\n", 203 | "# {\n", 204 | "# \"재검색\": \"retrieve\", # 관련성이 있으면 종료합니다.\n", 205 | "# \"종료\": END, # 관련성 체크 결과가 모호하다면 다시 답변을 생성합니다.\n", 206 | "# },\n", 207 | "# )\n", 208 | "\n", 209 | "# 조건부 엣지를 추가합니다. (4)\n", 210 | "# workflow.add_conditional_edges(\n", 211 | "# \"결과 종합\", # 관련성 체크 노드에서 나온 결과를 is_relevant 함수에 전달합니다.\n", 212 | "# decision,\n", 213 | "# {\n", 214 | "# \"재검색\": \"rewrite_query\", # 관련성이 있으면 종료합니다.\n", 215 | "# \"종료\": END, # 관련성 체크 결과가 모호하다면 다시 답변을 생성합니다.\n", 216 | "# },\n", 217 | "# )\n", 218 | "\n", 219 | "# 시작점을 설정합니다.\n", 220 | "workflow.set_entry_point(\"retrieve\")\n", 221 | "\n", 222 | "# 기록을 위한 메모리 저장소를 설정합니다.\n", 223 | "memory = MemorySaver()\n", 224 | "\n", 225 | "# 그래프를 컴파일합니다.\n", 226 | "app = workflow.compile(checkpointer=memory)\n", 227 | "\n", 228 | "# 그래프 시각화\n", 229 | "visualize_graph(app)" 230 | ] 231 | } 232 | ], 233 | "metadata": { 234 | "kernelspec": { 235 | "display_name": ".venv", 236 | "language": "python", 237 | "name": "python3" 238 | }, 239 | "language_info": { 240 | "codemirror_mode": { 241 | "name": "ipython", 242 | "version": 3 243 | }, 244 | "file_extension": ".py", 245 | "mimetype": "text/x-python", 246 | "name": "python", 247 | "nbconvert_exporter": "python", 248 | "pygments_lexer": "ipython3", 249 | "version": "3.12.8" 250 | } 251 | }, 252 | "nbformat": 4, 253 | "nbformat_minor": 2 254 | } 255 | -------------------------------------------------------------------------------- /02-Practice/03-Practice-Memory-Chatbot.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## 메모리 기능이 추가된 금융 고객센터 챗봇\n", 8 | "\n", 9 | "이 Practice는 금융 고객센터 상담 시나리오를 다룹니다. 고객이 자신의 금융 개인정보(예: 이름, 계좌번호 등)를 입력하고, `remember` 라는 키워드로 \"기억\"을 요청합니다. 이후 다른 상담 스레드(다른 thread_id)에서 동일한 고객(user_id)이 다시 질문하면, 이전에 저장된 메모리를 바탕으로 불필요한 재질문 없이 자연스럽게 응대하는 챗봇을 구현합니다.\n", 10 | "\n", 11 | "### 목표\n", 12 | "- 메시지 상태(`MessagesState`)를 사용하는 LangGraph 노드를 구성합니다.\n", 13 | "- 사용자별 메모리를 저장/검색하기 위한 Store와 Checkpointer를 구성합니다.\n", 14 | "- `remember` 키워드가 포함된 입력을 자동 추출하여 메모리에 저장합니다.\n", 15 | "- 서로 다른 thread에서 동일한 user_id로 상담 시, 저장된 메모리를 활용해 응답 품질을 높입니다.\n", 16 | "\n", 17 | "### 조건\n", 18 | "- LLM 모델: `gpt-4.1` (온도 0)\n", 19 | "- 메모리 저장: `user_id` 기반 네임스페이스로 구분\n", 20 | "- 키 포인트: `remember` 가 포함된 입력이면 메모리 추출 → 저장, 이후 대화에 반영\n", 21 | "\n", 22 | "---\n", 23 | "\n", 24 | "아래 각 소파트에는\n", 25 | "- 실습 코드: 스스로 채워 넣는 TODO 형태\n", 26 | "- 정답 코드: 참고용 구현\n", 27 | "이 함께 제공됩니다. 우선 실습 코드를 시도해 본 뒤, 정답 코드를 참고하세요.\n" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "# 공통 준비 코드 (실행)\n", 37 | "from dotenv import load_dotenv\n", 38 | "from langchain_teddynote import logging\n", 39 | "\n", 40 | "load_dotenv(override=True)\n", 41 | "logging.langsmith(\"LangGraph-Memory-Finance-CC-Practice\")" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "### 1) LLM과 메모리 추출기 준비\n", 49 | "\n", 50 | "금융 고객센터 시나리오에 사용할 LLM과 간단한 메모리 추출기를 준비합니다. 추출기는 `remember` 키워드가 포함된 메시지에서 개인정보(예: 이름, 계좌번호, 생년월일 등)를 구조화해 저장하기 위함입니다.\n", 51 | "\n", 52 | "요구사항\n", 53 | "- `ChatOpenAI`로 `gpt-4.1`, `temperature=0` 모델을 생성하세요.\n", 54 | "- `create_memory_extractor(model=\"gpt-4.1\")` 와 유사한 인터페이스의 추출기를 준비한다고 가정하고, 변수를 선언하세요.\n" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "# 실습 코드\n", 64 | "# TODO: LLM과 메모리 추출기 변수를 준비하세요.\n", 65 | "from langchain_openai import ChatOpenAI\n", 66 | "from langchain_teddynote.memory import create_memory_extractor\n", 67 | "\n", 68 | "llm = None # 코드 입력\n", 69 | "memory_extractor = None # 코드 입력" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "### 2) 상태와 그래프 골격 준비\n", 77 | "\n", 78 | "메시지 상태는 LangGraph의 `MessagesState`를 사용합니다. 고객의 메시지와 상담사의 답변이 누적되도록 구성하고, 나중에 체크포인팅과 Store를 연결합니다.\n", 79 | "\n", 80 | "요구사항\n", 81 | "- `MessagesState` 기반 `StateGraph` 빌더를 생성하세요.\n", 82 | "- START → `call_model` 단일 노드로 시작하는 최소 골격을 준비하세요.\n" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "# 실습 코드\n", 92 | "# TODO: MessagesState 기반 그래프 빌더를 생성하고 START→call_model 골격을 만드세요.\n", 93 | "from typing import Any\n", 94 | "from langgraph.graph import StateGraph, MessagesState, START\n", 95 | "\n", 96 | "builder = None # 코드 입력\n", 97 | "\n", 98 | "# 노드는 다음 소파트에서 구현 후 추가합니다.\n", 99 | "# 최소 골격: START → call_model\n", 100 | "# (노드 추가는 이후 단계에서 실제 함수 구현 후)" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "### 3) 메모리 로드/저장 로직이 포함된 노드 구현\n", 108 | "\n", 109 | "`call_model(state, config, *, store)` 형태로 노드를 구현합니다.\n", 110 | "- 최근 사용자 메시지에서 `user_id` 를 `config[\"configurable\"][\"user_id\"]` 로 가져옵니다.\n", 111 | "- 네임스페이스: `(\"memories\", user_id)` 로 지정합니다.\n", 112 | "- 마지막 메시지에 `remember` 가 포함되어 있으면 `memory_extractor` 를 이용해 개인정보를 추출하고 Store에 저장합니다.\n", 113 | "- 저장된 메모리를 조회해 system 프롬프트(컨텍스트)로 반영한 후 LLM을 호출합니다.\n", 114 | "\n", 115 | "요구사항\n", 116 | "- 함수 시그니처와 반환 스키마를 맞추세요: `{\"messages\": response}` 또는 `{\"messages\": [response]}` (여기서는 하나의 AI 메시지를 반환)\n", 117 | "- Store 검색 결과를 system 메시지로 앞단에 추가해 답변 품질 향상\n" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "# 실습 코드\n", 127 | "# TODO: 메모리 로드/저장 로직이 포함된 노드를 작성하세요.\n", 128 | "from typing import Any\n", 129 | "from langchain_core.runnables import RunnableConfig\n", 130 | "from langgraph.store.base import BaseStore\n", 131 | "\n", 132 | "\n", 133 | "def call_model(state, config: RunnableConfig, *, store: BaseStore) -> dict[str, Any]:\n", 134 | " # 1) user_id와 namespace 추출\n", 135 | " # 2) 마지막 메시지에 remember 포함 여부 확인 → 포함 시 memory_extractor로 추출 후 store.put\n", 136 | " # 3) store.search로 저장된 메모리 검색 → system 메시지 구성\n", 137 | " # 4) LLM 호출 후 메시지 반환\n", 138 | " raise NotImplementedError # 코드 입력" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "metadata": {}, 144 | "source": [ 145 | "### 4) Checkpointer/Store 연결 및 그래프 컴파일\n", 146 | "\n", 147 | "메모리를 유지하려면 체크포인터와 스토어가 필요합니다. 여기서는 메모리 기반 구현을 사용하지만, 실전에서는 외부 DB(PostgresSaver 등)를 권장합니다.\n", 148 | "\n", 149 | "요구사항\n", 150 | "- `InMemorySaver` 와 `InMemoryStore` 를 생성하여 그래프 컴파일 시 주입하세요.\n", 151 | "- 앞서 만든 `call_model` 노드를 그래프에 추가하고, `START → call_model` 엣지를 구성하세요.\n", 152 | "- 컴파일된 그래프를 `graph_with_memory` 라는 변수에 담으세요.\n" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "# 실습 코드\n", 162 | "# TODO: InMemorySaver/Store로 그래프 컴파일 및 노드/엣지 구성\n", 163 | "from langgraph.checkpoint.memory import InMemorySaver\n", 164 | "from langgraph.store.memory import InMemoryStore\n", 165 | "\n", 166 | "memory_saver = # 코드 입력\n", 167 | "memory_store = # 코드 입력\n", 168 | "\n", 169 | "builder. # 코드 입력\n", 170 | "builder. # 코드 입력\n", 171 | "\n", 172 | "graph_with_memory = # 코드 입력" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "metadata": {}, 178 | "source": [ 179 | "### 5) 시나리오 실행 1: 개인정보 등록 + remember\n", 180 | "\n", 181 | "아래 예시는 최초 상담에서 고객이 자신의 정보를 제공하며 `remember` 를 명시하는 경우입니다. 동일한 `user_id` 와 `thread_id` 를 전달하세요.\n", 182 | "\n", 183 | "요구사항\n", 184 | "- `RunnableConfig` 의 `configurable`에 `thread_id`, `user_id`를 설정하세요.\n", 185 | "- \"제 이름은 홍길동이고, 계좌번호는 110-123-456789 입니다. remember\" 와 같은 문장을 입력하여 메모리 저장을 유도하세요.\n", 186 | "- `stream_graph` 로 실행 결과를 확인하세요.\n" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": null, 192 | "metadata": {}, 193 | "outputs": [], 194 | "source": [ 195 | "# 실습 코드\n", 196 | "# TODO: 동일 thread_id/user_id로 개인정보 + remember 시나리오 실행\n", 197 | "from langchain_teddynote.messages import stream_graph\n", 198 | "from langchain_core.runnables import RunnableConfig\n", 199 | "\n", 200 | "config1 = RunnableConfig(\n", 201 | " configurable={\"thread_id\": \"t-001user-abc\", \"user_id\": \"user-abc\"}\n", 202 | ")\n", 203 | "inputs1 = {\n", 204 | " \"messages\": [\n", 205 | " {\n", 206 | " \"role\": \"user\",\n", 207 | " \"content\": \"제 이름은 홍길동이고, 계좌번호는 110-123-456789 입니다. remember\",\n", 208 | " }\n", 209 | " ]\n", 210 | "}\n", 211 | "\n", 212 | "# 코드 입력 (stream_graph 로 실행)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "metadata": {}, 218 | "source": [ 219 | "### 6) 시나리오 실행 2: 다른 thread에서 동일 user로 재상담\n", 220 | "\n", 221 | "이제 thread만 바꿔 동일한 `user_id`로 다시 질문합니다. 챗봇은 기존 메모리를 바탕으로 불필요한 개인정보 재질문 없이 바로 답변해야 합니다.\n", 222 | "\n", 223 | "요구사항\n", 224 | "- `thread_id`만 변경하고, `user_id`는 동일하게 유지하세요.\n", 225 | "- \"어제 안내해준 대출 상환 스케줄 다시 알려줘\" 같은 문장을 사용해 답변을 확인하세요.\n" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": null, 231 | "metadata": {}, 232 | "outputs": [], 233 | "source": [ 234 | "# 실습 코드\n", 235 | "# TODO: 다른 thread_id, 같은 user_id로 실행하여 메모리 활용 확인\n", 236 | "from langchain_core.runnables import RunnableConfig\n", 237 | "from langchain_teddynote.messages import stream_graph\n", 238 | "\n", 239 | "config2 = RunnableConfig(\n", 240 | " configurable={\"thread_id\": \"t-002user-abc\", \"user_id\": \"user-abc\"}\n", 241 | ")\n", 242 | "inputs2 = {\"messages\": [{\"role\": \"user\", \"content\": \"제 정보를 알려주세요\"}]}\n", 243 | "\n", 244 | "stream_graph(graph_with_memory, inputs=inputs2, config=config2)" 245 | ] 246 | } 247 | ], 248 | "metadata": { 249 | "kernelspec": { 250 | "display_name": ".venv", 251 | "language": "python", 252 | "name": "python3" 253 | }, 254 | "language_info": { 255 | "codemirror_mode": { 256 | "name": "ipython", 257 | "version": 3 258 | }, 259 | "file_extension": ".py", 260 | "mimetype": "text/x-python", 261 | "name": "python", 262 | "nbconvert_exporter": "python", 263 | "pygments_lexer": "ipython3", 264 | "version": "3.12.8" 265 | } 266 | }, 267 | "nbformat": 4, 268 | "nbformat_minor": 2 269 | } 270 | -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/02-LangGraph-ChatBot.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "eec680f5", 6 | "metadata": {}, 7 | "source": [ 8 | "# LangGraph 챗봇 구축\n", 9 | "\n", 10 | "먼저 `LangGraph`를 사용하여 간단한 챗봇을 만들어 보겠습니다. 이 챗봇은 사용자 메시지에 직접 응답할 것입니다. 비록 간단하지만, `LangGraph`로 구축하는 핵심 개념을 설명할 것입니다. 이 섹션이 끝나면 기본적인 챗봇을 구축하게 될 것입니다.\n", 11 | "\n", 12 | "`StateGraph`를 생성하는 것으로 시작하십시오. `StateGraph` 객체는 챗봇의 구조를 \"상태 기계(State Machine)\"로 정의합니다. \n", 13 | "\n", 14 | "`nodes`를 추가하여 챗봇이 호출할 수 있는 `llm`과 함수들을 나타내고, `edges`를 추가하여 봇이 이러한 함수들 간에 어떻게 전환해야 하는지를 지정합니다." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "id": "de9d9d8d", 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# API 키를 환경변수로 관리하기 위한 설정 파일\n", 25 | "from dotenv import load_dotenv\n", 26 | "\n", 27 | "# API 키 정보 로드\n", 28 | "load_dotenv()" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "6b5c6228", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n", 39 | "# !pip install -qU langchain-teddynote\n", 40 | "from langchain_teddynote import logging\n", 41 | "\n", 42 | "# 프로젝트 이름을 입력합니다.\n", 43 | "logging.langsmith(\"CH17-LangGraph-Modules\")" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "d836a929", 49 | "metadata": {}, 50 | "source": [ 51 | "## Step-by-Step 개념 이해하기!" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "id": "c38f326c", 57 | "metadata": {}, 58 | "source": [ 59 | "### STEP 1. 상태(State) 정의" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 3, 65 | "id": "2230e22c", 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "from typing import Annotated, TypedDict\n", 70 | "from langgraph.graph import StateGraph, START, END\n", 71 | "from langgraph.graph.message import add_messages\n", 72 | "\n", 73 | "\n", 74 | "class State(TypedDict):\n", 75 | " # 메시지 정의(list type 이며 add_messages 함수를 사용하여 메시지를 추가)\n", 76 | " messages: Annotated[list, add_messages]" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "id": "8d73ca7b", 82 | "metadata": {}, 83 | "source": [ 84 | "### STEP 2. 노드(Node) 정의" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "id": "cbc8fac2", 90 | "metadata": {}, 91 | "source": [ 92 | "다음으로 \"`chatbot`\" 노드를 추가합니다. \n", 93 | "\n", 94 | "노드는 작업의 단위를 나타내며, 일반적으로 정규 **Python** 함수입니다." 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 5, 100 | "id": "f4db3a07", 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "from langchain_openai import ChatOpenAI\n", 105 | "\n", 106 | "# LLM 정의\n", 107 | "llm = ChatOpenAI(model=\"gpt-4.1-nano\", temperature=0)\n", 108 | "\n", 109 | "\n", 110 | "# 챗봇 함수 정의\n", 111 | "def chatbot(state: State):\n", 112 | " # 메시지 호출 및 반환\n", 113 | " return {\"messages\": [llm.invoke(state[\"messages\"])]}" 114 | ] 115 | }, 116 | { 117 | "cell_type": "markdown", 118 | "id": "045f7223", 119 | "metadata": {}, 120 | "source": [ 121 | "### STEP 3. 그래프(Graph) 정의, 노드 추가" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "id": "09d1d2f1", 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "# 그래프 생성\n", 132 | "graph_builder = StateGraph(State)\n", 133 | "\n", 134 | "# 노드 이름, 함수 혹은 callable 객체를 인자로 받아 노드를 추가\n", 135 | "graph_builder.add_node(\"chatbot\", chatbot)" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "id": "684fc782", 141 | "metadata": {}, 142 | "source": [ 143 | "**참고**\n", 144 | "\n", 145 | "- `chatbot` 노드 함수는 현재 `State`를 입력으로 받아 \"messages\"라는 키 아래에 업데이트된 `messages` 목록을 포함하는 사전(TypedDict) 을 반환합니다. \n", 146 | "\n", 147 | "- `State`의 `add_messages` 함수는 이미 상태에 있는 메시지에 llm의 응답 메시지를 추가합니다. " 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "id": "a18b0aae", 153 | "metadata": {}, 154 | "source": [ 155 | "### STEP 4. 그래프 엣지(Edge) 추가\n", 156 | "\n", 157 | "다음으로, `START` 지점을 추가하세요. `START`는 그래프가 실행될 때마다 **작업을 시작할 위치** 입니다." 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "id": "2ddc4236", 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "# 시작 노드에서 챗봇 노드로의 엣지 추가\n", 168 | "graph_builder.add_edge(START, \"chatbot\")" 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "id": "000626da", 174 | "metadata": {}, 175 | "source": [ 176 | "\n", 177 | "마찬가지로, `END` 지점을 설정하십시오. 이는 그래프 흐름의 종료(끝지점) 를 나타냅니다." 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "id": "9e3b0a51", 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "# 그래프에 엣지 추가\n", 188 | "graph_builder.add_edge(\"chatbot\", END)" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "id": "3f5bd367", 194 | "metadata": {}, 195 | "source": [ 196 | "### STEP 5. 그래프 컴파일(compile)" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "id": "a599f6f8", 202 | "metadata": {}, 203 | "source": [ 204 | "마지막으로, 그래프를 실행할 수 있어야 합니다. 이를 위해 그래프 빌더에서 \"`compile()`\"을 호출합니다. 이렇게 하면 상태에서 호출할 수 있는 \"`CompiledGraph`\"가 생성됩니다." 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": 9, 210 | "id": "4f28795b", 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | "# 그래프 컴파일\n", 215 | "graph = graph_builder.compile()" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "id": "00ce8197", 221 | "metadata": {}, 222 | "source": [ 223 | "### STEP 6. 그래프 시각화" 224 | ] 225 | }, 226 | { 227 | "cell_type": "markdown", 228 | "id": "4572d38c", 229 | "metadata": {}, 230 | "source": [ 231 | "이제 그래프를 시각화해봅시다." 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": null, 237 | "id": "8235a6d2", 238 | "metadata": {}, 239 | "outputs": [], 240 | "source": [ 241 | "from langchain_teddynote.graphs import visualize_graph\n", 242 | "\n", 243 | "# 그래프 시각화\n", 244 | "visualize_graph(graph)" 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "id": "afb6eeba", 250 | "metadata": {}, 251 | "source": [ 252 | "### STEP 7. 그래프 실행" 253 | ] 254 | }, 255 | { 256 | "cell_type": "markdown", 257 | "id": "ffa2cc55", 258 | "metadata": {}, 259 | "source": [ 260 | "이제 챗봇을 실행해봅시다!" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": null, 266 | "id": "049fc976", 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "question = \"서울의 유명한 맛집 TOP 10 추천해줘\"\n", 271 | "\n", 272 | "# 그래프 이벤트 스트리밍\n", 273 | "for event in graph.stream({\"messages\": [(\"user\", question)]}):\n", 274 | " # 이벤트 값 출력\n", 275 | " for value in event.values():\n", 276 | " print(\"Assistant:\", value[\"messages\"][-1].content)" 277 | ] 278 | }, 279 | { 280 | "cell_type": "markdown", 281 | "id": "f82fb67c", 282 | "metadata": {}, 283 | "source": [ 284 | "자! 여기까지가 가장 기본적인 챗봇 구축이었습니다. \n", 285 | "\n", 286 | "아래는 이전 과정을 정리한 전체 코드입니다." 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "id": "dec091e3", 292 | "metadata": {}, 293 | "source": [ 294 | "## 전체 코드" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": null, 300 | "id": "f3bd4f86", 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [ 304 | "from typing import Annotated, TypedDict\n", 305 | "from langgraph.graph import StateGraph, START, END\n", 306 | "from langgraph.graph.message import add_messages\n", 307 | "from langchain_openai import ChatOpenAI\n", 308 | "from langchain_teddynote.graphs import visualize_graph\n", 309 | "\n", 310 | "\n", 311 | "###### STEP 1. 상태(State) 정의 ######\n", 312 | "class State(TypedDict):\n", 313 | " # 메시지 정의(list type 이며 add_messages 함수를 사용하여 메시지를 추가)\n", 314 | " messages: Annotated[list, add_messages]\n", 315 | "\n", 316 | "\n", 317 | "###### STEP 2. 노드(Node) 정의 ######\n", 318 | "# LLM 정의\n", 319 | "llm = ChatOpenAI(model=\"gpt-4.1-nano\", temperature=0)\n", 320 | "\n", 321 | "\n", 322 | "# 챗봇 함수 정의\n", 323 | "def chatbot(state: State):\n", 324 | " # 메시지 호출 및 반환\n", 325 | " return {\"messages\": [llm.invoke(state[\"messages\"])]}\n", 326 | "\n", 327 | "\n", 328 | "###### STEP 3. 그래프(Graph) 정의, 노드 추가 ######\n", 329 | "# 그래프 생성\n", 330 | "graph_builder = StateGraph(State)\n", 331 | "\n", 332 | "# 노드 이름, 함수 혹은 callable 객체를 인자로 받아 노드를 추가\n", 333 | "graph_builder.add_node(\"chatbot\", chatbot)\n", 334 | "\n", 335 | "###### STEP 4. 그래프 엣지(Edge) 추가 ######\n", 336 | "# 시작 노드에서 챗봇 노드로의 엣지 추가\n", 337 | "graph_builder.add_edge(START, \"chatbot\")\n", 338 | "\n", 339 | "# 그래프에 엣지 추가\n", 340 | "graph_builder.add_edge(\"chatbot\", END)\n", 341 | "\n", 342 | "###### STEP 5. 그래프 컴파일(compile) ######\n", 343 | "# 그래프 컴파일\n", 344 | "graph = graph_builder.compile()\n", 345 | "\n", 346 | "###### STEP 6. 그래프 시각화 ######\n", 347 | "# 그래프 시각화\n", 348 | "visualize_graph(graph)\n", 349 | "\n", 350 | "###### STEP 7. 그래프 실행 ######\n", 351 | "question = \"서울의 유명한 맛집 TOP 10 추천해줘\"\n", 352 | "\n", 353 | "# 그래프 이벤트 스트리밍\n", 354 | "for event in graph.stream({\"messages\": [(\"user\", question)]}):\n", 355 | " # 이벤트 값 출력\n", 356 | " for value in event.values():\n", 357 | " print(value[\"messages\"][-1].content)" 358 | ] 359 | } 360 | ], 361 | "metadata": { 362 | "kernelspec": { 363 | "display_name": "langchain-kr-lwwSZlnu-py3.11", 364 | "language": "python", 365 | "name": "python3" 366 | }, 367 | "language_info": { 368 | "codemirror_mode": { 369 | "name": "ipython", 370 | "version": 3 371 | }, 372 | "file_extension": ".py", 373 | "mimetype": "text/x-python", 374 | "name": "python", 375 | "nbconvert_exporter": "python", 376 | "pygments_lexer": "ipython3", 377 | "version": "3.11.9" 378 | } 379 | }, 380 | "nbformat": 4, 381 | "nbformat_minor": 5 382 | } 383 | -------------------------------------------------------------------------------- /03-Modules/02-RAG/02-LangGraph-Naive-RAG.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "6fa6fb7f", 6 | "metadata": {}, 7 | "source": [ 8 | "# Naive RAG\n", 9 | "\n", 10 | "**절차**\n", 11 | "\n", 12 | "1. Naive RAG 수행\n", 13 | "\n", 14 | "![langgraph-naive-rag](assets/langgraph-naive-rag.png)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "id": "f21c872b", 20 | "metadata": {}, 21 | "source": [ 22 | "## 환경 설정" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "id": "064d5c8c", 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "# API 키를 환경변수로 관리하기 위한 설정 파일\n", 33 | "from dotenv import load_dotenv\n", 34 | "\n", 35 | "# API 키 정보 로드\n", 36 | "load_dotenv(override=True)" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "id": "562b0043", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "# LangSmith 추적을 설정합니다.\n", 47 | "from langchain_teddynote import logging\n", 48 | "\n", 49 | "# 프로젝트 이름을 입력합니다.\n", 50 | "logging.langsmith(\"LangGraph-RAG\")" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "id": "06468c1c", 56 | "metadata": {}, 57 | "source": [ 58 | "## 기본 PDF 기반 Retrieval Chain 생성\n", 59 | "\n", 60 | "여기서는 PDF 문서를 기반으로 Retrieval Chain 을 생성합니다. 가장 단순한 구조의 Retrieval Chain 입니다.\n", 61 | "\n", 62 | "단, LangGraph 에서는 Retirever 와 Chain 을 따로 생성합니다. 그래야 각 노드별로 세부 처리를 할 수 있습니다." 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "id": "f905df18", 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "from rag.pdf import PDFRetrievalChain\n", 73 | "\n", 74 | "# PDF 문서를 로드합니다.\n", 75 | "pdf = PDFRetrievalChain([\"data/SPRI_AI_Brief_2023년12월호_F.pdf\"]).create_chain()\n", 76 | "\n", 77 | "# retriever와 chain을 생성합니다.\n", 78 | "pdf_retriever = pdf.retriever\n", 79 | "pdf_chain = pdf.chain" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "id": "fa6f7524", 85 | "metadata": {}, 86 | "source": [ 87 | "먼저, pdf_retriever 를 사용하여 검색 결과를 가져옵니다." 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "id": "0d532337", 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "search_result = pdf_retriever.invoke(\"앤스로픽에 투자한 기업과 투자금액을 알려주세요.\")\n", 98 | "search_result" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "id": "f6f95f50", 104 | "metadata": {}, 105 | "source": [ 106 | "이전에 검색한 결과를 chain 의 context 로 전달합니다." 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "id": "d957188f", 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "# 검색 결과를 기반으로 답변을 생성합니다.\n", 117 | "answer = pdf_chain.invoke(\n", 118 | " {\n", 119 | " \"question\": \"앤스로픽에 투자한 기업과 투자금액을 알려주세요.\",\n", 120 | " \"context\": search_result\n", 121 | " }\n", 122 | ")\n", 123 | "print(answer)" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "id": "d047f938", 129 | "metadata": {}, 130 | "source": [ 131 | "## State 정의\n", 132 | "\n", 133 | "`State`: Graph 의 노드와 노드 간 공유하는 상태를 정의합니다.\n", 134 | "\n", 135 | "일반적으로 `TypedDict` 형식을 사용합니다." 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "id": "f19a3df5", 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "from typing import Annotated, TypedDict\n", 146 | "from langgraph.graph.message import add_messages\n", 147 | "\n", 148 | "\n", 149 | "# GraphState 상태 정의\n", 150 | "class GraphState(TypedDict):\n", 151 | " question: Annotated[str, \"Question\"] # 질문\n", 152 | " context: Annotated[str, \"Context\"] # 문서의 검색 결과\n", 153 | " answer: Annotated[str, \"Answer\"] # 답변\n", 154 | " messages: Annotated[list, add_messages] # 메시지(누적되는 list)" 155 | ] 156 | }, 157 | { 158 | "cell_type": "markdown", 159 | "id": "c56d4095", 160 | "metadata": {}, 161 | "source": [ 162 | "## 노드(Node) 정의\n", 163 | "\n", 164 | "- `Nodes`: 각 단계를 처리하는 노드입니다. 보통은 Python 함수로 구현합니다. 입력과 출력이 상태(State) 값입니다.\n", 165 | " \n", 166 | "**참고** \n", 167 | "\n", 168 | "- `State`를 입력으로 받아 정의된 로직을 수행한 후 업데이트된 `State`를 반환합니다." 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": null, 174 | "id": "9ef0c055", 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "from langchain_teddynote.messages import messages_to_history\n", 179 | "from rag.utils import format_docs\n", 180 | "\n", 181 | "\n", 182 | "# 문서 검색 노드\n", 183 | "def retrieve_document(state: GraphState) -> GraphState:\n", 184 | " # 질문을 상태에서 가져옵니다.\n", 185 | " latest_question = state[\"question\"]\n", 186 | "\n", 187 | " # 문서에서 검색하여 관련성 있는 문서를 찾습니다.\n", 188 | " retrieved_docs = pdf_retriever.invoke(latest_question)\n", 189 | "\n", 190 | " # 검색된 문서를 형식화합니다.(프롬프트 입력으로 넣어주기 위함)\n", 191 | " retrieved_docs = format_docs(retrieved_docs)\n", 192 | "\n", 193 | " # 검색된 문서를 context 키에 저장합니다.\n", 194 | " return {\"context\": retrieved_docs}\n", 195 | "\n", 196 | "\n", 197 | "# 답변 생성 노드\n", 198 | "def llm_answer(state: GraphState) -> GraphState:\n", 199 | " # 질문을 상태에서 가져옵니다.\n", 200 | " latest_question = state[\"question\"]\n", 201 | "\n", 202 | " # 검색된 문서를 상태에서 가져옵니다.\n", 203 | " context = state[\"context\"]\n", 204 | "\n", 205 | " # 체인을 호출하여 답변을 생성합니다.\n", 206 | " response = pdf_chain.invoke(\n", 207 | " {\n", 208 | " \"question\": latest_question,\n", 209 | " \"context\": context,\n", 210 | " \"chat_history\": messages_to_history(state[\"messages\"]),\n", 211 | " }\n", 212 | " )\n", 213 | " # 생성된 답변, (유저의 질문, 답변) 메시지를 상태에 저장합니다.\n", 214 | " return {\n", 215 | " \"answer\": response,\n", 216 | " \"messages\": [(\"user\", latest_question), (\"assistant\", response)],\n", 217 | " }" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "id": "a3f7785d", 223 | "metadata": {}, 224 | "source": [ 225 | "## 그래프 생성\n", 226 | "\n", 227 | "- `Edges`: 현재 `State`를 기반으로 다음에 실행할 `Node`를 결정하는 Python 함수.\n", 228 | "\n", 229 | "일반 엣지, 조건부 엣지 등이 있습니다." 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "id": "a6015807", 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "from langgraph.graph import END, StateGraph\n", 240 | "from langgraph.checkpoint.memory import MemorySaver\n", 241 | "\n", 242 | "# 그래프 생성\n", 243 | "workflow = StateGraph(GraphState)\n", 244 | "\n", 245 | "# 노드 정의\n", 246 | "workflow.add_node(\"retrieve\", retrieve_document)\n", 247 | "workflow.add_node(\"llm_answer\", llm_answer)\n", 248 | "\n", 249 | "# 엣지 정의\n", 250 | "workflow.add_edge(\"retrieve\", \"llm_answer\") # 검색 -> 답변\n", 251 | "workflow.add_edge(\"llm_answer\", END) # 답변 -> 종료\n", 252 | "\n", 253 | "# 그래프 진입점 설정\n", 254 | "workflow.set_entry_point(\"retrieve\")\n", 255 | "\n", 256 | "# 체크포인터 설정\n", 257 | "memory = MemorySaver()\n", 258 | "\n", 259 | "# 컴파일\n", 260 | "app = workflow.compile(checkpointer=memory)" 261 | ] 262 | }, 263 | { 264 | "cell_type": "markdown", 265 | "id": "d9a15c32", 266 | "metadata": {}, 267 | "source": [ 268 | "컴파일한 그래프를 시각화 합니다." 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": null, 274 | "id": "2e09251d", 275 | "metadata": {}, 276 | "outputs": [], 277 | "source": [ 278 | "from langchain_teddynote.graphs import visualize_graph\n", 279 | "\n", 280 | "visualize_graph(app)" 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "id": "110daa16", 286 | "metadata": {}, 287 | "source": [ 288 | "## 그래프 실행\n", 289 | "\n", 290 | "- `config` 파라미터는 그래프 실행 시 필요한 설정 정보를 전달합니다.\n", 291 | "- `recursion_limit`: 그래프 실행 시 재귀 최대 횟수를 설정합니다.\n", 292 | "- `inputs`: 그래프 실행 시 필요한 입력 정보를 전달합니다.\n", 293 | "\n", 294 | "**참고**\n", 295 | "\n", 296 | "- 메시지 출력 스트리밍은 [LangGraph 스트리밍 모드의 모든 것](https://wikidocs.net/265770) 을 참고해주세요.\n", 297 | "\n", 298 | "아래의 `stream_graph` 함수는 특정 노드만 스트리밍 출력하는 함수입니다.\n", 299 | "\n", 300 | "손쉽게 특정 노드의 스트리밍 출력을 확인할 수 있습니다." 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": null, 306 | "id": "d2698eaa", 307 | "metadata": {}, 308 | "outputs": [], 309 | "source": [ 310 | "from langchain_core.runnables import RunnableConfig\n", 311 | "from langchain_teddynote.messages import invoke_graph, stream_graph, random_uuid\n", 312 | "\n", 313 | "# config 설정(재귀 최대 횟수, thread_id)\n", 314 | "config = RunnableConfig(recursion_limit=20, configurable={\"thread_id\": random_uuid()})\n", 315 | "\n", 316 | "# 질문 입력\n", 317 | "inputs = GraphState(question=\"앤스로픽에 투자한 기업과 투자금액을 알려주세요.\")\n", 318 | "\n", 319 | "# 그래프 실행\n", 320 | "invoke_graph(app, inputs, config)" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": null, 326 | "id": "d6a15fa0", 327 | "metadata": {}, 328 | "outputs": [], 329 | "source": [ 330 | "# 그래프를 스트리밍 출력\n", 331 | "stream_graph(app, inputs, config)" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "id": "a16ba031", 338 | "metadata": {}, 339 | "outputs": [], 340 | "source": [ 341 | "outputs = app.get_state(config).values\n", 342 | "\n", 343 | "print(f'Question: {outputs[\"question\"]}')\n", 344 | "print(\"===\" * 20)\n", 345 | "print(f'Answer:\\n{outputs[\"answer\"]}')" 346 | ] 347 | } 348 | ], 349 | "metadata": { 350 | "kernelspec": { 351 | "display_name": ".venv", 352 | "language": "python", 353 | "name": "python3" 354 | }, 355 | "language_info": { 356 | "codemirror_mode": { 357 | "name": "ipython", 358 | "version": 3 359 | }, 360 | "file_extension": ".py", 361 | "mimetype": "text/x-python", 362 | "name": "python", 363 | "nbconvert_exporter": "python", 364 | "pygments_lexer": "ipython3", 365 | "version": "3.12.8" 366 | } 367 | }, 368 | "nbformat": 4, 369 | "nbformat_minor": 5 370 | } 371 | -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/14-LangGraph-Subgraph-Transform-State.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "1031accb", 6 | "metadata": {}, 7 | "source": [ 8 | "# `subgraph`의 입력과 출력을 변환하는 방법\n", 9 | "\n", 10 | "`subgraph` **상태**가 `parent graph` 상태와 완전히 독립적일 수 있습니다. \n", 11 | "\n", 12 | "즉, 두 그래프 간에 중복되는 상태 키(state keys) 가 없을 수 있습니다. \n", 13 | "\n", 14 | "이러한 경우에는 `subgraph`를 호출하기 전에 입력을 변환하고, 반환하기 전에 출력을 변환해야 합니다. " 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "id": "d5e98914", 20 | "metadata": {}, 21 | "source": [ 22 | "## 환경설정" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "id": "f99850ee", 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "# API 키를 환경변수로 관리하기 위한 설정 파일\n", 33 | "from dotenv import load_dotenv\n", 34 | "\n", 35 | "# API 키 정보 로드\n", 36 | "load_dotenv()" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "id": "3355dd38", 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n", 47 | "# !pip install -qU langchain-teddynote\n", 48 | "from langchain_teddynote import logging\n", 49 | "\n", 50 | "# 프로젝트 이름을 입력합니다.\n", 51 | "logging.langsmith(\"CH17-LangGraph-Modules\")" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "id": "862478a0", 57 | "metadata": {}, 58 | "source": [ 59 | "## `graph`와 `subgraph` 정의\n", 60 | "\n", 61 | "다음과 같이 3개의 `graph`를 정의하겠습니다.\n", 62 | "\n", 63 | "- `parent graph`\n", 64 | " \n", 65 | "- `parent graph` 에 의해 호출될 `child subgraph`\n", 66 | "\n", 67 | "- `child graph` 에 의해 호출될 `grandchild subgraph`" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "id": "b5a78503", 73 | "metadata": {}, 74 | "source": [ 75 | "## `grandchild` 정의" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 3, 81 | "id": "5e075d2e", 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | "# 상태 관리를 위한 TypedDict와 StateGraph 관련 모듈 임포트\n", 86 | "from typing_extensions import TypedDict\n", 87 | "from langgraph.graph.state import StateGraph, START, END\n", 88 | "\n", 89 | "\n", 90 | "# 손자 노드의 상태를 정의하는 TypedDict 클래스, 문자열 타입의 my_grandchild_key 포함\n", 91 | "class GrandChildState(TypedDict):\n", 92 | " my_grandchild_key: str\n", 93 | "\n", 94 | "\n", 95 | "# 손자 노드의 상태를 처리하는 함수, 입력된 문자열에 인사말 추가\n", 96 | "def grandchild_1(state: GrandChildState) -> GrandChildState:\n", 97 | " # 자식 또는 부모 키는 여기서 접근 불가\n", 98 | " return {\"my_grandchild_key\": f'([GrandChild] {state[\"my_grandchild_key\"]})'}\n", 99 | "\n", 100 | "\n", 101 | "# 손자 노드의 상태 그래프 초기화\n", 102 | "grandchild = StateGraph(GrandChildState)\n", 103 | "\n", 104 | "# 상태 그래프에 손자 노드 추가\n", 105 | "grandchild.add_node(\"grandchild_1\", grandchild_1)\n", 106 | "\n", 107 | "# 시작 노드에서 손자 노드로의 엣지 연결\n", 108 | "grandchild.add_edge(START, \"grandchild_1\")\n", 109 | "\n", 110 | "# 손자 노드에서 종료 노드로의 엣지 연결\n", 111 | "grandchild.add_edge(\"grandchild_1\", END)\n", 112 | "\n", 113 | "# 정의된 상태 그래프 컴파일 및 실행 가능한 그래프 생성\n", 114 | "grandchild_graph = grandchild.compile()" 115 | ] 116 | }, 117 | { 118 | "cell_type": "markdown", 119 | "id": "c75c5756", 120 | "metadata": {}, 121 | "source": [ 122 | "그래프를 시각화 합니다." 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "id": "9e83abea", 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "from langchain_teddynote.graphs import visualize_graph\n", 133 | "\n", 134 | "visualize_graph(grandchild_graph, xray=True)" 135 | ] 136 | }, 137 | { 138 | "cell_type": "code", 139 | "execution_count": null, 140 | "id": "ad47dfe3", 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "# 그래프 호출\n", 145 | "for chunk in grandchild_graph.stream(\n", 146 | " {\"my_grandchild_key\": \"Hi, Teddy!\"}, subgraphs=True\n", 147 | "):\n", 148 | " print(chunk)" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "id": "10b214ab", 154 | "metadata": {}, 155 | "source": [ 156 | "## `child` 정의" 157 | ] 158 | }, 159 | { 160 | "cell_type": "code", 161 | "execution_count": 6, 162 | "id": "9fab75eb", 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "# 자식 상태 타입 정의를 위한 TypedDict 클래스\n", 167 | "class ChildState(TypedDict):\n", 168 | " my_child_key: str\n", 169 | "\n", 170 | "\n", 171 | "# 손자 그래프 호출 및 상태 변환 함수, 자식 상태를 입력받아 변환된 자식 상태 반환\n", 172 | "def call_grandchild_graph(state: ChildState) -> ChildState:\n", 173 | " # 참고: 부모 또는 손자 키는 여기서 접근 불가능\n", 174 | " # 자식 상태 채널에서 손자 상태 채널로 상태 변환\n", 175 | " grandchild_graph_input = {\"my_grandchild_key\": state[\"my_child_key\"]}\n", 176 | " # 손자 상태 채널에서 자식 상태 채널로 상태 변환 후 결과 반환\n", 177 | " grandchild_graph_output = grandchild_graph.invoke(grandchild_graph_input)\n", 178 | " return {\"my_child_key\": f'([Child] {grandchild_graph_output[\"my_grandchild_key\"]})'}\n", 179 | "\n", 180 | "\n", 181 | "# 자식 상태 그래프 초기화\n", 182 | "child = StateGraph(ChildState)\n", 183 | "# 참고: 컴파일된 그래프 대신 함수 전달\n", 184 | "# 자식 그래프에 노드 추가 및 시작-종료 엣지 연결\n", 185 | "child.add_node(\"child_1\", call_grandchild_graph)\n", 186 | "child.add_edge(START, \"child_1\")\n", 187 | "child.add_edge(\"child_1\", END)\n", 188 | "# 자식 그래프 컴파일\n", 189 | "child_graph = child.compile()" 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": null, 195 | "id": "daa4ba50", 196 | "metadata": {}, 197 | "outputs": [], 198 | "source": [ 199 | "visualize_graph(child_graph, xray=True)" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": null, 205 | "id": "5894b1c5", 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "# child_graph 그래프 호출\n", 210 | "for chunk in child_graph.stream({\"my_child_key\": \"Hi, Teddy!\"}, subgraphs=True):\n", 211 | " print(chunk)" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "id": "18ec6a76", 217 | "metadata": {}, 218 | "source": [ 219 | "`grandchild_graph`의 호출을 별도의 함수(`call_grandchild_graph`)로 감싸고 있습니다. \n", 220 | "\n", 221 | "이 함수는 grandchild 그래프를 호출하기 전에 입력 상태를 변환하고, grandchild 그래프의 출력을 다시 child 그래프 상태로 변환합니다. \n", 222 | "\n", 223 | "만약 이러한 변환 없이 `grandchild_graph`를 직접 `.add_node`에 전달하면, child와 grandchild 상태 간에 공유된 상태 키(State Key) 이 없기 때문에 LangGraph에서 오류가 발생하게 됩니다.\n", 224 | "\n", 225 | "**중요**\n", 226 | "\n", 227 | "`child subgraph` 와 `grandchild subgraph`는 `parent graph`와 공유되지 않는 자신만의 **독립적인** `state`를 가지고 있다는 점에 유의하시기 바랍니다." 228 | ] 229 | }, 230 | { 231 | "cell_type": "markdown", 232 | "id": "75bf1ed9", 233 | "metadata": {}, 234 | "source": [ 235 | "## `parent` 정의" 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": 9, 241 | "id": "1b914571", 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [ 245 | "# 부모 상태 타입 정의를 위한 TypedDict 클래스\n", 246 | "class ParentState(TypedDict):\n", 247 | " my_parent_key: str\n", 248 | "\n", 249 | "\n", 250 | "# 부모 상태의 my_parent_key 값에 '[Parent1]' 문자열을 추가하는 변환 함수\n", 251 | "def parent_1(state: ParentState) -> ParentState:\n", 252 | " # 참고: 자식 또는 손자 키는 여기서 접근 불가\n", 253 | " return {\"my_parent_key\": f'([Parent1] {state[\"my_parent_key\"]})'}\n", 254 | "\n", 255 | "\n", 256 | "# 부모 상태의 my_parent_key 값에 '[Parent2]' 문자열을 추가하는 변환 함수\n", 257 | "def parent_2(state: ParentState) -> ParentState:\n", 258 | " return {\"my_parent_key\": f'([Parent2] {state[\"my_parent_key\"]})'}\n", 259 | "\n", 260 | "\n", 261 | "# 부모 상태와 자식 상태 간의 데이터 변환 및 자식 그래프 호출 처리\n", 262 | "def call_child_graph(state: ParentState) -> ParentState:\n", 263 | " # 부모 상태 채널(my_parent_key)에서 자식 상태 채널(my_child_key)로 상태 변환\n", 264 | " child_graph_input = {\"my_child_key\": state[\"my_parent_key\"]}\n", 265 | " # 자식 상태 채널(my_child_key)에서 부모 상태 채널(my_parent_key)로 상태 변환\n", 266 | " child_graph_output = child_graph.invoke(child_graph_input)\n", 267 | " return {\"my_parent_key\": child_graph_output[\"my_child_key\"]}\n", 268 | "\n", 269 | "\n", 270 | "# 부모 상태 그래프 초기화 및 노드 구성\n", 271 | "parent = StateGraph(ParentState)\n", 272 | "parent.add_node(\"parent_1\", parent_1)\n", 273 | "\n", 274 | "# 참고: 컴파일된 그래프가 아닌 함수를 전달\n", 275 | "parent.add_node(\"child\", call_child_graph)\n", 276 | "parent.add_node(\"parent_2\", parent_2)\n", 277 | "\n", 278 | "# 상태 그래프의 실행 흐름을 정의하는 엣지 구성\n", 279 | "parent.add_edge(START, \"parent_1\")\n", 280 | "parent.add_edge(\"parent_1\", \"child\")\n", 281 | "parent.add_edge(\"child\", \"parent_2\")\n", 282 | "parent.add_edge(\"parent_2\", END)\n", 283 | "\n", 284 | "# 구성된 부모 상태 그래프의 컴파일 및 실행 가능한 그래프 생성\n", 285 | "parent_graph = parent.compile()" 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "id": "7f949634", 291 | "metadata": {}, 292 | "source": [ 293 | "그래프를 시각화 합니다." 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": null, 299 | "id": "da01775e", 300 | "metadata": {}, 301 | "outputs": [], 302 | "source": [ 303 | "visualize_graph(parent_graph, xray=True)" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "id": "74c53786", 309 | "metadata": {}, 310 | "source": [ 311 | "`child_graph` 호출을 별도의 함수 `call_child_graph` 로 감싸고 있는데, 이 함수는 자식 그래프를 호출하기 전에 입력 상태를 변환하고 자식 그래프의 출력을 다시 부모 그래프 상태로 변환합니다. \n", 312 | "\n", 313 | "변환 없이 `child_graph`를 직접 `.add_node`에 전달하면 부모와 자식 상태 간에 공유된 상태 키(State Key) 이 없기 때문에 LangGraph에서 오류가 발생합니다." 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "id": "32b06e95", 319 | "metadata": {}, 320 | "source": [ 321 | "그럼, 부모 그래프를 실행하여 자식 및 손자 하위 그래프가 올바르게 호출되는지 확인해보겠습니다." 322 | ] 323 | }, 324 | { 325 | "cell_type": "code", 326 | "execution_count": null, 327 | "id": "83396a7a", 328 | "metadata": {}, 329 | "outputs": [], 330 | "source": [ 331 | "# 그래프 실행 및 \"my_parent_key\" 매개변수를 통한 \"Hi, Teddy!\" 값 전달\n", 332 | "for chunk in parent_graph.stream({\"my_parent_key\": \"Hi, Teddy!\"}, subgraphs=True):\n", 333 | " print(chunk)" 334 | ] 335 | } 336 | ], 337 | "metadata": { 338 | "kernelspec": { 339 | "display_name": "langchain-kr-lwwSZlnu-py3.11", 340 | "language": "python", 341 | "name": "python3" 342 | }, 343 | "language_info": { 344 | "codemirror_mode": { 345 | "name": "ipython", 346 | "version": 3 347 | }, 348 | "file_extension": ".py", 349 | "mimetype": "text/x-python", 350 | "name": "python", 351 | "nbconvert_exporter": "python", 352 | "pygments_lexer": "ipython3", 353 | "version": "3.11.9" 354 | } 355 | }, 356 | "nbformat": 4, 357 | "nbformat_minor": 5 358 | } 359 | -------------------------------------------------------------------------------- /02-Practice/05-Practice-State-Customization.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Part 5. 상태 커스터마이징 실습\n", 8 | "\n", 9 | "목표\n", 10 | "- 웹 검색 도구를 통합한 챗봇을 구축하고, 상태 replay 기능을 활용하여 검색어를 수정하고 재실행하는 방법을 학습합니다.\n", 11 | "- 첫 번째 검색: `기계식 키보드 추천`으로 일반 검색을 수행합니다.\n", 12 | "- 두 번째 검색: replay 기능을 사용하여 검색어를 `기계식 키보드 추천 2025 site:quasarzone.com`으로 수정하고 재실행합니다.\n", 13 | "\n", 14 | "요구사항\n", 15 | "- `TavilySearch` 도구를 설정하고 LLM에 바인딩하세요.\n", 16 | "- 메모리 기능이 있는 그래프를 구축하세요.\n", 17 | "- 상태 히스토리를 확인하고 `update_tool_call`을 사용하여 검색어를 수정하세요.\n", 18 | "- 수정된 상태로 그래프를 재실행하세요." 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from dotenv import load_dotenv\n", 28 | "from langchain_teddynote import logging\n", 29 | "\n", 30 | "load_dotenv(override=True)\n", 31 | "\n", 32 | "# 프로젝트 이름\n", 33 | "logging.langsmith(\"LangGraph-Exercises\")" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "# 준비 코드\n", 43 | "from typing import Annotated\n", 44 | "from typing_extensions import TypedDict\n", 45 | "\n", 46 | "from langchain_openai import ChatOpenAI\n", 47 | "from langchain_core.messages import HumanMessage\n", 48 | "from langgraph.graph import StateGraph, START, END\n", 49 | "from langgraph.graph.message import add_messages\n", 50 | "from langgraph.checkpoint.memory import InMemorySaver\n", 51 | "from langchain_teddynote.messages import stream_graph\n", 52 | "from langchain_teddynote.graphs import visualize_graph" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "### 1. 웹 검색 도구 설정\n", 60 | "\n", 61 | "- `TavilySearch` 도구를 설정하세요.\n", 62 | "- 최대 검색 결과 수는 2개로 설정하세요." 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "# 실습 코드\n", 72 | "from langchain_tavily import TavilySearch\n", 73 | "from langgraph.prebuilt import ToolNode, tools_condition\n", 74 | "\n", 75 | "# TODO: TavilySearch 도구를 설정하세요 (max_results=2)\n", 76 | "tool = # 코드 입력\n", 77 | "tools = # 코드 입력" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "# 정답 코드\n", 87 | "from langchain_tavily import TavilySearch\n", 88 | "from langgraph.prebuilt import ToolNode, tools_condition\n", 89 | "\n", 90 | "# TODO: TavilySearch 도구를 설정하세요 (max_results=2)\n", 91 | "tool = TavilySearch(max_results=2)\n", 92 | "tools = [tool]" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "### 2. LLM과 State 정의\n", 100 | "\n", 101 | "- ChatOpenAI 모델을 생성하고 도구를 바인딩하세요.\n", 102 | "- 모델명: `gpt-4.1`, 온도: `0`\n", 103 | "- 그래프 State를 정의하세요." 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "# 실습 코드\n", 113 | "# TODO: LLM 모델을 생성하고 도구를 바인딩하세요\n", 114 | "llm = # 코드 입력\n", 115 | "llm_with_tools = # 코드 입력\n", 116 | "\n", 117 | "# TODO: State를 정의하세요\n", 118 | "class State(TypedDict):\n", 119 | " messages: # 코드 입력" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "# 정답 코드\n", 129 | "# TODO: LLM 모델을 생성하고 도구를 바인딩하세요\n", 130 | "llm = ChatOpenAI(model=\"gpt-4.1\", temperature=0)\n", 131 | "llm_with_tools = llm.bind_tools(tools)\n", 132 | "\n", 133 | "\n", 134 | "# TODO: State를 정의하세요\n", 135 | "class State(TypedDict):\n", 136 | " messages: Annotated[list, add_messages]" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "### 3. 챗봇 노드와 그래프 구성\n", 144 | "\n", 145 | "- 챗봇 노드를 작성하세요.\n", 146 | "- ToolNode를 생성하세요.\n", 147 | "- StateGraph를 구성하고 노드와 엣지를 추가하세요.\n", 148 | "- 메모리 기능을 추가하여 컴파일하세요." 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "# 실습 코드\n", 158 | "# TODO: 챗봇 노드를 작성하세요\n", 159 | "def chatbot(state: State):\n", 160 | " # 코드 입력\n", 161 | " pass\n", 162 | "\n", 163 | "# TODO: 그래프를 구성하세요\n", 164 | "builder = # 코드 입력\n", 165 | "builder.add_node(# 코드 입력)\n", 166 | "\n", 167 | "# TODO: ToolNode를 추가하세요\n", 168 | "tool_node = # 코드 입력\n", 169 | "builder.add_node(# 코드 입력)\n", 170 | "\n", 171 | "# TODO: 엣지를 추가하세요\n", 172 | "builder.add_conditional_edges(# 코드 입력)\n", 173 | "builder.add_edge(# 코드 입력)\n", 174 | "builder.add_edge(# 코드 입력)\n", 175 | "\n", 176 | "# TODO: 메모리와 함께 컴파일하세요\n", 177 | "memory = # 코드 입력\n", 178 | "graph = # 코드 입력" 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": null, 184 | "metadata": {}, 185 | "outputs": [], 186 | "source": [ 187 | "# 정답 코드\n", 188 | "# TODO: 챗봇 노드를 작성하세요\n", 189 | "def chatbot(state: State):\n", 190 | " message = llm_with_tools.invoke(state[\"messages\"])\n", 191 | " return {\"messages\": [message]}\n", 192 | "\n", 193 | "\n", 194 | "# TODO: 그래프를 구성하세요\n", 195 | "builder = StateGraph(State)\n", 196 | "builder.add_node(\"chatbot\", chatbot)\n", 197 | "\n", 198 | "# TODO: ToolNode를 추가하세요\n", 199 | "tool_node = ToolNode(tools=tools)\n", 200 | "builder.add_node(\"tools\", tool_node)\n", 201 | "\n", 202 | "# TODO: 엣지를 추가하세요\n", 203 | "builder.add_conditional_edges(\"chatbot\", tools_condition)\n", 204 | "builder.add_edge(\"tools\", \"chatbot\")\n", 205 | "builder.add_edge(START, \"chatbot\")\n", 206 | "\n", 207 | "# TODO: 메모리와 함께 컴파일하세요\n", 208 | "memory = InMemorySaver()\n", 209 | "graph = builder.compile(checkpointer=memory)" 210 | ] 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "metadata": {}, 215 | "source": [ 216 | "### 4. 첫 번째 검색 실행\n", 217 | "\n", 218 | "- `기계식 키보드 추천`이라는 검색어로 첫 번째 실행을 수행하세요.\n", 219 | "- thread_id는 `keyboard-001`로 설정하세요." 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": null, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "# 실습 코드\n", 229 | "# TODO: 첫 번째 검색을 실행하세요\n", 230 | "config = # 코드 입력\n", 231 | "inputs = # 코드 입력\n", 232 | "\n", 233 | "# 그래프 실행\n", 234 | "stream_graph(graph, inputs=inputs, config=config)" 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": null, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "# 정답 코드\n", 244 | "# TODO: 첫 번째 검색을 실행하세요\n", 245 | "config = {\"configurable\": {\"thread_id\": \"keyboard-001\"}}\n", 246 | "inputs = {\"messages\": [HumanMessage(content=\"기계식 키보드 추천 정보를 찾아주세요.\")]}\n", 247 | "\n", 248 | "# 그래프 실행\n", 249 | "stream_graph(graph, inputs=inputs, config=config)" 250 | ] 251 | }, 252 | { 253 | "cell_type": "markdown", 254 | "metadata": {}, 255 | "source": [ 256 | "### 5. 상태 히스토리 확인 및 Replay 대상 찾기\n", 257 | "\n", 258 | "- 상태 히스토리를 확인하고 도구 호출이 있는 체크포인트를 찾으세요.\n", 259 | "- 해당 체크포인트를 `to_replay` 변수에 저장하세요." 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "execution_count": null, 265 | "metadata": {}, 266 | "outputs": [], 267 | "source": [ 268 | "# 실습 코드\n", 269 | "# TODO: 상태 히스토리를 확인하고 replay할 체크포인트를 찾으세요\n", 270 | "to_replay = None\n", 271 | "\n", 272 | "for state in graph.get_state_history(config):\n", 273 | " # 도구 호출이 있는 상태를 찾으세요\n", 274 | " if state.next == (\"tools\",) and to_replay is None:\n", 275 | " # 코드 입력\n", 276 | " break\n", 277 | "\n", 278 | "if to_replay:\n", 279 | " print(\n", 280 | " f\"✅ Replay할 체크포인트 ID: {to_replay.config['configurable']['checkpoint_id']}\"\n", 281 | " )" 282 | ] 283 | }, 284 | { 285 | "cell_type": "code", 286 | "execution_count": null, 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [ 290 | "# 정답 코드\n", 291 | "# TODO: 상태 히스토리를 확인하고 replay할 체크포인트를 찾으세요\n", 292 | "to_replay = None\n", 293 | "\n", 294 | "for state in graph.get_state_history(config):\n", 295 | " # 도구 호출이 있는 상태를 찾으세요\n", 296 | " if state.next == (\"tools\",) and to_replay is None:\n", 297 | " to_replay = state\n", 298 | " break\n", 299 | "\n", 300 | "if to_replay:\n", 301 | " print(\n", 302 | " f\"✅ Replay할 체크포인트 ID: {to_replay.config['configurable']['checkpoint_id']}\"\n", 303 | " )" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "metadata": {}, 309 | "source": [ 310 | "### 6. 검색어 수정 및 재실행\n", 311 | "\n", 312 | "- `update_tool_call` 함수를 사용하여 검색어를 수정하세요.\n", 313 | "- 새로운 검색어: `기계식 키보드 추천 2025 site:quasarzone.com`\n", 314 | "- 수정된 상태로 그래프를 재실행하세요." 315 | ] 316 | }, 317 | { 318 | "cell_type": "code", 319 | "execution_count": null, 320 | "metadata": {}, 321 | "outputs": [], 322 | "source": [ 323 | "# 실습 코드\n", 324 | "from langchain_teddynote.tools import update_tool_call\n", 325 | "\n", 326 | "# TODO: 검색어를 수정하세요\n", 327 | "updated_message = update_tool_call(\n", 328 | " # 코드 입력 - 마지막 메시지\n", 329 | " tool_name=\"tavily_search\",\n", 330 | " tool_args=# 코드 입력 - 새로운 검색어\n", 331 | ")\n", 332 | "\n", 333 | "# TODO: 수정된 메시지로 상태를 업데이트하고 재실행하세요\n", 334 | "graph.update_state(\n", 335 | " to_replay.config,\n", 336 | " {\"messages\": # 코드 입력}\n", 337 | ")\n", 338 | "\n", 339 | "# 수정된 상태에서 재실행\n", 340 | "stream_graph(graph, inputs=None, config=to_replay.config)" 341 | ] 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": null, 346 | "metadata": {}, 347 | "outputs": [], 348 | "source": [ 349 | "# 정답 코드\n", 350 | "from langchain_teddynote.tools import update_tool_call\n", 351 | "\n", 352 | "# TODO: 검색어를 수정하세요\n", 353 | "updated_message = update_tool_call(\n", 354 | " to_replay.values[\"messages\"][-1],\n", 355 | " tool_name=\"tavily_search\",\n", 356 | " tool_args={\n", 357 | " \"query\": \"기계식 키보드 추천 2025 site:quasarzone.com\",\n", 358 | " \"search_depth\": \"basic\",\n", 359 | " },\n", 360 | ")\n", 361 | "\n", 362 | "# TODO: 수정된 메시지로 상태를 업데이트하고 재실행하세요\n", 363 | "updated_state = graph.update_state(to_replay.config, {\"messages\": [updated_message]})\n", 364 | "\n", 365 | "# 수정된 상태에서 재실행\n", 366 | "stream_graph(graph, inputs=None, config=updated_state)" 367 | ] 368 | } 369 | ], 370 | "metadata": { 371 | "kernelspec": { 372 | "display_name": ".venv", 373 | "language": "python", 374 | "name": "python3" 375 | }, 376 | "language_info": { 377 | "codemirror_mode": { 378 | "name": "ipython", 379 | "version": 3 380 | }, 381 | "file_extension": ".py", 382 | "mimetype": "text/x-python", 383 | "name": "python", 384 | "nbconvert_exporter": "python", 385 | "pygments_lexer": "ipython3", 386 | "version": "3.12.8" 387 | } 388 | }, 389 | "nbformat": 4, 390 | "nbformat_minor": 4 391 | } 392 | -------------------------------------------------------------------------------- /02-Practice/04-Practice-Human-in-the-Loop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Human in the Loop 실습 - 이메일 회신 챗봇\n", 8 | "\n", 9 | "목표\n", 10 | "- 고객사로부터 받은 이메일에 대한 회신을 작성하는 챗봇을 구현합니다.\n", 11 | "- 챗봇이 작성한 초안을 사람이 검토하고 승인/수정할 수 있는 Human-in-the-Loop 시스템을 구축합니다.\n", 12 | "- interrupt와 Command를 활용하여 실행을 제어합니다.\n", 13 | "\n", 14 | "시나리오\n", 15 | "- 고객사(TechCorp)로부터 제품 문의 이메일이 도착했습니다.\n", 16 | "- 챗봇이 자동으로 회신 초안을 작성합니다.\n", 17 | "- 담당자가 초안을 검토하여 승인 또는 수정 요청을 합니다.\n", 18 | "- 최종 승인된 이메일이 발송됩니다.\n", 19 | "\n", 20 | "샘플 이메일\n", 21 | "```\n", 22 | "보내는 사람: kim@techcorp.com\n", 23 | "제목: AI 솔루션 도입 문의\n", 24 | "\n", 25 | "안녕하세요,\n", 26 | "\n", 27 | "저희 회사에서 고객 서비스 자동화를 위한 AI 솔루션 도입을 검토하고 있습니다.\n", 28 | "귀사의 LangGraph 기반 솔루션에 대해 다음 사항을 문의드립니다:\n", 29 | "\n", 30 | "1. 기업용 라이선스 가격\n", 31 | "2. 기술 지원 범위\n", 32 | "3. 커스터마이징 가능 여부\n", 33 | "4. 도입 사례\n", 34 | "\n", 35 | "빠른 회신 부탁드립니다.\n", 36 | "\n", 37 | "감사합니다.\n", 38 | "김철수 부장\n", 39 | "TechCorp\n", 40 | "```" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "from dotenv import load_dotenv\n", 50 | "from langchain_teddynote import logging\n", 51 | "\n", 52 | "load_dotenv(override=True)\n", 53 | "\n", 54 | "# 프로젝트 이름\n", 55 | "logging.langsmith(\"LangGraph-Exercises\")" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "# 준비 코드\n", 65 | "from typing import Annotated, Literal\n", 66 | "from typing_extensions import TypedDict\n", 67 | "\n", 68 | "from langchain_openai import ChatOpenAI\n", 69 | "from langchain_core.messages import HumanMessage, AIMessage, SystemMessage\n", 70 | "from langgraph.graph import StateGraph, START, END\n", 71 | "from langgraph.graph.message import add_messages\n", 72 | "from langgraph.checkpoint.memory import InMemorySaver\n", 73 | "from langgraph.types import Command, interrupt" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "### 1. State 정의 및 모델 설정\n", 81 | "\n", 82 | "- 이메일 회신 챗봇을 위한 State를 정의하세요.\n", 83 | "- State에는 messages, email_draft, approval_status가 포함되어야 합니다.\n", 84 | "- ChatOpenAI 모델(gpt-4.1, temperature=0.3)을 생성하세요." 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "# 실습 코드\n", 94 | "# TODO: State를 정의하세요. messages, email_draft, approval_status, human_feedback 필드를 포함해야 합니다.\n", 95 | "\n", 96 | "class EmailState(TypedDict):\n", 97 | " messages: # 코드 입력: add_messages 사용\n", 98 | " email_draft: # 코드 입력: str 타입\n", 99 | " approval_status: # 코드 입력: Literal[\"pending\", \"approved\", \"rejected\"] 타입\n", 100 | " human_feedback: # 코드 입력: str 타입 (수정 요청 피드백 저장)\n", 101 | "\n", 102 | "# TODO: LLM 모델을 정의하세요.\n", 103 | "llm = # 코드 입력" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "### 2. 이메일 초안 작성 노드\n", 111 | "\n", 112 | "- 받은 이메일을 분석하고 회신 초안을 작성하는 노드를 구현하세요.\n", 113 | "- 시스템 프롬프트를 활용하여 전문적이고 친근한 톤으로 작성하도록 지시하세요.\n", 114 | "- email_draft와 approval_status를 \"pending\"으로 설정하여 반환하세요." 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": null, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "# 실습 코드\n", 124 | "# TODO: 이메일 초안을 작성하는 노드를 구현하세요.\n", 125 | "\n", 126 | "def draft_email(state: EmailState):\n", 127 | " \"\"\"받은 이메일에 대한 회신 초안을 작성합니다.\"\"\"\n", 128 | " \n", 129 | " # 시스템 프롬프트 설정\n", 130 | " system_prompt = # 코드 입력: 이메일 회신 작성 지시\n", 131 | " \n", 132 | " # human_feedback이 있으면 프롬프트에 추가\n", 133 | " if state.get(\"human_feedback\"):\n", 134 | " system_prompt += # 코드 입력: feedback 반영 지시 추가\n", 135 | " \n", 136 | " # 메시지 구성\n", 137 | " messages = # 코드 입력: SystemMessage와 state의 messages 결합\n", 138 | " \n", 139 | " # LLM으로 초안 생성\n", 140 | " response = # 코드 입력\n", 141 | " \n", 142 | " return {\n", 143 | " # 코드 입력: email_draft와 approval_status 반환\n", 144 | " }" 145 | ] 146 | }, 147 | { 148 | "cell_type": "markdown", 149 | "metadata": {}, 150 | "source": [ 151 | "### 3. Human Review 노드 구현\n", 152 | "\n", 153 | "- interrupt를 사용하여 사람의 검토를 요청하는 노드를 구현하세요.\n", 154 | "- 초안을 보여주고 승인/수정 요청을 받아 처리하세요.\n", 155 | "- Command 객체로 받은 피드백을 처리하여 상태를 업데이트하세요." 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": {}, 162 | "outputs": [], 163 | "source": [ 164 | "# 실습 코드\n", 165 | "# TODO: Human Review 노드를 구현하세요.\n", 166 | "\n", 167 | "def human_review(state: EmailState):\n", 168 | " \"\"\"사람의 검토를 요청하고 피드백을 받습니다.\"\"\"\n", 169 | " \n", 170 | " print(\"\\n===== 이메일 초안 검토 요청 =====\")\n", 171 | " print(f\"\\n{state['email_draft']}\")\n", 172 | " print(\"\\n================================\")\n", 173 | " print(\"옵션: approve(승인) / reject(수정요청)\")\n", 174 | " \n", 175 | " # interrupt를 호출하여 사람의 입력 대기\n", 176 | " human_input = # 코드 입력: interrupt 호출\n", 177 | " \n", 178 | " # 피드백 처리\n", 179 | " if human_input.get(\"action\") == \"approve\":\n", 180 | " return # 코드 입력: approval_status를 \"approved\"로 설정\n", 181 | " else:\n", 182 | " # 수정 요청시 피드백을 human_feedback에 저장\n", 183 | " feedback = # 코드 입력: human_input에서 feedback 가져오기\n", 184 | " return # 코드 입력: human_feedback과 approval_status를 \"rejected\"로 설정" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "### 4. 조건부 라우팅 구현\n", 192 | "\n", 193 | "- approval_status에 따라 다음 노드를 결정하는 라우팅 함수를 구현하세요.\n", 194 | "- pending: human_review 노드로\n", 195 | "- approved: send_email 노드로\n", 196 | "- rejected: draft_email 노드로 (재작성)" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": null, 202 | "metadata": {}, 203 | "outputs": [], 204 | "source": [ 205 | "# 실습 코드\n", 206 | "# TODO: 조건부 라우팅 함수를 구현하세요.\n", 207 | "\n", 208 | "def route_approval(state: EmailState) -> str:\n", 209 | " \"\"\"승인 상태에 따라 다음 노드를 결정합니다.\"\"\"\n", 210 | " status = # 코드 입력: state에서 approval_status 가져오기\n", 211 | " \n", 212 | " if status == \"pending\":\n", 213 | " return # 코드 입력: \"human_review\" 반환\n", 214 | " elif status == \"approved\":\n", 215 | " return # 코드 입력: \"send_email\" 반환\n", 216 | " else: # rejected\n", 217 | " return # 코드 입력: \"draft_email\" 반환\n", 218 | "\n", 219 | "# TODO: 이메일 발송 노드를 구현하세요.\n", 220 | "def send_email(state: EmailState):\n", 221 | " \"\"\"최종 승인된 이메일을 발송합니다.\"\"\"\n", 222 | " print(\"\\n✅ 이메일이 발송되었습니다!\")\n", 223 | " print(f\"\\n{state['email_draft']}\")\n", 224 | " return # 코드 입력: AIMessage로 발송 완료 메시지 반환" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "### 5. 완전한 그래프 구성 및 실행\n", 232 | "\n", 233 | "- 모든 노드를 연결하여 완전한 그래프를 구성하세요.\n", 234 | "- checkpointer를 설정하여 상태를 저장할 수 있도록 하세요.\n", 235 | "- 샘플 이메일로 그래프를 실행하고 interrupt와 Command를 사용하여 상호작용하세요." 236 | ] 237 | }, 238 | { 239 | "cell_type": "code", 240 | "execution_count": null, 241 | "metadata": {}, 242 | "outputs": [], 243 | "source": [ 244 | "# 실습 코드\n", 245 | "# TODO: 완전한 그래프를 구성하세요.\n", 246 | "\n", 247 | "# 그래프 빌더 생성\n", 248 | "builder = # 코드 입력: StateGraph 생성\n", 249 | "\n", 250 | "# 노드 추가\n", 251 | "builder.# 코드 입력: draft_email 노드 추가\n", 252 | "builder.# 코드 입력: human_review 노드 추가\n", 253 | "builder.# 코드 입력: send_email 노드 추가\n", 254 | "\n", 255 | "# 엣지 추가\n", 256 | "builder.# 코드 입력: START → draft_email\n", 257 | "builder.# 코드 입력: draft_email → route_approval (조건부)\n", 258 | "builder.# 코드 입력: human_review → route_approval (조건부)\n", 259 | "builder.# 코드 입력: send_email → END\n", 260 | "\n", 261 | "# 체크포인터 설정 및 컴파일\n", 262 | "memory = # 코드 입력: InMemorySaver 생성\n", 263 | "app = builder.# 코드 입력: checkpointer와 함께 컴파일" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "metadata": {}, 269 | "source": [ 270 | "### 6. 그래프 시각화 및 실행\n", 271 | "\n", 272 | "- 그래프를 시각화하고 샘플 이메일로 실행하세요.\n", 273 | "- interrupt 시점에서 Command를 사용하여 승인 또는 수정 요청을 보내세요." 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": null, 279 | "metadata": {}, 280 | "outputs": [], 281 | "source": [ 282 | "# 실습 코드\n", 283 | "# TODO: 그래프를 시각화하세요.\n", 284 | "from langchain_teddynote.graphs import visualize_graph\n", 285 | "\n", 286 | "# 코드 입력: 그래프 시각화" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "### 7. 테스트 및 결과 확인" 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": null, 299 | "metadata": {}, 300 | "outputs": [], 301 | "source": [ 302 | "# 실습 코드\n", 303 | "# TODO: 샘플 이메일로 그래프를 실행하세요.\n", 304 | "from langchain.core.runnables import RunnableConfig\n", 305 | "\n", 306 | "# 샘플 이메일\n", 307 | "customer_email = \"\"\"보내는 사람: kim@techcorp.com\n", 308 | "제목: AI 솔루션 도입 문의\n", 309 | "\n", 310 | "안녕하세요,\n", 311 | "\n", 312 | "저희 회사에서 고객 서비스 자동화를 위한 AI 솔루션 도입을 검토하고 있습니다.\n", 313 | "귀사의 LangGraph 기반 솔루션에 대해 다음 사항을 문의드립니다:\n", 314 | "\n", 315 | "1. 기업용 라이선스 가격\n", 316 | "2. 기술 지원 범위\n", 317 | "3. 커스터마이징 가능 여부\n", 318 | "4. 도입 사례\n", 319 | "\n", 320 | "빠른 회신 부탁드립니다.\n", 321 | "\n", 322 | "감사합니다.\n", 323 | "김철수 부장\n", 324 | "TechCorp\"\"\"\n", 325 | "\n", 326 | "# 설정\n", 327 | "config = # 코드 입력: RunnableConfig 생성 (thread_id 포함)\n", 328 | "\n", 329 | "# 그래프 실행 (interrupt에서 중단됨)\n", 330 | "result = # 코드 입력: app.invoke 실행" 331 | ] 332 | }, 333 | { 334 | "cell_type": "markdown", 335 | "metadata": {}, 336 | "source": [ 337 | "approve 나 reject 명령어를 사용하여 승인하거나 재작성을 요청하세요." 338 | ] 339 | }, 340 | { 341 | "cell_type": "code", 342 | "execution_count": null, 343 | "metadata": {}, 344 | "outputs": [], 345 | "source": [ 346 | "# 실습 코드\n", 347 | "# TODO: Command를 사용하여 초안을 승인하거나 수정 요청하세요.\n", 348 | "\n", 349 | "# 상태 확인\n", 350 | "snapshot = # 코드 입력: app.get_state로 현재 상태 확인\n", 351 | "print(f\"현재 노드: {snapshot.next}\")\n", 352 | "\n", 353 | "# 승인하는 경우\n", 354 | "app.invoke(\n", 355 | " # 코드 입력: Command 객체로 승인 액션 전달\n", 356 | ")\n", 357 | "\n", 358 | "# 또는 수정 요청하는 경우\n", 359 | "# app.invoke(\n", 360 | "# # 코드 입력: Command 객체로 수정 요청 전달\n", 361 | "# )" 362 | ] 363 | } 364 | ], 365 | "metadata": { 366 | "kernelspec": { 367 | "display_name": ".venv", 368 | "language": "python", 369 | "name": "python3" 370 | }, 371 | "language_info": { 372 | "codemirror_mode": { 373 | "name": "ipython", 374 | "version": 3 375 | }, 376 | "file_extension": ".py", 377 | "mimetype": "text/x-python", 378 | "name": "python", 379 | "nbconvert_exporter": "python", 380 | "pygments_lexer": "ipython3", 381 | "version": "3.12.8" 382 | } 383 | }, 384 | "nbformat": 4, 385 | "nbformat_minor": 4 386 | } 387 | -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/01-LangGraph-Introduction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# LangGraph 에 자주 등장하는 Python 문법" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## TypedDict\n", 15 | "\n", 16 | "`dict`와 `TypedDict`의 차이점과 `TypedDict`가 왜 `dict` 대신 사용되는지 설명해드리겠습니다.\n", 17 | "\n", 18 | "1. `dict`와 `TypedDict`의 주요 차이점:\n", 19 | "\n", 20 | " a) 타입 검사:\n", 21 | " - `dict`: 런타임에 타입 검사를 하지 않습니다.\n", 22 | " - `TypedDict`: 정적 타입 검사를 제공합니다. 즉, 코드 작성 시 IDE나 타입 체커가 오류를 미리 잡아낼 수 있습니다.\n", 23 | "\n", 24 | " b) 키와 값의 타입:\n", 25 | " - `dict`: 키와 값의 타입을 일반적으로 지정합니다 (예: Dict[str, str]).\n", 26 | " - `TypedDict`: 각 키에 대해 구체적인 타입을 지정할 수 있습니다.\n", 27 | "\n", 28 | " c) 유연성:\n", 29 | " - `dict`: 런타임에 키를 추가하거나 제거할 수 있습니다.\n", 30 | " - `TypedDict`: 정의된 구조를 따라야 합니다. 추가적인 키는 타입 오류를 발생시킵니다.\n", 31 | "\n", 32 | "2. `TypedDict`가 `dict` 대신 사용되는 이유:\n", 33 | "\n", 34 | " a) 타입 안정성: \n", 35 | " `TypedDict`는 더 엄격한 타입 검사를 제공하여 잠재적인 버그를 미리 방지할 수 있습니다.\n", 36 | "\n", 37 | " b) 코드 가독성:\n", 38 | " `TypedDict`를 사용하면 딕셔너리의 구조를 명확하게 정의할 수 있어 코드의 가독성이 향상됩니다.\n", 39 | "\n", 40 | " c) IDE 지원:\n", 41 | " `TypedDict`를 사용하면 IDE에서 자동 완성 및 타입 힌트를 더 정확하게 제공받을 수 있습니다.\n", 42 | "\n", 43 | " d) 문서화:\n", 44 | " `TypedDict`는 코드 자체가 문서의 역할을 하여 딕셔너리의 구조를 명확히 보여줍니다." 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 10, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "# TypedDict와 Dict의 차이점 예시\n", 54 | "from typing import Dict, TypedDict\n", 55 | "\n", 56 | "# 일반적인 파이썬 딕셔너리(dict) 사용\n", 57 | "sample_dict: Dict[str, str] = {\n", 58 | " \"name\": \"테디\",\n", 59 | " \"age\": \"30\", # 문자열로 저장 (dict 에서는 가능)\n", 60 | " \"job\": \"개발자\",\n", 61 | "}\n", 62 | "\n", 63 | "\n", 64 | "# TypedDict 사용\n", 65 | "class Person(TypedDict):\n", 66 | " name: str\n", 67 | " age: int # 정수형으로 명시\n", 68 | " job: str\n", 69 | "\n", 70 | "\n", 71 | "typed_dict: Person = {\"name\": \"셜리\", \"age\": 25, \"job\": \"디자이너\"}" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 11, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "# dict의 경우\n", 81 | "sample_dict[\"age\"] = 35 # 문자열에서 정수로 변경되어도 오류 없음\n", 82 | "sample_dict[\"new_field\"] = \"추가 정보\" # 새로운 필드 추가 가능\n", 83 | "\n", 84 | "# TypedDict의 경우\n", 85 | "typed_dict[\"age\"] = 35 # 정수형으로 올바르게 사용\n", 86 | "typed_dict[\"age\"] = \"35\" # 타입 체커가 오류를 감지함\n", 87 | "typed_dict[\"new_field\"] = (\n", 88 | " \"추가 정보\" # 타입 체커가 정의되지 않은 키라고 오류를 발생시킴\n", 89 | ")" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "하지만 TypedDict의 진정한 가치는 정적 타입 검사기를 사용할 때 드러납니다. \n", 97 | "\n", 98 | "예를 들어, mypy와 같은 정적 타입 검사기를 사용하거나 PyCharm, VS Code 등의 IDE에서 타입 검사 기능을 활성화하면, 이러한 타입 불일치와 정의되지 않은 키 추가를 오류로 표시합니다.\n", 99 | "정적 타입 검사기를 사용하면 다음과 같은 오류 메시지를 볼 수 있습니다." 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "## Annotated\n", 107 | "\n", 108 | "이 문법은 타입 힌트에 메타데이터를 추가할 수 있게 해줍니다." 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "`Annotated`를 사용하는 주요 이유\n", 116 | "\n", 117 | "**추가 정보 제공(타입 힌트) / 문서화** \n", 118 | "\n", 119 | "- 타입 힌트에 추가적인 정보를 포함시킬 수 있습니다. 이는 코드를 읽는 사람이나 도구에 더 많은 컨텍스트를 제공합니다.\n", 120 | "- 코드에 대한 추가 설명을 타입 힌트에 직접 포함시킬 수 있습니다.\n", 121 | "\n", 122 | "`name: Annotated[str, \"이름\"]`\n", 123 | "\n", 124 | "`age: Annotated[int, \"나이\"]`\n", 125 | "\n", 126 | "----" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": {}, 132 | "source": [ 133 | "`Annotated` 는 Python의 typing 모듈에서 제공하는 특별한 타입 힌트로, 기존 타입에 메타데이터를 추가할 수 있게 해줍니다.\n", 134 | "\n", 135 | "`Annotated` 는 타입 힌트에 추가 정보를 포함시킬 수 있는 기능을 제공합니다. 이를 통해 코드의 가독성을 높이고, 더 자세한 타입 정보를 제공할 수 있습니다.\n", 136 | "\n", 137 | "\n", 138 | "### Annotated 주요 기능(사용 이유)\n", 139 | "\n", 140 | "1. **추가 정보 제공**: 타입 힌트에 메타데이터를 추가하여 더 상세한 정보를 제공합니다.\n", 141 | "2. **문서화**: 코드 자체에 추가 설명을 포함시켜 문서화 효과를 얻을 수 있습니다.\n", 142 | "3. **유효성 검사**: 특정 라이브러리(예: Pydantic)와 함께 사용하여 데이터 유효성 검사를 수행할 수 있습니다.\n", 143 | "4. **프레임워크 지원**: 일부 프레임워크(예: LangGraph)에서는 `Annotated`를 사용하여 특별한 동작을 정의합니다.\n", 144 | "\n", 145 | "**기본 문법**\n", 146 | "\n", 147 | "- `Type`: 기본 타입 (예: `int`, `str`, `List[str]` 등)\n", 148 | "- `metadata1`, `metadata2`, ...: 추가하고자 하는 메타데이터\n", 149 | " \n", 150 | "```python\n", 151 | "from typing import Annotated\n", 152 | "\n", 153 | "variable: Annotated[Type, metadata1, metadata2, ...]\n", 154 | "```" 155 | ] 156 | }, 157 | { 158 | "cell_type": "markdown", 159 | "metadata": {}, 160 | "source": [ 161 | "### 사용 예시" 162 | ] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "metadata": {}, 167 | "source": [ 168 | "기본 사용" 169 | ] 170 | }, 171 | { 172 | "cell_type": "code", 173 | "execution_count": 12, 174 | "metadata": {}, 175 | "outputs": [], 176 | "source": [ 177 | "from typing import Annotated\n", 178 | "\n", 179 | "name: Annotated[str, \"사용자 이름\"]\n", 180 | "age: Annotated[int, \"사용자 나이 (0-150)\"]" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "metadata": {}, 186 | "source": [ 187 | "Pydantic과 함께 사용" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "from typing import Annotated, List\n", 197 | "from pydantic import Field, BaseModel, ValidationError\n", 198 | "\n", 199 | "\n", 200 | "class Employee(BaseModel):\n", 201 | " id: Annotated[int, Field(..., description=\"직원 ID\")]\n", 202 | " name: Annotated[str, Field(..., min_length=3, max_length=50, description=\"이름\")]\n", 203 | " age: Annotated[int, Field(gt=18, lt=65, description=\"나이 (19-64세)\")]\n", 204 | " salary: Annotated[\n", 205 | " float, Field(gt=0, lt=10000, description=\"연봉 (단위: 만원, 최대 10억)\")\n", 206 | " ]\n", 207 | " skills: Annotated[\n", 208 | " List[str], Field(min_items=1, max_items=10, description=\"보유 기술 (1-10개)\")\n", 209 | " ]\n", 210 | "\n", 211 | "\n", 212 | "# 유효한 데이터로 인스턴스 생성\n", 213 | "try:\n", 214 | " valid_employee = Employee(\n", 215 | " id=1, name=\"테디노트\", age=30, salary=1000, skills=[\"Python\", \"LangChain\"]\n", 216 | " )\n", 217 | " print(\"유효한 직원 데이터:\", valid_employee)\n", 218 | "except ValidationError as e:\n", 219 | " print(\"유효성 검사 오류:\", e)\n", 220 | "\n", 221 | "# 유효하지 않은 데이터로 인스턴스 생성 시도\n", 222 | "try:\n", 223 | " invalid_employee = Employee(\n", 224 | " name=\"테디\", # 이름이 너무 짧음\n", 225 | " age=17, # 나이가 범위를 벗어남\n", 226 | " salary=20000, # 급여가 범위를 벗어남\n", 227 | " skills=\"Python\", # 리스트가 아님\n", 228 | " )\n", 229 | "except ValidationError as e:\n", 230 | " print(\"유효성 검사 오류:\")\n", 231 | " for error in e.errors():\n", 232 | " print(f\"- {error['loc'][0]}: {error['msg']}\")" 233 | ] 234 | }, 235 | { 236 | "cell_type": "markdown", 237 | "metadata": {}, 238 | "source": [ 239 | "### LangGraph에서의 사용(add_messages)\n", 240 | "\n", 241 | "`add_messages` 는 LangGraph 에서 메시지를 리스트에 추가하는 함수입니다." 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": 14, 247 | "metadata": {}, 248 | "outputs": [], 249 | "source": [ 250 | "from typing import Annotated, TypedDict\n", 251 | "from langgraph.graph import add_messages\n", 252 | "\n", 253 | "\n", 254 | "class MyData(TypedDict):\n", 255 | " messages: Annotated[list, add_messages]" 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": 15, 261 | "metadata": {}, 262 | "outputs": [], 263 | "source": [ 264 | "from typing import Annotated, TypedDict\n", 265 | "from langgraph.graph import add_messages\n", 266 | "\n", 267 | "\n", 268 | "class MyData(TypedDict):\n", 269 | " messages: Annotated[list, add_messages]" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": {}, 275 | "source": [ 276 | "**참고**\n", 277 | "\n", 278 | "1. `Annotated`는 Python 3.9 이상에서 사용 가능합니다.\n", 279 | "2. 런타임에는 `Annotated`가 무시되므로, 실제 동작에는 영향을 주지 않습니다.\n", 280 | "3. 타입 검사 도구나 IDE가 `Annotated`를 지원해야 그 효과를 볼 수 있습니다." 281 | ] 282 | }, 283 | { 284 | "cell_type": "markdown", 285 | "metadata": {}, 286 | "source": [ 287 | "## add_messages\n", 288 | "\n", 289 | "`messages` 키는 [`add_messages`](https://langchain-ai.github.io/langgraph/reference/graphs/?h=add+messages#add_messages) 리듀서 함수로 주석이 달려 있으며, 이는 LangGraph에게 기존 목록에 새 메시지를 추가하도록 지시합니다. \n", 290 | "\n", 291 | "주석이 없는 상태 키는 각 업데이트에 의해 덮어쓰여져 가장 최근의 값이 저장됩니다. \n", 292 | "\n", 293 | "`add_messages` 함수는 2개의 인자(left, right)를 받으며 좌, 우 메시지를 병합하는 방식으로 동작합니다.\n", 294 | "\n", 295 | "**주요 기능**\n", 296 | " - 두 개의 메시지 리스트를 병합합니다.\n", 297 | " - 기본적으로 \"append-only\" 상태를 유지합니다.\n", 298 | " - 동일한 ID를 가진 메시지가 있을 경우, 새 메시지로 기존 메시지를 대체합니다.\n", 299 | "\n", 300 | "**동작 방식**\n", 301 | "\n", 302 | " - `right`의 메시지 중 `left`에 동일한 ID를 가진 메시지가 있으면, `right`의 메시지로 대체됩니다.\n", 303 | " - 그 외의 경우 `right`의 메시지가 `left`에 추가됩니다.\n", 304 | "\n", 305 | "**매개변수**\n", 306 | "\n", 307 | " - `left` (Messages): 기본 메시지 리스트\n", 308 | " - `right` (Messages): 병합할 메시지 리스트 또는 단일 메시지\n", 309 | "\n", 310 | "**반환값**\n", 311 | "\n", 312 | " - `Messages`: `right`의 메시지들이 `left`에 병합된 새로운 메시지 리스트" 313 | ] 314 | }, 315 | { 316 | "cell_type": "code", 317 | "execution_count": null, 318 | "metadata": {}, 319 | "outputs": [], 320 | "source": [ 321 | "from langchain_core.messages import AIMessage, HumanMessage\n", 322 | "from langgraph.graph import add_messages\n", 323 | "\n", 324 | "# 기본 사용 예시\n", 325 | "msgs1 = [HumanMessage(content=\"안녕하세요?\", id=\"1\")]\n", 326 | "msgs2 = [AIMessage(content=\"반갑습니다~\", id=\"2\")]\n", 327 | "\n", 328 | "result1 = add_messages(msgs1, msgs2)\n", 329 | "print(result1)" 330 | ] 331 | }, 332 | { 333 | "cell_type": "markdown", 334 | "metadata": {}, 335 | "source": [ 336 | "동일한 ID 를 가진 Message 가 있을 경우 대체됩니다." 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "execution_count": null, 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [ 345 | "# 동일한 ID를 가진 메시지 대체 예시\n", 346 | "msgs1 = [HumanMessage(content=\"안녕하세요?\", id=\"1\")]\n", 347 | "msgs2 = [HumanMessage(content=\"반갑습니다~\", id=\"1\")]\n", 348 | "\n", 349 | "result2 = add_messages(msgs1, msgs2)\n", 350 | "print(result2)" 351 | ] 352 | } 353 | ], 354 | "metadata": { 355 | "kernelspec": { 356 | "display_name": "langchain-kr-lwwSZlnu-py3.11", 357 | "language": "python", 358 | "name": "python3" 359 | }, 360 | "language_info": { 361 | "codemirror_mode": { 362 | "name": "ipython", 363 | "version": 3 364 | }, 365 | "file_extension": ".py", 366 | "mimetype": "text/x-python", 367 | "name": "python", 368 | "nbconvert_exporter": "python", 369 | "pygments_lexer": "ipython3", 370 | "version": "3.11.9" 371 | } 372 | }, 373 | "nbformat": 4, 374 | "nbformat_minor": 2 375 | } 376 | -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/10-LangGraph-ToolNode.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "2b573557", 6 | "metadata": {}, 7 | "source": [ 8 | "# ToolNode 를 사용하여 도구를 호출하는 방법\n", 9 | "\n", 10 | "이번 튜토리얼에서는 도구 호출을 위한 LangGraph의 사전 구축된 `pre-built`의 `ToolNode` 사용 방법을 다룹니다.\n", 11 | "\n", 12 | "`ToolNode`는 메시지 목록이 포함된 그래프 상태를 입력으로 받아 도구 호출 결과로 상태를 업데이트하는 LangChain Runnable입니다. \n", 13 | "\n", 14 | "이는 LangGraph의 사전 구축된 Agent 와 즉시 사용할 수 있도록 설계되었으며, 상태에 적절한 리듀서가 있는 `messages` 키가 포함된 경우 모든 `StateGraph` 와 함께 작동할 수 있습니다." 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "id": "020ce856", 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "# API 키를 환경변수로 관리하기 위한 설정 파일\n", 25 | "from dotenv import load_dotenv\n", 26 | "\n", 27 | "# API 키 정보 로드\n", 28 | "load_dotenv()" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "id": "7d1bb130", 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n", 39 | "# !pip install -qU langchain-teddynote\n", 40 | "from langchain_teddynote import logging\n", 41 | "\n", 42 | "# 프로젝트 이름을 입력합니다.\n", 43 | "logging.langsmith(\"CH17-LangGraph-Modules\")" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "id": "bcd34edb", 49 | "metadata": {}, 50 | "source": [ 51 | "## 도구 정의\n", 52 | "\n", 53 | "먼저, 도구를 정의해보겠습니다." 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 3, 59 | "id": "37f73a88", 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "from langchain_core.tools import tool\n", 64 | "from langchain_experimental.tools.python.tool import PythonAstREPLTool\n", 65 | "from langchain_teddynote.tools import GoogleNews\n", 66 | "from typing import List, Dict\n", 67 | "\n", 68 | "\n", 69 | "# 도구 생성\n", 70 | "@tool\n", 71 | "def search_news(query: str) -> List[Dict[str, str]]:\n", 72 | " \"\"\"Search Google News by input keyword\"\"\"\n", 73 | " news_tool = GoogleNews()\n", 74 | " return news_tool.search_by_keyword(query, k=5)\n", 75 | "\n", 76 | "\n", 77 | "@tool\n", 78 | "def python_code_interpreter(code: str):\n", 79 | " \"\"\"Call to execute python code.\"\"\"\n", 80 | " return PythonAstREPLTool().invoke(code)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "id": "fd7fa5dc", 86 | "metadata": {}, 87 | "source": [ 88 | "다음으로는 `ToolNode` 를 사용하여 도구를 호출하는 방법을 살펴보겠습니다." 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 4, 94 | "id": "22f527a6", 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "from langgraph.prebuilt import ToolNode, tools_condition\n", 99 | "\n", 100 | "# 도구 리스트 생성\n", 101 | "tools = [search_news, python_code_interpreter]\n", 102 | "\n", 103 | "# ToolNode 초기화\n", 104 | "tool_node = ToolNode(tools)" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "id": "94a366e9", 110 | "metadata": {}, 111 | "source": [ 112 | "## `ToolNode`를 수동으로 호출하기" 113 | ] 114 | }, 115 | { 116 | "cell_type": "markdown", 117 | "id": "98f9c1f4", 118 | "metadata": {}, 119 | "source": [ 120 | "`ToolNode`는 메시지 목록과 함께 그래프 상태에서 작동합니다. \n", 121 | "\n", 122 | "- **중요**: 이때 목록의 마지막 메시지는 `tool_calls` 속성을 포함하는 `AIMessage`여야 합니다.\n", 123 | "\n", 124 | "먼저 도구 노드를 수동으로 호출하는 방법을 살펴보겠습니다." 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "id": "a69ac316", 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "from langchain_core.messages import AIMessage\n", 135 | "\n", 136 | "# 단일 도구 호출을 포함하는 AI 메시지 객체 생성\n", 137 | "# AIMessage 객체이어야 함\n", 138 | "message_with_single_tool_call = AIMessage(\n", 139 | " content=\"\",\n", 140 | " tool_calls=[\n", 141 | " {\n", 142 | " \"name\": \"search_news\", # 도구 이름\n", 143 | " \"args\": {\"query\": \"AI\"}, # 도구 인자\n", 144 | " \"id\": \"tool_call_id\", # 도구 호출 ID\n", 145 | " \"type\": \"tool_call\", # 도구 호출 유형\n", 146 | " }\n", 147 | " ],\n", 148 | ")\n", 149 | "\n", 150 | "# 도구 노드를 통한 메시지 처리 및 날씨 정보 요청 실행\n", 151 | "tool_node.invoke({\"messages\": [message_with_single_tool_call]})" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "id": "1483c8cb", 157 | "metadata": {}, 158 | "source": [ 159 | "일반적으로 `AIMessage`를 수동으로 생성할 필요가 없으며, 도구 호출을 지원하는 모든 LangChain 채팅 모델에서 자동으로 생성됩니다.\n", 160 | "\n", 161 | "또한 `AIMessage`의 `tool_calls` 매개변수에 여러 도구 호출을 전달하면 `ToolNode`를 사용하여 병렬 도구 호출을 수행할 수 있습니다." 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": null, 167 | "id": "782927c0", 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [ 171 | "# 다중 도구 호출을 포함하는 AI 메시지 객체 생성 및 초기화\n", 172 | "message_with_multiple_tool_calls = AIMessage(\n", 173 | " content=\"\",\n", 174 | " tool_calls=[\n", 175 | " {\n", 176 | " \"name\": \"search_news\",\n", 177 | " \"args\": {\"query\": \"AI\"},\n", 178 | " \"id\": \"tool_call_id\",\n", 179 | " \"type\": \"tool_call\",\n", 180 | " },\n", 181 | " {\n", 182 | " \"name\": \"python_code_interpreter\",\n", 183 | " \"args\": {\"code\": \"print(1+2+3+4)\"},\n", 184 | " \"id\": \"tool_call_id\",\n", 185 | " \"type\": \"tool_call\",\n", 186 | " },\n", 187 | " ],\n", 188 | ")\n", 189 | "\n", 190 | "# 생성된 메시지를 도구 노드에 전달하여 다중 도구 호출 실행\n", 191 | "tool_node.invoke({\"messages\": [message_with_multiple_tool_calls]})" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "id": "2367a18b", 197 | "metadata": {}, 198 | "source": [ 199 | "## llm 과 함께 사용하기" 200 | ] 201 | }, 202 | { 203 | "cell_type": "markdown", 204 | "id": "d9da378f", 205 | "metadata": {}, 206 | "source": [ 207 | "도구 호출 기능이 있는 채팅 모델을 사용하기 위해서는 먼저 모델이 사용 가능한 도구들을 인식하도록 해야 합니다. \n", 208 | "\n", 209 | "이는 `ChatOpenAI` 모델에서 `.bind_tools` 메서드를 호출하여 수행합니다." 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 7, 215 | "id": "0af74c1d", 216 | "metadata": {}, 217 | "outputs": [], 218 | "source": [ 219 | "from langchain_openai import ChatOpenAI\n", 220 | "\n", 221 | "# LLM 모델 초기화 및 도구 바인딩\n", 222 | "model_with_tools = ChatOpenAI(model=\"gpt-4.1-nano\", temperature=0).bind_tools(tools)" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "id": "e407303f", 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "# 도구 호출 확인\n", 233 | "model_with_tools.invoke(\"처음 5개의 소수를 출력하는 python code 를 작성해줘\").tool_calls" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "id": "7e8fb9c4", 239 | "metadata": {}, 240 | "source": [ 241 | "보시다시피 채팅 모델이 생성한 AI 메시지에는 이미 `tool_calls`가 채워져 있으므로, 이를 `ToolNode`에 직접 전달할 수 있습니다." 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": null, 247 | "id": "f0bc2619", 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [ 251 | "# 도구 노드를 통한 메시지 처리 및 LLM 모델의 도구 기반 응답 생성\n", 252 | "tool_node.invoke(\n", 253 | " {\n", 254 | " \"messages\": [\n", 255 | " model_with_tools.invoke(\n", 256 | " \"처음 5개의 소수를 출력하는 python code 를 작성해줘\"\n", 257 | " )\n", 258 | " ]\n", 259 | " }\n", 260 | ")" 261 | ] 262 | }, 263 | { 264 | "cell_type": "markdown", 265 | "id": "d44807e0", 266 | "metadata": {}, 267 | "source": [ 268 | "## Agent 와 함께 사용하기" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "id": "45e1dc47", 274 | "metadata": {}, 275 | "source": [ 276 | "다음으로, LangGraph 그래프 내에서 `ToolNode`를 사용하는 방법을 살펴보겠습니다. \n", 277 | "\n", 278 | "Agent 의 그래프 구현을 설정해보겠습니다. 이 **Agent** 는 쿼리를 입력으로 받아, 쿼리를 해결하는 데 필요한 충분한 정보를 얻을 때까지 반복적으로 도구들을 호출합니다. \n", 279 | "\n", 280 | "방금 정의한 도구들과 함께 `ToolNode` 및 OpenAI 모델을 사용하게 됩니다." 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 10, 286 | "id": "183d54cd", 287 | "metadata": {}, 288 | "outputs": [], 289 | "source": [ 290 | "# LangGraph 워크플로우 상태 및 메시지 처리를 위한 타입 임포트\n", 291 | "from langgraph.graph import StateGraph, MessagesState, START, END\n", 292 | "\n", 293 | "\n", 294 | "# LLM 모델을 사용하여 메시지 처리 및 응답 생성, 도구 호출이 포함된 응답 반환\n", 295 | "def call_model(state: MessagesState):\n", 296 | " messages = state[\"messages\"]\n", 297 | " response = model_with_tools.invoke(messages)\n", 298 | " return {\"messages\": [response]}\n", 299 | "\n", 300 | "\n", 301 | "# 메시지 상태 기반 워크플로우 그래프 초기화\n", 302 | "workflow = StateGraph(MessagesState)\n", 303 | "\n", 304 | "# 에이전트와 도구 노드 정의 및 워크플로우 그래프에 추가\n", 305 | "workflow.add_node(\"agent\", call_model)\n", 306 | "workflow.add_node(\"tools\", tool_node)\n", 307 | "\n", 308 | "# 워크플로우 시작점에서 에이전트 노드로 연결\n", 309 | "workflow.add_edge(START, \"agent\")\n", 310 | "\n", 311 | "# 에이전트 노드에서 조건부 분기 설정, 도구 노드 또는 종료 지점으로 연결\n", 312 | "workflow.add_conditional_edges(\"agent\", tools_condition)\n", 313 | "\n", 314 | "# 도구 노드에서 에이전트 노드로 순환 연결\n", 315 | "workflow.add_edge(\"tools\", \"agent\")\n", 316 | "\n", 317 | "# 에이전트 노드에서 종료 지점으로 연결\n", 318 | "workflow.add_edge(\"agent\", END)\n", 319 | "\n", 320 | "\n", 321 | "# 정의된 워크플로우 그래프 컴파일 및 실행 가능한 애플리케이션 생성\n", 322 | "app = workflow.compile()" 323 | ] 324 | }, 325 | { 326 | "cell_type": "code", 327 | "execution_count": null, 328 | "id": "e8674ba2", 329 | "metadata": {}, 330 | "outputs": [], 331 | "source": [ 332 | "from langchain_teddynote.graphs import visualize_graph\n", 333 | "\n", 334 | "visualize_graph(app)" 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "id": "603778f5", 340 | "metadata": {}, 341 | "source": [ 342 | "실행하여 결과를 확인해보겠습니다." 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": null, 348 | "id": "104486d7", 349 | "metadata": {}, 350 | "outputs": [], 351 | "source": [ 352 | "# 실행 및 결과 확인\n", 353 | "for chunk in app.stream(\n", 354 | " {\"messages\": [(\"human\", \"처음 5개의 소수를 출력하는 python code 를 작성해줘\")]},\n", 355 | " stream_mode=\"values\",\n", 356 | "):\n", 357 | " # 마지막 메시지 출력\n", 358 | " chunk[\"messages\"][-1].pretty_print()" 359 | ] 360 | }, 361 | { 362 | "cell_type": "code", 363 | "execution_count": null, 364 | "id": "624c4f1a", 365 | "metadata": {}, 366 | "outputs": [], 367 | "source": [ 368 | "# 검색 질문 수행\n", 369 | "for chunk in app.stream(\n", 370 | " {\"messages\": [(\"human\", \"search google news about AI\")]},\n", 371 | " stream_mode=\"values\",\n", 372 | "):\n", 373 | " chunk[\"messages\"][-1].pretty_print()" 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "id": "af9323d2", 380 | "metadata": {}, 381 | "outputs": [], 382 | "source": [ 383 | "# 도구 호출이 필요 없는 질문 수행\n", 384 | "for chunk in app.stream(\n", 385 | " {\"messages\": [(\"human\", \"안녕? 반가워\")]},\n", 386 | " stream_mode=\"values\",\n", 387 | "):\n", 388 | " chunk[\"messages\"][-1].pretty_print()" 389 | ] 390 | }, 391 | { 392 | "cell_type": "markdown", 393 | "id": "082f0d6c", 394 | "metadata": {}, 395 | "source": [ 396 | "`ToolNode`는 도구 실행 중 발생하는 오류도 처리할 수 있습니다. \n", 397 | "\n", 398 | "`handle_tool_errors=True`를 설정하여 이 기능을 활성화/비활성화할 수 있습니다(기본적으로 활성화되어 있음)" 399 | ] 400 | } 401 | ], 402 | "metadata": { 403 | "kernelspec": { 404 | "display_name": "langchain-kr-lwwSZlnu-py3.11", 405 | "language": "python", 406 | "name": "python3" 407 | }, 408 | "language_info": { 409 | "codemirror_mode": { 410 | "name": "ipython", 411 | "version": 3 412 | }, 413 | "file_extension": ".py", 414 | "mimetype": "text/x-python", 415 | "name": "python", 416 | "nbconvert_exporter": "python", 417 | "pygments_lexer": "ipython3", 418 | "version": "3.11.9" 419 | } 420 | }, 421 | "nbformat": 4, 422 | "nbformat_minor": 5 423 | } 424 | -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/06-LangGraph-Human-In-the-Loop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "eec680f5", 6 | "metadata": {}, 7 | "source": [ 8 | "# Human-in-the-loop\n", 9 | "\n", 10 | "에이전트는 신뢰할 수 없으며 작업을 성공적으로 수행하기 위해 인간의 입력이 필요할 수 있습니다. \n", 11 | "\n", 12 | "마찬가지로, 일부 작업에 대해서는 모든 것이 의도한 대로 실행되고 있는지 확인하기 위해 실행 전에 **사람이 직접 개입하여 \"승인\"** 을 요구하고 싶을 수 있습니다.\n", 13 | "\n", 14 | "LangGraph는 여러 가지 방법으로 `human-in-the-loop` 워크플로를 지원합니다. \n", 15 | "\n", 16 | "이번 튜토리얼의 시작은 LangGraph의 `interrupt_before` 기능을 사용하여 항상 도구 노드를 중단하도록 하겠습니다." 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "id": "de9d9d8d", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# API 키를 환경변수로 관리하기 위한 설정 파일\n", 27 | "from dotenv import load_dotenv\n", 28 | "\n", 29 | "# API 키 정보 로드\n", 30 | "load_dotenv()" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "id": "6b5c6228", 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n", 41 | "# !pip install -qU langchain-teddynote\n", 42 | "from langchain_teddynote import logging\n", 43 | "\n", 44 | "# 프로젝트 이름을 입력합니다.\n", 45 | "logging.langsmith(\"CH17-LangGraph-Modules\")" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 3, 51 | "id": "a6bc201a", 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "from typing import Annotated, List, Dict\n", 56 | "from typing_extensions import TypedDict\n", 57 | "\n", 58 | "from langchain_core.tools import tool\n", 59 | "from langchain_openai import ChatOpenAI\n", 60 | "from langgraph.checkpoint.memory import MemorySaver\n", 61 | "from langgraph.graph import StateGraph, START, END\n", 62 | "from langgraph.graph.message import add_messages\n", 63 | "from langgraph.prebuilt import ToolNode, tools_condition\n", 64 | "from langchain_teddynote.graphs import visualize_graph\n", 65 | "from langchain_teddynote.tools import GoogleNews\n", 66 | "\n", 67 | "\n", 68 | "########## 1. 상태 정의 ##########\n", 69 | "# 상태 정의\n", 70 | "class State(TypedDict):\n", 71 | " # 메시지 목록 주석 추가\n", 72 | " messages: Annotated[list, add_messages]\n", 73 | "\n", 74 | "\n", 75 | "########## 2. 도구 정의 및 바인딩 ##########\n", 76 | "# 도구 초기화\n", 77 | "# 키워드로 뉴스 검색하는 도구 생성\n", 78 | "news_tool = GoogleNews()\n", 79 | "\n", 80 | "\n", 81 | "@tool\n", 82 | "def search_keyword(query: str) -> List[Dict[str, str]]:\n", 83 | " \"\"\"Look up news by keyword\"\"\"\n", 84 | " news_tool = GoogleNews()\n", 85 | " return news_tool.search_by_keyword(query, k=5)\n", 86 | "\n", 87 | "\n", 88 | "tools = [search_keyword]\n", 89 | "\n", 90 | "# LLM 초기화\n", 91 | "llm = ChatOpenAI(model=\"gpt-4.1-nano\")\n", 92 | "\n", 93 | "# 도구와 LLM 결합\n", 94 | "llm_with_tools = llm.bind_tools(tools)\n", 95 | "\n", 96 | "\n", 97 | "########## 3. 노드 추가 ##########\n", 98 | "# 챗봇 함수 정의\n", 99 | "def chatbot(state: State):\n", 100 | " # 메시지 호출 및 반환\n", 101 | " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", 102 | "\n", 103 | "\n", 104 | "# 상태 그래프 생성\n", 105 | "graph_builder = StateGraph(State)\n", 106 | "\n", 107 | "# 챗봇 노드 추가\n", 108 | "graph_builder.add_node(\"chatbot\", chatbot)\n", 109 | "\n", 110 | "\n", 111 | "# 도구 노드 생성 및 추가\n", 112 | "tool_node = ToolNode(tools=tools)\n", 113 | "\n", 114 | "# 도구 노드 추가\n", 115 | "graph_builder.add_node(\"tools\", tool_node)\n", 116 | "\n", 117 | "# 조건부 엣지\n", 118 | "graph_builder.add_conditional_edges(\n", 119 | " \"chatbot\",\n", 120 | " tools_condition,\n", 121 | ")\n", 122 | "\n", 123 | "########## 4. 엣지 추가 ##########\n", 124 | "\n", 125 | "# tools > chatbot\n", 126 | "graph_builder.add_edge(\"tools\", \"chatbot\")\n", 127 | "\n", 128 | "# START > chatbot\n", 129 | "graph_builder.add_edge(START, \"chatbot\")\n", 130 | "\n", 131 | "# chatbot > END\n", 132 | "graph_builder.add_edge(\"chatbot\", END)\n", 133 | "\n", 134 | "########## 5. MemorySaver 추가 ##########\n", 135 | "\n", 136 | "# 메모리 저장소 초기화\n", 137 | "memory = MemorySaver()" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "id": "c8aa1673", 143 | "metadata": {}, 144 | "source": [ 145 | "이제 그래프를 컴파일하고, `tools` 노드 전에 `interrupt_before`를 지정하십시오." 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 4, 151 | "id": "60cfaf0d", 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [ 155 | "########## 6. interrupt_before 추가 ##########\n", 156 | "\n", 157 | "# 그래프 빌더 컴파일\n", 158 | "graph = graph_builder.compile(checkpointer=memory)" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "id": "20e87724", 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "########## 7. 그래프 시각화 ##########\n", 169 | "# 그래프 시각화\n", 170 | "visualize_graph(graph)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "id": "b26d4039", 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "from langchain_teddynote.messages import pretty_print_messages\n", 181 | "from langchain_core.runnables import RunnableConfig\n", 182 | "\n", 183 | "# 질문\n", 184 | "question = \"AI 관련 최신 뉴스를 알려주세요.\"\n", 185 | "\n", 186 | "# 초기 입력 State 를 정의\n", 187 | "input = State(messages=[(\"user\", question)])\n", 188 | "\n", 189 | "# config 설정\n", 190 | "config = RunnableConfig(\n", 191 | " recursion_limit=10, # 최대 10개의 노드까지 방문. 그 이상은 RecursionError 발생\n", 192 | " configurable={\"thread_id\": \"1\"}, # 스레드 ID 설정\n", 193 | " tags=[\"my-rag\"], # Tag\n", 194 | ")\n", 195 | "\n", 196 | "for event in graph.stream(\n", 197 | " input=input,\n", 198 | " config=config,\n", 199 | " stream_mode=\"values\",\n", 200 | " interrupt_before=[\"tools\"], # tools 실행 전 interrupt(tools 노드 실행 전 중단)\n", 201 | "):\n", 202 | " for key, value in event.items():\n", 203 | " # key 는 노드 이름\n", 204 | " print(f\"\\n[{key}]\\n\")\n", 205 | "\n", 206 | " # value 는 노드의 출력값\n", 207 | " # print(value)\n", 208 | " pretty_print_messages(value)\n", 209 | "\n", 210 | " # value 에는 state 가 dict 형태로 저장(values 의 key 값)\n", 211 | " if \"messages\" in value:\n", 212 | " print(f\"메시지 개수: {len(value['messages'])}\")" 213 | ] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "id": "889d388e", 218 | "metadata": {}, 219 | "source": [ 220 | "그래프 상태를 확인하여 제대로 작동했는지 확인해 봅시다." 221 | ] 222 | }, 223 | { 224 | "cell_type": "code", 225 | "execution_count": null, 226 | "id": "ebcdde46", 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [ 230 | "# 그래프 상태 스냅샷 생성\n", 231 | "snapshot = graph.get_state(config)\n", 232 | "\n", 233 | "# 다음 스냅샷 상태\n", 234 | "snapshot.next" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "id": "b80ebad2", 240 | "metadata": {}, 241 | "source": [ 242 | "(이전의 튜토리얼에서는) `__END__` 도달했기 때문에 `.next` 가 존재하지 않았습니다.\n", 243 | "\n", 244 | "하지만, 지금은 `.next` 가 `tools` 로 지정되어 있습니다.\n", 245 | "\n", 246 | "이제 그럼 도구 호출을 확인해 봅시다." 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": null, 252 | "id": "1570ed38", 253 | "metadata": {}, 254 | "outputs": [], 255 | "source": [ 256 | "from langchain_teddynote.messages import display_message_tree\n", 257 | "\n", 258 | "# 메시지 스냅샷에서 마지막 메시지 추출\n", 259 | "existing_message = snapshot.values[\"messages\"][-1]\n", 260 | "\n", 261 | "# 메시지 트리 표시\n", 262 | "display_message_tree(existing_message.tool_calls)" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "id": "b7062b94", 268 | "metadata": {}, 269 | "source": [ 270 | "다음으로는 이전에 종료된 지점 이후부터 **이어서 그래프를 진행** 해 보는 것입니다.\n", 271 | "\n", 272 | "LangGraph 는 계속 그래프를 진행하는 것 쉽게 할 수 있습니다.\n", 273 | "\n", 274 | "- 단지 입력에 `None`을 전달하면 됩니다." 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": null, 280 | "id": "8f48c339", 281 | "metadata": {}, 282 | "outputs": [], 283 | "source": [ 284 | "# `None`는 현재 상태에 아무것도 추가하지 않음\n", 285 | "events = graph.stream(None, config, stream_mode=\"values\")\n", 286 | "\n", 287 | "# 이벤트 반복 처리\n", 288 | "for event in events:\n", 289 | " # 메시지가 이벤트에 포함된 경우\n", 290 | " if \"messages\" in event:\n", 291 | " # 마지막 메시지의 예쁜 출력\n", 292 | " event[\"messages\"][-1].pretty_print()" 293 | ] 294 | }, 295 | { 296 | "cell_type": "markdown", 297 | "id": "8d655b90", 298 | "metadata": {}, 299 | "source": [ 300 | "이제, `interrupt`를 사용하여 챗봇에 인간이 개입할 수 있는 실행을 추가하여 필요할 때 인간의 감독과 개입을 가능하게 했습니다. 이는 추후에 시스템으로 구현할때, 잠재적인 UI를 제공할 수 있ㅅ브니다.\n", 301 | "\n", 302 | "이미 **checkpointer**를 추가했기 때문에, 그래프는 **무기한** 일시 중지되고 언제든지 다시 시작할 수 있습니다." 303 | ] 304 | }, 305 | { 306 | "cell_type": "markdown", 307 | "id": "76a3baa8", 308 | "metadata": {}, 309 | "source": [ 310 | "아래는 `get_state_history` 메서드를 사용하여 상태 기록을 가져오는 방법입니다.\n", 311 | "\n", 312 | "상태 기록을 통해 원하는 상태를 지정하여 **해당 지점에서 다시 시작** 할 수 있습니다." 313 | ] 314 | }, 315 | { 316 | "cell_type": "code", 317 | "execution_count": null, 318 | "id": "0b9d5d8d", 319 | "metadata": {}, 320 | "outputs": [], 321 | "source": [ 322 | "to_replay = None\n", 323 | "\n", 324 | "# 상태 기록 가져오기\n", 325 | "for state in graph.get_state_history(config):\n", 326 | " # 메시지 수 및 다음 상태 출력\n", 327 | " print(\"메시지 수: \", len(state.values[\"messages\"]), \"다음 노드: \", state.next)\n", 328 | " print(\"-\" * 80)\n", 329 | " # 특정 상태 선택 기준: 채팅 메시지 수\n", 330 | " if len(state.values[\"messages\"]) == 3:\n", 331 | " to_replay = state" 332 | ] 333 | }, 334 | { 335 | "cell_type": "markdown", 336 | "id": "ff8faca6", 337 | "metadata": {}, 338 | "source": [ 339 | "그래프의 모든 단계에 대해 체크포인트가 저장된다는 점에 **주목** 할 필요가 있습니다.\n", 340 | "\n", 341 | "원하는 지점은 `to_replay` 변수에 저장합니다. 이를 활용하여 다시 시작할 수 있는 지점을 지정할 수 있습니다." 342 | ] 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": null, 347 | "id": "6da2eeda", 348 | "metadata": {}, 349 | "outputs": [], 350 | "source": [ 351 | "# 다음 항목의 다음 요소 출력\n", 352 | "print(to_replay.next)\n", 353 | "\n", 354 | "# 다음 항목의 설정 정보 출력\n", 355 | "print(to_replay.config)" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "id": "cf90af22", 361 | "metadata": {}, 362 | "source": [ 363 | "`to_replay.config` 에 `checkpoint_id` 가 포함되어 있습니다." 364 | ] 365 | }, 366 | { 367 | "cell_type": "code", 368 | "execution_count": null, 369 | "id": "74548a50", 370 | "metadata": {}, 371 | "outputs": [], 372 | "source": [ 373 | "to_replay.config" 374 | ] 375 | }, 376 | { 377 | "cell_type": "markdown", 378 | "id": "81b261e1", 379 | "metadata": {}, 380 | "source": [ 381 | "이 `checkpoint_id` 값을 제공하면 LangGraph의 체크포인터가 그 시점의 상태를 **로드** 할 수 있습니다.\n", 382 | "\n", 383 | "- 단, 이때는 입력값을 `None`으로 전달해야 합니다.\n", 384 | "\n", 385 | "아래의 예제를 통해 확인해 봅시다." 386 | ] 387 | }, 388 | { 389 | "cell_type": "code", 390 | "execution_count": null, 391 | "id": "18f5474a", 392 | "metadata": {}, 393 | "outputs": [], 394 | "source": [ 395 | "# `to_replay.config`는 `checkpoint_id`는 체크포인터에 저장된 상태에 해당\n", 396 | "for event in graph.stream(None, to_replay.config, stream_mode=\"values\"):\n", 397 | " # 메시지가 이벤트에 포함된 경우\n", 398 | " if \"messages\" in event:\n", 399 | " # 마지막 메시지 출력\n", 400 | " event[\"messages\"][-1].pretty_print()" 401 | ] 402 | } 403 | ], 404 | "metadata": { 405 | "kernelspec": { 406 | "display_name": "langchain-kr-lwwSZlnu-py3.11", 407 | "language": "python", 408 | "name": "python3" 409 | }, 410 | "language_info": { 411 | "codemirror_mode": { 412 | "name": "ipython", 413 | "version": 3 414 | }, 415 | "file_extension": ".py", 416 | "mimetype": "text/x-python", 417 | "name": "python", 418 | "nbconvert_exporter": "python", 419 | "pygments_lexer": "ipython3", 420 | "version": "3.11.9" 421 | } 422 | }, 423 | "nbformat": 4, 424 | "nbformat_minor": 5 425 | } 426 | -------------------------------------------------------------------------------- /03-Modules/01-Core-Features/04-LangGraph-Agent-With-Memory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "eec680f5", 6 | "metadata": {}, 7 | "source": [ 8 | "# Agent 에 메모리(memory) 추가\n", 9 | "\n", 10 | "현재 챗봇은 과거 상호작용을 스스로 기억할 수 없어 일관된 다중 턴 대화를 진행하는 데 제한이 있습니다. \n", 11 | "\n", 12 | "이번 튜토리얼에서는 이를 해결하기 위해 **memory** 를 추가합니다.\n", 13 | "\n", 14 | "**참고**\n", 15 | "\n", 16 | "이번에는 pre-built 되어있는 `ToolNode` 와 `tools_condition` 을 활용합니다.\n", 17 | "\n", 18 | "1. [ToolNode](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.tool_node.ToolNode): 도구 호출을 위한 노드\n", 19 | "2. [tools_condition](https://langchain-ai.github.io/langgraph/reference/prebuilt/#langgraph.prebuilt.tool_node.tools_condition): 도구 호출 여부에 따른 조건 분기" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "id": "1b2184f4", 25 | "metadata": {}, 26 | "source": [ 27 | "\n", 28 | "우리의 챗봇은 이제 도구를 사용하여 사용자 질문에 답할 수 있지만, 이전 상호작용의 **context**를 기억하지 못합니다. 이는 멀티턴(multi-turn) 대화를 진행하는 능력을 제한합니다.\n", 29 | "\n", 30 | "`LangGraph`는 **persistent checkpointing** 을 통해 이 문제를 해결합니다. \n", 31 | "\n", 32 | "그래프를 컴파일할 때 `checkpointer`를 제공하고 그래프를 호출할 때 `thread_id`를 제공하면, `LangGraph`는 각 단계 후 **상태를 자동으로 저장** 합니다. 동일한 `thread_id`를 사용하여 그래프를 다시 호출하면, 그래프는 저장된 상태를 로드하여 챗봇이 이전에 중단한 지점에서 대화를 이어갈 수 있게 합니다.\n", 33 | "\n", 34 | "**checkpointing** 는 LangChain 의 메모리 기능보다 훨씬 강력합니다. (아마 이 튜토리얼을 완수하면 자연스럽게 이를 확인할 수 있습니다.)" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "id": "de9d9d8d", 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "# API 키를 환경변수로 관리하기 위한 설정 파일\n", 45 | "from dotenv import load_dotenv\n", 46 | "\n", 47 | "# API 키 정보 로드\n", 48 | "load_dotenv()" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "id": "6b5c6228", 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n", 59 | "# !pip install -qU langchain-teddynote\n", 60 | "from langchain_teddynote import logging\n", 61 | "\n", 62 | "# 프로젝트 이름을 입력합니다.\n", 63 | "logging.langsmith(\"CH17-LangGraph-Modules\")" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "id": "5d2e7318", 69 | "metadata": {}, 70 | "source": [ 71 | "하지만 너무 앞서 나가기 전에, 멀티턴(multi-turn) 대화를 가능하게 하기 위해 **checkpointing**을 추가해 보도록 하겠습니다.\n", 72 | "\n", 73 | "`MemorySaver` checkpointer를 생성합니다." 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 3, 79 | "id": "53d80de1", 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "from langgraph.checkpoint.memory import MemorySaver\n", 84 | "\n", 85 | "# 메모리 저장소 생성\n", 86 | "memory = MemorySaver()" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "id": "6fc89fcf", 92 | "metadata": {}, 93 | "source": [ 94 | "**참고**\n", 95 | "\n", 96 | "이번 튜토리얼에서는 `in-memory checkpointer` 를 사용합니다. \n", 97 | "\n", 98 | "하지만, 프로덕션 단계에서는 이를 `SqliteSaver` 또는 `PostgresSaver` 로 변경하고 자체 DB에 연결할 수 있습니다. " 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "id": "51549b1d", 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "from typing import Annotated\n", 109 | "from typing_extensions import TypedDict\n", 110 | "from langchain_openai import ChatOpenAI\n", 111 | "from langchain_teddynote.tools.tavily import TavilySearch\n", 112 | "from langgraph.graph import StateGraph, START, END\n", 113 | "from langgraph.graph.message import add_messages\n", 114 | "from langgraph.prebuilt import ToolNode, tools_condition\n", 115 | "\n", 116 | "\n", 117 | "########## 1. 상태 정의 ##########\n", 118 | "# 상태 정의\n", 119 | "class State(TypedDict):\n", 120 | " # 메시지 목록 주석 추가\n", 121 | " messages: Annotated[list, add_messages]\n", 122 | "\n", 123 | "\n", 124 | "########## 2. 도구 정의 및 바인딩 ##########\n", 125 | "# 도구 초기화\n", 126 | "tool = TavilySearch(max_results=3)\n", 127 | "tools = [tool]\n", 128 | "\n", 129 | "# LLM 초기화\n", 130 | "llm = ChatOpenAI(model=\"gpt-4.1-nano\")\n", 131 | "\n", 132 | "# 도구와 LLM 결합\n", 133 | "llm_with_tools = llm.bind_tools(tools)\n", 134 | "\n", 135 | "\n", 136 | "########## 3. 노드 추가 ##########\n", 137 | "# 챗봇 함수 정의\n", 138 | "def chatbot(state: State):\n", 139 | " # 메시지 호출 및 반환\n", 140 | " return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n", 141 | "\n", 142 | "\n", 143 | "# 상태 그래프 생성\n", 144 | "graph_builder = StateGraph(State)\n", 145 | "\n", 146 | "# 챗봇 노드 추가\n", 147 | "graph_builder.add_node(\"chatbot\", chatbot)\n", 148 | "\n", 149 | "# 도구 노드 생성 및 추가\n", 150 | "tool_node = ToolNode(tools=[tool])\n", 151 | "\n", 152 | "# 도구 노드 추가\n", 153 | "graph_builder.add_node(\"tools\", tool_node)\n", 154 | "\n", 155 | "# 조건부 엣지\n", 156 | "graph_builder.add_conditional_edges(\n", 157 | " \"chatbot\",\n", 158 | " tools_condition,\n", 159 | ")\n", 160 | "\n", 161 | "########## 4. 엣지 추가 ##########\n", 162 | "\n", 163 | "# tools > chatbot\n", 164 | "graph_builder.add_edge(\"tools\", \"chatbot\")\n", 165 | "\n", 166 | "# START > chatbot\n", 167 | "graph_builder.add_edge(START, \"chatbot\")\n", 168 | "\n", 169 | "# chatbot > END\n", 170 | "graph_builder.add_edge(\"chatbot\", END)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "id": "c762780f", 176 | "metadata": {}, 177 | "source": [ 178 | "마지막으로, 제공된 `checkpointer`를 사용하여 그래프를 컴파일합니다." 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 5, 184 | "id": "3d4ff857", 185 | "metadata": {}, 186 | "outputs": [], 187 | "source": [ 188 | "# 그래프 빌더 컴파일\n", 189 | "graph = graph_builder.compile(checkpointer=memory)" 190 | ] 191 | }, 192 | { 193 | "cell_type": "markdown", 194 | "id": "0ffe3565", 195 | "metadata": {}, 196 | "source": [ 197 | "그래프의 연결성은 `LangGraph-Agent` 와 동일합니다.\n", 198 | "\n", 199 | "단지, 이번에 추가된 것은 그래프가 각 노드를 처리하면서 `State`를 체크포인트하는 것뿐입니다." 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": null, 205 | "id": "5622b194", 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "from langchain_teddynote.graphs import visualize_graph\n", 210 | "\n", 211 | "# 그래프 시각화\n", 212 | "visualize_graph(graph)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "markdown", 217 | "id": "0e32dbc5", 218 | "metadata": {}, 219 | "source": [ 220 | "## RunnableConfig 설정\n", 221 | "\n", 222 | "`RunnableConfig` 을 정의하고 `recursion_limit` 과 `thread_id` 를 설정합니다.\n", 223 | "\n", 224 | "- `recursion_limit`: 최대 방문할 노드 수. 그 이상은 RecursionError 발생\n", 225 | "- `thread_id`: 스레드 ID 설정\n", 226 | "\n", 227 | "`thread_id` 는 대화 세션을 구분하는 데 사용됩니다. 즉, 메모리의 저장은 `thread_id` 에 따라 개별적으로 이루어집니다." 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 7, 233 | "id": "2fea0653", 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "from langchain_core.runnables import RunnableConfig\n", 238 | "\n", 239 | "config = RunnableConfig(\n", 240 | " recursion_limit=10, # 최대 10개의 노드까지 방문. 그 이상은 RecursionError 발생\n", 241 | " configurable={\"thread_id\": \"1\"}, # 스레드 ID 설정\n", 242 | ")" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "id": "ab5c0bad", 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "# 첫 질문\n", 253 | "question = (\n", 254 | " \"내 이름은 `테디노트` 입니다. YouTube 채널을 운영하고 있어요. 만나서 반가워요\"\n", 255 | ")\n", 256 | "\n", 257 | "for event in graph.stream({\"messages\": [(\"user\", question)]}, config=config):\n", 258 | " for value in event.values():\n", 259 | " value[\"messages\"][-1].pretty_print()" 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "execution_count": null, 265 | "id": "4bac57c9", 266 | "metadata": {}, 267 | "outputs": [], 268 | "source": [ 269 | "# 이어지는 질문\n", 270 | "question = \"내 이름이 뭐라고 했지?\"\n", 271 | "\n", 272 | "for event in graph.stream({\"messages\": [(\"user\", question)]}, config=config):\n", 273 | " for value in event.values():\n", 274 | " value[\"messages\"][-1].pretty_print()" 275 | ] 276 | }, 277 | { 278 | "cell_type": "markdown", 279 | "id": "574988ab", 280 | "metadata": {}, 281 | "source": [ 282 | "이번에는 `RunnableConfig` 의 `thread_id` 를 변경한 뒤, 이전 대화 내용을 기억하고 있는지 물어보겠습니다." 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "id": "68ce68f9", 289 | "metadata": {}, 290 | "outputs": [], 291 | "source": [ 292 | "from langchain_core.runnables import RunnableConfig\n", 293 | "\n", 294 | "question = \"내 이름이 뭐라고 했지?\"\n", 295 | "\n", 296 | "config = RunnableConfig(\n", 297 | " recursion_limit=10, # 최대 10개의 노드까지 방문. 그 이상은 RecursionError 발생\n", 298 | " configurable={\"thread_id\": \"2\"}, # 스레드 ID 설정\n", 299 | ")\n", 300 | "\n", 301 | "for event in graph.stream({\"messages\": [(\"user\", question)]}, config=config):\n", 302 | " for value in event.values():\n", 303 | " value[\"messages\"][-1].pretty_print()" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "id": "857821c4", 309 | "metadata": {}, 310 | "source": [ 311 | "## 스냅샷: 저장된 State 확인\n", 312 | "\n", 313 | "지금까지 두 개의 다른 스레드에서 몇 개의 체크포인트를 만들었습니다. \n", 314 | "\n", 315 | "`Checkpoint` 에는 현재 상태 값, 해당 구성, 그리고 처리할 `next` 노드가 포함되어 있습니다.\n", 316 | "\n", 317 | "주어진 설정에서 그래프의 `state`를 검사하려면 언제든지 `get_state(config)`를 호출하세요." 318 | ] 319 | }, 320 | { 321 | "cell_type": "code", 322 | "execution_count": null, 323 | "id": "16d9c636", 324 | "metadata": {}, 325 | "outputs": [], 326 | "source": [ 327 | "from langchain_core.runnables import RunnableConfig\n", 328 | "\n", 329 | "config = RunnableConfig(\n", 330 | " configurable={\"thread_id\": \"1\"}, # 스레드 ID 설정\n", 331 | ")\n", 332 | "# 그래프 상태 스냅샷 생성\n", 333 | "snapshot = graph.get_state(config)\n", 334 | "snapshot.values[\"messages\"]" 335 | ] 336 | }, 337 | { 338 | "cell_type": "markdown", 339 | "id": "5ef39bd5", 340 | "metadata": {}, 341 | "source": [ 342 | "`snapshot.config` 를 출력하게 설정된 config 정보를 확인할 수 있습니다." 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": null, 348 | "id": "a203fa62", 349 | "metadata": {}, 350 | "outputs": [], 351 | "source": [ 352 | "# 설정된 config 정보\n", 353 | "snapshot.config" 354 | ] 355 | }, 356 | { 357 | "cell_type": "markdown", 358 | "id": "4917d010", 359 | "metadata": {}, 360 | "source": [ 361 | "`snapshot.value` 를 출력하게 지금까지 저장된 state 값을 확인할 수 있습니다." 362 | ] 363 | }, 364 | { 365 | "cell_type": "code", 366 | "execution_count": null, 367 | "id": "683a655b", 368 | "metadata": {}, 369 | "outputs": [], 370 | "source": [ 371 | "# 저장된 값(values)\n", 372 | "snapshot.values" 373 | ] 374 | }, 375 | { 376 | "cell_type": "markdown", 377 | "id": "6223078c", 378 | "metadata": {}, 379 | "source": [ 380 | "`snapshot.next` 를 출력하여 현재 시점에서 앞으로 찾아갈 **다음 노드를 확인** 할 수 있습니다.\n", 381 | "\n", 382 | "__END__ 에 도달하였기 때문에 다음 노드는 빈 값이 출력됩니다." 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": null, 388 | "id": "c38af74d", 389 | "metadata": {}, 390 | "outputs": [], 391 | "source": [ 392 | "# 다음 노드\n", 393 | "snapshot.next" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": null, 399 | "id": "5576e3e7", 400 | "metadata": {}, 401 | "outputs": [], 402 | "source": [ 403 | "snapshot.metadata[\"writes\"][\"chatbot\"][\"messages\"][0]" 404 | ] 405 | }, 406 | { 407 | "cell_type": "markdown", 408 | "id": "a040e572", 409 | "metadata": {}, 410 | "source": [ 411 | "복잡한 구조의 metadata 를 시각화하기 위해 `display_message_tree` 함수를 사용합니다." 412 | ] 413 | }, 414 | { 415 | "cell_type": "code", 416 | "execution_count": null, 417 | "id": "fe2dbdcb", 418 | "metadata": {}, 419 | "outputs": [], 420 | "source": [ 421 | "from langchain_teddynote.messages import display_message_tree\n", 422 | "\n", 423 | "# 메타데이터(tree 형태로 출력)\n", 424 | "display_message_tree(snapshot.metadata)" 425 | ] 426 | } 427 | ], 428 | "metadata": { 429 | "kernelspec": { 430 | "display_name": "langchain-kr-lwwSZlnu-py3.11", 431 | "language": "python", 432 | "name": "python3" 433 | }, 434 | "language_info": { 435 | "codemirror_mode": { 436 | "name": "ipython", 437 | "version": 3 438 | }, 439 | "file_extension": ".py", 440 | "mimetype": "text/x-python", 441 | "name": "python", 442 | "nbconvert_exporter": "python", 443 | "pygments_lexer": "ipython3", 444 | "version": "3.11.9" 445 | } 446 | }, 447 | "nbformat": 4, 448 | "nbformat_minor": 5 449 | } 450 | --------------------------------------------------------------------------------