From 683cd24e19b8c0185ae6a9f8166aa6718e887b46 Mon Sep 17 00:00:00 2001 From: pykido Date: Mon, 25 May 2026 01:39:21 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=202=EC=A3=BC=EC=B0=A8=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=9C=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assignments/pykido/week2/graph.png | Bin 0 -> 18448 bytes assignments/pykido/week2/graph.py | 110 +++++++ assignments/pykido/week2/run.ipynb | 461 +++++++++++++++++++++++++++++ assignments/pykido/week2/schema.py | 10 + assignments/pykido/week2/state.py | 7 + assignments/pykido/week2/tools.py | 302 +++++++++++++++++++ 6 files changed, 890 insertions(+) create mode 100644 assignments/pykido/week2/graph.png create mode 100644 assignments/pykido/week2/graph.py create mode 100644 assignments/pykido/week2/run.ipynb create mode 100644 assignments/pykido/week2/schema.py create mode 100644 assignments/pykido/week2/state.py create mode 100644 assignments/pykido/week2/tools.py diff --git a/assignments/pykido/week2/graph.png b/assignments/pykido/week2/graph.png new file mode 100644 index 0000000000000000000000000000000000000000..28ad3c081549b52b30d9b0f66f8163e4bd7a67c3 GIT binary patch literal 18448 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 dict: + messages = state["messages"] + if not any(isinstance(m, SystemMessage) for m in messages): + messages = [SystemMessage(content=SYSTEM_PROMPT), *messages] + return {"messages": [model_with_tools.invoke(messages)]} + + +def review_gate(state: AgentState) -> Command[Literal["tools", "agent"]]: + last = state["messages"][-1] + decision = interrupt( + { + "question": "풀이 리뷰를 어떻게 받을까요?", + "options": {"hint": "막힌 부분 힌트만", "full": "레퍼런스 기반 전체 리뷰"}, + } + ) + if str(decision) == "full": + return Command(goto="tools") + tool_messages = [ + ToolMessage(content=HINT_DIRECTIVE, tool_call_id=tc["id"], name=tc["name"]) + for tc in last.tool_calls + ] + return Command(goto="agent", update={"messages": tool_messages}) + + +def format_answer_node(state: AgentState) -> dict: + prompt = ( + "바로 앞 assistant 답변을 ReActAnswer 스키마로 정리하세요. " + "answer에는 그 답변 내용을 담되 직전 질문과 무관한 이전 추천·설명까지 다시 끌어오지는 마세요. " + "used_tools: 실제 호출한 도구만 / sources: 근거 ID·패턴 키 / " + "confidence: 도구 직접 근거 0.85+, 추측이면 ≤0.7" + ) + answer = structured_model.invoke([*state["messages"], HumanMessage(content=prompt)]) + return {"final_answer": answer.model_dump()} + + +def route_after_agent(state: AgentState) -> Literal["review_gate", "tools", "format_answer"]: + last = state["messages"][-1] + if not last.tool_calls: + return "format_answer" + if any(tc["name"] == "review_solution" for tc in last.tool_calls): + return "review_gate" + return "tools" + + +def build_graph(): + builder = StateGraph(AgentState) + builder.add_node("agent", agent_node) + builder.add_node("review_gate", review_gate) + builder.add_node("tools", ToolNode(TOOLS)) + builder.add_node("format_answer", format_answer_node) + + builder.add_edge(START, "agent") + builder.add_conditional_edges( + "agent", + route_after_agent, + {"review_gate": "review_gate", "tools": "tools", "format_answer": "format_answer"}, + ) + builder.add_edge("tools", "agent") + builder.add_edge("format_answer", END) + + return builder.compile(checkpointer=InMemorySaver()) + + +graph = build_graph() diff --git a/assignments/pykido/week2/run.ipynb b/assignments/pykido/week2/run.ipynb new file mode 100644 index 0000000..f62cbf3 --- /dev/null +++ b/assignments/pykido/week2/run.ipynb @@ -0,0 +1,461 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1a80aa88", + "metadata": {}, + "source": [ + "# Week 2: 알고리즘 코치에 State · Memory · Control 더하기\n", + "\n", + "1주차 알고리즘 코딩테스트 코치(`StateGraph` ReAct 그래프)를 그대로 가져와, 한 번 실행되고 끝나던 그래프를 상태가 유지되는 workflow로 확장했다.\n", + "\n", + "1주차 대비 달라진 점\n", + "\n", + "- 노트북 단일 파일을 `state.py` / `schema.py` / `tools.py` / `graph.py` 모듈로 분리 (1주차 PR 리뷰 반영)\n", + "- `InMemorySaver` checkpointer + `thread_id`로 대화 상태를 분리·복원\n", + "- 풀이 정답을 공개하기 직전 `interrupt()`로 멈춰 학습자가 *힌트만* 받을지 *전체 리뷰*를 받을지 선택 (HITL)\n", + "\n", + "도구 3개(`get_algorithm_pattern`, `recommend_problems`, `review_solution`)와 응답 스키마 `ReActAnswer`는 1주차와 동일하다." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d1065f35", + "metadata": { + "execution": { + "iopub.execute_input": "2026-05-24T16:23:15.320703Z", + "iopub.status.busy": "2026-05-24T16:23:15.320631Z", + "iopub.status.idle": "2026-05-24T16:23:15.970248Z", + "shell.execute_reply": "2026-05-24T16:23:15.969793Z" + } + }, + "outputs": [], + "source": [ + "from langchain_core.messages import HumanMessage\n", + "from langgraph.types import Command\n", + "\n", + "from graph import graph" + ] + }, + { + "cell_type": "markdown", + "id": "5f3ee543", + "metadata": {}, + "source": [ + "## 그래프 구조\n", + "\n", + "`agent`가 도구 호출 여부를 정하면 `route_after_agent`가 세 갈래로 나눈다.\n", + "\n", + "- 도구 호출 없음 → `format_answer` (구조화 응답)\n", + "- `review_solution` 호출 포함 → `review_gate` (HITL 검토)\n", + "- 그 외 도구 호출 → `tools` (`ToolNode`)\n", + "\n", + "`review_gate`는 `interrupt()`로 멈춘 뒤 선택에 따라 `tools`(전체 리뷰)나 `agent`(힌트만)로 `Command(goto=...)` 라우팅한다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "db7f285f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-05-24T16:23:15.971506Z", + "iopub.status.busy": "2026-05-24T16:23:15.971435Z", + "iopub.status.idle": "2026-05-24T16:23:15.974675Z", + "shell.execute_reply": "2026-05-24T16:23:15.974308Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Image\n", + "\n", + "Image(filename=\"graph.png\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "06676035", + "metadata": { + "execution": { + "iopub.execute_input": "2026-05-24T16:23:15.975950Z", + "iopub.status.busy": "2026-05-24T16:23:15.975859Z", + "iopub.status.idle": "2026-05-24T16:23:15.978669Z", + "shell.execute_reply": "2026-05-24T16:23:15.978344Z" + } + }, + "outputs": [], + "source": [ + "def show(state):\n", + " answer = state.get(\"final_answer\")\n", + " if not answer:\n", + " return\n", + " print(f\"used_tools={answer['used_tools']} sources={answer['sources']} confidence={answer['confidence']}\")\n", + " print(answer[\"answer\"])\n", + "\n", + "\n", + "def ask(question, thread_id):\n", + " config = {\"configurable\": {\"thread_id\": thread_id}}\n", + " print(f\"\\nQ [{thread_id}] {question}\")\n", + " print(\"-\" * 80)\n", + " state = graph.invoke({\"messages\": [HumanMessage(content=question)], \"final_answer\": None}, config)\n", + " if \"__interrupt__\" in state:\n", + " payload = state[\"__interrupt__\"][0].value\n", + " print(f\"INTERRUPT · {payload['question']} {payload['options']}\")\n", + " return\n", + " show(state)\n", + "\n", + "\n", + "def resume(choice, thread_id):\n", + " config = {\"configurable\": {\"thread_id\": thread_id}}\n", + " print(f\"resume({choice!r})\")\n", + " print(\"-\" * 80)\n", + " show(graph.invoke(Command(resume=choice), config))" + ] + }, + { + "cell_type": "markdown", + "id": "74fd914d", + "metadata": {}, + "source": [ + "## 1. 같은 `thread_id` — 대화 맥락을 기억한다\n", + "\n", + "`alice` thread 하나로 세 질문을 이어서 던진다. 두 번째·세 번째 질문은 앞 발화에서 다룬 문제를 직접 지칭하지 않고 \"방금 그 문제\", \"그거\"로만 가리킨다. checkpointer가 이전 메시지를 복원하므로 맥락이 이어진다." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9b313a44", + "metadata": { + "execution": { + "iopub.execute_input": "2026-05-24T16:23:15.980037Z", + "iopub.status.busy": "2026-05-24T16:23:15.979975Z", + "iopub.status.idle": "2026-05-24T16:23:38.985336Z", + "shell.execute_reply": "2026-05-24T16:23:38.984537Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Q [alice] binary-search 토픽에 Lv.3 문제 하나 추천해줘.\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "used_tools=['recommend_problems'] sources=['pgs-43238'] confidence=0.95\n", + "Lv.3 난이도의 binary-search 토픽 문제로 \"입국심사\" 문제를 추천합니다. 문제 링크: https://school.programmers.co.kr/learn/courses/30/lessons/43238. 문제 설명: n명과 각 입국 심사대 처리 시간 times[]가 주어질 때, 모두 심사하기 위한 최소 시간을 구하는 문제입니다. 답이 시간 t에 대해 단조성을 가지므로 parametric binary search 기법을 사용합니다.\n", + "\n", + "Q [alice] 방금 추천한 그 문제, 핵심 접근법만 한 줄로 알려줘.\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "used_tools=['binary-search'] sources=['pgs-43238', 'binary-search'] confidence=0.9\n", + "입국심사 문제의 핵심 접근법은, 모든 심사가 끝나는 최소 시간을 이분 탐색으로 찾되, 주어진 시간 t에 대해 심사 가능한 인원 수가 n명 이상인지 단조성을 이용해 판단하는 parametric binary search입니다.\n", + "\n", + "Q [alice] 그거 풀 때 자주 하는 실수도 짚어줘.\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "used_tools=['get_algorithm_pattern'] sources=['pgs-43238', 'binary-search'] confidence=0.7\n", + "\"입국심사\" 문제를 풀 때 자주 하는 실수는 다음과 같습니다:\n", + "\n", + "1. 이분 탐색 범위 설정 실수: 최소 시간(lo)을 0으로, 최대 시간(hi)을 충분히 큰 값(예: 가장 오래 걸리는 심사대 시간 × n)으로 정확히 잡지 않아 탐색이 제대로 이루어지지 않는 경우가 있습니다.\n", + "\n", + "2. 단조성 조건 판단 오류: 주어진 시간 t에 대해 심사 가능한 인원 수를 계산할 때, n명 이상인지 정확히 비교하지 않거나, 등호 포함 여부를 헷갈려 답이 틀리는 경우가 있습니다.\n", + "\n", + "3. 이분 탐색 종료 조건 혼동: lo <= hi 구간을 정확히 유지하지 않고 lo < hi로 하여 무한 루프나 오답이 발생할 수 있습니다.\n", + "\n", + "4. 시간 초과 주의: 심사 가능한 인원 수 계산 시 반복문을 잘못 써서 시간 복잡도가 커지는 실수를 합니다. (times 배열을 한 번만 순회하며 계산해야 합니다.)\n", + "\n", + "이 점들 유의하면 문제 해결에 도움이 됩니다.\n" + ] + } + ], + "source": [ + "ask(\"binary-search 토픽에 Lv.3 문제 하나 추천해줘.\", \"alice\")\n", + "ask(\"방금 추천한 그 문제, 핵심 접근법만 한 줄로 알려줘.\", \"alice\")\n", + "ask(\"그거 풀 때 자주 하는 실수도 짚어줘.\", \"alice\")" + ] + }, + { + "cell_type": "markdown", + "id": "5881c0f4", + "metadata": {}, + "source": [ + "## 2. 다른 `thread_id` — 맥락이 분리된다\n", + "\n", + "`bob` thread는 위 대화를 모른다. `alice`에서 추천받은 문제를 가리켜도 무슨 문제인지 알 수 없어, 어떤 문제인지 되묻는다." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a14eef1d", + "metadata": { + "execution": { + "iopub.execute_input": "2026-05-24T16:23:38.987754Z", + "iopub.status.busy": "2026-05-24T16:23:38.987500Z", + "iopub.status.idle": "2026-05-24T16:23:41.673952Z", + "shell.execute_reply": "2026-05-24T16:23:41.673257Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Q [bob] 방금 내가 추천받은 그 문제 번호가 뭐였지? 그거 다시 복습하고 싶어.\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "used_tools=[] sources=[] confidence=0.7\n", + "제가 이전 대화 내용을 확인해보니, 아직 문제 추천을 해드린 적이 없습니다. 어떤 주제나 난이도의 문제를 복습하고 싶으신지 알려주시면, 그에 맞는 문제를 추천해드리고 복습을 도와드릴게요!\n" + ] + } + ], + "source": [ + "ask(\"방금 내가 추천받은 그 문제 번호가 뭐였지? 그거 다시 복습하고 싶어.\", \"bob\")" + ] + }, + { + "cell_type": "markdown", + "id": "de4bce76", + "metadata": {}, + "source": [ + "## 3. interrupt (HITL) — 정답 공개 전에 멈춘다 · 전체 리뷰\n", + "\n", + "풀이 리뷰를 요청하면 `review_gate`가 `review_solution` 호출 직전에 멈추고 선택지를 제시한다. `full`로 재개하면 레퍼런스 rubric을 받아 전체 리뷰를 작성한다." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "8d2e3c91", + "metadata": { + "execution": { + "iopub.execute_input": "2026-05-24T16:23:41.675255Z", + "iopub.status.busy": "2026-05-24T16:23:41.675158Z", + "iopub.status.idle": "2026-05-24T16:23:49.044667Z", + "shell.execute_reply": "2026-05-24T16:23:49.043628Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Q [review-full] pgs-43238 입국심사 풀이 리뷰해줘.\n", + "\n", + "def solution(n, times):\n", + " t = 0\n", + " while True:\n", + " t += 1\n", + " if sum(t // x for x in times) >= n:\n", + " return t\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INTERRUPT · 풀이 리뷰를 어떻게 받을까요? {'hint': '막힌 부분 힌트만', 'full': '레퍼런스 기반 전체 리뷰'}\n", + "resume('full')\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "used_tools=['review_solution'] sources=['pgs-43238', 'binary-search'] confidence=0.9\n", + "사용자 코드는 시간 t를 1씩 증가시키며 처리 가능한 인원 수를 계산하는 선형 탐색 방식으로, n이 매우 클 경우 시간 초과가 발생합니다. 문제의 핵심은 처리 인원 수가 시간에 대해 단조 증가하므로 이분 탐색으로 최소 시간을 찾는 것입니다. 따라서 효율적인 풀이를 위해서는 이분 탐색을 적용해 탐색 범위를 좁히는 방식으로 개선해야 합니다.\n" + ] + } + ], + "source": [ + "review_code = '''pgs-43238 입국심사 풀이 리뷰해줘.\n", + "\n", + "def solution(n, times):\n", + " t = 0\n", + " while True:\n", + " t += 1\n", + " if sum(t // x for x in times) >= n:\n", + " return t\n", + "'''\n", + "\n", + "ask(review_code, \"review-full\")\n", + "resume(\"full\", \"review-full\")" + ] + }, + { + "cell_type": "markdown", + "id": "1b92ccd9", + "metadata": {}, + "source": [ + "## 4. interrupt (HITL) — 힌트만\n", + "\n", + "같은 요청을 `hint`로 재개하면 `review_solution`을 실행하지 않고 정답 접근을 감춘 채 방향성 힌트만 준다. `used_tools`가 비어 있는 것으로 레퍼런스를 열지 않았음을 확인할 수 있다." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "97201c00", + "metadata": { + "execution": { + "iopub.execute_input": "2026-05-24T16:23:49.047765Z", + "iopub.status.busy": "2026-05-24T16:23:49.047543Z", + "iopub.status.idle": "2026-05-24T16:23:55.023780Z", + "shell.execute_reply": "2026-05-24T16:23:55.023375Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Q [review-hint] pgs-43238 입국심사 풀이 리뷰해줘.\n", + "\n", + "def solution(n, times):\n", + " t = 0\n", + " while True:\n", + " t += 1\n", + " if sum(t // x for x in times) >= n:\n", + " return t\n", + "\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INTERRUPT · 풀이 리뷰를 어떻게 받을까요? {'hint': '막힌 부분 힌트만', 'full': '레퍼런스 기반 전체 리뷰'}\n", + "resume('hint')\n", + "--------------------------------------------------------------------------------\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "used_tools=[] sources=['pgs-43238'] confidence=0.7\n", + "입국심사 문제는 이분 탐색을 활용하는 것이 핵심입니다. 현재 코드는 시간을 1씩 늘리며 처리 가능한 사람 수를 계산하는데, 이는 n과 times가 클 때 매우 비효율적입니다. 힌트로는 (1) 시간을 1씩 증가시키지 말고 가능한 시간 범위를 이분 탐색으로 좁혀가면서 최소 시간을 찾아보세요. (2) 중간 시간에서 처리 가능한 사람 수를 계산해 n과 비교하며 탐색 범위를 조절하는 방식을 생각해보세요.\n" + ] + } + ], + "source": [ + "ask(review_code, \"review-hint\")\n", + "resume(\"hint\", \"review-hint\")" + ] + }, + { + "cell_type": "markdown", + "id": "5522519c", + "metadata": {}, + "source": [ + "## 5. State history — 누적된 스냅샷\n", + "\n", + "`alice` thread는 매 superstep마다 상태 스냅샷이 쌓인다. time travel과 디버깅의 출발점이다." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "fc520d2e", + "metadata": { + "execution": { + "iopub.execute_input": "2026-05-24T16:23:55.025250Z", + "iopub.status.busy": "2026-05-24T16:23:55.025145Z", + "iopub.status.idle": "2026-05-24T16:23:55.029441Z", + "shell.execute_reply": "2026-05-24T16:23:55.029014Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "alice thread snapshots: 16\n", + "누적 메시지 수: 10\n" + ] + } + ], + "source": [ + "history = list(graph.get_state_history({\"configurable\": {\"thread_id\": \"alice\"}}))\n", + "print(f\"alice thread snapshots: {len(history)}\")\n", + "print(f\"누적 메시지 수: {len(history[0].values['messages'])}\")" + ] + }, + { + "cell_type": "markdown", + "id": "63cb861c", + "metadata": {}, + "source": [ + "## 정리\n", + "\n", + "- 같은 `thread_id`는 앞 발화를 복원해 \"방금 그 문제\" 같은 지시어를 해석하고, 다른 `thread_id`는 맥락이 분리된다.\n", + "- 제어 패턴으로 **interrupt(HITL)** 를 골랐다. 학습 코치 도메인에서 *정답 공개*는 되돌릴 수 없는 행동이라, 그 직전에 학습자가 개입할 지점을 두는 것이 자연스럽기 때문이다. 단순 패턴 조회·문제 추천은 멈추지 않는다.\n", + "- `Command(resume=...)`로 같은 thread에서 이어 실행하며, `full`/`hint` 선택이 이후 그래프 경로(`tools` vs `agent`)를 바꾼다." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/assignments/pykido/week2/schema.py b/assignments/pykido/week2/schema.py new file mode 100644 index 0000000..ef4beaf --- /dev/null +++ b/assignments/pykido/week2/schema.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel, Field + + +class ReActAnswer(BaseModel): + """ReAct agent의 최종 답변 형식.""" + + answer: str = Field(description="한국어 마크다운 답변. 코드/패턴 키는 영어 원형.") + used_tools: list[str] = Field(default_factory=list, description="실제 호출된 도구 이름들.") + sources: list[str] = Field(default_factory=list, description="근거 식별자 (예: 'pgs-43238', 'binary-search').") + confidence: float = Field(ge=0.0, le=1.0, description="도구 결과 직접 근거면 0.85+, 추측 섞이면 ≤0.7.") diff --git a/assignments/pykido/week2/state.py b/assignments/pykido/week2/state.py new file mode 100644 index 0000000..8d4c5fd --- /dev/null +++ b/assignments/pykido/week2/state.py @@ -0,0 +1,7 @@ +from typing import Optional + +from langgraph.graph import MessagesState + + +class AgentState(MessagesState): + final_answer: Optional[dict] diff --git a/assignments/pykido/week2/tools.py b/assignments/pykido/week2/tools.py new file mode 100644 index 0000000..bb2bb79 --- /dev/null +++ b/assignments/pykido/week2/tools.py @@ -0,0 +1,302 @@ +from langchain_core.tools import tool + + +PROBLEMS: dict[str, dict] = { + "pgs-43238": { + "id": "pgs-43238", + "title": "입국심사", + "platform": "Programmers", + "level": "Lv.3", + "topics": ["binary-search"], + "url": "https://school.programmers.co.kr/learn/courses/30/lessons/43238", + "description": ( + "n명과 각 입국 심사대 처리 시간 times[]가 주어질 때, 모두 심사하기 위한 최소 시간을 구하는 문제. " + "답이 시간 t에 대해 단조 → parametric binary search." + ), + "reference_approach": ( + "f(t) = sum(t // x for x in times)는 t에 대해 단조 증가. f(t) >= n 인 최소 t를 이분 탐색." + ), + "reference_complexity": "O(M log(max(times) * n)), M = len(times)", + "key_checkpoints": [ + "lo=1, hi=max(times)*n 으로 범위를 충분히 크게", + "단조성을 명시 (시간↑ → 처리 인원↑)", + "f(mid) >= n 일 때 hi=mid-1 (lower bound)", + ], + "common_pitfalls": [ + "lo=0 이면 무한 루프", + "hi=max(times) 만 두면 사람 수 많을 때 답 못 찾음", + "O(N) 선형 탐색은 N=10^9 스케일에서 TLE", + ], + }, + "pgs-43236": { + "id": "pgs-43236", + "title": "징검다리", + "platform": "Programmers", + "level": "Lv.4", + "topics": ["binary-search"], + "url": "https://school.programmers.co.kr/learn/courses/30/lessons/43236", + "description": "바위 좌표가 주어지고 n개를 제거할 수 있을 때 가장 짧은 점프 거리의 최댓값. parametric search.", + "reference_approach": "최소 점프 거리 d 이분 탐색. f(d) = 거리 d 미만이 되는 인접 쌍을 그리디 제거 횟수.", + "reference_complexity": "O(R log(distance)), R = len(rocks)", + "key_checkpoints": [ + "바위 정렬", + "제거 시뮬레이션을 그리디로 (마지막 위치 추적)", + "upper bound 형태 — 답=lo-1 또는 hi 처리", + ], + "common_pitfalls": [ + "distance 배열을 정렬하는 잘못된 접근 (단조성 깨짐)", + "d 범위를 좌표값이 아닌 인덱스로 잡음", + ], + }, + "pgs-43165": { + "id": "pgs-43165", + "title": "타겟 넘버", + "platform": "Programmers", + "level": "Lv.2", + "topics": ["bfs-dfs"], + "url": "https://school.programmers.co.kr/learn/courses/30/lessons/43165", + "description": "각 원소 앞에 +/-를 붙여 target을 만드는 경우의 수. DFS/백트래킹 입문.", + "reference_approach": "각 i에서 +/- 이진 분기 DFS. i==len 도달 시 누적합==target 검사.", + "reference_complexity": "O(2^N)", + "key_checkpoints": [ + "base case (i == len(numbers))", + "+ 와 - 두 분기 모두 호출", + "DP 메모이제이션 ((i, sum)) 도 가능", + ], + "common_pitfalls": [ + "DFS를 BFS로 짜며 메모리 폭발", + "누적합을 list append/pop 부수효과로", + ], + }, + "pgs-118667": { + "id": "pgs-118667", + "title": "두 큐 합 같게 만들기", + "platform": "Programmers", + "level": "Lv.2", + "topics": ["two-pointers", "queue"], + "url": "https://school.programmers.co.kr/learn/courses/30/lessons/118667", + "description": "두 큐 합을 같게 만드는 최소 작업 횟수. pop/insert를 두 포인터처럼.", + "reference_approach": "deque 1개로 합쳐 보고 합이 큰 쪽에서 pop → 작은 쪽에 push. 종료 상한 4*N.", + "reference_complexity": "O(N)", + "key_checkpoints": [ + "전체 합 홀수면 -1", + "두 합을 O(1) 로 갱신 (재계산 X)", + "4*N 초과 시 -1", + ], + "common_pitfalls": [ + "매 step sum() 호출로 O(N^2) → TLE", + "list pop(0) 으로 O(N^2)", + "종료 조건 누락", + ], + }, + "pgs-67258": { + "id": "pgs-67258", + "title": "보석 쇼핑", + "platform": "Programmers", + "level": "Lv.3", + "topics": ["sliding-window", "hash-map"], + "url": "https://school.programmers.co.kr/learn/courses/30/lessons/67258", + "description": "모든 종류의 보석을 포함하는 가장 짧은 연속 구간. 가변 슬라이딩 윈도우.", + "reference_approach": "left/right 두 포인터로 윈도우 늘리며 dict 카운트 갱신, 모든 종류 포함되면 left 줄여 최소화.", + "reference_complexity": "O(N)", + "key_checkpoints": [ + "전체 보석 종류 수 미리 계산 (set)", + "left 이동 시 dict 카운트 0이면 키 삭제", + "답 [s+1, e+1] 1-index 반환", + ], + "common_pitfalls": [ + "left 이동을 if가 아닌 while로 좁혀야 최소 윈도우", + ], + }, + "pgs-42898": { + "id": "pgs-42898", + "title": "등굣길", + "platform": "Programmers", + "level": "Lv.3", + "topics": ["dp"], + "url": "https://school.programmers.co.kr/learn/courses/30/lessons/42898", + "description": "M x N 격자에서 (1,1)→(M,N) 오른쪽/아래만 이동, 물웅덩이 피해 가는 경로 수 (mod 1e9+7).", + "reference_approach": "dp[r][c] = dp[r-1][c] + dp[r][c-1]. 물웅덩이는 0. 매 갱신마다 mod.", + "reference_complexity": "O(M * N)", + "key_checkpoints": [ + "dp 인덱싱 (1-based vs 0-based) 일관성", + "물웅덩이를 갱신 전에 0", + "매 갱신마다 mod", + ], + "common_pitfalls": [ + "DFS 재귀로 짜면 메모이제이션 없으면 지수", + ], + }, +} + +PATTERNS: dict[str, dict] = { + "binary-search": { + "name_en": "Binary Search", + "name_ko": "이분 탐색", + "when_to_use": "정렬 배열에서 값 찾기, 또는 답이 단조성을 가지는 최적화 (parametric search).", + "complexity": "O(log N)", + "template": ( + "def binary_search(arr, target):\n" + " lo, hi = 0, len(arr) - 1\n" + " while lo <= hi:\n" + " mid = (lo + hi) // 2\n" + " if arr[mid] == target: return mid\n" + " elif arr[mid] < target: lo = mid + 1\n" + " else: hi = mid - 1\n" + " return -1" + ), + "common_pitfalls": [ + "lo <= hi vs lo < hi 혼동 (전자가 닫힌 구간에 안전)", + "parametric search 단조성 증명 누락", + "lower/upper bound 모호 → bisect 사용", + ], + }, + "sliding-window": { + "name_en": "Sliding Window", + "name_ko": "슬라이딩 윈도우", + "when_to_use": "연속 부분배열/문자열의 통계. 가변 크기는 'longest/shortest with X' 패턴.", + "complexity": "O(N)", + "template": ( + "def longest_unique_substring(s):\n" + " seen = {}\n" + " left = best = 0\n" + " for right, c in enumerate(s):\n" + " if c in seen and seen[c] >= left:\n" + " left = seen[c] + 1\n" + " seen[c] = right\n" + " best = max(best, right - left + 1)\n" + " return best" + ), + "common_pitfalls": [ + "left 갱신 누락으로 윈도우 안 좁혀짐", + "빠지는 원소 통계 미반영 (해시맵 누수)", + ], + }, + "two-pointers": { + "name_en": "Two Pointers", + "name_ko": "투 포인터", + "when_to_use": "정렬 배열에서 양 끝에서 좁혀가며 쌍/구간 찾기, 또는 동방향 다른 속도.", + "complexity": "O(N)", + "template": ( + "def two_sum_sorted(arr, target):\n" + " lo, hi = 0, len(arr) - 1\n" + " while lo < hi:\n" + " s = arr[lo] + arr[hi]\n" + " if s == target: return (lo, hi)\n" + " elif s < target: lo += 1\n" + " else: hi -= 1\n" + " return None" + ), + "common_pitfalls": [ + "정렬 안 된 배열에 적용", + "포인터 이동 조건 반전으로 무한 루프", + ], + }, + "bfs-dfs": { + "name_en": "BFS / DFS", + "name_ko": "너비/깊이 우선 탐색", + "when_to_use": "그래프/격자 도달 가능, 가중치 1 최단 거리 (BFS), 연결 요소.", + "complexity": "O(V + E)", + "template": ( + "from collections import deque\n" + "def bfs(graph, start):\n" + " visited = {start}\n" + " q = deque([start])\n" + " while q:\n" + " node = q.popleft()\n" + " for nxt in graph[node]:\n" + " if nxt not in visited:\n" + " visited.add(nxt); q.append(nxt)" + ), + "common_pitfalls": [ + "방문 처리를 pop 시점에 해서 중복 push", + "DFS 재귀 한도(1000) 초과 → sys.setrecursionlimit", + ], + }, + "dp": { + "name_en": "Dynamic Programming", + "name_ko": "동적 계획법", + "when_to_use": "최적 부분 구조 + 중복 부분 문제. 부분 답을 재사용.", + "complexity": "보통 상태 수 × 전이 비용", + "template": ( + "# bottom-up 격자 DP\n" + "def grid_paths(m, n, blocked):\n" + " dp = [[0]*(n+1) for _ in range(m+1)]\n" + " dp[1][1] = 0 if (1,1) in blocked else 1\n" + " for r in range(1, m+1):\n" + " for c in range(1, n+1):\n" + " if (r, c) in blocked: dp[r][c] = 0; continue\n" + " if (r, c) != (1, 1):\n" + " dp[r][c] = dp[r-1][c] + dp[r][c-1]\n" + " return dp[m][n]" + ), + "common_pitfalls": [ + "top-down에서 메모이제이션 누락", + "상태 정의 모호 (필요 변수 누락)", + "mod 연산 누락", + ], + }, +} + + +@tool +def get_algorithm_pattern(pattern_name: str) -> dict: + """알고리즘 패턴의 설명/시간복잡도/템플릿/실수 모음을 조회한다. + + Args: + pattern_name: 'binary-search', 'sliding-window', 'two-pointers', 'bfs-dfs', 'dp' 중 하나. + """ + if pattern_name not in PATTERNS: + return {"error": f"pattern '{pattern_name}' not found", "available_patterns": list(PATTERNS)} + return PATTERNS[pattern_name] + + +@tool +def recommend_problems( + topic: str | None = None, + level: str | None = None, + problem_id: str | None = None, +) -> list[dict]: + """프로그래머스 문제를 토픽/레벨로 추천하거나 ID로 단건 조회한다. + + Args: + topic: 토픽 키 ('binary-search', 'sliding-window', 'two-pointers', 'bfs-dfs', 'dp', 'hash-map', 'queue' 등). + level: 'Lv.2', 'Lv.3' 등 레벨 부분 문자열. + problem_id: 'pgs-<번호>' 지정 시 단건 조회 (다른 필터 무시). + """ + def project(p): + return {k: p[k] for k in ("id", "title", "level", "topics", "url", "description")} + + if problem_id is not None: + return [project(PROBLEMS[problem_id])] if problem_id in PROBLEMS else [] + return [ + project(p) for p in PROBLEMS.values() + if (not topic or topic in p["topics"]) + and (not level or level.lower() in p["level"].lower()) + ] + + +@tool +def review_solution(problem_id: str, user_code: str) -> dict: + """풀이 코드 리뷰용 reference rubric을 가져온다. + + 이 도구는 코드를 실행하지 않는다. 정답 접근법/복잡도/체크포인트/실수 목록을 반환하므로 + 호출자(=agent)가 user_code와 rubric을 비교해 직접 리뷰를 작성한다. + """ + if problem_id not in PROBLEMS: + return {"error": f"problem_id '{problem_id}' not found", "available_ids": list(PROBLEMS)} + p = PROBLEMS[problem_id] + return { + "problem_id": p["id"], + "title": p["title"], + "level": p["level"], + "topics": p["topics"], + "reference_approach": p["reference_approach"], + "reference_complexity": p["reference_complexity"], + "key_checkpoints": p["key_checkpoints"], + "common_pitfalls": p["common_pitfalls"], + "user_code_excerpt": user_code[:600], + } + + +TOOLS = [get_algorithm_pattern, recommend_problems, review_solution]