diff --git a/08-Core-Features/06-LangGraph-Human-In-the-Loop.ipynb b/08-Core-Features/06-LangGraph-Human-In-the-Loop.ipynb index 0f0b45b..16554ea 100644 --- a/08-Core-Features/06-LangGraph-Human-In-the-Loop.ipynb +++ b/08-Core-Features/06-LangGraph-Human-In-the-Loop.ipynb @@ -18,10 +18,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "de9d9d8d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# API 키를 환경변수로 관리하기 위한 설정 파일\n", "from dotenv import load_dotenv\n", @@ -32,10 +43,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "6b5c6228", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "LangSmith 추적을 시작합니다.\n", + "[프로젝트명]\n", + "LangGraph-V1-Tutorial\n" + ] + } + ], "source": [ "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n", "# !pip install -qU langchain-teddynote\n", @@ -56,12 +77,11 @@ "from typing_extensions import TypedDict\n", "\n", "from langchain_core.tools import tool\n", - "from langchain_openai import ChatOpenAI\n", + "from langchain.chat_models import init_chat_model\n", "from langgraph.checkpoint.memory import MemorySaver\n", "from langgraph.graph import StateGraph, START, END\n", "from langgraph.graph.message import add_messages\n", "from langgraph.prebuilt import ToolNode, tools_condition\n", - "from langchain_teddynote.graphs import visualize_graph\n", "from langchain_teddynote.tools import GoogleNews\n", "\n", "\n", @@ -87,8 +107,8 @@ "\n", "tools = [search_keyword]\n", "\n", - "# LLM 초기화\n", - "llm = ChatOpenAI(model=\"gpt-4o-mini\")\n", + "# LLM 초기화 (OpenAI 키 사용 시 gpt-5.2, gpt-4.1-mini 등으로 변경)\n", + "llm = init_chat_model(\"gpt-4.1-mini\")\n", "\n", "# 도구와 LLM 결합\n", "llm_with_tools = llm.bind_tools(tools)\n", @@ -141,7 +161,17 @@ "cell_type": "markdown", "id": "c8aa1673", "metadata": {}, - "source": "## 그래프 컴파일 (interrupt_before 설정)\n\n이제 그래프를 컴파일합니다. `compile()` 메서드의 `interrupt_before` 파라미터에 `[\"tools\"]`를 전달하면, `tools` 노드가 실행되기 전에 그래프 실행이 중단됩니다. 이를 통해 사람이 도구 호출을 검토하고 승인할 수 있는 기회를 제공합니다.\n\n또한 `checkpointer`를 설정하여 중단된 상태를 저장하고, 나중에 이어서 실행할 수 있게 합니다.\n\n> 참고 문서: [LangGraph Interrupts](https://langchain-ai.github.io/langgraph/concepts/interrupts/)\n\n아래 코드에서는 `interrupt_before`와 체크포인터를 설정하여 그래프를 컴파일합니다." + "source": [ + "## 그래프 컴파일 (interrupt_before 설정)\n", + "\n", + "이제 그래프를 컴파일합니다. `compile()` 메서드의 `interrupt_before` 파라미터에 `[\"tools\"]`를 전달하면, `tools` 노드가 실행되기 전에 그래프 실행이 중단됩니다. 이를 통해 사람이 도구 호출을 검토하고 승인할 수 있는 기회를 제공합니다.\n", + "\n", + "또한 `checkpointer`를 설정하여 중단된 상태를 저장하고, 나중에 이어서 실행할 수 있게 합니다.\n", + "\n", + "> 참고 문서: [LangGraph Interrupts](https://langchain-ai.github.io/langgraph/concepts/interrupts/)\n", + "\n", + "아래 코드에서는 `interrupt_before`와 체크포인터를 설정하여 그래프를 컴파일합니다." + ] }, { "cell_type": "code", @@ -152,28 +182,82 @@ "source": [ "########## 6. interrupt_before 추가 ##########\n", "\n", - "# 그래프 빌더 컴파일\n", - "graph = graph_builder.compile(checkpointer=memory)" + "# 그래프 빌더 컴파일 (interrupt_before 설정)\n", + "graph = graph_builder.compile(\n", + " checkpointer=memory,\n", + " interrupt_before=[\"tools\"], # tools 노드 실행 전 중단\n", + ")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "20e87724", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABCcAAAObCAYAAABgmuskAAAACXBIWXMAAAsTAAALEwEAmpwYAAAgAElEQVR4nOzde9SlV13Ycf8oXkvVWtTamxWlF9u1XFptbWtR6ZJKtd5qV63LVamuWhZiFVREEJKQhElCLpAhEC4hhBASyI2ECUlgSDIJ5DK53+aSSSYzmdyTCUkIqECfrt8J58w5533O5TnnOWc/z34+n7X2Isy878x5z+yZZH9n7/18QwEAAACQ0Dek/MkBAAAAxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJISJwAAAICkxAkAAAAgKXECAAAASEqcAAAAAJIS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EU7ALIl/6ewnpOhcVdFazUbG9aT5TaUyBoOED81q/i73G0ZYLQEA1JVwAq6CQUMKZQzMgXrOAVs4AIC6Ek7AVQ4pJjuTQhkDc6BecyD+JsTfBgCAOhNOQPK2D2FFfnOojEFGGOGQSwCA9wknYMjCitaKJqZZGmgNtDkw/HOg9Xe2GUI0gwhhBABAb8IJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJUubHeAAAACS0lEQVRwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAABSCScAAACAVMIJAAAAIJVwAgAAAEglnAAAAACKTP8PEzwUDELJ0k0AAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ + "from IPython.display import Image\n", + "\n", "########## 7. 그래프 시각화 ##########\n", - "# 그래프 시각화\n", - "visualize_graph(graph)" + "# 그래프 시각화 (Excalidraw로 생성된 PNG 이미지)\n", + "Image(filename=\"assets/06-human-in-the-loop-graph.png\")" + ] + }, + { + "cell_type": "markdown", + "id": "c77w16a7vh", + "metadata": {}, + "source": [ + "## 그래프 실행 및 Interrupt 테스트\n", + "\n", + "이제 그래프를 실행하여 `interrupt_before` 설정이 제대로 작동하는지 확인해 봅니다. \n", + "\n", + "도구 호출이 필요한 질문을 입력하면, `tools` 노드 실행 전에 그래프가 중단되는 것을 확인할 수 있습니다." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "b26d4039", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "[messages]\n", + "\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "AI 관련 최신 뉴스를 알려주세요.\n", + "\n", + "[messages]\n", + "\n", + "================================\u001b[1m Human Message \u001b[0m=================================\n", + "\n", + "AI 관련 최신 뉴스를 알려주세요.\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "Tool Calls:\n", + " search_keyword (call_hoctw7zDeOOFOPfxm1pnQmS0)\n", + " Call ID: call_hoctw7zDeOOFOPfxm1pnQmS0\n", + " Args:\n", + " query: AI\n" + ] + } + ], "source": [ "from langchain_teddynote.messages import pretty_print_messages\n", "from langchain_core.runnables import RunnableConfig\n", @@ -195,7 +279,6 @@ " input=input,\n", " config=config,\n", " stream_mode=\"values\",\n", - " interrupt_before=[\"tools\"], # tools 실행 전 interrupt(tools 노드 실행 전 중단)\n", "):\n", " for key, value in event.items():\n", " # key 는 노드 이름\n", @@ -214,14 +297,31 @@ "cell_type": "markdown", "id": "889d388e", "metadata": {}, - "source": "## 그래프 상태 확인\n\n그래프 상태를 확인하여 `interrupt_before` 설정이 제대로 작동했는지 확인해 봅니다. `get_state()` 메서드를 통해 현재 스냅샷을 가져오고, `next` 속성을 확인하면 다음에 실행될 노드를 알 수 있습니다.\n\n아래 코드에서는 스냅샷의 `next` 속성을 출력합니다." + "source": [ + "## 그래프 상태 확인\n", + "\n", + "그래프 상태를 확인하여 `interrupt_before` 설정이 제대로 작동했는지 확인해 봅니다. `get_state()` 메서드를 통해 현재 스냅샷을 가져오고, `next` 속성을 확인하면 다음에 실행될 노드를 알 수 있습니다.\n", + "\n", + "아래 코드에서는 스냅샷의 `next` 속성을 출력합니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "ebcdde46", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "('tools',)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# 그래프 상태 스냅샷 생성\n", "snapshot = graph.get_state(config)\n", @@ -234,14 +334,32 @@ "cell_type": "markdown", "id": "b80ebad2", "metadata": {}, - "source": "이전 튜토리얼에서는 `__END__`에 도달했기 때문에 `.next`가 존재하지 않았습니다.\n\n하지만 지금은 `.next`가 `('tools',)`로 지정되어 있습니다. 이는 `interrupt_before=[\"tools\"]` 설정으로 인해 `tools` 노드 실행 전에 그래프가 중단되었음을 의미합니다.\n\n다음으로 도구 호출 정보를 확인해 봅시다." + "source": [ + "이전 튜토리얼에서는 `__END__`에 도달했기 때문에 `.next`가 존재하지 않았습니다.\n", + "\n", + "하지만 지금은 `.next`가 `('tools',)`로 지정되어 있습니다. 이는 `interrupt_before=[\"tools\"]` 설정으로 인해 `tools` 노드 실행 전에 그래프가 중단되었음을 의미합니다.\n", + "\n", + "다음으로 도구 호출 정보를 확인해 봅시다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "1570ed38", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " \u001b[96mindex [0]\u001b[0m\n", + " \u001b[94mname\u001b[0m: \"search_keyword\"\n", + " \u001b[94margs\u001b[0m: {\"query\": \"AI\"}\n", + " \u001b[94mid\u001b[0m: \"call_hoctw7zDeOOFOPfxm1pnQmS0\"\n", + " \u001b[94mtype\u001b[0m: \"tool_call\"\n" + ] + } + ], "source": [ "from langchain_teddynote.messages import display_message_tree\n", "\n", @@ -256,14 +374,50 @@ "cell_type": "markdown", "id": "b7062b94", "metadata": {}, - "source": "## 그래프 이어서 실행 (Resume)\n\n다음으로는 이전에 종료된 지점 이후부터 **이어서 그래프를 진행**해 봅니다. LangGraph는 중단된 그래프를 쉽게 재개할 수 있는 기능을 제공합니다.\n\n그래프를 이어서 실행하려면 `stream()` 또는 `invoke()` 메서드에 입력값으로 `None`을 전달하면 됩니다. 이렇게 하면 현재 체크포인트 상태에서 이어서 실행됩니다.\n\n아래 코드에서는 `None`을 입력으로 전달하여 그래프를 이어서 실행합니다." + "source": [ + "## 그래프 이어서 실행 (Resume)\n", + "\n", + "다음으로는 이전에 종료된 지점 이후부터 **이어서 그래프를 진행**해 봅니다. LangGraph는 중단된 그래프를 쉽게 재개할 수 있는 기능을 제공합니다.\n", + "\n", + "그래프를 이어서 실행하려면 `stream()` 또는 `invoke()` 메서드에 입력값으로 `None`을 전달하면 됩니다. 이렇게 하면 현재 체크포인트 상태에서 이어서 실행됩니다.\n", + "\n", + "아래 코드에서는 `None`을 입력으로 전달하여 그래프를 이어서 실행합니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "8f48c339", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "Tool Calls:\n", + " search_keyword (call_hoctw7zDeOOFOPfxm1pnQmS0)\n", + " Call ID: call_hoctw7zDeOOFOPfxm1pnQmS0\n", + " Args:\n", + " query: AI\n", + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: search_keyword\n", + "\n", + "[{\"url\": \"https://news.google.com/rss/articles/CBMigwFBVV95cUxOOXZHM3FiX19LTVdjYTNsTnBfU2RadXRKM0dERUp6c19YOHhra09GdmJfWFpnWl9ueVVoeUpnWmxvQzFXdFk1QVR6WFBhb3JRX2VfbkJnNjk0UjhQU0RWLUQxWHEycFQ2RFREU0dhbDBxVW9Rbkp5b1g1eVJad0RrUE5pQdIBlwFBVV95cUxNU0FRSl94SjhvZDlFNzY4cklpY2NpMFF4ZEZGY01OcnBDZlUxeWl2M09pbWd5a1JxRmN2dmFHcXJacHBhV1pJZU8zQ1VNRHVBeklNa0wzQ0ZmZUdOZlBScVJ4ZmE1Q3BsSmZSZmZHcWxjMUdJSnVEOGhuYlBqQkpCaTFGZUd3dV9xak42dVltTEVDRzd3a2Q4?oc=5\", \"content\": \"ABC 대 CBC… AI는 중국인 전쟁 - 조선일보\"}, {\"url\": \"https://news.google.com/rss/articles/CBMibEFVX3lxTE5DaDhRMlE3dXRmdVJuVUFyY3ByVEFRMWhvS19aUmhHRC1kOE9LOVZDOGhKQ0NKMlFPTVMtMkFFYm82R1JaWGxBclZpUklyRmxNdzBCc09kb2VZSUNEV1lIemR3SzYwOEFKQ05GYg?oc=5\", \"content\": \"'AI 수혜'의 배신…美증시 집어삼킨 '클로드 코워크' 뭐길래 - 유니콘팩토리\"}, {\"url\": \"https://news.google.com/rss/articles/CBMiaEFVX3lxTE40UHNmMUNoYkUwemlZYklaaU5zbDNQS0RHVEtFWjFWNmpNdEtkREFGTDdZY3o2ckFleGowZUJJc0cxb2J3QVZaejNKTHlubGhLQ29ydXVSNmVQOVNmOTVZTDVScmFEN3pz?oc=5\", \"content\": \"AI 버블 우려보다 더 큰 충격…고평가 기술주에서 탈출 러시[오미주] - 머니투데이\"}, {\"url\": \"https://news.google.com/rss/articles/CBMic0FVX3lxTE9wLXZ2OHR2eUo4VGd0S0VyS0gzZDExOFBvTDFSNnNfcEtnRXFVaFNxaE9ZZ0VWd0RpY2VzWm5KeUdKZklVSFlwckVkSlNCRTlGM1pDNGZCRTFKX0k5VmJNdkg1NEpBSTBCNzNHNnlreVlTMDA?oc=5\", \"content\": \"앤스로픽發 AI 쇼크…SW·법률·광고 산업까지 흔들 - 마켓인\"}, {\"url\": \"https://news.google.com/rss/articles/CBMiTkFVX3lxTFBreXZmNEsxaXJiLXVPV2xhYkZrRGxDU0FBQXpoTjcyVXpJZFJlc0RDeVBTcXpiNThoUURZcGJQalRxcm9yNC1TYmxuSHJwUQ?oc=5\", \"content\": \"국가AI전략위-교육위, 현장과 연결된 AI 전환기 교육 정책 논의 - 전자신문\"}]\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "최근 AI 관련 뉴스 주요 내용입니다:\n", + "\n", + "1. ABC 대 CBC… AI는 중국인 전쟁 - 조선일보\n", + "2. 'AI 수혜'의 배신…미 증시 집어삼킨 '클로드 코워크' 뭐길래 - 유니콘팩토리\n", + "3. AI 버블 우려보다 더 큰 충격…고평가 기술주에서 탈출 러시 - 머니투데이\n", + "4. 앤스로픽發 AI 쇼크…SW·법률·광고 산업까지 흔들 - 마켓인\n", + "5. 국가AI전략위-교육위, 현장과 연결된 AI 전환기 교육 정책 논의 - 전자신문\n", + "\n", + "더 자세한 내용이 필요하시면 말씀해 주세요.\n" + ] + } + ], "source": [ "# `None`는 현재 상태에 아무것도 추가하지 않음\n", "events = graph.stream(None, config, stream_mode=\"values\")\n", @@ -276,17 +430,13 @@ " event[\"messages\"][-1].pretty_print()" ] }, - { - "cell_type": "markdown", - "id": "8d655b90", - "metadata": {}, - "source": "## 정리\n\n이제 `interrupt`를 사용하여 챗봇에 인간이 개입할 수 있는 실행을 추가하여 필요할 때 인간의 감독과 개입을 가능하게 했습니다. 이를 통해 추후 시스템 구현 시 잠재적인 UI를 제공할 수 있습니다.\n\n이미 **checkpointer**를 추가했기 때문에, 그래프는 **무기한** 일시 중지되고 언제든지 다시 시작할 수 있습니다." - }, { "cell_type": "markdown", "id": "76a3baa8", "metadata": {}, "source": [ + "## 상태 기록 조회 (State History)\n", + "\n", "아래는 `get_state_history` 메서드를 사용하여 상태 기록을 가져오는 방법입니다.\n", "\n", "상태 기록을 통해 원하는 상태를 지정하여 **해당 지점에서 다시 시작** 할 수 있습니다." @@ -294,10 +444,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "0b9d5d8d", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "메시지 수: 4 다음 노드: ()\n", + "--------------------------------------------------------------------------------\n", + "메시지 수: 3 다음 노드: ('chatbot',)\n", + "--------------------------------------------------------------------------------\n", + "메시지 수: 2 다음 노드: ('tools',)\n", + "--------------------------------------------------------------------------------\n", + "메시지 수: 1 다음 노드: ('chatbot',)\n", + "--------------------------------------------------------------------------------\n", + "메시지 수: 0 다음 노드: ('__start__',)\n", + "--------------------------------------------------------------------------------\n" + ] + } + ], "source": [ "to_replay = None\n", "\n", @@ -315,14 +482,27 @@ "cell_type": "markdown", "id": "ff8faca6", "metadata": {}, - "source": "그래프의 모든 단계에 대해 체크포인트가 저장된다는 점에 **주목**할 필요가 있습니다. 이를 통해 특정 시점의 상태로 되돌아가 다시 실행할 수 있습니다.\n\n원하는 지점은 `to_replay` 변수에 저장합니다. 이를 활용하여 특정 시점에서 다시 시작할 수 있습니다." + "source": [ + "그래프의 모든 단계에 대해 체크포인트가 저장된다는 점에 **주목**할 필요가 있습니다. 이를 통해 특정 시점의 상태로 되돌아가 다시 실행할 수 있습니다.\n", + "\n", + "원하는 지점은 `to_replay` 변수에 저장합니다. 이를 활용하여 특정 시점에서 다시 시작할 수 있습니다." + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "6da2eeda", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('chatbot',)\n", + "{'configurable': {'thread_id': '1', 'checkpoint_ns': '', 'checkpoint_id': '1f102843-4f0c-65dc-8002-bd531a1f71d5'}}\n" + ] + } + ], "source": [ "# 다음 항목의 다음 요소 출력\n", "print(to_replay.next)\n", @@ -341,10 +521,23 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "74548a50", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'configurable': {'thread_id': '1',\n", + " 'checkpoint_ns': '',\n", + " 'checkpoint_id': '1f102843-4f0c-65dc-8002-bd531a1f71d5'}}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "to_replay.config" ] @@ -363,10 +556,32 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "id": "18f5474a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=================================\u001b[1m Tool Message \u001b[0m=================================\n", + "Name: search_keyword\n", + "\n", + "[{\"url\": \"https://news.google.com/rss/articles/CBMigwFBVV95cUxOOXZHM3FiX19LTVdjYTNsTnBfU2RadXRKM0dERUp6c19YOHhra09GdmJfWFpnWl9ueVVoeUpnWmxvQzFXdFk1QVR6WFBhb3JRX2VfbkJnNjk0UjhQU0RWLUQxWHEycFQ2RFREU0dhbDBxVW9Rbkp5b1g1eVJad0RrUE5pQdIBlwFBVV95cUxNU0FRSl94SjhvZDlFNzY4cklpY2NpMFF4ZEZGY01OcnBDZlUxeWl2M09pbWd5a1JxRmN2dmFHcXJacHBhV1pJZU8zQ1VNRHVBeklNa0wzQ0ZmZUdOZlBScVJ4ZmE1Q3BsSmZSZmZHcWxjMUdJSnVEOGhuYlBqQkpCaTFGZUd3dV9xak42dVltTEVDRzd3a2Q4?oc=5\", \"content\": \"ABC 대 CBC… AI는 중국인 전쟁 - 조선일보\"}, {\"url\": \"https://news.google.com/rss/articles/CBMibEFVX3lxTE5DaDhRMlE3dXRmdVJuVUFyY3ByVEFRMWhvS19aUmhHRC1kOE9LOVZDOGhKQ0NKMlFPTVMtMkFFYm82R1JaWGxBclZpUklyRmxNdzBCc09kb2VZSUNEV1lIemR3SzYwOEFKQ05GYg?oc=5\", \"content\": \"'AI 수혜'의 배신…美증시 집어삼킨 '클로드 코워크' 뭐길래 - 유니콘팩토리\"}, {\"url\": \"https://news.google.com/rss/articles/CBMiaEFVX3lxTE40UHNmMUNoYkUwemlZYklaaU5zbDNQS0RHVEtFWjFWNmpNdEtkREFGTDdZY3o2ckFleGowZUJJc0cxb2J3QVZaejNKTHlubGhLQ29ydXVSNmVQOVNmOTVZTDVScmFEN3pz?oc=5\", \"content\": \"AI 버블 우려보다 더 큰 충격…고평가 기술주에서 탈출 러시[오미주] - 머니투데이\"}, {\"url\": \"https://news.google.com/rss/articles/CBMic0FVX3lxTE9wLXZ2OHR2eUo4VGd0S0VyS0gzZDExOFBvTDFSNnNfcEtnRXFVaFNxaE9ZZ0VWd0RpY2VzWm5KeUdKZklVSFlwckVkSlNCRTlGM1pDNGZCRTFKX0k5VmJNdkg1NEpBSTBCNzNHNnlreVlTMDA?oc=5\", \"content\": \"앤스로픽發 AI 쇼크…SW·법률·광고 산업까지 흔들 - 마켓인\"}, {\"url\": \"https://news.google.com/rss/articles/CBMiTkFVX3lxTFBreXZmNEsxaXJiLXVPV2xhYkZrRGxDU0FBQXpoTjcyVXpJZFJlc0RDeVBTcXpiNThoUURZcGJQalRxcm9yNC1TYmxuSHJwUQ?oc=5\", \"content\": \"국가AI전략위-교육위, 현장과 연결된 AI 전환기 교육 정책 논의 - 전자신문\"}]\n", + "==================================\u001b[1m Ai Message \u001b[0m==================================\n", + "\n", + "최신 AI 관련 뉴스입니다:\n", + "\n", + "1. \"ABC 대 CBC… AI는 중국인 전쟁\" - 조선일보\n", + "2. \"'AI 수혜'의 배신…美증시 집어삼킨 '클로드 코워크' 뭐길래\" - 유니콘팩토리\n", + "3. \"AI 버블 우려보다 더 큰 충격…고평가 기술주에서 탈출 러시\" - 머니투데이\n", + "4. \"앤스로픽發 AI 쇼크…SW·법률·광고 산업까지 흔들\" - 마켓인\n", + "5. \"국가AI전략위-교육위, 현장과 연결된 AI 전환기 교육 정책 논의\" - 전자신문\n", + "\n", + "더 자세한 내용을 원하시면 특정 뉴스 제목을 알려주세요.\n" + ] + } + ], "source": [ "# `to_replay.config`는 `checkpoint_id`는 체크포인터에 저장된 상태에 해당\n", "for event in graph.stream(None, to_replay.config, stream_mode=\"values\"):\n", @@ -375,11 +590,29 @@ " # 마지막 메시지 출력\n", " event[\"messages\"][-1].pretty_print()" ] + }, + { + "cell_type": "markdown", + "id": "2wsob5r5djl", + "metadata": {}, + "source": [ + "## 정리\n", + "\n", + "이번 튜토리얼에서는 `interrupt_before`를 사용하여 human-in-the-loop 워크플로를 구현하는 방법을 알아보았습니다.\n", + "\n", + "**핵심 내용:**\n", + "- `compile()` 메서드의 `interrupt_before` 파라미터를 사용하여 특정 노드 실행 전에 그래프를 중단할 수 있습니다.\n", + "- `checkpointer`를 설정하면 중단된 상태가 저장되어 나중에 이어서 실행할 수 있습니다.\n", + "- `get_state()` 메서드로 현재 상태를 확인하고, `get_state_history()`로 전체 상태 기록을 조회할 수 있습니다.\n", + "- 특정 `checkpoint_id`를 지정하여 원하는 시점에서 그래프를 재시작할 수 있습니다.\n", + "\n", + "이를 통해 필요할 때 인간의 감독과 개입을 가능하게 하는 에이전트 시스템을 구축할 수 있습니다." + ] } ], "metadata": { "kernelspec": { - "display_name": "langchain-kr-lwwSZlnu-py3.11", + "display_name": "langgraph-v1-tutorial", "language": "python", "name": "python3" }, @@ -393,9 +626,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.11.13" } }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/08-Core-Features/assets/06-human-in-the-loop-graph.excalidraw b/08-Core-Features/assets/06-human-in-the-loop-graph.excalidraw new file mode 100644 index 0000000..35ae695 --- /dev/null +++ b/08-Core-Features/assets/06-human-in-the-loop-graph.excalidraw @@ -0,0 +1,471 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "langgraph-tutorial", + "elements": [ + { + "id": "start-node", + "type": "ellipse", + "x": 200, + "y": 50, + "width": 120, + "height": 50, + "angle": 0, + "strokeColor": "#6b7280", + "backgroundColor": "#f3f4f6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { "type": 2 }, + "seed": 1001, + "version": 1, + "versionNonce": 1001, + "isDeleted": false, + "boundElements": [ + { "type": "text", "id": "start-node-text" }, + { "type": "arrow", "id": "arrow-start-chatbot" } + ], + "updated": 1700000000000, + "link": null, + "locked": false + }, + { + "id": "start-node-text", + "type": "text", + "x": 220, + "y": 62, + "width": 80, + "height": 25, + "angle": 0, + "strokeColor": "#6b7280", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": null, + "seed": 1002, + "version": 1, + "versionNonce": 1002, + "isDeleted": false, + "boundElements": null, + "updated": 1700000000000, + "link": null, + "locked": false, + "text": "__start__", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "start-node", + "originalText": "__start__", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "chatbot-node", + "type": "rectangle", + "x": 185, + "y": 170, + "width": 150, + "height": 70, + "angle": 0, + "strokeColor": "#ca8a04", + "backgroundColor": "#fef08a", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": { "type": 3 }, + "seed": 1003, + "version": 1, + "versionNonce": 1003, + "isDeleted": false, + "boundElements": [ + { "type": "text", "id": "chatbot-node-text" }, + { "type": "arrow", "id": "arrow-start-chatbot" }, + { "type": "arrow", "id": "arrow-chatbot-tools" }, + { "type": "arrow", "id": "arrow-chatbot-end" }, + { "type": "arrow", "id": "arrow-tools-chatbot" } + ], + "updated": 1700000000000, + "link": null, + "locked": false + }, + { + "id": "chatbot-node-text", + "type": "text", + "x": 215, + "y": 192, + "width": 90, + "height": 25, + "angle": 0, + "strokeColor": "#ca8a04", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": null, + "seed": 1004, + "version": 1, + "versionNonce": 1004, + "isDeleted": false, + "boundElements": null, + "updated": 1700000000000, + "link": null, + "locked": false, + "text": "chatbot", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "chatbot-node", + "originalText": "chatbot", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "tools-node", + "type": "rectangle", + "x": 60, + "y": 320, + "width": 150, + "height": 70, + "angle": 0, + "strokeColor": "#0d9488", + "backgroundColor": "#ccfbf1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": { "type": 3 }, + "seed": 1005, + "version": 1, + "versionNonce": 1005, + "isDeleted": false, + "boundElements": [ + { "type": "text", "id": "tools-node-text" }, + { "type": "arrow", "id": "arrow-chatbot-tools" }, + { "type": "arrow", "id": "arrow-tools-chatbot" } + ], + "updated": 1700000000000, + "link": null, + "locked": false + }, + { + "id": "tools-node-text", + "type": "text", + "x": 105, + "y": 342, + "width": 60, + "height": 25, + "angle": 0, + "strokeColor": "#0d9488", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": null, + "seed": 1006, + "version": 1, + "versionNonce": 1006, + "isDeleted": false, + "boundElements": null, + "updated": 1700000000000, + "link": null, + "locked": false, + "text": "tools", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "tools-node", + "originalText": "tools", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "end-node", + "type": "ellipse", + "x": 310, + "y": 330, + "width": 120, + "height": 50, + "angle": 0, + "strokeColor": "#6b7280", + "backgroundColor": "#f3f4f6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a6", + "roundness": { "type": 2 }, + "seed": 1007, + "version": 1, + "versionNonce": 1007, + "isDeleted": false, + "boundElements": [ + { "type": "text", "id": "end-node-text" }, + { "type": "arrow", "id": "arrow-chatbot-end" } + ], + "updated": 1700000000000, + "link": null, + "locked": false + }, + { + "id": "end-node-text", + "type": "text", + "x": 340, + "y": 342, + "width": 60, + "height": 25, + "angle": 0, + "strokeColor": "#6b7280", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a7", + "roundness": null, + "seed": 1008, + "version": 1, + "versionNonce": 1008, + "isDeleted": false, + "boundElements": null, + "updated": 1700000000000, + "link": null, + "locked": false, + "text": "__end__", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "end-node", + "originalText": "__end__", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "arrow-start-chatbot", + "type": "arrow", + "x": 260, + "y": 100, + "width": 0, + "height": 70, + "angle": 0, + "strokeColor": "#6b7280", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a8", + "roundness": null, + "seed": 1009, + "version": 1, + "versionNonce": 1009, + "isDeleted": false, + "boundElements": null, + "updated": 1700000000000, + "link": null, + "locked": false, + "points": [[0, 0], [0, 70]], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "start-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0.5, 1] + }, + "endBinding": { + "elementId": "chatbot-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0.5, 0] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "arrow-chatbot-tools", + "type": "arrow", + "x": 220, + "y": 240, + "width": 85, + "height": 80, + "angle": 0, + "strokeColor": "#0d9488", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a9", + "roundness": null, + "seed": 1010, + "version": 1, + "versionNonce": 1010, + "isDeleted": false, + "boundElements": null, + "updated": 1700000000000, + "link": null, + "locked": false, + "points": [[0, 0], [-85, 80]], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "chatbot-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0.25, 1] + }, + "endBinding": { + "elementId": "tools-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0.5, 0] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "arrow-tools-chatbot", + "type": "arrow", + "x": 60, + "y": 355, + "width": 125, + "height": 150, + "angle": 0, + "strokeColor": "#ca8a04", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aA", + "roundness": null, + "seed": 1011, + "version": 1, + "versionNonce": 1011, + "isDeleted": false, + "boundElements": null, + "updated": 1700000000000, + "link": null, + "locked": false, + "points": [[0, 0], [-40, 0], [-40, -150], [125, -150]], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "tools-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0, 0.5] + }, + "endBinding": { + "elementId": "chatbot-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0, 0.5] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true + }, + { + "id": "arrow-chatbot-end", + "type": "arrow", + "x": 300, + "y": 240, + "width": 70, + "height": 90, + "angle": 0, + "strokeColor": "#6b7280", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aB", + "roundness": null, + "seed": 1012, + "version": 1, + "versionNonce": 1012, + "isDeleted": false, + "boundElements": null, + "updated": 1700000000000, + "link": null, + "locked": false, + "points": [[0, 0], [70, 90]], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "chatbot-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0.75, 1] + }, + "endBinding": { + "elementId": "end-node", + "focus": 0, + "gap": 1, + "fixedPoint": [0.5, 0] + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + } + ], + "appState": { + "theme": "light", + "viewBackgroundColor": "#ffffff", + "currentItemFontFamily": 1, + "gridSize": 20, + "gridStep": 5 + }, + "files": {} +} diff --git a/08-Core-Features/assets/06-human-in-the-loop-graph.png b/08-Core-Features/assets/06-human-in-the-loop-graph.png new file mode 100644 index 0000000..5173802 Binary files /dev/null and b/08-Core-Features/assets/06-human-in-the-loop-graph.png differ