From 998c1d774e0399b7711a8fd8c7d69dbcd335be74 Mon Sep 17 00:00:00 2001 From: Hank Kim Date: Fri, 23 Jan 2026 15:36:31 +0900 Subject: [PATCH] Update [03-Agent] 04-LangGraph-Runtime tutorial --- 03-Agent/04-LangGraph-Runtime.ipynb | 552 +++++++++++++++++++++------- 1 file changed, 415 insertions(+), 137 deletions(-) diff --git a/03-Agent/04-LangGraph-Runtime.ipynb b/03-Agent/04-LangGraph-Runtime.ipynb index 7f62e93..feee466 100644 --- a/03-Agent/04-LangGraph-Runtime.ipynb +++ b/03-Agent/04-LangGraph-Runtime.ipynb @@ -3,107 +3,107 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Runtime\n\nLangChain의 `create_agent`는 내부적으로 LangGraph의 런타임을 사용합니다. Runtime은 에이전트 실행 중 도구와 미들웨어에서 접근할 수 있는 컨텍스트 정보를 제공합니다.\n\n**Runtime 구성 요소:**\n\n| 구성 요소 | 설명 |\n|:---|:---|\n| **Context** | 사용자 ID, 데이터베이스 연결 등 정적 정보 |\n| **Store** | 장기 메모리를 위한 `BaseStore` 인스턴스 |\n| **Stream Writer** | `\"custom\"` 스트림 모드로 정보 스트리밍 |\n\n런타임 정보는 도구와 미들웨어 내에서 `runtime` 매개변수를 통해 액세스할 수 있습니다.\n\n> 참고 문서: [LangGraph Persistence](https://docs.langchain.com/oss/python/langgraph/persistence.md)" - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "## 환경 설정\n\nRuntime 튜토리얼을 시작하기 전에 필요한 환경을 설정합니다. `dotenv`를 사용하여 API 키를 로드합니다.\n\n아래 코드는 환경 변수를 로드합니다." - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "from dotenv import load_dotenv\n", + "# Runtime\n", + "\n", + "LangChain의 `create_agent`는 내부적으로 LangGraph의 런타임을 사용합니다. Runtime은 에이전트 실행 중 도구와 미들웨어에서 접근할 수 있는 컨텍스트 정보를 제공합니다.\n", + "\n", + "**Runtime 구성 요소:**\n", "\n", - "load_dotenv(override=True)" + "| 구성 요소 | 설명 |\n", + "|:---|:---|\n", + "| **Context** | 사용자 ID, 데이터베이스 연결 등 정적 정보 |\n", + "| **Store** | 장기 메모리를 위한 `BaseStore` 인스턴스 |\n", + "| **Stream Writer** | `\"custom\"` 스트림 모드로 정보 스트리밍 |\n", + "\n", + "런타임 정보는 도구와 미들웨어 내에서 `runtime` 매개변수를 통해 액세스할 수 있습니다.\n", + "\n", + "> 참고 문서: [LangChain Runtime](https://docs.langchain.com/oss/python/langchain/runtime)" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## Context 정의 및 사용\n\n`create_agent`로 에이전트를 생성할 때 `context_schema`를 지정하여 에이전트 `Runtime`에 저장될 `context`의 구조를 정의할 수 있습니다. Context는 dataclass 또는 Pydantic 모델로 정의하며, 에이전트 호출 시 `context` 매개변수로 전달합니다.\n\nContext는 도구와 미들웨어에서 `runtime.context`를 통해 접근할 수 있으며, 사용자별 설정이나 세션 정보를 전달하는 데 유용합니다.\n\n아래 코드는 Context 스키마를 정의하고 에이전트에 전달하는 예시입니다." + "source": [ + "## 환경 설정\n", + "\n", + "Runtime 튜토리얼을 시작하기 전에 필요한 환경을 설정합니다. `dotenv`를 사용하여 API 키를 로드하고, LangSmith 추적을 설정합니다.\n", + "\n", + "아래 코드는 환경 변수를 로드하고 LangSmith 추적을 활성화합니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "I don't have access to your name. Could you please tell me your name?\n" + "LangSmith 추적을 시작합니다.\n", + "[프로젝트명]\n", + "LangGraph-V1-Tutorial\n" ] } ], "source": [ - "from dataclasses import dataclass\n", - "from langchain.agents import create_agent\n", - "from langchain_openai import ChatOpenAI\n", - "from langchain.tools import tool\n", - "\n", - "\n", - "@dataclass\n", - "class Context:\n", - " user_name: str\n", + "# 환경 변수 로드\n", + "from dotenv import load_dotenv\n", + "from langchain_teddynote import logging\n", "\n", + "load_dotenv(override=True)\n", "\n", - "@tool\n", - "def greet_user() -> str:\n", - " \"\"\"Greet the user.\"\"\"\n", - " return \"Hello!\"\n", + "# LangSmith 추적 설정\n", + "logging.langsmith(\"LangGraph-V1-Tutorial\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", "\n", + "## 도구에서 Runtime 액세스\n", "\n", - "model = ChatOpenAI(model=\"gpt-4.1-mini\")\n", + "도구 내에서 `ToolRuntime` 매개변수를 사용하여 `Runtime` 객체에 액세스할 수 있습니다. 이를 통해 다음 기능을 수행할 수 있습니다:\n", "\n", - "agent = create_agent(\n", - " model=model, tools=[greet_user], context_schema=Context # Context 스키마 정의\n", - ")\n", + "- **Context 접근**: 사용자 정보, 세션 데이터 등\n", + "- **Store 읽기/쓰기**: 장기 메모리 관리\n", + "- **Stream Writer**: 진행 상황 스트리밍\n", "\n", - "# Context를 전달하여 에이전트 호출\n", - "result = agent.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"What's my name?\"}]},\n", - " context=Context(user_name=\"John Smith\"), # Context 전달\n", - ")\n", + "`ToolRuntime` 매개변수는 도구 시그니처에 추가하면 자동으로 주입되며, LLM에는 노출되지 않습니다.\n", "\n", - "print(result[\"messages\"][-1].content)" + "다음 섹션에서 Context, Store, Stream Writer에 접근하는 구체적인 예시를 확인할 수 있습니다." ] }, { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## 도구에서 Runtime 액세스\n\n도구 내에서 `ToolRuntime` 매개변수를 사용하여 `Runtime` 객체에 액세스할 수 있습니다. 이를 통해 다음 기능을 수행할 수 있습니다:\n\n- **Context 접근**: 사용자 정보, 세션 데이터 등\n- **Store 읽기/쓰기**: 장기 메모리 관리\n- **Stream Writer**: 진행 상황 스트리밍\n\n`ToolRuntime` 매개변수는 도구 시그니처에 추가하면 자동으로 주입되며, LLM에는 노출되지 않습니다." - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": "### Context 액세스\n\n도구에서 `runtime.context`를 통해 Context 객체에 접근할 수 있습니다. `ToolRuntime[ContextType]` 형태로 타입 힌트를 지정하면 IDE에서 자동 완성을 지원받을 수 있습니다.\n\n아래 코드는 도구에서 Context에 접근하여 사용자 정보를 활용하는 예시입니다." + "source": [ + "### Context 액세스\n", + "\n", + "`create_agent`로 에이전트를 생성할 때 `context_schema`를 지정하여 에이전트 `Runtime`에 저장될 `context`의 구조를 정의할 수 있습니다. Context는 dataclass 또는 Pydantic 모델로 정의하며, 에이전트 호출 시 `context` 매개변수로 전달합니다.\n", + "\n", + "도구에서는 `runtime.context`를 통해 Context 객체에 접근할 수 있습니다. `ToolRuntime[ContextType]` 형태로 타입 힌트를 지정하면 IDE에서 자동 완성을 지원받을 수 있습니다.\n", + "\n", + "아래 코드는 도구에서 Context에 접근하여 사용자 정보를 활용하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ + "from langchain.agents import create_agent\n", + "from langchain.chat_models import init_chat_model\n", "from langchain.tools import tool, ToolRuntime\n", "from dataclasses import dataclass\n", "from langchain_teddynote.messages import invoke_graph\n", "\n", "\n", + "# 사용자 정보를 담는 Context 스키마\n", "@dataclass\n", "class UserContext:\n", " user_id: str\n", @@ -111,6 +111,7 @@ " user_email: str\n", "\n", "\n", + "# 사용자 정보를 가져오는 도구\n", "@tool\n", "def get_user_info(runtime: ToolRuntime[UserContext]) -> str:\n", " \"\"\"Get information about the current user.\"\"\"\n", @@ -122,23 +123,28 @@ " return f\"User ID: {user_id}, Name: {user_name}, Email: {user_email}\"\n", "\n", "\n", + "# 개인화된 인사를 생성하는 도구\n", "@tool\n", "def personalized_greeting(runtime: ToolRuntime[UserContext]) -> str:\n", " \"\"\"Generate a personalized greeting for the user.\"\"\"\n", " user_name = runtime.context.user_name\n", " return f\"안녕하세요, {user_name}님! 무엇을 도와드릴까요?\"\n", "\n", + "# 챗 모델 초기화\n", + "# OpenAI 사용 시, \"gpt-4\" 등으로 변경 가능\n", + "model = init_chat_model(\"claude-haiku-4-5\")\n", "\n", + "# 에이전트 생성\n", "agent = create_agent(\n", " model=model,\n", - " tools=[get_user_info, personalized_greeting],\n", + " tools=[personalized_greeting, get_user_info],\n", " context_schema=UserContext,\n", ")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -150,12 +156,14 @@ "🔄 Node: \u001b[1;36mmodel\u001b[0m 🔄\n", "- - - - - - - - - - - - - - - - - - - - - - - - - \n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'text': '안녕하세요! 반갑습니다! 👋\\n\\n저를 통해 무엇을 도와드릴 수 있을까요? 저는 당신에 대한 정보를 조회하거나 맞춤형 인사말을 생성할 수 있습니다. \\n\\n먼저 당신에 대해 알아보고 개인화된 인사말을 드릴까요?', 'type': 'text'}, {'id': 'toolu_017QNQPoRnBfeQeEaq9HndLd', 'input': {}, 'name': 'get_user_info', 'type': 'tool_use'}, {'id': 'toolu_01ADQyT2X7U8gVvGbn72yD8c', 'input': {}, 'name': 'personalized_greeting', 'type': 'tool_use'}]\n", "Tool Calls:\n", - " get_user_info (call_aCCGYgoqyD3iBVtKs0lnPdEK)\n", - " Call ID: call_aCCGYgoqyD3iBVtKs0lnPdEK\n", + " get_user_info (toolu_017QNQPoRnBfeQeEaq9HndLd)\n", + " Call ID: toolu_017QNQPoRnBfeQeEaq9HndLd\n", " Args:\n", - " personalized_greeting (call_9Xzn8buotGRVJAIqj406k5AB)\n", - " Call ID: call_9Xzn8buotGRVJAIqj406k5AB\n", + " personalized_greeting (toolu_01ADQyT2X7U8gVvGbn72yD8c)\n", + " Call ID: toolu_01ADQyT2X7U8gVvGbn72yD8c\n", " Args:\n", "==================================================\n", "\n", @@ -182,7 +190,9 @@ "- - - - - - - - - - - - - - - - - - - - - - - - - \n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", - "안녕하세요, 김철수님! 반갑습니다. 무엇을 도와드릴까요?\n", + "안녕하세요, **김철수님**! 무엇을 도와드릴까요? 😊\n", + "\n", + "오늘 하루는 어떠신가요? 궁금하신 점이나 도움이 필요한 부분이 있으시면 편하게 말씀해 주세요!\n", "==================================================\n" ] } @@ -201,11 +211,19 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### Store 액세스 (장기 메모리)\n\n도구 내에서 `runtime.store`를 사용하여 장기 메모리에 액세스할 수 있습니다. Store는 대화 세션을 넘어서 데이터를 영구 저장하며, `get()`, `put()` 메서드로 데이터를 읽고 씁니다.\n\nStore의 키는 네임스페이스(튜플)와 키(문자열)로 구성되어 데이터를 체계적으로 관리할 수 있습니다.\n\n아래 코드는 Store를 사용하여 사용자 설정을 저장하고 조회하는 예시입니다." + "source": [ + "### Store 액세스 (장기 메모리)\n", + "\n", + "도구 내에서 `runtime.store`를 사용하여 장기 메모리에 액세스할 수 있습니다. Store는 대화 세션을 넘어서 데이터를 영구 저장하며, `get()`, `put()` 메서드로 데이터를 읽고 씁니다.\n", + "\n", + "Store의 키는 네임스페이스(튜플)와 키(문자열)로 구성되어 데이터를 체계적으로 관리할 수 있습니다.\n", + "\n", + "아래 코드는 Store를 사용하여 사용자 설정을 저장하고 조회하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -214,18 +232,20 @@ "from langgraph.store.memory import InMemoryStore\n", "\n", "\n", + "# 사용자 ID만 포함하는 간단한 Context\n", "@dataclass\n", "class Context:\n", " user_id: str\n", "\n", "\n", + "# 사용자 이메일 설정을 가져오는 도구\n", "@tool\n", "def fetch_user_email_preferences(runtime: ToolRuntime[Context]) -> str:\n", - " \"\"\"Fetch the user's email preferences from the store.\"\"\"\n", + " \"\"\"Fetch the user's email writing style preferences from the store.\"\"\"\n", " user_id = runtime.context.user_id\n", "\n", " # 기본 설정\n", - " preferences: str = \"The user prefers you to write a brief and polite email.\"\n", + " preferences: str = \"사용자는 간결하고 정중한 이메일 작성을 선호합니다.\"\n", "\n", " # Store에서 사용자 설정 가져오기\n", " if runtime.store:\n", @@ -235,63 +255,134 @@ " return preferences\n", "\n", "\n", + "# 사용자 설정을 저장하는 도구\n", "@tool\n", "def save_user_preference(preference: str, runtime: ToolRuntime[Context]) -> str:\n", - " \"\"\"Save user preference to the store.\"\"\"\n", + " \"\"\"Save user's preference settings to the store.\"\"\"\n", " user_id = runtime.context.user_id\n", "\n", " if runtime.store:\n", " runtime.store.put((\"users\",), user_id, {\"preferences\": preference})\n", - " return f\"Saved preference: {preference}\"\n", + " return f\"설정이 저장되었습니다: {preference}\"\n", "\n", - " return \"Store not available\"\n", + " return \"Store를 사용할 수 없습니다.\"\n", "\n", "\n", "# Store와 함께 에이전트 생성\n", "store = InMemoryStore()\n", "\n", - "# 초기 데이터 설정\n", + "# 초기 데이터 설정 - 사용자별 이메일 작성 스타일 저장\n", "store.put(\n", " (\"users\",),\n", " \"user_123\",\n", - " {\"preferences\": \"The user prefers detailed and technical explanations.\"},\n", + " {\"preferences\": \"사용자는 상세하고 기술적인 설명을 선호합니다.\"},\n", ")\n", "\n", + "# 에이전트 생성 (Store 전달)\n", "agent = create_agent(\n", " model=model,\n", " tools=[fetch_user_email_preferences, save_user_preference],\n", " context_schema=Context,\n", " store=store, # Store 전달\n", - ")\n", - "\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "==================================================\n", + "🔄 Node: \u001b[1;36mmodel\u001b[0m 🔄\n", + "- - - - - - - - - - - - - - - - - - - - - - - - - \n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "[{'text': '사용자분의 이메일 작성 스타일 설정을 확인해드리겠습니다.', 'type': 'text'}, {'id': 'toolu_0123K2htiw8hYfGz7Gm8LpQt', 'input': {}, 'name': 'fetch_user_email_preferences', 'type': 'tool_use'}]\n", + "Tool Calls:\n", + " fetch_user_email_preferences (toolu_0123K2htiw8hYfGz7Gm8LpQt)\n", + " Call ID: toolu_0123K2htiw8hYfGz7Gm8LpQt\n", + " Args:\n", + "==================================================\n", + "\n", + "==================================================\n", + "🔄 Node: \u001b[1;36mtools\u001b[0m 🔄\n", + "- - - - - - - - - - - - - - - - - - - - - - - - - \n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: fetch_user_email_preferences\n", + "\n", + "사용자는 상세하고 기술적인 설명을 선호합니다.\n", + "==================================================\n", + "\n", + "==================================================\n", + "🔄 Node: \u001b[1;36mmodel\u001b[0m 🔄\n", + "- - - - - - - - - - - - - - - - - - - - - - - - - \n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "현재 귀하의 이메일 작성 스타일 설정은 다음과 같습니다:\n", + "\n", + "**상세하고 기술적인 설명을 선호합니다.**\n", + "\n", + "이는 이메일 작성 시 상세한 정보와 기술적인 내용을 포함하는 것을 선호한다는 의미입니다. 필요하시면 이 설정을 변경할 수도 있습니다. 어떤 스타일로 변경하고 싶으신가요?\n", + "==================================================\n" + ] + } + ], + "source": [ "# Store에서 설정 가져오기\n", - "result = agent.invoke(\n", - " {\"messages\": [{\"role\": \"user\", \"content\": \"What are my email preferences?\"}]},\n", + "invoke_graph(\n", + " agent,\n", + " {\"messages\": [{\"role\": \"user\", \"content\": \"제 이메일 작성 스타일 설정이 어떻게 되어 있나요?\"}]},\n", " context=Context(user_id=\"user_123\"),\n", - ")\n", - "\n", - "print(result[\"messages\"][-1].content)" + ")" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### Stream Writer 액세스\n\n도구 내에서 `runtime.get_stream_writer()` 또는 `runtime.stream_writer`를 사용하여 커스텀 업데이트를 스트리밍할 수 있습니다. 이는 장시간 실행되는 작업에서 사용자에게 진행 상황을 실시간으로 알려줄 때 유용합니다.\n\n스트리밍된 업데이트는 `stream_mode=\"custom\"`으로 수신할 수 있습니다.\n\n아래 코드는 Stream Writer를 사용하여 진행 상황을 스트리밍하는 예시입니다." + "source": [ + "### Stream Writer 액세스\n", + "\n", + "도구 내에서 `runtime.get_stream_writer()` 또는 `runtime.stream_writer`를 사용하여 커스텀 업데이트를 스트리밍할 수 있습니다. 이는 장시간 실행되는 작업에서 사용자에게 진행 상황을 실시간으로 알려줄 때 유용합니다.\n", + "\n", + "스트리밍된 업데이트는 `stream_mode=\"custom\"`으로 수신할 수 있습니다.\n", + "\n", + "아래 코드는 Stream Writer를 사용하여 진행 상황을 스트리밍하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Progress: 20%\n", + "Progress: 40%\n", + "Progress: 60%\n", + "Progress: 80%\n", + "Progress: 100%\n", + "Completed processing 50 items!\n" + ] + } + ], "source": [ "from langchain.tools import tool, ToolRuntime\n", "import time\n", "\n", "\n", + "# 대용량 데이터 처리를 시뮬레이션하는 도구\n", "@tool\n", "def process_large_dataset(num_items: int, runtime: ToolRuntime) -> str:\n", " \"\"\"Process a large dataset and report progress.\"\"\"\n", - " writer = runtime.get_stream_writer()\n", + " # Stream Writer 가져오기\n", + " writer = runtime.stream_writer\n", "\n", " # 진행 상황 스트리밍\n", " for i in range(0, num_items, 10):\n", @@ -299,10 +390,12 @@ " writer({\"stage\": \"processing\", \"progress\": progress, \"total\": num_items})\n", " time.sleep(0.1) # 작업 시뮬레이션\n", "\n", + " # 완료 상태 스트리밍\n", " writer({\"stage\": \"completed\", \"total\": num_items})\n", " return f\"Successfully processed {num_items} items!\"\n", "\n", "\n", + "# 에이전트 생성\n", "agent = create_agent(\n", " model=model,\n", " tools=[process_large_dataset],\n", @@ -314,27 +407,57 @@ " stream_mode=\"custom\",\n", "):\n", " if \"progress\" in chunk:\n", + " # 진행률 계산 및 출력\n", " percentage = (chunk[\"progress\"] / chunk[\"total\"]) * 100\n", " print(f\"Progress: {percentage:.0f}%\")\n", " elif \"stage\" in chunk and chunk[\"stage\"] == \"completed\":\n", + " # 완료 메시지 출력\n", " print(f\"Completed processing {chunk['total']} items!\")" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## 미들웨어에서 Runtime 액세스\n\n미들웨어에서 `Runtime` 객체에 액세스하여 동적 프롬프트를 생성하거나, 메시지를 수정하거나, 사용자 컨텍스트에 따라 에이전트 동작을 제어할 수 있습니다. 미들웨어 함수의 `runtime` 매개변수를 통해 Context, Store 등에 접근합니다." + "source": [ + "---\n", + "\n", + "## 미들웨어에서 Runtime 액세스\n", + "\n", + "미들웨어에서 `Runtime` 객체에 액세스하여 동적 프롬프트를 생성하거나, 메시지를 수정하거나, 사용자 컨텍스트에 따라 에이전트 동작을 제어할 수 있습니다. 미들웨어 함수의 `runtime` 매개변수를 통해 Context, Store 등에 접근합니다.\n", + "\n", + "아래 섹션에서는 Dynamic Prompt와 Before/After Model 미들웨어에서 Runtime을 사용하는 방법을 살펴봅니다." + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### Dynamic Prompt에서 Runtime 사용\n\n`@dynamic_prompt` 데코레이터로 정의된 미들웨어에서 `request.runtime`을 통해 Context에 접근할 수 있습니다. 이를 활용하여 사용자별로 다른 시스템 프롬프트를 동적으로 생성할 수 있습니다.\n\n아래 코드는 사용자 역할에 따라 다른 시스템 프롬프트를 생성하는 예시입니다." + "source": [ + "### Dynamic Prompt에서 Runtime 사용\n", + "\n", + "`@dynamic_prompt` 데코레이터로 정의된 미들웨어에서 `request.runtime`을 통해 Context에 접근할 수 있습니다. 이를 활용하여 사용자별로 다른 시스템 프롬프트를 동적으로 생성할 수 있습니다.\n", + "\n", + "아래 코드는 사용자 언어에 따라 다른 시스템 프롬프트를 생성하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Korean User ===\n", + "김철수님, SF의 날씨는 맑습니다! ☀️\n", + "\n", + "좋은 날씨가 계속되면 좋겠네요. 혹시 다른 도시의 날씨도 알고 싶으신가요?\n", + "\n", + "=== English User ===\n", + "The weather in SF (San Francisco) is sunny, John! That sounds like a beautiful day. Is there anything else you'd like to know about the weather?\n" + ] + } + ], "source": [ "from dataclasses import dataclass\n", "from langchain.agents import create_agent\n", @@ -342,33 +465,37 @@ "from langchain.tools import tool\n", "\n", "\n", + "# 사용자 이름과 언어를 담는 Context 스키마\n", "@dataclass\n", "class Context:\n", " user_name: str\n", - " user_role: str\n", + " language: str\n", "\n", "\n", + "# 날씨 정보를 가져오는 간단한 도구\n", "@tool\n", "def get_weather(city: str) -> str:\n", " \"\"\"Get the weather in a city.\"\"\"\n", " return f\"The weather in {city} is sunny!\"\n", "\n", "\n", + "# 동적 시스템 프롬프트 미들웨어\n", "@dynamic_prompt\n", "def dynamic_system_prompt(request: ModelRequest) -> str:\n", " # Runtime에서 Context 가져오기\n", " user_name = request.runtime.context.user_name\n", - " user_role = request.runtime.context.user_role\n", + " language = request.runtime.context.language\n", "\n", - " # 사용자 역할에 따라 다른 프롬프트\n", - " if user_role == \"admin\":\n", - " system_prompt = f\"You are a helpful assistant with full access. Address the user as {user_name}.\"\n", + " # 사용자 언어에 따라 다른 프롬프트\n", + " if language == \"Korean\":\n", + " system_prompt = f\"You are a helpful assistant. Address the user as '{user_name}'. Always respond in Korean.\"\n", " else:\n", - " system_prompt = f\"You are a helpful assistant. Address the user as {user_name}. Provide brief answers.\"\n", + " system_prompt = f\"You are a helpful assistant. Address the user as '{user_name}'. Always respond in English.\"\n", "\n", " return system_prompt\n", "\n", "\n", + "# 미들웨어를 포함한 에이전트 생성\n", "agent = create_agent(\n", " model=model,\n", " tools=[get_weather],\n", @@ -376,19 +503,19 @@ " context_schema=Context,\n", ")\n", "\n", - "# Admin 사용자로 호출\n", - "print(\"=== Admin User ===\")\n", + "# 한국어 사용자로 호출\n", + "print(\"=== Korean User ===\")\n", "result = agent.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"What is the weather in SF?\"}]},\n", - " context=Context(user_name=\"Admin Kim\", user_role=\"admin\"),\n", + " context=Context(user_name=\"김철수\", language=\"Korean\"),\n", ")\n", "print(result[\"messages\"][-1].content)\n", "\n", - "# 일반 사용자로 호출\n", - "print(\"\\n=== Regular User ===\")\n", + "# 영어 사용자로 호출\n", + "print(\"\\n=== English User ===\")\n", "result = agent.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"What is the weather in SF?\"}]},\n", - " context=Context(user_name=\"User Lee\", user_role=\"user\"),\n", + " context=Context(user_name=\"John\", language=\"English\"),\n", ")\n", "print(result[\"messages\"][-1].content)" ] @@ -396,13 +523,36 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### Before/After Model에서 Runtime 사용\n\n`@before_model`과 `@after_model` 데코레이터로 정의된 미들웨어에서도 `runtime` 매개변수를 통해 Context에 접근할 수 있습니다. 이를 활용하여 모델 호출 전후에 로깅, 검증, 변환 등의 작업을 수행할 수 있습니다.\n\n아래 코드는 모델 호출 전후에 사용자 정보를 로깅하는 예시입니다." + "source": [ + "### Before/After Model에서 Runtime 사용\n", + "\n", + "`@before_model`과 `@after_model` 데코레이터로 정의된 미들웨어에서도 `runtime` 매개변수를 통해 Context에 접근할 수 있습니다. 이를 활용하여 모델 호출 전후에 로깅, 검증, 변환 등의 작업을 수행할 수 있습니다.\n", + "\n", + "아래 코드는 모델 호출 전후에 사용자 정보를 로깅하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Before Model] User: John Smith, Session: session_456\n", + "[Before Model] Messages count: 1\n", + "[After Model] User: John Smith\n", + "[After Model] Response generated for session: session_456\n", + "[Before Model] User: John Smith, Session: session_456\n", + "[Before Model] Messages count: 3\n", + "[After Model] User: John Smith\n", + "[After Model] Response generated for session: session_456\n", + "\n", + "Final response: The weather in Seoul is sunny! ☀️\n" + ] + } + ], "source": [ "from langchain.agents import AgentState\n", "from langchain.agents.middleware import before_model, after_model\n", @@ -410,12 +560,14 @@ "from dataclasses import dataclass\n", "\n", "\n", + "# 세션 정보를 담는 Context 스키마\n", "@dataclass\n", "class Context:\n", " user_name: str\n", " session_id: str\n", "\n", "\n", + "# 모델 호출 전 로깅 미들웨어\n", "@before_model\n", "def log_before_model(state: AgentState, runtime: Runtime[Context]) -> dict | None:\n", " \"\"\"모델 호출 전 로깅\"\"\"\n", @@ -426,6 +578,7 @@ " return None\n", "\n", "\n", + "# 모델 호출 후 로깅 미들웨어\n", "@after_model\n", "def log_after_model(state: AgentState, runtime: Runtime[Context]) -> dict | None:\n", " \"\"\"모델 호출 후 로깅\"\"\"\n", @@ -434,6 +587,7 @@ " return None\n", "\n", "\n", + "# 미들웨어를 포함한 에이전트 생성\n", "agent = create_agent(\n", " model=model,\n", " tools=[get_weather],\n", @@ -441,6 +595,7 @@ " context_schema=Context,\n", ")\n", "\n", + "# 에이전트 호출\n", "result = agent.invoke(\n", " {\"messages\": [{\"role\": \"user\", \"content\": \"What's the weather in Seoul?\"}]},\n", " context=Context(user_name=\"John Smith\", session_id=\"session_456\"),\n", @@ -452,13 +607,59 @@ { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## 종합 예제: 사용자 컨텍스트 기반 에이전트\n\nRuntime의 모든 기능을 활용한 실용적인 예제입니다. Context로 사용자 정보를 전달하고, Store로 검색 기록을 관리하며, 미들웨어로 동적 프롬프트와 사용량 추적을 구현합니다.\n\n아래 코드는 사용자 등급에 따라 다른 기능을 제공하는 에이전트 예시입니다." + "source": [ + "---\n", + "\n", + "## 종합 예제: 사용자 컨텍스트 기반 에이전트\n", + "\n", + "Runtime의 모든 기능을 활용한 실용적인 예제입니다. Context로 사용자 정보를 전달하고, Store로 검색 기록을 관리하며, 미들웨어로 동적 프롬프트와 사용량 추적을 구현합니다.\n", + "\n", + "아래 코드는 사용자 등급에 따라 다른 기능을 제공하는 에이전트 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Test 1: Enterprise User (Korean) ===\n", + "[Usage Tracker] User: user_001, Tier: enterprise\n", + "[Usage Tracker] User: user_001, Tier: enterprise\n", + "좋습니다! 김철수님의 검색이 완료되었습니다.\n", + "\n", + "**검색 결과:**\n", + "- 쿼리: \"machine learning\"\n", + "- 접근 수준: 엔터프라이즈 전체 데이터베이스 접근\n", + "- 검색 기록: 자동으로 저장되었습니다\n", + "\n", + "엔터프라이즈 회원이시므로 전체 데이터베이스에 대한 검색 결과를 모두 조회하실 수 있습니다. 추가로 도움이 필요하신 부분이 있으신가요?\n", + "\n", + "=== Test 2: Free User (English) ===\n", + "[Usage Tracker] User: user_002, Tier: free\n", + "[Usage Tracker] User: user_002, Tier: free\n", + "John Doe, I've completed your search for \"data science\". Here are the results:\n", + "\n", + "**Search Results:** Basic search results for \"data science\" (Limited to 10 results)\n", + "\n", + "I've also saved this search to your history for future reference. If you'd like to see more detailed results, refine your search, or explore related topics, just let me know!\n", + "\n", + "=== Test 3: Check Search History ===\n", + "[Usage Tracker] User: user_001, Tier: enterprise\n", + "[Usage Tracker] User: user_001, Tier: enterprise\n", + "김철수님의 최근 검색 기록은 다음과 같습니다:\n", + "\n", + "1. **Python tutorial**\n", + "2. **LangChain guide**\n", + "3. **machine learning**\n", + "\n", + "추가로 검색하고 싶으신 내용이 있으시면 말씀해주세요!\n" + ] + } + ], "source": [ "from dataclasses import dataclass\n", "from langchain.agents import create_agent, AgentState\n", @@ -468,6 +669,7 @@ "from langgraph.runtime import Runtime\n", "\n", "\n", + "# 사용자 정보를 담는 Context 스키마\n", "@dataclass\n", "class UserContext:\n", " user_id: str\n", @@ -476,7 +678,7 @@ " language: str # \"ko\", \"en\"\n", "\n", "\n", - "# 도구 정의\n", + "# 데이터베이스 검색 도구\n", "@tool\n", "def search_database(query: str, runtime: ToolRuntime[UserContext]) -> str:\n", " \"\"\"Search the database. Access level depends on user tier.\"\"\"\n", @@ -491,6 +693,7 @@ " return f\"Basic search results for: {query} (Limited to 10 results)\"\n", "\n", "\n", + "# 사용자 검색 기록을 가져오는 도구\n", "@tool\n", "def get_user_history(runtime: ToolRuntime[UserContext]) -> str:\n", " \"\"\"Get user's search history from store.\"\"\"\n", @@ -503,6 +706,7 @@ " return \"No search history found\"\n", "\n", "\n", + "# 검색 기록을 저장하는 도구\n", "@tool\n", "def save_search(query: str, runtime: ToolRuntime[UserContext]) -> str:\n", " \"\"\"Save search query to user history.\"\"\"\n", @@ -513,11 +717,9 @@ " existing = runtime.store.get((\"history\",), user_id)\n", " searches = existing.value[\"searches\"] if existing else []\n", "\n", - " # 새 검색어 추가\n", + " # 새 검색어 추가 (최근 5개만 유지)\n", " searches.append(query)\n", - " runtime.store.put(\n", - " (\"history\",), user_id, {\"searches\": searches[-5:]}\n", - " ) # 최근 5개만 유지\n", + " runtime.store.put((\"history\",), user_id, {\"searches\": searches[-5:]})\n", "\n", " return f\"Saved search: {query}\"\n", "\n", @@ -620,11 +822,21 @@ { "cell_type": "markdown", "metadata": {}, - "source": "---\n\n## 실전 패턴\n\n### 데이터베이스 연결 전달\n\nContext를 사용하여 데이터베이스 연결 객체를 도구에 전달할 수 있습니다. 이 패턴을 사용하면 도구에서 직접 데이터베이스에 접근하여 쿼리를 실행할 수 있습니다.\n\n아래 코드는 데이터베이스 연결을 Context로 전달하는 예시입니다." + "source": [ + "---\n", + "\n", + "## 실전 패턴\n", + "\n", + "### 데이터베이스 연결 전달\n", + "\n", + "Context를 사용하여 데이터베이스 연결 객체를 도구에 전달할 수 있습니다. 이 패턴을 사용하면 도구에서 직접 데이터베이스에 접근하여 쿼리를 실행할 수 있습니다.\n", + "\n", + "아래 코드는 데이터베이스 연결을 Context로 전달하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -632,12 +844,14 @@ "from typing import Any\n", "\n", "\n", + "# 데이터베이스 연결을 담는 Context 스키마\n", "@dataclass\n", "class DatabaseContext:\n", " db_connection: Any # 실제로는 데이터베이스 연결 객체\n", " user_id: str\n", "\n", "\n", + "# 데이터베이스 쿼리를 실행하는 도구\n", "@tool\n", "def query_database(sql: str, runtime: ToolRuntime[DatabaseContext]) -> str:\n", " \"\"\"Execute SQL query on the database.\"\"\"\n", @@ -666,24 +880,41 @@ { "cell_type": "markdown", "metadata": {}, - "source": "### 인증 및 권한 검사\n\n미들웨어에서 Context를 사용하여 사용자 인증 및 권한을 검사할 수 있습니다. `@before_agent` 미들웨어에서 권한이 없는 요청을 사전에 차단하면 보안을 강화할 수 있습니다.\n\n아래 코드는 사용자 권한을 검사하는 미들웨어 예시입니다." + "source": [ + "### 인증 및 권한 검사\n", + "\n", + "미들웨어에서 Context를 사용하여 사용자 인증 및 권한을 검사할 수 있습니다. `@before_agent` 미들웨어에서 권한이 없는 요청을 사전에 차단하면 보안을 강화할 수 있습니다.\n", + "\n", + "아래 코드는 사용자 권한을 검사하는 미들웨어 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "You don't have permission to perform this action.\n" + ] + } + ], "source": [ - "from langchain.agents.middleware import before_agent, hook_config\n", + "from langchain.agents.middleware import before_agent\n", + "from langgraph.runtime import Runtime\n", "from typing import Any\n", "\n", "\n", + "# 인증 정보를 담는 Context 스키마\n", "@dataclass\n", "class AuthContext:\n", " user_id: str\n", " permissions: list[str]\n", "\n", "\n", + "# 권한 검사 미들웨어\n", "@before_agent(can_jump_to=[\"end\"])\n", "def check_permissions(\n", " state: AgentState, runtime: Runtime[AuthContext]\n", @@ -712,30 +943,53 @@ "\n", "\n", "# 사용 예시\n", - "# agent = create_agent(\n", - "# model=model,\n", - "# tools=[search_tool],\n", - "# middleware=[check_permissions],\n", - "# context_schema=AuthContext\n", - "# )\n", - "#\n", - "# result = agent.invoke(\n", - "# {\"messages\": [{\"role\": \"user\", \"content\": \"Delete user data\"}]},\n", - "# context=AuthContext(user_id=\"user_123\", permissions=[\"read\", \"write\"])\n", - "# )" + "agent = create_agent(\n", + " model=model,\n", + " tools=[],\n", + " middleware=[check_permissions],\n", + " context_schema=AuthContext\n", + ")\n", + "\n", + "result = agent.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": \"Delete user data\"}]},\n", + " context=AuthContext(user_id=\"user_123\", permissions=[\"user\", \"read\", \"write\"])\n", + ")\n", + "\n", + "print(result[\"messages\"][-1].content)" ] }, { "cell_type": "markdown", "metadata": {}, - "source": "### 요청별 설정\n\n각 요청에 대한 특정 설정을 Context를 통해 전달할 수 있습니다. 타임아웃, 토큰 제한, 로깅 수준 등 요청마다 다른 설정이 필요한 경우 유용합니다.\n\n아래 코드는 요청별 설정을 Context로 전달하는 예시입니다." + "source": [ + "### 요청별 설정\n", + "\n", + "각 요청에 대한 특정 설정을 Context를 통해 전달할 수 있습니다. 타임아웃, 토큰 제한, 로깅 수준 등 요청마다 다른 설정이 필요한 경우 유용합니다.\n", + "\n", + "아래 코드는 요청별 설정을 Context로 전달하는 예시입니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "I'd be happy to help you process your request! However, I need you to provide more details about what you'd like me to do.\n", + "\n", + "Could you please clarify:\n", + "- What specific request would you like me to process?\n", + "- What information or action do you need?\n", + "\n", + "Please provide the details of your request, and I'll process it for you right away.\n" + ] + } + ], "source": [ + "# 요청별 설정을 담는 Context 스키마\n", "@dataclass\n", "class RequestContext:\n", " user_id: str\n", @@ -744,6 +998,7 @@ " max_tokens: int\n", "\n", "\n", + "# 요청 설정에 따라 처리하는 도구\n", "@tool\n", "def process_request(query: str, runtime: ToolRuntime[RequestContext]) -> str:\n", " \"\"\"Process request with custom settings.\"\"\"\n", @@ -757,6 +1012,7 @@ " return f\"Processed: {query}\"\n", "\n", "\n", + "# 에이전트 생성\n", "agent = create_agent(\n", " model=model, tools=[process_request], context_schema=RequestContext\n", ")\n", @@ -774,13 +1030,35 @@ }, { "cell_type": "markdown", - "source": "---\n\n## 정리\n\n이 튜토리얼에서는 LangGraph 에이전트의 Runtime 기능을 학습했습니다.\n\n**핵심 개념 요약:**\n\n| 개념 | 설명 | 접근 방법 |\n|:---|:---|:---|\n| **Context** | 사용자 정보, 세션 데이터 등 정적 정보 | `runtime.context` |\n| **Store** | 대화 세션을 넘어선 장기 메모리 | `runtime.store.get()`, `put()` |\n| **Stream Writer** | 커스텀 진행 상황 스트리밍 | `runtime.get_stream_writer()` |\n\n**실전 패턴:**\n- 데이터베이스 연결을 Context로 전달하여 도구에서 직접 쿼리 실행\n- 미들웨어에서 사용자 권한 검사로 보안 강화\n- 요청별 설정(타임아웃, 토큰 제한 등)을 Context로 전달\n\n**다음 단계:**\n- 구조화된 출력(Structured Output)을 사용한 에이전트 구축 학습", - "metadata": {} + "metadata": {}, + "source": [ + "---\n", + "\n", + "## 정리\n", + "\n", + "이 튜토리얼에서는 LangGraph 에이전트의 Runtime 기능을 학습했습니다.\n", + "\n", + "**핵심 개념 요약:**\n", + "\n", + "| 개념 | 설명 | 접근 방법 |\n", + "|:---|:---|:---|\n", + "| **Context** | 사용자 정보, 세션 데이터 등 정적 정보 | `runtime.context` |\n", + "| **Store** | 대화 세션을 넘어선 장기 메모리 | `runtime.store.get()`, `put()` |\n", + "| **Stream Writer** | 커스텀 진행 상황 스트리밍 | `runtime.stream_writer` |\n", + "\n", + "**실전 패턴:**\n", + "- 데이터베이스 연결을 Context로 전달하여 도구에서 직접 쿼리 실행\n", + "- 미들웨어에서 사용자 권한 검사로 보안 강화\n", + "- 요청별 설정(타임아웃, 토큰 제한 등)을 Context로 전달\n", + "\n", + "**다음 단계:**\n", + "- 구조화된 출력(Structured Output)을 사용한 에이전트 구축 학습" + ] } ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": ".venv (3.11.14)", "language": "python", "name": "python3" }, @@ -794,9 +1072,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.11" + "version": "3.11.14" } }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +}