├── 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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | [](https://pypi.org/project/langgraph/)
17 | [](https://pepy.tech/project/langgraph)
18 | [](https://github.com/langchain-ai/langgraph/issues)
19 | [](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 | ""
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 | ""
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 |
--------------------------------------------------------------------------------