{ "cells": [ { "cell_type": "markdown", "id": "69ad077f-af4e-49b8-b549-fb3112299aa8", "metadata": {}, "source": [ "# Competitive Programming\n", "\n", "In this tutorial, you will build a computing olympiad agent that leverages three complementary techniques to boost performance: **reflection**, **retrieval**, and **human-in-the-loop** collaboration. These techniques and data are all adapted from the paper \"Can Language Models Solve Olympiad Programming?\" by Quan Shi, Michael Tang, Karthik Narasimhan, and Shunyu Yao. You can check out their paper at the following link:\n", "\n", "[![arXiv](http://img.shields.io/badge/cs.CL-arXiv%3A2404.10952v1-B31B1B.svg)](https://arxiv.org/abs/2404.10952v1)\n", "\n", "You will construct an agentic graph capable of answering programming questions of increasing difficulty.\n", "\n", "1. **Reflection**: In part 1, you will create a zero-shot tool calling agent and prompt it to reflect on the test case results to correct its initial errors. This is similar to the agent the paper reported as having a pass rate of 12.38 on the USACO benchmark.\n", "2. **Retrieval**: In Part 2, you will implement an initial retrieval step as \"episodic memory\" for the agent that retrieves high-quality few-shot examples from our corpora of programming problems to help solve the **bronze** level question. This agent is similar to the one the paper benchmarked at 20.2.\n", "3. **Human-in-the-loop**: In part 3, you will use `interrupt_after` to let the user copilot the agent to a better answer. The benchmark performance then is constrained only by the competitiveness of the human it is paired with.\n", "\n", "Your final agent graph will be structured like the diagram below:\n", "\n", "![diagram](./img/diagram.png)\n", "\n", "Parts 1 and 2 are analogous to the systems benchmarked in the paper as having a pass rate of 12.38 and 20.2 respectively.\n", "\n", "![Benchmark system results](./img/benchmark.png)\n", "\n", "\n", "While LLMs are not yet capable of autonomously solving all these problems, we can design the system that far surpasses the capabilities of a basic ReAct agent at answering these questions. \n", "\n", "Before diving in, let's set up our machine. This will involve installing dependencies, fetching the dataset, and defining a utility function.\n", "\n", "## Setup\n", "\n", "For this tutorial, we will need to install some dependencies, fetch the Olympiad dataset, and define a utility function to help run the candidate solutions to see if they pass the test cases.\n", "\n", "First, install the requirements." ] }, { "cell_type": "code", "execution_count": 1, "id": "c686827a-8078-4fd4-af7a-638ca1362796", "metadata": {}, "outputs": [], "source": ["%%capture --no-stderr\n%pip install -U langgraph langsmith langchain_anthropic datasets langchain langchainhub"] }, { "cell_type": "code", "execution_count": 2, "id": "e2e542bb-a99e-44d3-8ebb-6a952dcbf2bf", "metadata": {}, "outputs": [], "source": ["import getpass\nimport os\n\n\ndef _get_env(var: str):\n if not os.environ.get(var):\n os.environ[var] = getpass.getpass(f\"{var}: \")\n\n\n_get_env(\"ANTHROPIC_API_KEY\")\n# Recommended\n_get_env(\"LANGCHAIN_API_KEY\")\nos.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\""] }, { "cell_type": "markdown", "id": "c86ebbeb-c070-45d4-99ef-de33b53d447d", "metadata": {}, "source": [ "#### Data\n", "\n", "Fetch the USACO benchmark data using the util below:" ] }, { "cell_type": "code", "execution_count": 3, "id": "f7a0c7bd-512d-4e5b-ab43-1bc3b8c97fd4", "metadata": {}, "outputs": [], "source": ["import os\nimport zipfile\n\nimport datasets\nimport requests\n\nusaco_url = \"https://storage.googleapis.com/benchmarks-artifacts/usaco/usaco_sampled_with_tests.zip\"\nzip_path = \"usaco.zip\"\nextract_path = \"usaco_datasets\"\n\nresponse = requests.get(usaco_url)\nwith open(zip_path, \"wb\") as file:\n file.write(response.content)\n\nwith zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n zip_ref.extractall(extract_path)\n\nos.remove(zip_path)\n\nds = datasets.load_from_disk(os.path.join(extract_path, \"usaco_v3_sampled_with_tests\"))"] }, { "cell_type": "markdown", "id": "2e4ad035-2c1a-4e31-93b4-58793d219bc9", "metadata": {}, "source": [ "#### Test Evaluation Utils\n", "\n", "We also need a way to evaluate our generated code. We will use this unsafe code execution program to run the generated code against our test cases.\n", "**Note:** The code below runs arbitrary code on your local machine! Proceed with caution." ] }, { "cell_type": "code", "execution_count": 4, "id": "54f9d037-121e-412f-857a-3e0ccc73892e", "metadata": {}, "outputs": [], "source": ["import multiprocessing\nimport queue\nimport subprocess\nimport sys\nimport time\nimport traceback\n\nmultiprocessing.set_start_method(\"fork\", force=True)\n# WARNING\n# This program exists to execute untrusted model-generated code. Although\n# it is highly unlikely that model-generated code will do something overtly\n# malicious in response to this test suite, model-generated code may act\n# destructively due to a lack of model capability or alignment.\n# Users are strongly encouraged to sandbox this evaluation suite so that it\n# does not perform destructive actions on their host or network.\n# Proceed at your own risk:\n\n\ndef exec_program(q, program, input_data, expected_output, timeout):\n try:\n start_time = time.time()\n process = subprocess.Popen(\n [sys.executable, \"-c\", program],\n stdin=subprocess.PIPE,\n stdout=subprocess.PIPE,\n stderr=subprocess.PIPE,\n text=True,\n )\n stdout, stderr = process.communicate(input=input_data, timeout=timeout)\n if time.time() - start_time > timeout:\n raise TimeoutError(\"Execution timed out.\")\n if process.returncode != 0:\n q.put(f\"failed: {stderr}\")\n else:\n if stdout.strip() == expected_output.strip():\n q.put(\"passed\")\n else:\n q.put(f\"wrong answer. Expected '{expected_output}', got '{stdout}'\")\n except subprocess.TimeoutExpired:\n process.kill()\n q.put(\"timed out\")\n except Exception:\n q.put(f\"failed: {traceback.format_exc()}\")\n\n\ndef check_correctness(\n program: str, input_data: str, expected_output: str, timeout: float\n) -> str:\n q = multiprocessing.Queue()\n process = multiprocessing.Process(\n target=exec_program, args=(q, program, input_data, expected_output, timeout)\n )\n process.start()\n process.join(timeout=timeout + 1)\n if process.is_alive():\n process.terminate()\n process.join()\n result = \"timed out\"\n else:\n try:\n result = q.get_nowait()\n except queue.Empty:\n result = \"no result returned\"\n return result"] }, { "cell_type": "markdown", "id": "1e799866-6334-4c3a-8d03-7b7b1cf730ab", "metadata": {}, "source": [ "Let's check an example program and output to see how it works:" ] }, { "cell_type": "code", "execution_count": 5, "id": "411cfc6a-d430-4642-8f48-2d6335430dd9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Example 1: passed\n", "Example 2: wrong answer. Expected 'hi there', got 'goodbye\n", "'\n" ] } ], "source": ["program_code = \"print('hello, world!')\"\ninput_data = \"\"\nexpected_output = \"hello, world!\"\ntimeout = 2\n\ntest_result = check_correctness(program_code, input_data, expected_output, timeout)\nprint(\"Example 1: \", test_result)\ntest_result = check_correctness(\"print('goodbye')\", input_data, \"hi there\", timeout)\nprint(\"Example 2: \", test_result)"] }, { "cell_type": "markdown", "id": "1d5da52f-f804-47b9-989a-25a1b2f461f6", "metadata": {}, "source": [ "## Part 1: Zero-Shot with Reflection\n", "\n", "In our first section, we will build a simple zero-shot tool-calling agent to try to solve these problems. We will incorporate a simple form of [reflection](https://www.youtube.com/watch?v=v5ymBTXNqtk) directly in the agent's tool calling schema by adding a \"reasoning\" field. Furthermore, Claude was trained to \"reason\" with freeform text prior to invoking any tools. Together, this should induce reflective \"chain-of-thought\" prompting.\n", "\n", "_Note: this diverges somewhat from the paper's implementation, which uses an explicit reflection step with a variation of the [Reflexion](../reflexion/reflexion.ipynb) prompt._\n", "\n", "By the end of this section, we will have built a reflective zero-shot programming agent that looks like the section marked \"Part 1\" in the system diagram below:\n", "\n", "![Part 1 diagram](./img/diagram-part-1.png)\n" ] }, { "cell_type": "markdown", "id": "00f91dac-d13b-4221-be1b-9254ca849c8d", "metadata": {}, "source": [ "### State\n", "\n", "LangGraph's main primitive is the `StateGraph`, which you use to define an agent as a controllable state machine. The graph has `node`'s (python functions) that perform the work, and `edge`s that define how to route between the nodes.\n", "The `State` defines the interface between each node and carries all the information your agent needs.\n", "\n", "Below, define a `State` for our programming olympiad agent. The `messages` will track the sequence of submissions (and test case feedback) as chat history. The `status` field will flip from `in_progress` to `success` if the submission passes all test cases.\n", "The other fields (test_cases, runtime_limit) are used by the `evaluation` node to test the agent's submissions. These values are not seen by the agent itself." ] }, { "cell_type": "code", "execution_count": 8, "id": "f43d68d9-10be-4544-879a-88a33db18bea", "metadata": {}, "outputs": [], "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass TestCase(TypedDict):\n inputs: str\n outputs: str\n\n\nclass State(TypedDict):\n # Append-only chat memory so the agent can try to recover from initial mistakes.\n messages: Annotated[list[AnyMessage], add_messages]\n # From the dataset. These are used for testing.\n test_cases: list[TestCase]\n runtime_limit: int\n status: str"] }, { "cell_type": "markdown", "id": "64921a60-411a-4a9b-aaf0-8476d02d8a3a", "metadata": {}, "source": [ "Now, convert the dataset into inputs our graph will accept." ] }, { "cell_type": "code", "execution_count": 6, "id": "6d56776f-993b-4ca7-89ef-21dec01dc9d3", "metadata": {}, "outputs": [], "source": ["input_states = [\n {\n \"messages\": [(\"user\", row[\"description\"])],\n \"test_cases\": row[\"test_cases\"],\n \"runtime_limit\": row[\"runtime_limit\"],\n \"status\": \"in_progress\",\n \"problem_level\": row[\"problem_level\"],\n }\n for row in ds\n]"] }, { "cell_type": "markdown", "id": "9e7883ee-b6b3-4d89-b5a5-2e139fe361c9", "metadata": {}, "source": [ "#### Node 1: Solver\n", "\n", "Create a `solver` node that prompts an LLM \"agent\" to use a [writePython tool](https://python.langchain.com/v0.2/docs/integrations/chat/anthropic/#integration-details) to generate the submitted code." ] }, { "cell_type": "code", "execution_count": 9, "id": "7b9e7742-16a3-4ad2-bc63-5f9cd4fd734b", "metadata": {}, "outputs": [], "source": ["from langchain_core.language_models import BaseChatModel\nfrom langchain_core.prompts import ChatPromptTemplate\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass writePython(BaseModel):\n \"\"\"Write python code that resolves the problem.\"\"\"\n\n reasoning: str = Field(..., description=\"Conceptual solution.\")\n pseudocode: str = Field(..., description=\"Detailed English pseudocode.\")\n code: str = Field(..., description=\"Valid Python 3 solution to the problem\")\n\n\nclass Solver:\n def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n self.runnable = prompt | llm.bind_tools([writePython])\n\n def __call__(self, state: State) -> dict:\n # Our agent only can see the \"messages\" and will ignore the test info\n return {\"messages\": [self.runnable.invoke({\"messages\": state[\"messages\"]})]}"] }, { "cell_type": "markdown", "id": "c22eaa6d-36ae-4526-9b80-49d615fc055c", "metadata": {}, "source": [ "Now, create the solver below. We'll use Claude Opus" ] }, { "cell_type": "code", "execution_count": 10, "id": "6cc472f1-b9b3-4f81-a797-c64704bb07d5", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "***********************************Prompt***********************************\n", "================================\u001b[1m System Message \u001b[0m================================\n", "\n", "You are a world-class competitive programmer.\n", "Please reply with a Python 3 solution to the problem below. \n", "First, reason through the problem and conceptualize a solution.\n", "Then write detailed pseudocode to uncover any potential logical errors or omissions.\n", "Finally output the working Python code for your solution, ensuring to fix any errors uncovered while writing pseudocode.\n", "\n", "No outside libraries are allowed.\u001b[33;1m\u001b[1;3m{examples}\u001b[0m\n", "\n", "=============================\u001b[1m Messages Placeholder \u001b[0m=============================\n", "\n", "\u001b[33;1m\u001b[1;3m{messages}\u001b[0m\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/Users/wfh/.pyenv/versions/3.11.2/lib/python3.11/site-packages/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: The function `bind_tools` is in beta. It is actively being worked on, so the API may change.\n", " warn_beta(\n" ] } ], "source": ["from langchain import hub\nfrom langchain_anthropic import ChatAnthropic\n\n# For this section, we are testing zero-shot performance and won't have\n# any examples. Partial them out to pre-fill the template.\nprompt = hub.pull(\"wfh/usaco-draft-solver\").partial(examples=\"\")\nprint(\"*\" * 35 + \"Prompt\" + \"*\" * 35)\nprompt.pretty_print()\n\n# Use Haiku if you want to save $$ while (almost) never correctly answering the question\n# llm = ChatAnthropic(model=\"claude-3-haiku-20240307\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n\nsolver = Solver(llm, prompt)"] }, { "cell_type": "code", "execution_count": 11, "id": "5d9560ba-900a-43d1-ad38-f132fd660337", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "********************************** Example **********************************\n", "==================================\u001b[1m Ai Message \u001b[0m==================================\n", "\n", "[{'text': \"\\nTo address this problem, we need to use the writePython function, which requires the following parameters:\\n- reasoning: a conceptual solution to the problem\\n- pseudocode: detailed pseudocode for the solution\\n- code: working Python code implementing the solution\\n\\nThe key aspects to address in the solution are:\\n1. We have an infinite stream, so we can't store all elements. Need an online algorithm.\\n2. Need to ensure each element has an equal probability of being in the final sample.\\n\\nI believe I have enough information to provide values for all the required parameters.\\n\", 'type': 'text'}, {'id': 'toolu_01UqpLYyueky5GtYMidS9oLF', 'input': {'reasoning': 'To get a perfectly random sample of size k from an infinite stream:\\n\\n1. Store the first k elements in an array (reservoir). \\n2. For each ith element after the kth element (i > k):\\n - Generate a random integer j between 0 and i (inclusive)\\n - If j < k, replace the jth element of the reservoir with the ith element\\n3. At the end, the reservoir contains the random sample.\\n\\nThis works because for any element, when we process the nth element, the probability that it is in the reservoir is:\\n- k/n when n <= k (first k elements always selected)\\n- k/n * k/(n-1) * k/(n-2) * ... * k/(k+1) = k/n when n > k\\n\\nSo any element has k/n probability of being in final reservoir, giving a perfectly random sample.', 'pseudocode': '```\\nfunction selectKItems(stream, k):\\n reservoir = [0..k-1] # store first k elements\\n\\n i = k\\n while stream has next item:\\n item = stream.next()\\n j = random(0, i) # generate random index between 0 and i\\n if j < k:\\n reservoir[j] = item # replace element at random index with new item\\n i += 1\\n\\n return reservoir\\n```', 'code': 'import random\\n\\ndef reservoir_sampling(stream, k):\\n reservoir = []\\n \\n # Store first k elements in reservoir\\n for i in range(k):\\n reservoir.append(next(stream))\\n\\n i = k\\n for item in stream:\\n # Generate random index between 0 and i\\n j = random.randint(0, i) \\n \\n # Replace element at random index with new item\\n if j < k:\\n reservoir[j] = item\\n i += 1\\n\\n return reservoir'}, 'name': 'writePython', 'type': 'tool_use'}]\n" ] } ], "source": ["print(\"*\" * 34 + \" Example \" + \"*\" * 34)\nresult = solver(\n {\n \"messages\": [\n (\n \"user\",\n \"How do I get a perfectly random sample from an infinite stream\",\n )\n ]\n }\n)\nresult[\"messages\"][0].pretty_print()\n# Could expand to include (1)\n# 1. Restate the problem in plain English\n# 2. Closely following the explanation, restate and explain the solution in plain English\n# 3. Write a pseudocode solution\n# 4. Output the final Python solution with your solution steps in comments."] }, { "cell_type": "markdown", "id": "58c5fa7a-a2eb-4954-be34-4194a58f00d3", "metadata": {}, "source": [ "#### Node 2: Evaluate\n", "\n", "Now define the \"`evaluate`\" node. This node takes the `solver`'s submitted code and executes it against the `test_cases` in our `State`.\n", "This uses the unsafe `check_correctness` utility we defined in the setup above." ] }, { "cell_type": "code", "execution_count": 12, "id": "1785015b-24f8-415f-b950-e229b5137887", "metadata": {}, "outputs": [], "source": ["from langchain_core.messages import AIMessage, HumanMessage, ToolMessage\n\n\n# This is the node we will add to the graph.\n# Most tool-calling APIs require that the `ToolMessage` contain the ID\n# of the\ndef format_tool_message(response: str, ai_message: AIMessage):\n return ToolMessage(\n content=response + \"\\nMake all fixes using the writePython tool.\",\n tool_call_id=ai_message.tool_calls[0][\"id\"],\n )\n\n\ndef evaluate(state: State):\n test_cases = state[\"test_cases\"]\n ai_message: AIMessage = state[\"messages\"][-1]\n if not ai_message.tool_calls:\n return {\n \"messages\": [\n HumanMessage(\n content=\"No code submitted. Please try again using the correct python code.\"\n )\n ]\n }\n try:\n code = ai_message.tool_calls[0][\"args\"][\"code\"]\n except Exception as e:\n return {\"messages\": [format_tool_message(repr(e), ai_message)]}\n num_test_cases = len(test_cases)\n succeeded = 0\n test_results = []\n # TODO: Multiprocess\n for test_case in test_cases:\n input_data = test_case[\"inputs\"]\n expected_output = test_case[\"outputs\"]\n test_result = check_correctness(code, input_data, expected_output, timeout)\n test_results.append(test_result)\n if test_result == \"passed\":\n succeeded += 1\n pass_rate = succeeded / num_test_cases if num_test_cases else \"N/A\"\n if pass_rate == 1:\n return {\"status\": \"success\"}\n\n responses = \"\\n\".join(\n [f\"\\n{r}\\n\" for i, r in enumerate(test_results)]\n )\n response = f\"Incorrect submission. Please respond with updated code.\\nPass rate: {succeeded}/{num_test_cases}\\nResults:\\n{responses}\"\n formatted_message = format_tool_message(response, ai_message)\n return {\"messages\": [formatted_message]}"] }, { "cell_type": "markdown", "id": "76f2a46d-c3e2-49d4-b44b-cadb6fd4e5a0", "metadata": {}, "source": [ "#### Create Graph\n", "\n", "Now, put it all together! Once you've defined each node, defining the connectivity / state transitions is fairly easy.\n", "\n", "Our Zero-shot graph defines a loop. If we visualize the data flow, we want the logic to:\n", "1. First go to the `solver`, which attempts a first solution.\n", "2. Next go to the `evaluate` node, which tests the solution.\n", "3. If the solution passes, end, otherwise, return to the `solver` to try again.\n", "\n", "In LangGraph, we use `conditional_edges` to define state transitions that contain conditional logic.\n", "Below, define the graph, adding a `control_edge` to handle step (3) above." ] }, { "cell_type": "code", "execution_count": 13, "id": "caf1560e-1517-4229-8a43-186816da6a3a", "metadata": {}, "outputs": [], "source": ["from langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"solver\", solver)\nbuilder.add_edge(START, \"solver\")\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"solver\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solver\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solver\": \"solver\"})\ngraph = builder.compile()"] }, { "cell_type": "code", "execution_count": 14, "id": "7275a2c3-1818-4d14-a7a5-97bc56243a9b", "metadata": {}, "outputs": [ { "data": { "image/jpeg": "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", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", "id": "fc4b1bb4-262f-41c3-822d-e015daf0744a", "metadata": {}, "source": [ "Now that we've created our graph, let's see the type of question it will have to solve." ] }, { "cell_type": "code", "execution_count": 15, "id": "308d6e18-69ad-4e8d-9c5f-06111b0806ee", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "Farmer John has $N$ ($1 \\leq N \\leq 2 \\cdot 10^5$) farms, numbered from $1$ to\n", "$N$. It is known that FJ closes farm $i$ at time $c_i$. Bessie wakes up at time\n", "$S$, and wants to maximize the productivity of her day by visiting as many farms\n", "as possible before they close. She plans to visit farm $i$ on time $t_i + S$.\n", "Bessie must arrive at a farm strictly before Farmer John closes it to actually visit it.\n", "\n", "Bessie has $Q$ $(1 \\leq Q \\leq 2 \\cdot 10^5)$ queries. For each query, she gives\n", "you two integers $S$ and $V$. For each query, output whether Bessie can visit at\n", "least $V$ farms if she wakes up at time $S$.\n", "\n", "INPUT FORMAT (input arrives from the terminal / stdin):\n", "The first line consists of $N$ and $Q$.\n", "\n", "The second line consists of $c_1, c_2, c_3 \\dots c_N$ ($1 \\leq c_i \\leq 10^6$).\n", "\n", "The third line consists of $t_1, t_2, t_3 \\dots t_N$ ($1 \\leq t_i \\leq 10^6$).\n", "\n", "The next $Q$ lines each consist of two integers $V$ ($1 \\leq V \\leq N$) and $S$\n", "($1 \\leq S \\leq 10^6$).\n", "\n", "OUTPUT FORMAT (print output to the terminal / stdout):\n", "For each of the $Q$ queries, output YES or NO on a new line.\n", "\n", "SAMPLE INPUT:\n", "5 5\n", "3 5 7 9 12\n", "4 2 3 3 8\n", "1 5\n", "1 6\n", "3 3\n", "4 2\n", "5 1\n", "SAMPLE OUTPUT: \n", "YES\n", "NO\n", "YES\n", "YES\n", "NO\n", "\n", "For the first query, Bessie will visit the farms at time $t = [9, 7, 8, 8, 13]$,\n", "so she will only get to visit farm $4$ on time before FJ closes the farm.\n", "\n", "For the second query, Bessie will not be able to visit any of the farms on time.\n", "\n", "For the third query, Bessie will visit farms $3, 4, 5$ on time.\n", "\n", "For the fourth and fifth queries, Bessie will be able to visit all but the first\n", "farm on time.\n", "\n", "SCORING:\n", "Inputs 2-4: $N,Q\\le 10^3$Inputs 5-9: $c_i, t_i \\le 20$Inputs 10-17: No additional constraints.\n", "\n", "\n", "Problem credits: Chongtian Ma\n", "\n" ] } ], "source": ["input_state = input_states[0].copy()\n# We will reduce the test cases to speed this notebook up\ninput_state[\"test_cases\"] = input_state[\"test_cases\"][:3]\nprint(input_state[\"messages\"][0][1])"] }, { "cell_type": "markdown", "id": "3c444be0-4c7d-49cc-9aaa-2b99dabe3922", "metadata": {}, "source": [ "Pretty difficult! Let's run our simple \"zero-shot\" agent below to see how it fares. **It most likely will not be able to solve this question** (unless you are using a more powerful model than what I had available at the time of writing this tutorial (2024/04/20).\n", "We will trace the trajectory to LangSmith to review the series of submissions. To reduce the packet size, we will use \"`hide_inputs`\" and filter out the test_cases. All this is optional but useful for development. \n", "\n", "**Note:** We _expect_ a **GraphRecursionError** here from it not being able to answer it correctly in the allocated number of steps." ] }, { "cell_type": "code", "execution_count": 25, "id": "2ebe0da0-8f27-4805-829c-54bc1dc29ba4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Assistant: [{'text': '\\nThe key steps to solve this\n", "Assistant: KeyError('code')\\nMake all fixes using the writePy\n", "Assistant: [{'id': 'toolu_01KimhKt8aqQjGZJmrHVnAtE', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_01CMZTqAd7BZQ2nSgtk9djRW', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_01Kbaq9gX4BnHvps6TMfVGHL', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_01MiSnpiGK5Yy4Cpp6GGbjmT', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_01GWuvJezXLMVurUBG84odDP', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_01W8DGmhcpFVctySmx58scf9', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_018bhYtCKDK6S4MHiAxUZCrb', 'input':\n", "Assistant: KeyError('code')\\nMake all fixes using the writePy\n", "Assistant: [{'id': 'toolu_01LCwaCjX9uZBV3jt9eAkmAa', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_01WqJvdE2WDeTZXoKp2V7PWb', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_01DGevkunt9zWx7SVDCHdBuv', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'id': 'toolu_013comYKVxNSzTM4ZbH3L3FP', 'input':\n", "Assistant: Incorrect submission. Please respond with updated \n" ] }, { "ename": "GraphRecursionError", "evalue": "Recursion limit of 25 reachedwithout hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mGraphRecursionError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[25], line 17\u001b[0m\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m tracing_v2_enabled(client\u001b[38;5;241m=\u001b[39mclient):\n\u001b[1;32m 16\u001b[0m events \u001b[38;5;241m=\u001b[39m graph\u001b[38;5;241m.\u001b[39mstream(input_state)\n\u001b[0;32m---> 17\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mevent\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mevents\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 18\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mevent\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalues\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 19\u001b[0m \u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mmessages\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m~/.pyenv/versions/3.11.2/lib/python3.11/site-packages/langgraph/pregel/__init__.py:645\u001b[0m, in \u001b[0;36mPregel.stream\u001b[0;34m(self, input, config, stream_mode, output_keys, input_keys, interrupt_before_nodes, interrupt_after_nodes, debug)\u001b[0m\n\u001b[1;32m 643\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[1;32m 644\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m step \u001b[38;5;241m==\u001b[39m config[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrecursion_limit\u001b[39m\u001b[38;5;124m\"\u001b[39m]:\n\u001b[0;32m--> 645\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m GraphRecursionError(\n\u001b[1;32m 646\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mRecursion limit of \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mconfig[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrecursion_limit\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m reached\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 647\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mwithout hitting a stop condition. You can increase the \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 648\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlimit by setting the `recursion_limit` config key.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 649\u001b[0m )\n\u001b[1;32m 651\u001b[0m \u001b[38;5;66;03m# before execution, check if we should interrupt\u001b[39;00m\n\u001b[1;32m 652\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m _should_interrupt(\n\u001b[1;32m 653\u001b[0m checkpoint,\n\u001b[1;32m 654\u001b[0m interrupt_before_nodes,\n\u001b[1;32m 655\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstream_channels_list,\n\u001b[1;32m 656\u001b[0m next_tasks,\n\u001b[1;32m 657\u001b[0m ):\n", "\u001b[0;31mGraphRecursionError\u001b[0m: Recursion limit of 25 reachedwithout hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key." ] } ], "source": ["from langchain_core.tracers.context import tracing_v2_enabled\nfrom langsmith import Client\n\n\n# We don't need to include all the test cases in our traces.\ndef _hide_test_cases(inputs):\n copied = inputs.copy()\n # These are tens of MB in size. No need to send them up\n copied[\"test_cases\"] = \"...\"\n return copied\n\n\nclient = Client(hide_inputs=_hide_test_cases, hide_outputs=_hide_test_cases)\nwith tracing_v2_enabled(client=client):\n events = graph.stream(input_state)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )"] }, { "cell_type": "markdown", "id": "2c496d9a-a95b-4cab-86fa-daafc7ddd065", "metadata": {}, "source": [ "It wasn't able to solve it in time **but that's OK**! If it were easy, this paper would be a lot shorter :)\n", "\n", "You can view the [agent's full LangSmith trace](https://smith.langchain.com/public/61c84ad0-51db-40f1-b50d-6983d9481ca1/r) at the provided link.\n", "\n", "In the next section we will add an improvement the paper terms \"episodic memory\", which in this case is really few-shot retrieval." ] }, { "cell_type": "markdown", "id": "090a1639-09c5-4682-813f-71100f236eb3", "metadata": {}, "source": [ "## Part 2: Few-shot Retrieval\n", "\n", "Even with reflective tool calling, our baseline agent from part 1 struggled with this difficult task. One way to \"teach\" an LLM how to better perform a task is through demonstrations, also known as \"few-shot examples.\"\n", "\n", "What the authors of the USACO paper call \"episodic memory\" **is really just few-shot prompting over similar examples.**\n", "\n", "Each examples in this case is a different problems + solution within the dataset. The term \"episodic memory\" makes sense if you pretend your agent has already \"solved\" these problems and is recalling its solutions to them.\n", "\n", "This section adds the \"Episodic Memory\" components from \"Part 2\" in the diagram below.\n", "\n", "![Part 2 diagram](./img/diagram-part-2.png)\n", "\n", "Note that this memory step is performed **one time**, **before** the logic of our zero-shot loop from part 1. The steps are as follows:\n", "\n", "1. Prompt the LLM to generate a candidate solution.\n", "2. Use the text of the candidate solution to retrieve the N most similar (problem, solution) pairs.\n", "3. Format this result in the Zero-shot agent's prompt.\n", "\n", "Below, let's implement our episodic memory as a retriever. We will follow the paper's retriever selection and use [BM25](https://en.wikipedia.org/wiki/Okapi_BM25)." ] }, { "cell_type": "code", "execution_count": 26, "id": "d612dd8d-31af-426c-944b-203acd55ace0", "metadata": {}, "outputs": [], "source": ["%%capture --no-stderr\n%pip install --upgrade --quiet rank_bm25"] }, { "cell_type": "markdown", "id": "32e0c485-8eba-41e6-ae29-64bcea98d19e", "metadata": {}, "source": [ "#### State\n", "\n", "The state is mostly recycled from part 1. Add additional \"candidate\" and \"examples\" fields to store the information for the memory steps." ] }, { "cell_type": "code", "execution_count": 27, "id": "16937fef-58b9-4ab2-bbfc-5237aad235ec", "metadata": {}, "outputs": [], "source": ["from typing import Annotated\n\nfrom typing_extensions import TypedDict\n\nfrom langgraph.graph.message import AnyMessage, add_messages\n\n\nclass TestCase(TypedDict):\n inputs: str\n outputs: str\n\n\nclass State(TypedDict):\n # NEW! Candidate for retrieval + formatted fetched examples as \"memory\"\n candidate: AIMessage\n examples: str\n # Repeated from Part 1\n messages: Annotated[list[AnyMessage], add_messages]\n test_cases: list[TestCase]\n runtime_limit: int\n status: str"] }, { "cell_type": "markdown", "id": "61fb9104-e73f-47db-9af5-84a375a8d323", "metadata": {}, "source": [ "#### Nodes 1 and 3: Draft & Solver\n", "\n", "Let's create our \"agent\". We will modify the `Solver` from Part 1 to reuse it for for the agent node and for the candidate program generation node (\"draft\")." ] }, { "cell_type": "code", "execution_count": 28, "id": "25f947a7-15bb-4119-a47e-b5c33ca0a249", "metadata": {}, "outputs": [], "source": ["from langchain import hub\nfrom langchain_anthropic import ChatAnthropic\n\n\nclass Solver:\n def __init__(self, llm: BaseChatModel, prompt: ChatPromptTemplate):\n self.runnable = prompt | llm.bind_tools([writePython])\n\n def __call__(self, state: State) -> dict:\n # Our agent only can see the \"messages\" and will ignore the test info\n inputs = {\"messages\": state[\"messages\"]}\n has_examples = bool(state.get(\"examples\"))\n output_key = \"candidate\" # Used in the draft node\n if has_examples:\n output_key = \"messages\"\n # Used in the solve node\n inputs[\"examples\"] = state[\"examples\"]\n response = self.runnable.invoke(inputs)\n if not response.content:\n return {\n output_key: AIMessage(\n content=\"I'll need to think about this step by step.\"\n )\n }\n return {output_key: response}\n\n\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\")\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nsolver = Solver(llm, prompt)"] }, { "cell_type": "markdown", "id": "273a526b-3b52-4630-a58e-0317a0034609", "metadata": {}, "source": [ "#### Node 2: Retrieve\n", "\n", "The retrieve node takes a candidate solution (made by the 'solver' node), uses _this_ to search for similar examples, then formats those in the message." ] }, { "cell_type": "code", "execution_count": 29, "id": "e5e0aa40-79a4-4071-9ad2-9aa2f36599ce", "metadata": {}, "outputs": [], "source": ["# We will test our agent on index 0 (the same as above).\n# Later, we will test on index 2 (the first 'silver difficulty' question)\ntest_indices = [0, 2]\ntrain_ds = [row for i, row in enumerate(ds) if i not in test_indices]\ntest_ds = [row for i, row in enumerate(ds) if i in test_indices]"] }, { "cell_type": "code", "execution_count": 30, "id": "96a1ff96-7556-4959-9f54-1ade3bd1c01a", "metadata": {}, "outputs": [], "source": ["from langchain_community.retrievers import BM25Retriever\n\n\ndef format_example(row):\n question = row[\"description\"]\n answer = row[\"solution\"]\n return f\"\"\"\n{question}\n\n\n{answer}\n\"\"\"\n\n\n# Skip our 'test examples' to avoid cheating\n# This is \"simulating\" having seen other in-context examples\nretriever = BM25Retriever.from_texts([format_example(row) for row in train_ds])"] }, { "cell_type": "markdown", "id": "6ad20d3a-291c-41a5-a343-bd14284f76f2", "metadata": {}, "source": [ "Now define the node. Any node can optionally accept a second `config` positional argument. This contains `configurable` params you can adjust when invoking the graph. For instance, we can\n", "adjust the top `k` examples to retrieve for our agent." ] }, { "cell_type": "code", "execution_count": 31, "id": "af42962d-c06e-4b6e-96df-72ad48f17617", "metadata": {}, "outputs": [], "source": ["from langchain_core.runnables import RunnableConfig\n\n\ndef retrieve_examples(state: State, config: RunnableConfig):\n top_k = config[\"configurable\"].get(\"k\") or 2\n ai_message: AIMessage = state[\"candidate\"]\n if not ai_message.tool_calls:\n # We err here. To make more robust, you could loop back\n raise ValueError(\"Draft agent did not produce a valid code block\")\n code = ai_message.tool_calls[0][\"args\"][\"code\"]\n examples_str = \"\\n\".join(\n [doc.page_content for doc in retriever.invoke(code)[:top_k]]\n )\n examples_str = f\"\"\"\nYou previously solved the following problems in this competition:\n\n{examples_str}\n\nApproach this new question with similar sophistication.\"\"\"\n return {\"examples\": examples_str}"] }, { "cell_type": "markdown", "id": "df5cbe72-fca0-4974-a769-284267d3df91", "metadata": {}, "source": [ "#### Graph\n", "\n", "Now let's put it all together. The graph is slightly more complicated than in part 1, since we have to add the initial \"draft\" and \"retrieve\" nodes to our agent loop." ] }, { "cell_type": "code", "execution_count": 32, "id": "e6e73e85-1232-4848-beba-3139ac7d0a64", "metadata": {}, "outputs": [], "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\n# Add connectivity\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\n\n\ncheckpointer = SqliteSaver.from_conn_string(\":memory:\")\ngraph = builder.compile(checkpointer=checkpointer)"] }, { "cell_type": "code", "execution_count": 33, "id": "57acf78d-5e68-46dd-adae-2259e5c5d3f1", "metadata": {}, "outputs": [ { "data": { "image/jpeg": "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", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", "id": "3bb633e0-0d02-4050-96a8-18265594385b", "metadata": {}, "source": [ "Let's try again on this problem:" ] }, { "cell_type": "code", "execution_count": 34, "id": "dc6f0455-a6e9-46a0-8f8b-03de00fb1f8d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[{'text': \"\\nThis problem essentially asks to find the number of farms Bessie can visit before they close at each query. The key insights are:\\n\\n1. Bessie's arrival time at each farm is S +\n", "Retrieved examples:\n", "\n", " \n", "You previously solved the following problems in this competition:\n", "\n", "\n", "\n", "Farmer John...\n", "Assistant: [{'text': \"\\nThe key information given i\n" ] } ], "source": ["config = {\"configurable\": {\"thread_id\": \"question-recall\", \"k\": 3}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(input_state, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] }, { "cell_type": "markdown", "id": "1389ffb0-839e-4646-be68-dbc79279c8ae", "metadata": {}, "source": [ "**No recursion error!** You can view the [full LangSmith trace](https://smith.langchain.com/public/1f1c4db3-b53c-49bf-a287-a2b51c081156/r/31f90ddd-8ae9-4b23-a2b5-b0c0d67c5cc3) of the graph's execution at the provided link to confirm the results. You can also check the graph state to confirm that it passed all test cases successfully:" ] }, { "cell_type": "code", "execution_count": 35, "id": "d55a1d95-9fa4-45b2-9f5e-7ca02fb8415b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'success'" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": ["checkpoint = graph.get_state(config)\ncheckpoint.values[\"status\"]"] }, { "cell_type": "markdown", "id": "50348c66-4d38-44d0-bb20-ae1a14517f78", "metadata": {}, "source": [ "**Congrats!** You added \"episodic memory\" to your agent to fetch few-shot examples and solve this bronze level programming olympiad question!\n", "\n", "Our agent is still limited, however. Let's test it out on a more challenging 🪙🏆silver✨ level question:" ] }, { "cell_type": "code", "execution_count": 36, "id": "1ef0b06f-c448-49a5-8f1f-f7041a5d6b87", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'silver'" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": ["silver_row = test_ds[1]\nsilver_row[\"problem_level\"]"] }, { "cell_type": "code", "execution_count": 37, "id": "2ae55ddc-e629-4be7-badd-756608eec50e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[{'text': \"\\nThe relevant tool for this problem is writePython. It requires the following parameters:\\n- reasoning: To solve this problem, we need to simulate the cruise by following the seq\n", "Retrieved examples:\n", "\n", " \n", "You previously solved the following problems in this competition:\n", "\n", "\n", "\n", "Farmer John...\n", "Assistant: [{'text': \"\\nTo solve this problem, we n\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': \"\\nAfter reviewing the failed \n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': \"\\nAfter reviewing the latest \n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': \"\\nOops, looks like I made a s\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': \"\\nHmm, some of the test cases\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': '\\nOops, looks like I accident\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': \"\\nLooks like the code is now \n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': '\\nOops, looks like I accident\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': \"\\nHmm, the optimization to si\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': \"\\nOops, I did it again - acci\n", "Assistant: Incorrect submission. Please respond with updated \n", "Assistant: [{'text': \"\\nHmm, the latest code is sti\n", "Assistant: Incorrect submission. Please respond with updated \n" ] }, { "ename": "GraphRecursionError", "evalue": "Recursion limit of 25 reachedwithout hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mGraphRecursionError\u001b[0m Traceback (most recent call last)", "Cell \u001b[0;32mIn[37], line 12\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m tracing_v2_enabled(client\u001b[38;5;241m=\u001b[39mclient):\n\u001b[1;32m 11\u001b[0m events \u001b[38;5;241m=\u001b[39m graph\u001b[38;5;241m.\u001b[39mstream(silver_input, config)\n\u001b[0;32m---> 12\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mevent\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mevents\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 13\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mevent\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalues\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[1;32m 14\u001b[0m \u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mvalue\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[38;5;124;43mmessages\u001b[39;49m\u001b[38;5;124;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n", "File \u001b[0;32m~/.pyenv/versions/3.11.2/lib/python3.11/site-packages/langgraph/pregel/__init__.py:645\u001b[0m, in \u001b[0;36mPregel.stream\u001b[0;34m(self, input, config, stream_mode, output_keys, input_keys, interrupt_before_nodes, interrupt_after_nodes, debug)\u001b[0m\n\u001b[1;32m 643\u001b[0m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[1;32m 644\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m step \u001b[38;5;241m==\u001b[39m config[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mrecursion_limit\u001b[39m\u001b[38;5;124m\"\u001b[39m]:\n\u001b[0;32m--> 645\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m GraphRecursionError(\n\u001b[1;32m 646\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mRecursion limit of \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mconfig[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mrecursion_limit\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m reached\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 647\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mwithout hitting a stop condition. You can increase the \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 648\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mlimit by setting the `recursion_limit` config key.\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 649\u001b[0m )\n\u001b[1;32m 651\u001b[0m \u001b[38;5;66;03m# before execution, check if we should interrupt\u001b[39;00m\n\u001b[1;32m 652\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m _should_interrupt(\n\u001b[1;32m 653\u001b[0m checkpoint,\n\u001b[1;32m 654\u001b[0m interrupt_before_nodes,\n\u001b[1;32m 655\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mstream_channels_list,\n\u001b[1;32m 656\u001b[0m next_tasks,\n\u001b[1;32m 657\u001b[0m ):\n", "\u001b[0;31mGraphRecursionError\u001b[0m: Recursion limit of 25 reachedwithout hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key." ] } ], "source": ["silver_input = {\n \"messages\": [(\"user\", silver_row[\"description\"])],\n \"test_cases\": silver_row[\"test_cases\"],\n \"runtime_limit\": silver_row[\"runtime_limit\"],\n \"status\": \"in_progress\",\n}\n\n\nconfig = {\"configurable\": {\"thread_id\": \"silver-question-1\", \"k\": 2}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(silver_input, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] }, { "cell_type": "markdown", "id": "7b4c9217-38de-4f48-9070-2092fd9ecc15", "metadata": {}, "source": [ "**Still too hard!** AGI not achieved yet. To investigate our agent's trajectory in detail, check out the [full LangSmith trace](https://smith.langchain.com/public/13018b44-0c4f-4f1a-9e6d-dea1f3fd4705/r).\n", "\n", "Our agent isn't good enough to be autonomous. The great thing about LangGraph is you don't have to decide between \"autonomous agent\" and \"simple DAG\": you can inject control and user-interfaces wherever it can usefully benefit your application." ] }, { "cell_type": "markdown", "id": "59177162-3fb8-4307-8e7f-ea67e785d4cf", "metadata": {}, "source": [ "## Part 3: Human-in-the-loop\n", "\n", "Our retrieval-enhanced agent was able to solve the `bronze`-level question but still failed for those with the more challenging **silver** difficulty. \n", "\n", "Recall that the paper presented 3 complementary techniques that improved performance:\n", "\n", "1. Reflection: explicitly prompting the LLM to \"reflect\" on its mistakes can help it\n", "2. Few-shot prompting: retrieving relevant, high-quality examples as \"memory\"\n", "3. **Human-in-the-loop collaboration:** without giving the correct answer, the human is allowed to help the agent reflect on its approach and point it in a better direction.\n", "\n", "\n", "In this section, we will add the \"human\" node (marked as \"part 3\" in the diagram below), completing our agent graph:\n", "\n", "![Diagram](./img/diagram.png)\n", "\n", "From an ML perspective, this is a bit of a [clever hans](https://en.wikipedia.org/wiki/Clever_Hans), but from the application designer's perspective, where the primary goal is to achieve a higher combined success rate, letting the human interject with thoughts and insights is only natural. \n", "\n", "In either case, adding a human check to a LangGraph instance requires no extra lines of code. Let's do so by instructing the graph to `interrupt_after` the \"`evaluate`\" node to give the user a chance to modify the trajectory.\n", "\n", "Start assembling your graph below. The following section is identical to our application in part 2:" ] }, { "cell_type": "code", "execution_count": 38, "id": "3c6456ba-363c-4133-8631-6dabb042b6ce", "metadata": {}, "outputs": [], "source": ["# This is all the same as before\nfrom langgraph.checkpoint.sqlite import SqliteSaver\nfrom langgraph.graph import END, StateGraph, START\n\nbuilder = StateGraph(State)\nprompt = hub.pull(\"wfh/usaco-draft-solver\")\nllm = ChatAnthropic(model=\"claude-3-opus-20240229\", max_tokens_to_sample=4000)\n\ndraft_solver = Solver(llm, prompt.partial(examples=\"\"))\nbuilder.add_node(\"draft\", draft_solver)\nbuilder.add_edge(START, \"draft\")\nbuilder.add_node(\"retrieve\", retrieve_examples)\nsolver = Solver(llm, prompt)\nbuilder.add_node(\"solve\", solver)\nbuilder.add_node(\"evaluate\", evaluate)\nbuilder.add_edge(\"draft\", \"retrieve\")\nbuilder.add_edge(\"retrieve\", \"solve\")\nbuilder.add_edge(\"solve\", \"evaluate\")\n\n\ndef control_edge(state: State):\n if state.get(\"status\") == \"success\":\n return END\n return \"solve\"\n\n\nbuilder.add_conditional_edges(\"evaluate\", control_edge, {END: END, \"solve\": \"solve\"})\ncheckpointer = SqliteSaver.from_conn_string(\":memory:\")"] }, { "cell_type": "markdown", "id": "3d837103-ede4-4515-a0cd-0b3319382013", "metadata": {}, "source": [ "Now finish by compiling the graph. Set`interrupt_after=[\"evaluate\"]` to instruct the agent to wait for human input before continuing execution." ] }, { "cell_type": "code", "execution_count": 39, "id": "461c13ba-01cc-44e1-b837-6a64d03069d9", "metadata": {}, "outputs": [], "source": ["graph = builder.compile(\n checkpointer=checkpointer,\n # New: this tells the graph to break any time it goes to the \"human\" node\n interrupt_after=[\"evaluate\"],\n)"] }, { "cell_type": "code", "execution_count": 40, "id": "040b2100-5b2e-40c9-b6af-a1b680e16ee3", "metadata": {}, "outputs": [ { "data": { "image/jpeg": "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", "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(graph.get_graph().draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] }, { "cell_type": "markdown", "id": "372462bf-c74e-4c18-bbee-c21c8132b2e5", "metadata": {}, "source": [ "As you can see in the graph above, the structure is the same as Part 2, except that we've inserted a \"`human`\" breakpoint between the \"`evaluate`\" and \"`solve`\" nodes.\n", "\n", "Let's try this question again!" ] }, { "cell_type": "code", "execution_count": 41, "id": "5f9ad0d0-cdaf-4ba2-9527-b5b9739e7b67", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[{'text': \"\\nTo solve this problem, we need to:\\n1. Read in the input data - number of ports N, length of direction sequence M, number of repetitions K, the port connections, and the directi\n", "Retrieved examples:\n", "\n", " \n", "You previously solved the following problems in this competition:\n", "\n", "\n", "Farmer John ...\n", "Assistant: [{'text': '\\nTo determine where Bessie e\n", "Assistant: Incorrect submission. Please respond with updated \n" ] } ], "source": ["config = {\"configurable\": {\"thread_id\": \"silver-hl-1\", \"k\": 2}}\nwith tracing_v2_enabled(client=client):\n events = graph.stream(silver_input, config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])"] }, { "cell_type": "markdown", "id": "947e3089-6ae5-40e4-b993-7ef7e8630a12", "metadata": {}, "source": [ "**⏰Time to weigh in⏰:** our model failed in its first attempt, so we have the opportunity to give it some advice.\n", "\n", "Recall the original question:" ] }, { "cell_type": "code", "execution_count": 42, "id": "4fcdf8c9-6a5a-4463-90ae-eec89a57cd05", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Problem 3: Luxury River Cruise [Josh Alman and Nathan Pinsker, 2013]\n", "\n", "Farmer John is taking Bessie and the cows on a cruise! They are sailing on a \n", "network of rivers with N ports (1 <= N <= 1,000) labeled 1..N, and Bessie \n", "starts at port 1. Each port has exactly two rivers leading out of it which \n", "lead directly to other ports, and rivers can only be sailed one way.\n", "\n", "At each port, the tour guides choose either the \"left\" river or the \"right\" \n", "river to sail down next, but they keep repeating the same choices over and \n", "over. More specifically, the tour guides have chosen a short sequence of M \n", "directions (1 <= M <= 500), each either \"left\" or \"right\", and have\n", "repeated it K times (1 <= K <= 1,000,000,000). Bessie thinks she is going\n", "in circles -- help her figure out where she ends up!\n", "\n", "PROBLEM NAME: cruise\n", "\n", "INPUT FORMAT:\n", "\n", "* Line 1: Three space-separated integers N, M, and K.\n", "\n", "* Lines 2..N+1: Line i+1 has two space-separated integers,\n", " representing the number of the ports that port i's left and\n", " right rivers lead to, respectively.\n", "\n", "* Line N+2: M space-separated characters, either 'L' or 'R'. 'L'\n", " represents a choice of 'left' and 'R' represents a choice of\n", " 'right'.\n", "\n", "SAMPLE INPUT:\n", "\n", "4 3 3\n", "2 4\n", "3 1\n", "4 2\n", "1 3\n", "L L R\n", "\n", "INPUT DETAILS:\n", "\n", "The port numbers are arranged clockwise in a circle, with 'L' being a \n", "clockwise rotation and 'R' being a counterclockwise rotation. The sequence \n", "taken is LLRLLRLLR.\n", "\n", "OUTPUT FORMAT:\n", "\n", "* Line 1: A single integer giving the number of the port where\n", " Bessie's cruise ends.\n", "\n", "SAMPLE OUTPUT:\n", "\n", "4\n", "\n", "OUTPUT DETAILS:\n", "\n", "After the first iteration of the sequence of directions, Bessie is at port\n", "2 (1 -> 2 -> 3 -> 2); after the second, she is at port 3 (2 -> 3 -> 4 ->\n", "3), and at the end she is at port 4 (3 -> 4 -> 1 -> 4).\n", "\n" ] } ], "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"messages\"][0].content)"] }, { "cell_type": "markdown", "id": "232ed165-e2cf-495f-a4bb-a0f3f87a719c", "metadata": {}, "source": [ "And then review the agent's current submission:" ] }, { "cell_type": "code", "execution_count": 43, "id": "2049980a-0a0c-4135-98c7-d4de1af4757b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "To determine where Bessie ends up, we need to:\n", "1. Simulate the cruise by following the sequence of left/right directions\n", "2. Repeat this sequence K times to find the final destination port\n", "\n", "The problem provides:\n", "- The number of ports N\n", "- The connections between ports (left and right rivers for each port)\n", "- The sequence of M directions (L or R) to follow\n", "- The number of times K to repeat the sequence\n", "\n", "With this information, we have everything needed to simulate the cruise and find the ending port. The key steps will be:\n", "1. Read in the input data to initialize the river connections and direction sequence \n", "2. Iterate K times:\n", " - For each direction in the M-length sequence:\n", " - Move to the next port based on the current port and direction \n", "3. Output the final port number after K iterations\n", "\n", "The solution will require loops to repeat the sequence K times and follow the M directions. Since K can be up to 1 billion, simulating all K iterations directly would be too slow. Instead, we can find a pattern in how the port changes after each M-length sequence, and then \"fast-forward\" by calculating which port we reach after K repetitions of the pattern.\n", "\n", "\n", "\n", "Code:\n", "\n", "\n", "N, M, K = map(int, input().split())\n", "\n", "ports = []\n", "for _ in range(N):\n", " left, right = map(int, input().split())\n", " ports.append((left, right))\n", "\n", "directions = input().split()\n", "\n", "cur = 1\n", "pattern = []\n", "seen = set() \n", "steps = 0\n", "\n", "while cur not in seen:\n", " seen.add(cur)\n", " for d in directions:\n", " steps += 1\n", " if d == 'L': \n", " cur = ports[cur-1][0]\n", " else:\n", " cur = ports[cur-1][1]\n", " pattern.append((cur, steps))\n", "\n", "K %= steps\n", "for port, step in pattern:\n", " if step > K:\n", " cur = port\n", " break\n", " K -= step\n", " \n", "print(cur)\n" ] } ], "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"messages\"][-2].content[0][\"text\"])\nprint(\"\\n\\nCode:\\n\\n\")\nprint(snapshot.values[\"messages\"][-2].tool_calls[0][\"args\"][\"code\"])"] }, { "cell_type": "code", "execution_count": 44, "id": "69fd7efa-acbc-4450-927e-fb2b760066b2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Incorrect submission. Please respond with updated code.\n", "Pass rate: 4/10\n", "Results:\n", "\n", "wrong answer. Expected '4\n", "', got '3\n", "'\n", "\n", "\n", "wrong answer. Expected '50\n", "', got '2\n", "'\n", "\n", " 0, move s0 to the next position using get_next() and decrement K\n\nPrint the final position (converted to 1-based indexing).\n\nPay close attention to the initialization and movement of pointers during cycle detection and length calculation. Ensure that the logic is correct and handles all cases accurately.\"\"\",\n )\n ]\n },\n)"] }, { "cell_type": "markdown", "id": "e0542e9b-0992-407e-967f-c43bf1a75cc6", "metadata": {}, "source": [ "Now the graph's state contains our new message." ] }, { "cell_type": "code", "execution_count": 46, "id": "818e93f2-2204-4704-a832-f5c102d330f8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "HumanMessage(content=\"Consider breaking down the algorithm into separate parts: reading inputs, detecting cycles using the tortoise and hare algorithm, and determining Bessie's final position by skipping ahead K steps.\\n\\nRead the inputs into three arrays:\\n- Two arrays L and R for the ports (adjust for 0-based indexing)\\n- A third array S for the direction sequence\\n\\nOptimize by multiplying K by M before the main loop to convert the number of repetitions into the total number of steps.\\n\\nUse the tortoise and hare algorithm to detect the cycle:\\n- Define a helper function get_next(v) that returns the next position and direction index\\n- Initialize two pointers s0 and s1 to (0, 0)\\n- In each iteration:\\n - Move s0 by 1 step and s1 by 2 steps using get_next()\\n - If s0 equals s1, decrement K by 1 and break out of the loop\\n - Otherwise, decrement K by 1\\n- After the loop, if K is not 0, there is a cycle\\n\\nTo find the cycle length:\\n- Initialize a counter variable rho to 1\\n- Move s0 by 1 step using get_next()\\n- Enter a loop:\\n - Move s0 by 1 step using get_next()\\n - Increment rho\\n - If s0 equals s1, break out of the loop\\n\\nSkip ahead by reducing K modulo rho.\\n\\nSimulate the remaining steps:\\n- While K > 0, move s0 to the next position using get_next() and decrement K\\n\\nPrint the final position (converted to 1-based indexing).\\n\\nPay close attention to the initialization and movement of pointers during cycle detection and length calculation. Ensure that the logic is correct and handles all cases accurately.\", id='98888982-a469-4c5a-ab65-743d2f2608dc')" ] }, "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": ["graph.get_state(config).values[\"messages\"][-1]"] }, { "cell_type": "markdown", "id": "4eb67198-c84f-458b-8baf-783d7246dddc", "metadata": {}, "source": [ "Let's let the agent try again. Call `stream` with `None` to just use the inputs loaded from the memory. We will skip our human review for the next few attempats\n", "to see if it can correct itself." ] }, { "cell_type": "code", "execution_count": 47, "id": "d5d76d8f-e49b-46bf-a762-a6c6978ee96c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Assistant: [{'text': '\\nThank you for the detailed \n", "Assistant: Incorrect submission. Please respond with updated \n", "Continuing...\n" ] } ], "source": ["num_trials = 1\nwith tracing_v2_enabled(client=client):\n for _ in range(num_trials):\n events = graph.stream(None, updated_config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])\n if graph.get_state(config).values[\"status\"] == \"success\":\n break\n print(\"Continuing...\")"] }, { "cell_type": "code", "execution_count": 48, "id": "20ee7535-1bc8-4105-87c4-0e7a89a011ff", "metadata": {}, "outputs": [], "source": ["most_recent_state = list(graph.get_state_history(config))[0]"] }, { "cell_type": "markdown", "id": "f9a492fd-03fc-4de5-8ba5-3ccdaeb1791c", "metadata": {}, "source": [ "OK so the agent tried again. Check out the [LangSmith trace](https://smith.langchain.com/public/707be522-9eaf-4b6a-994e-1742f421a433/r/add3d8e7-85b1-40cf-bbd3-e78c50f835e8) from this step to see its update." ] }, { "cell_type": "code", "execution_count": 49, "id": "1f691881-f56e-4e2c-8b6a-5febb7ccf3ae", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[{'text': '\\nThank you for the detailed algorithm breakdown! Let me go through each step to make sure I understand and have the necessary information to implement the solution.\\n\\nReading inputs:\\n- Read N, M, K and store in separate variables\\n- Create arrays L and R to store the left and right port connections (adjust for 0-based indexing)\\n- Create array S to store the M-length direction sequence \\n- Multiply K by M upfront to get the total number of steps\\n\\nDetecting cycles with tortoise and hare:\\n- Define get_next(v) to return the next position and direction index\\n - It will use the current position and direction to look up the next port in L/R\\n- Initialize two pointers s0 and s1 to (0, 0) \\n- Loop until s0 equals s1 or all K steps are taken:\\n - Move s0 by 1 step and s1 by 2 steps using get_next()\\n - Decrement K\\n- After the loop, check if K is 0 to determine if a cycle was found\\n\\nFinding cycle length:\\n- If a cycle was found, initialize rho to 1\\n- Move s0 by 1 step \\n- Loop until s0 equals s1 again:\\n - Move s0 by 1 step and increment rho\\n- rho will equal the cycle length\\n\\nSkipping ahead:\\n- Reduce K by taking it modulo rho\\n\\nSimulating remaining steps:\\n- While K is greater than 0:\\n - Move s0 using get_next()\\n - Decrement K\\n- s0 will hold the final position\\n\\nPrinting result:\\n- Add 1 to the final position to convert back to 1-based indexing before printing\\n\\nThe key aspects are:\\n- Handling the input format and 0-based indexing \\n- Defining get_next() to handle moving to the next port based on direction\\n- Correctly implementing the tortoise and hare cycle detection\\n- Finding the cycle length after detection\\n- Skipping ahead with modulo and simulating any remaining steps\\n- Adjusting the output back to 1-based indexing\\n\\nI believe I have all the necessary pieces to implement this solution now. Let me code it up using the writePython tool.\\n', 'type': 'text'}, {'id': 'toolu_01EDrYeHJU7GxApRb1QfMA1b', 'input': {'reasoning': \"Here's the problem-solving approach:\\n\\n1. Read in the input data:\\n - N ports, M-length direction sequence, K repetitions\\n - L and R arrays for left/right port connections\\n - S array for direction sequence\\n - Multiply K by M to get total steps\\n\\n2. Define get_next(v) helper function:\\n - Takes current position and direction index\\n - Returns next position and incremented direction index\\n - Looks up next port in L/R arrays based on current direction\\n\\n3. Detect cycle using tortoise and hare algorithm:\\n - Initialize s0 and s1 pointers to (0, 0)\\n - Loop until match or all steps taken:\\n - Move s0 by 1 step, s1 by 2 steps\\n - Decrement K\\n - Check if K is 0 after loop\\n\\n4. If cycle found, find cycle length:\\n - Initialize rho to 1\\n - Move s0 by 1 step\\n - Loop until s0 equals s1 again:\\n - Move s0 and increment rho\\n - rho is the cycle length\\n\\n5. Skip ahead by K % rho steps\\n\\n6. Simulate remaining steps:\\n - While K > 0:\\n - Move s0 with get_next()\\n - Decrement K\\n \\n7. Print final position (+1 for 1-based indexing)\\n\\nKey points:\\n- Multiplying K*M avoids nested loop\\n- get_next() handles port transitions \\n- Tortoise and hare finds cycles\\n- Modulo skips ahead in cycle\\n- Adjust 0-based indexing for input/output\", 'pseudocode': \"1. Read input:\\n N, M, K = read_ints()\\n L = [0] * N\\n R = [0] * N\\n for i in 0..N-1:\\n L[i], R[i] = read_ints()\\n S = read_direction_sequence()\\n K *= M\\n\\n2. Define get_next(v):\\n def get_next(pos, dir_idx):\\n if S[dir_idx] == 'L':\\n next_pos = L[pos]\\n else:\\n next_pos = R[pos]\\n next_dir_idx = (dir_idx + 1) % M\\n return (next_pos, next_dir_idx)\\n\\n3. Find cycle:\\n s0 = (0, 0)\\n s1 = (0, 0) \\n while K:\\n s0 = get_next(s0[0], s0[1])\\n s1 = get_next(s1[0], get_next(s1[0], s1[1])[1])\\n K -= 1\\n if s0 == s1: break\\n if K != 0: no cycle, print s0[0] + 1\\n\\n4. Find cycle length:\\n rho = 1\\n s0 = get_next(s0[0], s0[1])\\n while s0 != s1:\\n s0 = get_next(s0[0], s0[1]) \\n rho += 1\\n\\n5. Skip steps:\\n K %= rho\\n\\n6. Remaining steps: \\n while K:\\n s0 = get_next(s0[0], s0[1])\\n K -= 1\\n \\n7. Print result:\\n print(s0[0] + 1)\", 'code': \"def read_ints():\\n return map(int, input().split())\\n\\nN, M, K = read_ints()\\n\\nL = [0] * N\\nR = [0] * N\\nfor i in range(N):\\n L[i], R[i] = read_ints()\\n L[i] -= 1\\n R[i] -= 1\\n\\nS = input().split()\\n\\nK *= M\\n\\ndef get_next(pos, dir_idx):\\n if S[dir_idx] == 'L':\\n next_pos = L[pos] \\n else:\\n next_pos = R[pos]\\n next_dir_idx = (dir_idx + 1) % M\\n return (next_pos, next_dir_idx)\\n\\ns0 = (0, 0) \\ns1 = (0, 0)\\n\\nwhile K:\\n if s0 == s1: break\\n \\n s0 = get_next(s0[0], s0[1])\\n s1 = get_next(s1[0], get_next(s1[0], s1[1])[1])\\n \\n K -= 1\\n \\nif K:\\n rho = 1\\n s0 = get_next(s0[0], s0[1])\\n while s0 != s1:\\n s0 = get_next(s0[0], s0[1])\\n rho += 1\\n \\n K %= rho\\n \\nwhile K: \\n s0 = get_next(s0[0], s0[1])\\n K -= 1\\n \\nprint(s0[0] + 1)\"}, 'name': 'writePython', 'type': 'tool_use'}]\n", "\n", "\n", "Code:\n", "\n", "\n", "def read_ints():\n", " return map(int, input().split())\n", "\n", "N, M, K = read_ints()\n", "\n", "L = [0] * N\n", "R = [0] * N\n", "for i in range(N):\n", " L[i], R[i] = read_ints()\n", " L[i] -= 1\n", " R[i] -= 1\n", "\n", "S = input().split()\n", "\n", "K *= M\n", "\n", "def get_next(pos, dir_idx):\n", " if S[dir_idx] == 'L':\n", " next_pos = L[pos] \n", " else:\n", " next_pos = R[pos]\n", " next_dir_idx = (dir_idx + 1) % M\n", " return (next_pos, next_dir_idx)\n", "\n", "s0 = (0, 0) \n", "s1 = (0, 0)\n", "\n", "while K:\n", " if s0 == s1: break\n", " \n", " s0 = get_next(s0[0], s0[1])\n", " s1 = get_next(s1[0], get_next(s1[0], s1[1])[1])\n", " \n", " K -= 1\n", " \n", "if K:\n", " rho = 1\n", " s0 = get_next(s0[0], s0[1])\n", " while s0 != s1:\n", " s0 = get_next(s0[0], s0[1])\n", " rho += 1\n", " \n", " K %= rho\n", " \n", "while K: \n", " s0 = get_next(s0[0], s0[1])\n", " K -= 1\n", " \n", "print(s0[0] + 1)\n" ] } ], "source": ["snapshot = graph.get_state(most_recent_state.config)\nai_message = snapshot.values[\"messages\"][-2]\nif ai_message.content:\n print(ai_message.content)\nprint(\"\\n\\nCode:\\n\\n\")\nprint(ai_message.tool_calls[0][\"args\"][\"code\"] if ai_message.tool_calls else \"N/A\")"] }, { "cell_type": "code", "execution_count": 50, "id": "b6fdebe7-557e-4751-97f3-15901c95600a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Incorrect submission. Please respond with updated code.\n", "Pass rate: 3/10\n", "Results:\n", "\n", "passed\n", "\n", "\n", "timed out\n", "\n", "\n", "timed out\n", "\n", "\n", "timed out\n", "\n", "\\nThe algorithm looks mostly \n" ] } ], "source": ["num_trials = 2\nwith tracing_v2_enabled(client=client):\n for _ in range(num_trials):\n events = graph.stream(None, updated_config)\n for event in events:\n for value in event.values():\n messages = value.get(\"messages\")\n if messages:\n if isinstance(messages, list):\n messages = value[\"messages\"][-1]\n print(\n \"Assistant:\",\n str(messages.content).replace(\"\\n\", \"\\\\n\")[:50],\n )\n elif value.get(\"examples\"):\n print(\"Retrieved examples:\\n\\n\", value[\"examples\"][:100] + \"...\")\n elif value.get(\"candidate\"):\n print(str(value[\"candidate\"].content)[:200])\n if graph.get_state(config).values[\"status\"] == \"success\":\n break\n print(\"Continuing...\")"] }, { "cell_type": "markdown", "id": "28446ca4-0063-4ac2-bd87-bb9319fef84c", "metadata": {}, "source": [ "You can review [a LangSmith trace (link)](https://smith.langchain.com/public/d383e743-f8f1-4206-9dce-47627f152612/r/3f89582f-9107-461a-a34e-608d52641eeb) of the agent's response to your feedback at the provided link." ] }, { "cell_type": "code", "execution_count": 55, "id": "28afec75-d661-4b88-b295-edb886231693", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "success\n" ] } ], "source": ["snapshot = graph.get_state(config)\nprint(snapshot.values[\"status\"])"] }, { "cell_type": "markdown", "id": "72327753-b8b6-4cbc-b5cc-b3cd90b7dd2e", "metadata": {}, "source": [ "**Success!** - the LLM really wouldn't have been able to come to the correct answer without detailed human involvement." ] }, { "cell_type": "markdown", "id": "d1e2d6e8-3daf-4cbe-acb9-387e3058e602", "metadata": {}, "source": [ "## Conclusion\n", "\n", "Congrats on making it to the end! In this tutorial, you implemented an agent in LangGraph capable of solving challenging programming problems. You did so by leveraging a few common techniques to improve performance, including:\n", "\n", "1. **Reflection**: while we didn't implement an explicit reflection step, our prompt and tool invocation was designed to encourage critique of previous outputs. You added this in Part 1.\n", "2. **Retrieval**: the \"episodic memory\" of the agent retrieves high-quality few-shot examples from our corpora of programming problems to help solve the **bronze** level question. In Part 2, you implemented a retrieval memory as an initial step.\n", "3. **Human-in-the-loop**: LLM-powered agents are still too weak to answer all these questions autonomously, but at times, they can get most of the way there and land on the right answer with human feedback. In Part 3, you used `interrupt_after` on the `evaluate` node and then included your feedback by using `update_state` on the graph.\n", "\n", "\n", "LLMs are not capable of solving all these problems autonomously, but through better prompting and clever engineering, you can create a system that is able to more reliably arrive at the proper solution." ] }, { "cell_type": "code", "execution_count": null, "id": "c71b4ba7-96ba-4643-b2de-eb2acdf4daba", "metadata": {}, "outputs": [], "source": [""] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.11.2" } }, "nbformat": 4, "nbformat_minor": 5 }