diff --git a/examples/human-in-the-loop.ipynb b/examples/human-in-the-loop.ipynb index 1ab370ed7..f13c24a0f 100644 --- a/examples/human-in-the-loop.ipynb +++ b/examples/human-in-the-loop.ipynb @@ -11,8 +11,8 @@ "This can be helpful when giving them access to tools.\n", "Often in these situations you may want to manually approve an action before taking.\n", "\n", - "This can be in several ways, but the primary supported way is to add an \"interupt\" before a node is executed.\n", - "This interupts execution at that node.\n", + "This can be in several ways, but the primary supported way is to add an \"interrupt\" before a node is executed.\n", + "This interrupts execution at that node.\n", "You can then resume from that spot to continue." ] }, diff --git a/examples/multi_agent/hierarchical_agent_teams.ipynb b/examples/multi_agent/hierarchical_agent_teams.ipynb index 37c9e46bf..1b34da98e 100644 --- a/examples/multi_agent/hierarchical_agent_teams.ipynb +++ b/examples/multi_agent/hierarchical_agent_teams.ipynb @@ -626,7 +626,7 @@ ")\n", "\n", "authoring_graph.set_entry_point(\"supervisor\")\n", - "chain = research_graph.compile()\n", + "chain = authoring_graph.compile()\n", "\n", "\n", "# The following functions interoperate between the top level graph state\n", diff --git a/examples/rag/langgraph_rag_agent_llama3_local.ipynb b/examples/rag/langgraph_rag_agent_llama3_local.ipynb index 0f1186256..0b5c83e1b 100644 --- a/examples/rag/langgraph_rag_agent_llama3_local.ipynb +++ b/examples/rag/langgraph_rag_agent_llama3_local.ipynb @@ -7,7 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "! pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python" + "! pip install -U langchain-nomic langchain_community tiktoken langchainhub chromadb langchain langgraph tavily-python gpt4all" ] }, { @@ -229,7 +229,7 @@ "# Prompt\n", "prompt = PromptTemplate(\n", " template=\"\"\" <|begin_of_text|><|start_header_id|>system<|end_header_id|> You are a grader assessing whether \n", - " an answer is grounded in / supported by a set of facts. Give a binary score 'yes' or 'no' score to indicate \n", + " an answer is grounded in / supported by a set of facts. Give a binary 'yes' or 'no' score to indicate \n", " whether the answer is grounded in / supported by a set of facts. Provide the binary score as a JSON with a \n", " single key 'score' and no preamble or explanation. <|eot_id|><|start_header_id|>user<|end_header_id|>\n", " Here are the facts:\n", diff --git a/examples/storm/storm.ipynb b/examples/storm/storm.ipynb index b40d6ff2b..f329bfd12 100644 --- a/examples/storm/storm.ipynb +++ b/examples/storm/storm.ipynb @@ -313,7 +313,7 @@ " description=\"Primary affiliation of the editor.\",\n", " )\n", " name: str = Field(\n", - " description=\"Name of the editor.\",\n", + " description=\"Name of the editor.\", pattern=r\"^[a-zA-Z0-9_-]{1,64}$\"\n", " )\n", " role: str = Field(\n", " description=\"Role of the editor in the context of the topic.\",\n", @@ -730,7 +730,7 @@ "async def gen_answer(\n", " state: InterviewState,\n", " config: Optional[RunnableConfig] = None,\n", - " name: str = \"Subject Matter Expert\",\n", + " name: str = \"Subject_Matter_Expert\",\n", " max_str_len: int = 15000,\n", "):\n", " swapped_state = swap_roles(state, name) # Convert all other AI messages\n", @@ -803,7 +803,7 @@ "max_num_turns = 5\n", "\n", "\n", - "def route_messages(state: InterviewState, name: str = \"Subject Matter Expert\"):\n", + "def route_messages(state: InterviewState, name: str = \"Subject_Matter_Expert\"):\n", " messages = state[\"messages\"]\n", " num_responses = len(\n", " [m for m in messages if isinstance(m, AIMessage) and m.name == name]\n", @@ -886,7 +886,7 @@ " \"messages\": [\n", " AIMessage(\n", " content=f\"So you said you were writing an article on {example_topic}?\",\n", - " name=\"Subject Matter Expert\",\n", + " name=\"Subject_Matter_Expert\",\n", " )\n", " ],\n", "}\n", @@ -1395,7 +1395,7 @@ " \"messages\": [\n", " AIMessage(\n", " content=f\"So you said you were writing an article on {topic}?\",\n", - " name=\"Subject Matter Expert\",\n", + " name=\"Subject_Matter_Expert\",\n", " )\n", " ],\n", " }\n", diff --git a/examples/time-travel.ipynb b/examples/time-travel.ipynb index 337428bcb..fed24cd44 100644 --- a/examples/time-travel.ipynb +++ b/examples/time-travel.ipynb @@ -331,18 +331,18 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "c9ab60eb-679b-4eef-9e64-5ffbf3dffc70", "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWkAAAFyCAIAAAC4CLxBAAAABmJLR0QA/wD/AP+gvaeTAAAgAElEQVR4nOzdd1hT1xsH8DeLPcOQvRRZLpaoWBEBRetEHGhxj1YsqHVjHbWto25brdbZuv25t4gLFQQVRRBElCF7hBVCdn5/JAKiooaQm8D7eXh8kpubmzeRfLn33HPPIYlEIkAIoa9EJroAhJBSwuxACEkDswMhJA0q0QUg5cDlcmtqasrLy2tqarhcLofDYbFY4odEIlFFRUXdmioqKpqamnV39fX1AUBHR0dTU1NDQ0NPT0/OlaMWgtnR1tXW1mZnZxcUFBQVFZW+U1xcXFRcXFpaymQyqyqrmMxqPp8vq1fU1NTS0NTQ1NQ0NDQ0MjQyMjI0NDQ0NjYW3zI2NraysjI1NSWRSLJ6RdQSSHiepY3gcrmvX79OS0t78+ZNTk5OVnZ2Tk7O27e5ZaUl4hXIZLKuPl2HbqCtT9fWo+saGmnr09U1NDS0ddQ0NFXVNdTUNTR1dFTV1GmqqhQqVU1Tq27jmlraJLLk+JfLZnO5HPFtIZ9fW8MEAFZ1NbuWxWXX1lRXsVksTi2LXVNTVc6oKmcwKxjVFYyqstIKRlltTY34iTQVFXNzc0tLKxtrK2traysrK3t7e0dHRxMTE/l9ZKhJmB2tU21tbVJS0rNnz9LT01PT0tLS0nKys/l8PolEMjQxNTKzoJuYGpqaG5qaG5lZGJiY0Y3b6dANCP9Tz2WzK8pKygryS/JzSwvzSwvyywrySvNzi/NzWUwmAOjo6HZ06Ojk6Ojg4ODk5OTq6mpra0tszW0WZkcrUV1d/fjx48TExMTExMdPnqS/fMnn8zW0tMxtO5ja2JnbdTCzsTO1aW9u215FTY3oYqXBKC7Kz8zIz3qTn/k6701GYfabwrc5QqFQT0/f1bWbm5ubq6urm5ubo6Mj4QnYRmB2KLGioqL4+Pj79+/fjYl59OgRj8vV0tG17NDRzqWLnUuX9p26WNjZ1x1KtD58Hq8g+83r5KQ3KUlZL56/Sn7G5XC0tLW9vLx6e3v37t37m2++UVVVJbrMVguzQ8nU1NRER0dfvnz5RnT064wMMoVi5+ji4Obp5OHVsau7gYkp0QUShs/jZaWlpD1JSH388OWThPLSEnUNjR5eXoGBgQMHDuzcuTPRBbY2mB3K4dWrVxcvXrx8+crdmLt8Hq9Dpy5dvPs6uXd3cPVQb9BmierkZ75Oe5KQHP/gWcytCkaZuYXFt4MGDRw4MCAgoOEpZCQ1zA6Flpube+rUqeMnTsQ+eKCmodHJy9vDN8C9bwDduB3RpSkNkVD4JjU56UHMk9tRaYmPaCoq/v7+EydMGDZsmIqKCtHVKTHMDkVUVVV15MiRQ4cOP3hwX1tXz6v/IO9Bw5w9e1Ao2B+nWSrLSmOvXXxw+VzqkwQdXd1RwcGTJk3q1asX0XUpJcwOxZKWlvbXX38dOHBQIBR4+g3w/nZ4N+++VBqN6Lpam9KC/PtXzt27cOZNanI3V9cfZ88OCQlRV1cnui5lgtmhKG7cuLFu3fro6BumVtb9x07sNzJEU0eH6KJav1fPnlw5vP/B1YtaWpozZ8yYN2+ekZER0UUpB8wO4j148GDp0qV37tzp5u0zKHSqW59+rfjEqmKqKCu5ceLw1cP7eWz2vHlz582bp6urS3RRig6zg0gvX7786af5ly5ddPHsETJnsZN7d6IratM4taxL/+09v3cnlUKJXLokIiKCSsUGpk/C7CCGQCDYtGnT8hUrzG07jP9paVdvH6IrQhI1VVXn9/99Yf/fTk7OBw/s79KlC9EVKSjMDgKkpaVNmDDxWdKzUWHzhk39Ac+eKKD8rDc7ls579TwxcunSZcuW4Q7IhzA75C0qKio4eJSJjV3Ymi0W7e2JLgd9kkgovHx4/5FNv/v29T1x4riWFvbBew+2ycnVvn37Bn37bWdvn1/+O62kwcGqrjqwduWj21EKuDXZIpHJ34ZO/e3ohYTExB49e759+5boihQLZof8/PXXX9OmTRsx48e5m3bSlPMarRcJcWEDvC8c2C3gyWAoINlurYXYODqvPny2ms3t4+NTWFhIdDkKBLNDTq5evRoeHj5u7uKxP85X3ovEM1OTqxhlACCTtyDbrbUcY3PLVf+d5gF5yJChXC6X6HIUBbYAyQODwZg4aXLvb4cHzfiR6Fo+o5ZZ/eDqxaK32bU1TF0DQwdXj05e3uLv9vO4e6+SEsWrJT+8z2JWefj219LVAwCBgP/oZlRmWkp1OUNNQ8Oivb2X/0ANbUnftpT4ByX5uWqaWp6+ATdPHy8tyO/m7cPn8z61NQWko09fvPPgwpEDVqxYsWbNGqLLUQjYVioPc+bM+ffwkW1XYuq+TorpRULcutlTmJUVDRd6Dxo6b9PfALDmh4mPbr3XMLHhTJStk4tQIFg6btirZ08aPmRqbbvsn8MmVjYAsP7HqQ+jrhhbWHXy6nXz1DEAsOzg0M7S6qNba5l3JhtXjxw4sHZl6osX7du3J7oW4uExS4urrKzc/c8/Qd9HKHhwAMDWhbOZlRXtLK1Hfh8xecmqTl7eAHD/8vm7508BQDsLq7rrd43NLW0cnVXV1ADgwsHd4uBw8/EbPHF6e5cuAFCQnXl06/qGGy/Je3vz1DFVdQ0KhdpnaNCntqbIAsZ8Z2Ritm3bNqILUQh4zNLiLly4wOcL+g4LJrqQz6goLS4tyAcAZ48eo8PmUWm0ASETT/y50dTGzs65MwBMiVzdztJ63+/LAWDyklXd/QPFT9TQ0uk3cqy6huaUyNUAwKllTe7ZmcOuLch603D7IpHIpXuvn/85XMuqIVMoWjq6H92aIqNQqH1Hjj125MDWrVuJroV4mB0t7uHDh+1dOiv+ToeugZGWrh6zsuLWmePx0VdcPHt26dUnYMx3xuaWTT8xYPT4gNHjAaCirCT96ZOU+Afi5bWsmkZrDp82i6aqqqTnmMQ6eXkf2bw2KyvLxsaG6FoIhtnR4goLC+ntlGAoQBKJFPbbpg0RMwQCfk1VVXz0tfjoa3tWR3b19pmxYo245eKjBAL+2X/+irt+OTM1uWHz2YdnT8xs7FqoeLkxNDEDgPz8fMwObO9ocSoqKgI+j+gqvkh3/8C/b8aPnv2TfRdXMoUiXvjs/p11YVOaeNaGiBlHtqx78+K5k4fXpMUrN569IQ6aD68GVtPQaJnC5YfH5QAADqEMuN8hBzY2NjEPE4iu4vNEIlFZYUF+5ut+QWPGzP6JVV319N7tg+tXlxbk5bxKqygt1jM0hne7EkKRUHyjrLAg/sZVAOgRMGjB9j3ihazqKgAgQeP9Dirt/TH+Ptia4st7k0EikXCnA3C/Qw58fX1z32QUZGcSXchnxN+4OtPXY9WUMX8tncdlszW0dTz7DdA3MgYAmqqqtp4+ANSNYPY2Pa0wJ6uWWc0oKhAvqa1hig9Yrh09WFXOAIDamupGL0F+f0/kw6214NuTkce3b7h06mRgYEB0IcTD7Ghxvr6+FpZWFw/+Q3Qhn9Hdb4BL914A8Dzu3oTujj8N9//Oo6O4+9aQiTMoVBoAmNtK+jUc274hrH+v9KREawdn8anWZw/u/uDn9YOf1+5VS8RXBleXlwsFgiZe8cOtteT7k4HKstKYi6enTmnqCK7twOxocRQKZcXyn2+cPJyV9oLoWppCIpMjd/83ZNIMdU0tHpeblfaCz+Pp6NMnLlo+bs4i8Tounj17BAwS36bSaLU1TBU1tQXb95ha2wJASX5uVTkjdP6yCQt/BgAOu/b5w/tNvOKHW2vBtycLhzb9pqujM336dKILUQjYr1QehEJh376+WQUFvx+7qKGlTXQ5nyHg88qKCqvLGfrGJvpGxh+eLqkoLa4qZ5jbthfvjACASCgsznvL43LNbdt/7YCJH25NMcVcPLN1wezTp08PHz6c6FoUAmaHnOTm5nbv7mVkbbf474Oqajget5J5Hnvv95mhs2eHbdy4kehaFAVmh/w8e/bMz8/fwNxy0c4DegayHIw7P/P1hogZTayQl5nB5/GsOzo1sU7EH39aOzS1QktT2Hdx98LpnZE/jRo96t+DB8k4DPU7eI5Wfrp27RoXFxs4cFDk2CGLdx607OAgqy3zeNzCt9lNrMDn8QCg6XXEPRcIpIDvQiQS/W/nluPbNyxYsGDt2rUKPlaAnOF+h7yVlZUNGzb80eNHYyMWDp4wva4LFlI0RW+zd/48P/XRwz///HPmzJlEl6NwcAdM3gwMDG7duvnzsmVHt6z7efzw3Ix0oitCjYmEwkv/7pk3zE9UUxUbG4vB8VG430GYlJSUyVOmJiY+8R40bGz4gs9ecobkI+lBzKENv2a/Svtp3rxVq1Zh9/NPwewgkkAgOHjw4MpVvxQXF/UPmRg0fbYOHTssEubZg7vHtqx79fzpyODgX1evdnCQWYNUq4TZQTwul3vgwIHlK1YyGIxeA4cMmTRTwYfPamV4XG7CzWuXDv6TlvjIz89/3bq17u7uRBelBDA7FEVNTc2///67ffufqakvXDy8AsdP9vQLpKmofP6ZSFrFuTnXj/9389RRFpM5atSo8B9/9PLyIroopYHZoXDu3bu3ddu2s2fOqGlouvsG9Aoc4trHF+eOkyFmZcWjW1Ex5089i40xMm43edLE2bNnW1hYEF2XksHsUFB5eXnHjx8/cuTo48eP6EbGPQOHePYb4OThVXftKfpajKLCx3ei465fTIq9p6GuMXzE8HEhIQEBAThfpHQwOxRdenr60aNHjx0/kZb6Ql1Ts0vPb1z79HP9pp+hqRnRpSkBgYD/8smjJzE3n8XcepOaoqauPmDAgHEhIUOGDFFXxysDmgWzQ2lkZWVduXLl0qXLN2/drGWxrO0dHNy6O7l3d3Tvjud3G+Jxua+Tn6U+epj2JD71cXxNdZVd+/bfDho0aNAgHx8fjAxZwexQPmw2++7duzdv3rwbc+/RowQel2tkaubo7mXf1c3OubONo7O6ZpubdbnobXZmavLr5KS0J/EZz59yOZx2JiZ9+vTp8803AwYMsLdXyql/FRxmh3Krra1NSEi4e/fuvXv34+Pjy8sZZDLZzNrW2snF1qmznXMnc7sOBiZmrexCDA67Nj/zzdtXaW9Sk7NSkzNfJDOrKslksn3Hjr29vb/55pvevXvj9EstDbOjVcnOzk5MTExMTHySmPjkyZP8vDwAUFNXN7ftYGJja25nb27b3tTa1sDEVM/QmOhivwiPyy0rzC/OfZuf9TrvTUZB1uv8zDfF+bkikYimouLi4uLu5ubq6urq6tq1a1dNTU2i621DMDtas7KyspcvX6alpb18+TLt5cvU1NTMzEzx1ag0FVVjM3MDE1O6iZmRmYW+cTt9Q2NtfbqOPl2HbiAenVQ+eFxudTmjklFWWVZSVc6oLC0pKcgrK8grKywoK8xnlBSLV9PXp3d06Ojs5OTg4NCxY0cnJ6f27dvT8KwTcTA72hYej5eTk/P27ducnJx3N95m52Tn5+VXNpiGlkKl6urTdfTpGto6KmrqGto6ahoaquoaahoaGto6NBXVutkSKFTahzMnCAXCuoGOhUIhq7qay2FzWCwWs7q2hsllszm1LFZVJau6qryshMWsH2qQRqMZGBpaW1tbW1lZWlpaWVlZWVlZWlpaW1sbGhq28GeDvg5mB5LgcrmlpaWlpaUlJSXFxcUlJSWlpaVVVVU1NTWVlZXV1UxmTU1NDbO8vLy2tpbDlgyTweaw2bW1H25Nr8Gei46ujoaGhqampp6urra2tqampqampr6+vo6OTrt27QwNDY2MjAwNDY2NjfX09OT0blGzYXYg2di7d+/cuXOrqqqILgTJCY7fgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWlgdiCEpIHZgRCSBmYHQkgamB0IIWmQRCIR0TUgpZSWltajRw+BQCC+y+fzORyOpqam+C6JRAoICDh16hRxBaKWRSW6AKSsHB0d27Vrl56e3nAhk8msuz1w4EC5F4XkB49ZkPQmTJhAo9E++hCVSg0KCpJzPUieMDuQ9EJCQvh8/ofLqVTqwIED6XS6/EtCcoPZgaRnZ2fn5uZGIpEaLRcIBN999x0hJSG5wexAzTJhwgQKhdJooaqq6rfffktIPUhuMDtQs4wdO1YoFDZcQqPRRo4cWXfCBbVWmB2oWYyNjX18fBruevB4vPHjxxNYEpIPzA7UXKGhoQ17Cenq6vr7+xNYD5IPzA7UXCNHjqRSJR2FaDTauHHjPnXiFrUmmB2ouXR0dAYOHCiODx6PFxISQnRFSB4wO5AMfPfdd+LO6SYmJt7e3kSXg+QBswPJwLfffquurg4AoaGhZDL+UrUJ+N+MZEBdXX3kyJEAgAcsbQdmB5KNcePGOTo6urq6El0IkhPMDiQb/v7+c+fOJboKJD84fgdqilAoLCgoyMrKysnJyc3NLS4uZjAYDEYpg1HMYJSxWCyRSFRRUQUAAoGwtpajpaUOAKqqKhoa6iQSiU6n0+lGdLoxnU6n0+kmJibW1tY2NjZWVlba2toEvzfUPJgdqB6Xy01LS0tJSXn+/HlKyvOUlKc5OQU8ngAAqFSSmRmtXTsSnS6g0/kGBkCng4YGkMmgqwsAQCKBujqwWOLtQE0NiETAYIh/SGVlVAaDnJ8vZDB44tfS19fu0MGuc2d3FxeXzp07u7i4mJmZEfbO0dfD7GjrCgoKYmNjHzx4EBt79/HjpxwOj0YjOzjQXFy4nTqJbG3B2hqsrcHMDD645E0aTCZkZ0N2NuTkQFoapKRQk5PJhYVcADAzM+zVq0+vXr179Ojh5uamqqoqg9dDLQazoy1is9l37969evXqlSvn0tLeUCikTp1ovXpxe/QANzdwcAA59wstK4OkJIiPhwcPyHFxlOJinrq6St++fQMDvx04cKC9vb1cq0FfBrOjDampqTl//vzx40eioqJYLE7nziqBgdyAAOjRAxSq8SEjA+7cgatXyTdukCsq+HZ2FiNHhowbN65bt25El4bqYXa0fkKh8Nq1a4cO/Xfu3BkOh+PvTwkK4g8cCBYWRFf2OXw+xMbC5ctw/DgtM5Pn5NQ+JGTipEmTLC0tiS4NYXa0ahUVFfv379+xY+vr19m9e9NCQnjBwWBkRHRZX08kgrg4OHYMTpyglZYKhg4dMnt2hK+vL9F1tW0i1BoVFBRERERoaqppa1PDwiA1FUSi1vDD5cLRo+DtTQOAzp0djx8/LhAIiP6w2yjMjtampKRkwYIFGhqqZma0zZuhspL4L3xL/Dx+DCEhZDKZ1KWL09mzZ4VCIdEffJuD2dF6CIXCgwcP0unahobUtWuBxSL+G97SPykpEBpKIZNJvXv3SE5OJvp/oG3BPumtxLNnz3r29Jw2bfKMGdVZWfxFi0BdneiaWp6zM/z7ryAuTlRb+9jNrdvy5cvZbDbRRbUVmB2twe7du7t396BSnz15IlyzBtraMMOenhAfz9u+nb9165qePT0yMjKIrqhNwOxQbtXV1WPHjp416/vly/kxMfxOnYguiCBkMsyYAU+f8imUlx4e3c6ePUt0Ra0fZocSKy8v9/f3uXnzzOXLoshI+GCKpTbH1hbu3+dPnMgKCgrasmUL0eW0cjiXtbIqLi4OCPCtrHwVG8tv357oahSGqips3Srq2BF+/HFudXX1zz//THRFrRZmh1JiMpn+/j5s9uuYGB72sfxQWBgAwI8/LqfT6WHiO0jW8JhFKU2bNqWw8PWNG60qOCorYd48uHhRNlsLC4Nff4W5cyPu378vmy2i92F2KJ8///zz1KlTx47xrKyILkV27t4Fe3vYvBl4PJltc8kSGDQIRo0azmAwZLZR9A5mh5JhMBjLly9duFDYrx/RpchUYiKUlACALFt8SSQ4eFAgEFT+9ttvMtsoegfbO5TM6tWraTT2okUy2yCfDxcuwNOnUFoKWlrg5AQjRkiGAquTkAA3b0JVFXh7Q2AgxMZCRgaoqEDDQdGTkuD2bcjKAkdH6NMHHB3rH3r4ENLSgEqF8eMhKwuuX4eXL8HFBYKCQE8PAODmTYiPl6x86xZUVsKQIUCny+Dd6erCypW8OXO2zZo1qz02KcsW0R1b0VdgsViammqbN8usTzefD15ejX8l7O0hI6N+nYUL39sXGDECRo0CANDTk6wgEEBkJDSclYVKhTVrQCiUrDBrFgCAujqcPg0aGvWrWVtLXmjIkMY1JCbK7D3yeGBrS1uwYAHR/3utDWaHMvnf//5HJpMKCmT2vfrjD8l3ddAgmDMH3N0ld0NCJCucPClZQiLB8OHQs6fkdsPs+OcfyToGBjB9ev2wIMePv5cdJBKQydC9O4SHg42NZJ3p00EkgvBwqBur1MYGunaFtDRZXvayZAnY2Jjh9XKyhdmhTCZNmujjQ5Xhl2r3bpgyBcLDJXeZTMl+gbu7ZIm4oyqZDHFxkiUHDki+5Lq6IBIBhwPGxpIoYTIlf+fFjbhOTpJdD3F2AMDw4ZKNvHolWeLlJVlS15PrzBlZpob4JyEBACApKYno/8BWBdtKlcmrVy+6dePLcIPTp8PevbB1KxQVwblzsGyZZDmTCQDA40FqKgBAjx71hzYTJ0K7dvVbeP0aiosBAPz8gM2GsjKorIRBgwAAUlOhsPC9l6sLkQ4dwNAQAKCsTIbv5pO6dgUymfSqLrGQLGBbqTLJzc0dNkyWG+TzYd06OH1a0r5QR3xUkpMDAgEANB6d0MICiookt+u+j6dOwalTjbeflwempvV3Gw5ZJt7BEW+/pdFoYGxMy83NlceLtRmYHcqktpYt2yvrR40C8VVjffrA8OHQrx8EB0NGhqThs24A5IbdI1gsSEmpv1s3onq3buDh0Xj7amrv3W04a4KcZ7zW0CCxxJPHIBnB7FAmZmYm+fnlstpabq4kOIKC6ncZKioA3u13GBuDri5UVkJcHDAYkpOm0dHQcIgMOzvJDW3t+kbTlBTQ0gIrq8adNZrou1H3kFDYnPf0cSIR5OfzLBR/cGelgu0dysTS0jYzU2Z9p/LyJDeqqyUHLDt3QmkpAEBVleShKVMAAJhM8PKCf/6BZctg4sT3NuLgAG5uAAD37sHx4yAQQHY2eHuDjQ106wZc7pcWo6IiuZGcDBkZ9QXIREEBsNlCzA4ZI7qxFn2FDRs26OlRORzZnH1gserPjFpbS86bUqkAABoawOeDSARFReDi8t4vTIcOIL6Ipu4c7c2b9b02DA0lByNUav2pmbom0oZDLotfzta2fiMNXb8uy/Msu3aBhoYqk8kk+j+wVcH9DmUycuTIykrB7duy2Zq6Opw6BeJJ17KzobQU1q2T9PhgsSRfZmNjiImBadOgfXswM4Px4+H+fckpkro9BV9fePAA3N2BSoXSUlBRgYAAOHz4I73OmuDjA0FBktsqKlBdLZv3KHbqFHXgwIGabW08tRaG87MoGR+fXlRqQnS0zM7UCoWQlQUcDjg4fKT9Mi4O9PTA2vq90U9tbSErCxwcIC3tvZXZbHj1Cjp0kH6o1MJCKC2V8aSWT56ApyfpzJmzQ4cOldlGEeAxi7KJi4sjkUiXLsm+A9VHfwYMkPyezJsHHA6w2XDihCRi6vqeKviPnx+1Rw8P7FQqc7jfoXxGjQp68uTio0c8ff0Wf60jR2D8eMltNTUQCCTXyFOp8OwZODu3eAHNtG8fTJtGun//fk9xd3okO9jeoXz+/vsfodB4zBiqHDpWjRsH+/dL2kTYbODxgEQCDw+4fl0JguPZM5g9mxIZGYnB0RJwv0MpPXz40Mfnmx9+4G/aJJLPEMdVVZCfDxQKWFm918VLYWVmQt++NAeH3leuRFEoFKLLaYUwO5TViRMnvvtu3IwZou3bhThCeiPp6eDvTzMycoyKuk2XyUAg6APYr1RZjR49mkajjR07uqpKtGuXqC3MAveF4uIgKIhmbd31ypUoPfHgQqgFYHuHEhsxYsSFC5cuXdLu1YuGc6GJbd8OPj5kNze/69dvYnC0KMwO5da/f/9Hj55SKE4eHtSDB6EtH4AWFkJwMHnuXPLy5b9cuHBZu+5KPtQyMDuUnq2t7b17DydNCps6ldyvH+3lS6ILkjuhEHbuBCcn6pMnZtevR0VGRpKwBajlYXa0Bmpqalu2bImNjausdOjalTxvnmQ8nrbg+nXo0YMWEUGZMWNecvLLfq1s/HgFhtnRenh6esbHJ27YsPXoUUM7O+qSJXIalYsod++Cjw9lwAAwNOz76NGTdevWaTQcSRm1NIL7taIWUFNTs2XLFhMTAzU1cmgo6elT4juGy/CHw4ETJ8DbmwYA3t49bt26RfTn3UZh/45Wi8lkHjhw4K+/tqSlvfbxoU2bxhs2DJS6ATElBQ4dgn37qOXloqCgET/+OMfb25vootouzI5WTiQSRUdH//XXtsuXr1AoosGDRSEhwsBA6S91lb83b+D4cTh6lPb8Oc/S0mTKlJkzZ840bTgOKiICZrVzi+UAACAASURBVEdbUVFRcf78+ZMnj1y9GkWjkby9wd9f4O9fPyeLQuHzIS4OLl6EGzdUnzzh6OlpDx48fMKECf369SPLeaRT9AmYHW1OQUHB5cuXr169fOPG9YoKpo2Nirc3r2dPkbc3dO4MBF75wWBAbCzExsL9+9T4eBGLJejUqWNg4NDAwEAfHx8qFftAKxbMjraLz+fHxcXdvHnz2LEjubk51dW1WlqUrl0pLi5cFxcQ/5iYtNSrc7nw6hWkpEByMrx4QX7+nPbqFQeA5OBg07Nn3969e/fv3x9HGFVkmOVtF5VK7d279507d9LS0vfu3evp6RkbG/vs2bMXL56dPv28tLQSADQ0KDY2VGtrvpWVwNISTEyATq//0dAAGg20tBpvWSiEykoAgLIyYDDqf/LyIDsbcnJUsrKgoIAnFIqoVEr79padO7uFhHT28PDYvXu3lZVVZGQkNmcoPtzvaNMEAsHw4cMHDx48c+bMRg8VFRWlpKRkZWXl5ORkZWVFR0eJRPzqamZV1cdnOVFTI6urU8rLeR99lEql0Ok6pqbtrK072NjYWVlZWVtbd+jQwcnJSbXBJf03b9708/NTVVWNiIhYuHChgYGBrN4pkjnMjrZOJBJ9tgd3enq6o6PjtWvXAgIC+Hw+4x0Wi8Xn86urqwGgtraWzWbr6uqSyWQKhaKjowMA9HfEd79Ep06dXrx4QaFQVFRUFi1aNHfuXLwyRTFhdqDP27Fjx7p16zIzM+VwjuPgwYNTpkwRCoUAQKVSNTQ0li5dGh4erq5EZ5XbBswO9EWqqqq+fN+hObhcrrm5eal4jikAAKBSqTo6OgsXLpwzZ46qUoxZ1jbgqXL0ReQTHACgoqISHh7e8Iys+CgpMjLSzs5u9+7dAvnMf40+B/c72paKigrFHxGHwWCYmZlxOJxGy8XtMu3bt//999+Dg4PxQnti4X5HG/LTTz95e3vzeB8/FaI46HR6aGgo7YP5ncSXYL1582b06NFhYWGE1Ibq4H5HW/Hnn3/OnTv30KFDY8aMIbqWz3vx4kWnTp0++stJpVJ79Ohx+TKODEYwzI62orq6OjY2tn///kQX8qX8/f3v3LnD5783eyaFQvH39z9z5gyediEcZgdSUNeuXQsMDGy4hEQiffvtt6dPn/7wcAbJH7Z3oE+6fv361q1biXr1AQMGODo61vUoIZPJdDr99evX4q5oiHCYHeiTdu3adfXqVQIL+Omnn8Q3SCTSggULkpKSWCzW8OHD2Ww2gVUhMcwO9HF8Pv/OnTvDhg0jsIbx48fr6OiQSKQ1a9asXbvWzMzs8uXLycnJEyZMEHc8RQTC9o7W6cyZM3379tXX12/ORqqrq8lksqampqyqksLKlSv19fUjIiLqlty9e7d///5hYWEbN24ksDCEYx23QocOHSKTyTt27CC6EBkQCAQfLjx27BiZTN68ebP860F1cL+jteHz+d27d+/Xr9+GDRuIrqUFrV+/fsmSJcePHw8ODia6ljYKs6MVqq6u1tLSavVdtiMiInbv3h0VFdW7d2+ia2mLMDuQshIKhaNGjbp9+/b9+/cdHR2JLqfNwexASqy2ttbPz6+wsDA+Pt7Q0JDoctoWPEeLlJi6uvq5c+cAYOTIkVwul+hy2hbMDtRYTk7OvXv3lGWH1MjI6OLFi8+ePftwyFXUojA7lFtWVlZoaCiTyZThNo8ePapco2M4OzsfPXr0v//+27ZtG9G1tCE4x4ISKykpCQwMVFNTa3SxaTPFxcX16tVLhhuUg4EDB65evXrevHkdOnQYNGgQ0eW0CZgdSozFYtnY2Ozfv1+2Q4HNmzdPGedtXLJkyYsXL0JCQmJjY52dnYkup/XD8yyo9WCz2b6+vuXl5XFxcYo/tKKyU74/Lwh9ipqa2pkzZ1gs1ujRo2V7HIc+hNmBWhUTE5Nz5849ePBgwYIFRNfSymF2oNbG1dV13759W7du3bt3L9G1tGbYVopaodGjRycnJ4eFhXXp0sXT05Poclon3O9QGjExMWPHjsUhs77QypUrfX19g4ODy8rKiK6ldcLsUA6vX78eOXJkTU0NDvP7hchk8uHDh8lk8sSJE/FkYkvA7FAOXC7X19f3xIkTFAql5V7l4cOH3t7elZWVLfcS8kSn048dOxYVFfXHH38QXUsrhP07UL1///135syZNTU1ytg37FM2bdq0aNGi6OjoPn36EF1Lq9J6fkVQ89XW1np4eLSm4ACAuXPnDhs2bPTo0QUFBUTX0qrgfgdq/aqrqz09Pc3MzKKiolr0oK9NaVV/YRD6KG1t7RMnTsTFxa1atYroWloPzA7UJnTp0mXLli2//fYbsbNVtSaYHYro7du3ERER2JVDtmbMmBEaGjp+/PisrCyia2kNsL1D4dTW1vr4+DCZzNjYWF1dXaLLaVVqamq8vLzodPqtW7ew4aOZcL9D4WRnZ3M4nIsXL2JwyJympubx48cTEhLWr19PdC1KD/c7FJFIJFKiIf+UzsaNGxcvXnzv3j0vLy+ia1FimB2oXlZWFp1O19HRIbqQliUUCvv375+dnZ2YmKilpUV0OcoKj1lQPXd39yNHjhBdRYsjk8kHDx4sLy/HMT6aA7MD1VNXV6+trSW6CnkwNzffuXPnrl27rly5QnQtygqPWVC9P/74o3v37j4+PkQXIifjxo27c+dOcnKyvr4+0bUoH8wO4j148MDT0xMvrpe/srIyFxeXwYMH79mzh+halA8esxAsISGhX79+Bw4cILqQtsjAwGD37t379u3DzqZSwP0OIolEIg8PDyMjo8uXL7eyq1eVyJgxY+7fv5+cnIzTMnwVzA6C5eTkaGpqGhgYEF1I21VSUuLi4hIcHLxjxw6ia1EmmB0IwaFDhyZOnBgbG9u9e3eia1EauJ/cSoSEhPj5+RFdxZcqLS2ta+LR19dft24doeXAd99917dv31mzZgkEAmIrUSKYHYgAM2fOPHHihPj2+PHjO3fuTGw9ALB9+/akpKS///6b6EKUBmYHes+vv/567969ln4VoVBYd8HOn3/+qQgz1zs7O8+dO3fp0qU4NOEXwuxQCPv27XNzc9PW1u7evfuFCxfqlvv4+Fy/fv3HH3+0tLS0tLScP38+j8cTPyQUCn/55ZfOnTtbWVmtWLFCKBTKpJLjx49fvHjxow/9888/ffv2NTIyCg0NvXPnjnihQCDYsmWLs7OzuPgzZ858tvjw8PDbt2/fu3fPw8OjoKDA19f34MGDn32/PXv2PH78eN3Gly5dOnPmzLq7n/oAv8qKFSvodPqiRYuke3qbI0JylJGRMXbs2PLy8oYLN2zYQKFQxowZc+rUqfDwcBKJdPr0afFDOjo6FhYWvXr12rFjx/Tp0wHgjz/+ED/066+/qqurb9y48cSJEx4eHqqqqv369Wt+haGhoQMGDPhw+f79+6lUamRk5OnTp4cOHaqjo1NaWioSiZYtW0alUpctW3bu3DlxhQcOHGi6+OvXr7u5uXXp0mX//v3V1dV6enpr16797PtVUVHZtm1bXT1jx47t27fvZz/Ar3Xq1CkSiXTz5k3pnt6mYHbID5/P9/b27tatG5vNrltYUVGhq6srnn9IbNSoUfb29uLbOjo67u7uQqFQfNfLy8vf318kEpWUlFAolK1bt4qXMxgMTU1NX1/f5hdZVFTEYrEaLeTxeOrq6osXLxbfra2ttbS0/OOPP96+fUuj0X777be6NceNG2diYsLlcpsoXiQSDR8+fNCgQeLbjbLjU0/5VHY0/QFK4dtvv3V2dha/BdQEPGaRn6KiIjab/e+//6qqqtYtfPr0aWVlpaen5+N3nJ2dX716VVpaKl7B19e3rmnA3t6+qqoKAJ4/fy4QCEaMGCFerq+vP2DAAJkUaWxsrK6u3mhhenp6bW3tqFGjxHfV1NSys7Pnz5//9OlTHo8XGhpat+aECRMKCwtfv37dRPFN+9qnfPYD/Fpbt2598+bNli1bpHt624FzWcuPmZnZo0ePGi0Uj505e/bsD5cbGhoCgJGRUd1CNTU1cbtGeno6AJiYmNQ9ZG5uXl5e3iJ1A6SmpgKAqalp3RLx1zszM5NEIpmZmdUtt7CwAIC8vDxHR8dPFd+0r33KZz/Ar9W+ffsFCxb89ttvkyZNalgMagT3OwgmvoIzJiam5n1ubm7iFT46gJj4K9owLJhMZssVKR79sOFck+Xl5Twez8DAQCQSVVRU1C0XTxxtZ2cnvivF6GdNPIXL5dbdZjAYIpEIvuADlMLChQs1NDR++eUXqbfQFmB2EMzJyQkAzp8/r/HOiRMnvv/++7rzCx/VrVs3AIiJiRHfFYlE8fHxLVekg4MDiUR6+PBh3RI/P7/vv//e2dkZAG7fvl23/Pbt21paWjY2Nk1vkEQife2JIS0tLQaDIb4tFApfvnwpvi3dB/jZ11q5cuXff/8t3uFCH4XZQTB7e/tRo0bt27dvx44d5eXlUVFR4eHhFhYWDdtEPmRubh4SErJw4cLExMScnJwZM2akpKS0XJGWlpZjxoyJjIw8cOBAWVnZ1q1bk5KSlixZ0q1bt8DAwPnz58fFxXE4nPPnz2/cuPH777//7O6GlpZWampqTEzMlw815OrqumfPnocPH2ZlZYWFhdX1wpDuA/ysqVOnOjo6RkZGNmcjrRzBbbVIJCovL58wYQKVSgUAU1PT6dOn153p0NHRWb9+fd2a06ZN8/DwEN+ura0dPXq0uro6iUTy9vYODQ2VyTnaT2EwGGPHjhXPS6ChobFlyxbx8pKSkuDgYDKZTCKRjI2NFy1aVPeUJoqPiooSj4p6+/btRudZPvWUpKQkcRsKhUIZO3bswoUL687RNvEBNselS5cAIDo6uvmbapXwWriWJRQKv/Diei6Xm5eXZ2Nj81VtBLW1tZWVlQ0bTWVCJBLduHHD2tq6Y8eOjV4uLy/PyspKRUWl0fLCwkJbW9uveony8nI6nf5VheXl5enq6n50gGLpPsCm9e/fv7KyMi4uDoet/xBmRwuKj4+fOnVqVFSUzL/bcuDs7PzNN9/s2rWL6EKI9OzZMzc3t0OHDoWEhBBdi8LB7GgpXC7X3d3dyMgoOjq6ib9ay5Yt+/DEbXP89ttv7u7uzd/Orl27IiIi3r59q6mpGRQU1PwNypmmpuapU6eav53Jkyffvn07LS2tmQ0orQ/272gpLBbL1dV11apVTe/uurq6yna4KlkNIzR58mRXV1cjIyMul+vv7y+TbcqTrL7qq1evdnBw2LFjx9y5c2WywVYD9zsQ+oz58+f/+++/b968wYmgGsJztAh9xuLFi9lsNo5I2AhmB0KfYWhoOHv27PXr13/J9ThtB2YHQp+3cOFCgUCwbds2ogtRIJgdCH2enp5eRETExo0bW+6CQ6WD2SFLshq8SwFxudyG18K1QXPnziWTyXhtfh3MDpmprKzs3LnzrVu3iC6kRQQFBYkH8mqzdHV1f/rpp02bNpWUlBBdi0LA7JCZyMjIoqIiRRjyuyVERET873//azhiaBsUERGhoaGxefNmogtRCJgdMmNmZrZ9+3bpxptRfAEBAbt27fLx8SG6ECJpamrOnz9/+/btdaMBtGXYNwyhr1BTU2NjYzN79uwVK1YQXQvBcL8Doa+gqakZFha2bdu26upqomshGGYHQl8nPDycx+Pt3r2b6EIIhtmB0Neh0+kzZszYsGEDm80muhYiYXYg9NXmz59fUVFRN51d24TZgaRUWFi4ceNGoqsghomJycSJE9evX8/n84muhTCYHdI7fPhweHg40VUQ5tGjRwsXLty6dSvRhRBj4cKFOTk5J06cILoQwmB2SKm6unrhwoXNGchf2Q0ePHjNmjXHjx8XCARE10IAOzu7MWPGrFmzps32csD+HVJ6+vTp5MmTb9y4IatxupQUh8Nps4PxPX/+vGvXrhcvXhw0aBDRtRAAswMh6QUGBvL5/Bs3bhBdCAHwmAUh6c2dOzc6Ovrp06dEF0IA3O9AqFm6du3q6up64MABoguRN9zvQKhZIiIijhw5kpubS3Qh8obZgWSpoqIiNDS0sLCQ6ELkZ/z48QYGBjt37iS6EHnD7ECyxGKx4uPjfX196+aabvVUVVV/+OGHnTt3MplMomuRK8yOr7B169YWnW6+FTAzM7t9+7azs7N41us2Yvbs2RwOp611Uce20i8lnpr0yJEjY8aMIboWpHBmzpx569attLS0L5y6vBVoK++z+Q4ePOjh4TF69GiiC0GKKCIiIiMj4+rVq0QXIj+43/GlRCJRWVlZax1SEDWfv7+/qqrqpUuXiC5ETnC/40uRSCQMDtSEsLCwK1eupKenE12InGB2IHlgsVi//vorl8slupAWNHToUGtr6127dhFdiJxgdiB5eP78+fr16/v3719WVkZ0LS2FQqHMmDFj3759NTU1RNciD5gdSB68vLzu3bsnFApb8dR5ADB9+nQ2m33o0CGiC5EHbCt9j1AobDvn2FBLmDx5ckJCwvPnz0kkEtG1tCz8ntRjMpldunQ5ffp0XZ4mJSURWxJSOuHh4SkpKXfv3iW6kBaH2VHv1atXKSkpI0eOdHNzi4qKev78uZubW5s6Y4+az9XV1cvL6++//ya6kBaH2VEvPT1dvJ+ZnJzcv3//Pn36dOjQoX///kTX1cq1vstApk+ffvr06VY/5TVmR7309HQVFRUAEA9+zWQyX7586e/vn5iYSHRprZZIJBo0aFBISEhFRQXRtcjM2LFj1dTUDh8+THQhLQuzo96rV68aDpkvvh0TE+Pu7h4SEvL69WviSmu1SCTSsmXLbt++PWPGDKJrkRlNTc0xY8a0+onj8DxLPXd39ydPnnzq0fnz5//xxx/yrKftYDAYNTU1lpaWRBciM/Hx8V5eXrGxsT169CC6lpaC2VFPR0fnUxMUL1q0aO3atXKuByk1V1dXd3f3PXv2EF1IS8FjFonS0tIPg4NEIpFIpI0bN2JwoK81ZcqUY8eOVVVVEV1IS8HskPjwEiZxcOzZs2fevHmElISU+lRFaGioSCQ6duwY0YW0FMwOifT09IY9SslkMoVCOXHixJQpUwisqi1js9lubm7BwcHZ2dlE1yINPT29ESNG4DFL6/fq1SsajSa+TaFQVFRULl26NHLkSGKrasvU1NT27t2bnJysvFPeTps2LSEhobXO3oJtpRLBwcFnzpwRCoVUKlVdXf3atWs9e/YkuigEXC6Xx+NpamoSXYg0RCKRg4NDYGDgtm3biK5F9nC/QyI5OVkoFNJoNF1d3Xv37mFwKAgVFRUlDQ4AIJFIU6dO/e+//1gsFtG1yB5mBwCASCTKysoCAFNT0/j4+C5duhBdEWoWxemlOmnSpJqamtOnTxNdiOxhdgAA5ObmcjgcBweHuLg4Ozs7ostBn7dx48YtW7aw2ewPHxJfD3379m25F/UR7dq1Gzx4cKtsMcXsAABIT0/39PS8d++eqakp0bWgL8JkMiMjI729vT9ssNu7d+/bt28DAwOvXbtGSG2NTJs27c6dO2lpaUQXImPK1FZaWVlZWFhYXFxcXFxcWFhYWVlZVVVVUVHBZDKrqyqrqyqqqiqYTCaPxxOvz2Sy6m6LaWtrUamUd7e11dU1tLV19eiGPB7PzMxMX19fT0/PyMjIyMioXbt27dq1MzY2Fl8dhxRQfn5+amqqn59fw4V8Pt/a2lo8K534LPuIESPkWRWXy83Ly8vLy8vJySkoKCgrKysvLz979qylpRmJJKyqqhQP2srhcFms2rpnaWlp0mhUAFBTU9PR0dXXN9TXNxT/QhoYGJibm1tYWFhZWZmYmFCpVHm+nSYoXHZwudy3b99mZmZmZWWJ/816k56Tk1NSWs7h1geBkS5NT5Osow666kJtNYGWqlBLDfQ0QI0G6u++7OoqoEZ7b+MVLKh7uxUs4PCgmg2VLKhiU5kccjWbVMmC4kp+LUdQ9xR9XW0TEyNrm/a2dh1sbGxsbGxsbW1tbGyMjIxa9oNAUjl8+LC4Uxa869138ODB7777riVeSyAQZGZmpqWlpaampqWlpaY+y8zMLCoqF786jUZu145qaEjS0xPq6/P19UX6+qClBerqAAA0Gmhp1W+qqgoEAgCAmhpgMqG8HMrLyeXllIoKcmmpqKiIJxCIAIBCIbdrR7e37+jo2NnR0dHJycnR0dHKyoqQMcoIzg4OhyP+6FNSUlJfpCQ/T3yd+ZbPFwCAljrFxphqa8CzNRJaGkA7XTDSBlN9MNYBI22gtuSMhUw2FFZCcSUUV0FhJRRWQGYJZJXRskpJeaU8gVAEAPq62s7OTs6dujo7O7u4uDg7O5ubm7dgTejLdO7c+cWLFw1HRSWTyXv27Jk8eXLzNy4SiTIyMhISEh49epSQ8CAx8VlNDRsALCxUHB2Fjo58e3uwsABzc7C0BBMTkNXwlXw+FBZCTg7k5kJuLqSnw8uXtBcvoLiYBwC6upru7u6enj09PT09PT2trKxk86qfI+/sEAgEKSkp8WJx91JS0/l8AY1K7mBKczblOZsLXSzAzhhsjcBQW551fSmeAHJKIasUXuZDci6kFdKS30JJJQ8ADPR1unv18Ozew9PTs3v37sbGxkQX2+Zcv359wIABHy4nkUjbtm2bPXu2dJt9/fp1dHR0dHTUrVs3SkoqaDRy585UT0+upyd06QIODqCj07y6pcVgQFoaPH0Kjx5BQoJqaipXIBCZmxv5+QX6+fn7+fm16N8zeWQHh8OJi4u7devWrejrj58k1rDYmmoUN1uypy2ve3twsYCOJqCiKAdx0iithuRceJYN8W9ICZkqr/I5AGBjZeb9Td9+/fx8fX1tbW2JrrFN8PX1vXfvXsNBWBratGnT3Llzv3BTfD7/zp07p0+fvnz5bFZWvqYmpU8f8PMT9O4NXbuCmprsipYdJhMSE+HuXbhxgxobK+RwhI6OdoMHB40cOdLLy0vmxzUtmB1Pnz69evXqzeio+/fvs2o5tiaqvo7cXvYiTztwsQBK6z3Dw2BCwhuIfw0x6ZT7L4HFEdham/XzD/Tz8w8MDNTX1ye6wNbp2bNnrq6uTf8+r1q1avny5U2sIBAIoqKiTp48ce7c6bKyyi5dVIcN4wQEgJcXKFejOYsF9+7BjRtw5gwtI4Nnbm40YsSYMWPG9O7dW1YvIePsEAgE9+/fP3v27NnTJzKz80zptH5OfF9nUT8XsG2TbYscHsRlwM0XcPMF7WGGAIDUt2+f4SOChw0bhu0jshUSEnLy5EmBQND0asuXL1+1atWHy3Nycvbt27dv367c3MLu3WlBQbygIOjQoWVqla+kJDh9Gk6dUklO5jo6tp869fsJEyY0/5haZtmRkJBw4MCBk8ePlJRVOFmqDHfljvAED1to7ZNUfIVKFlx6Cmcfk688I9WwhV6ebqETp4SEhOCeSPPl5+fb29vXdf0mkUg0Go1EIvH5/A/TZP78+evXr6/bh4+JiVm37rcrV64bGVEnTuRNnQodO8q1eLlJTIR//oEjR6i1tRAUFLRo0ZJu3bpJvbXmZkdxcfGhQ4f2792d/OKls6XKd724QZ7ggB2smsTmQXQynIwn/S+eLBCRhw8fPmny1ICAAJxWqpkYDIa4B1B+fn5xcXFBQUFhYWFBQUF2dnZpaWl5eXndb3tYWNi2bduuXLmyZs2v9+/H9e5NnTOHP3Qo0GhNv0JrwGLByZOweTM1KYk/cGD/JUt+lu5ARvrsyMjI2L596+5du1SpoqFuggm9RX4uuJfxdWq5cDERdt+mRT/n2VhZRMydP336dA0NDaLrap0EAkFxcXFRUVF+fn5cXNz586eSkl4MGkRdvJgvu0YApSESweXLsHYt9d49fmBgwObN2xwdHb9qC9JkR0xMzPp1ay9dvuJkQZs/kDu2Z313LCSd9ALYcpV0IIaspaU9O3xuWFiYgYEB0UW1Tvn5+YsXLzx06IiPD3XTJp6rK9EFEe3WLZg3j5aSIpw1K2zFipVffgT9ddmRnp6+LHLxyf+dcbejhvfnj/duzadL5K+kCv6Kgj9v0DgC6k/zFy5evFhNMU8GKq2TJ0/OnDlVS6v211/5oaG4mywhFMKhQ7BwIQ1AZ9++fwcNGvQlz/rS7CgrK1u5cuXff+/sZEnZGMLt59K8YtGnMdmw/iJsuEw2N7dYv2GLnC/HaK0qKyvDwmYdOXJk9mzSunUiccdw1FB5OXz/PfnkSeHcuXN//32Nqqpq0+t/UXZERUVNDB3Hq61YNow/uz/ua8hDXjn8coa855Zo2NAh/+zZh4cwzZGdnR0Y6MdgZO/dyx88mOhqFNvJkzBjBtXBoeuFC1eavmjrM9nB4XAWLVq0bdu28d6kvyYJdTCt5etmCkzcTQMV+n+Hj/Xt25focpRSUlLSwIEBRkblly/zzMyIrkYZpKbCwIFUDQ3rK1eira2tP7VaU9lRVlY2KDAg7cXzHZP4471bpkz0OQwmTN9LOftIuGXL1h9//JHocpRMcnLyN9/0dHNjnznDJ+qqE2WUnw8DB9IYDHpc3ONPdWL8ZHbk5+f39+/HYmReW8i1N2nJMhXG7VQorwEaBQYrXtv7+ouw+BisXv1rZGQk0bUojcLCwh493G1siq9d43/u4J0wz59DRgYAgJ8fYdfUfVRFBXh7U1VVHWNi4j46ZOzHs6OoqKhXD081QeH1RTzzNtPp0fNnePQGtNWgai/RpXzM7pvww37Szz//vHLlR7pUo0b4fP433/RgMJJiY3l0OtHVvFNZCatWQb9+UNfsMmcOiOeQSEqCzp0JLO0jMjOhRw9anz6DT578yHirH2n2FAqFE0LHkziFdyLbUHAovhn9YNcU0S+/rL5y5QrRtSiBv//+OzEx8dw5BQqOu3fB3h42b4b3R7NTXLa2cOwY79SpMxcuXPjw0Y9kx7p1627fvnVsFk8xR9BoG6fdSwAAEgRJREFUy6b5woRvYGLouLy8PKJrUWhlZWUrVy776SfhV3aVbFmJiSCeJLNhv5I5cyA2FmJjoX17oupqiq8vhISQ58wJ+3BY6cbDZmRnZ69aueL30UIPxR4tPCkHbqdCVgk4mkEfR3Bs0H7+MAPSCoBKhvHekFUC15/DywJwsYAgT9B7v7f3wwy4nQoVLOhpD0MUr43jo/6aJOoWWbNo4YJDh48QXYvi2rFjB5XKWrpUxpvl8+HCBXj6FEpLQUsLnJxgxAjQ1X1vnfx8uHEDkpOBQgEHBxg9GsTXGNy8CfHxknVu3YLKShgyBOh0KCmBly8BABwcoOHVCAKBZDCOoiLo1An69YOGTZYPH0JaGlCpMH48ZGXB9evw8iW4uEBQEOjpyfhd//GHsH37/OPHj0+cOLHh8sbtHXPmzDl3dEf6Bh6tJQf1aw6hCJb/D9acA+G7wqkUWB0Mi4ZI4jzsAOyIAnUVOBwG3/0FLK5kNWtDiF4K7dsBAIhEMP8IbLpcv9lvXSGjEF4WKG57R51jsfDdTvLr12+aOH/Wxrm42Pfvn7F5syy3KRCAtzc8fPjeQnt7uHKlfpfh4EEID4eqqvoVjIzgyhVwd4ehQ6HRjn9iInTr9vH2jowM8PWF3Nz6lbW0YP16+OEHyd2wMNixA9TV4fBh+O47qJs6ytoaoqNlvwsTFERhsfpevXqj4cL3jllEItHRw/9O9VHc4ACAfbfht7MgFIGBFkz3BQs68AWw5DicfP8/lc2D4C3QyRLCB4CNEQBAdimse/efd+KhJDhIJBjiBr06wqVEeFkg33cirZHdga5FacUTrDdTcnLyixcZISEy3uzmzZLgGDQI5swBd3cAgFev4OefJSvExcHkyVBVBSQSBARA375AJkNJCQwZAmw22NpCXe8SGxvo2hU+1bc1MxP69ZMER8+eMGgQaGgAkwmzZsGBA++tyWZDcDB06gTh4WBjAwCQnQ3r1sn4jQPAuHGCGzduMRiMhgvfy460tLTi0vJABZ4UjcuHyBMAAHoakL0Ndk+DzC1gZQAAsPI0NNyFEolgqDs8/AW2ToCoJZKFSW8lN35512x8eQGc/wnur4B/psnpLTQfjQL+Lvy7d24RXYiCSk1NpVBIbm4y3qyuLkyZAuHhcOkSbN4Md+5IDjHS0yUrLFwIIhGQyRAbC9evw61bEBYGJBKoqsLDh7B1KyxcKFlz82Z4+hQcHD7+QsuWwdu3AABbtsCDB3DpEjx+LBm1bN48KC+vX1MkgqFDJRuPipIsTEqS8RsHAA8PEAiEGeKTye+8lx3ieRUdFbjv3esiKK4CAPDrBGwelDGhshYGdQMASM2Dwsr3Vp7lL7nRoZ1k5OSyagAAnkCyi2GgBf3fBeWUvqCrPNe+O5qKMl+/IroKBfX27VtTU5rMpzGZPh327oWtW6GoCM6dg2XLJMuZTAAAkQgSEgAAPD3By0vy0Jo1UFEBmZng4/MVL3TrFgCAqipMnSpZ4ugIvr4AAOXlklepM2uW5EaHDmBoCABQVva17+zzLCyASiXl5OQ0XPjeB8zhcABAVYGHP3lVJLlxKh5OxTd+NI8Bpg0aiowa9LTRUAEAEAgBAN6WSW74OAH5XYs3mQQWdKhUkimHVWnA5nxkOkUEADU1NZqasr9Cls+Hdevg9GlITHxvD1fcypabC+ITEQ27vUsxCXdmJhQUAAD07fveBC6DB4N4lruUFOjfv355wytOxPtBnxt0URpUKqiqkqurq99b2PCOoaEhABRWgKWiXnhV1xDTzRo+PBOk9v4wIqoN3lzDEbnqrsrhNfiU+QLIKZVRlS2voAKMjHAOh4+zsLDIzf34UOnNMWoUnD0LANCnDwwfDv36QXAwZGRIfrXqvucNG0qlYGICNBrweNDoLHxdu2mj4TUa9pdtuWHnGAyoqRE0mvnlvezo2rUrhUJ+8Eo4RlGzw+7d90Vbrb6FIiUXtNTAyqDxcAyfGp3BUBt0NaCSBY8zQSiS7HrEZkC18vwhf5BB8wjoSXQVCsra2rqmRlBUBO3ayWybubmS4AgKglOnJAsrKgDe/Zrp64OhIZSWQlIScDiSr/SlSxAWBp06wfffw+DB9b+QDWaeakxdHbp1g4QESE6GN2+gbmL18+clNxr1PZXPECSvXwMANMqO95JKW1vbt2+fIw8U9yyLgym42QAA3EuH43EgEEJ2KXivApsI6LYUuF/8x2aEBwBAfjnMPgBVtVBaDb+eaaGSZS+9AB695g0dOpToQhSUl5eXlpaa+KsuK3V7AdXVkgOWnTuhtBSgwY7G6NEAACUlMHo0PHoET57A8uWQnQ2XLkl2FupmaUhOhoyMT+6h9OsnuREWBq9eQWkp/PorpKYCAPj4gMzbgL/E2bNgZWXa4f1h4xvv5cwKC7+YKHyWAwprw3jQUAGRCMZuB5NZYDcHKllApcDuqV8xQdSvoyVHLjtvAH0GGP8AN19Iun4ovt/PkzrYWQcEBBBdiILS1NQcOnT44cOybLfr0kXSkBEVBba2YGsLs2aBuDm2rEzSxLB6NZiYAACcPw+enuDuDk+eAACMGgXe3gBQf2JlxQqwt2/cVaTO6tUQHAwAcPUqdOwIRkaS08AGBrBnDwFjnYlEcPQobdy4iY1mh2qcHcOHD+/R3WPiLhpbUbvc+zrDg1XgbgtUCpRWgwoVAjrD4Vng9TVTaZjrw8NfwNUGAEAgBFM9OP8T+CnDYGgXnsC/MaLf126gUBR395BwU6dOj4nhRUfLbIPq6nDqFNjbAwBkZ0NpKaxbB3/8AQDAYsHNmwAAdDo8fQojRtQPtq6hAfPnw8GDkrs+PhAUJLmtogLvtzzWo9Hg2DFYtAjqOtSrqsLIkZCSQsx8MQcOwNu3gkadSuGj19Hm5OS4du08xqN6x2Qip7n+LDYPXhVCh3bNGmm5qBKq2dBBSfY4chngGkkdFhy6Z+8+omtRdMOHD0lPv/bsGU+G0yYIhZCVBRwOODg01TDJ5UJ6OqipgZXVR2aTKyyE0lJwcPii+RxKS6G4GDp2BJmfcv5CVVXg6EgbPfqHLVu2Nnro49fgnz59Ojg4+PfRosV4TK0wCisgYB0NtOweJjzBeRg+KzMz08XFcdYs7oYNRJeitEQiGDOGfPeuXlraa70PrpP5eJoFBQXt2LEjLGxWBQvWjpXZ3keXxU09+rIAuHzobNnUOodmQRerplaQiZcFMKpxyDZeQc6l5pSB/1oaWdMy6moUBseXsLW1PXDgv7Fjx3boIPr+e6KrUU7Ll8PZs6SrV09+GBzwqewAgO+//15LS2vypIkFlaS/Jgq1ZDHW/5viph4VnyVpeh2O7E/bf7wShSo15iWM20EzNOt4Lepm86cRbTtGjx794sWL8PDVRkbCkSOJrkbZbN8Ov/0Ge/bs7ld34ud9nxnr+MqVKxNDx+upMg//wPNU7KvyWyW+AFadhjXnSYMHf7v/wL84c+3XEolEERERf/3157ZtorAwoqtREiIRLF0K69bB2rXrFtZdhPOBz8+xUFBQMHHC+Nu37ywaLFw8FDQVddzH1ufRGwg7SH2eS960eev3uNvdDGvXrl26dOmcOaK1az/SeIkaqqqCmTPJp06R9uzZN2HChCbW/KL5Wf7f3v0GNXHmcQD/LflDpGxMwETMHwmMNAkG0NJwDFA7EMYRSo/K1OuMXEcdlR7TF8y0OnccvXO8wnW4qX3hC2bOtloVsSOlUI62tqSpVHsoYMNA2YCWBDQkQDS5kASS7G72XmBBHatIQxNkP7MvszPPi8w3mzx5vj+Koo4ePXro71XRLH/NDvzVnPljILSlYHHAX88hpy9BVmbGv98/npycHOoVLXsNDQ2vvbYvKYk4cwZXKkO9mnDV2QmlpUyPBz1z5lx+fv7DX7ygf8AjCFJRUXHturHo5T1734/IOMRq6Zmv3qEF0YQTqs7B0wcYHSOis2c//u5SJx0cQbFz5069vo/NTnn2WcaRI+D3P/qWFcXthspK2LIlQqnU9PUZHhkcsIhZ1v39/X97q+o/bW1Pi1gHCvx/zA7rc7fLyPVxOPIFcvIiwuWufuPAnysqKuhhtEFHEERNTU1t7T+lUuq99/AXXgj1gsIARcHp01BZyZye5tTU1JaXlyML++/qY2fHLIPB8O67/6o/XR+LInuew3dvgRUywyXocBK+6IUPOxif6wOJsvVvHvzLrl27VtHjUpfS6OjowYNvNDZ+qtEwq6qI2WqMFYiioLUVqqtZej25f//+t9+unj1Jv0CLzI5ZFoulrq7u5IkPxqwTOQrWnufwHb+DoOzmrgQ/muFEB9T/l3lritTkPl/2p9dLSkoilu4cNe1eFy9ePHTorW+//S4zk1VZib/4YgiOioQKQcDZs1Bby8Iwori46PDh6tTUx64L/FXZMSsQCOh0ulMnTzQ1NQVIIkcORZvIP2TeU8NDmzNghsYr0NYXefUnn0S0tvTV3WVlZYmJ9AZ4aOj1+nfeqf7kk2axmFFaSpSXw5NdIH3zJjQ0QF0d22zGCwsLDx/+xzOLPZkbhOyYY7fbW1paWpo/1Wrb/X48W8EqSvNrVLApfqXvyzg80GGAr/uhVc8cu00kxIteKnll+/btOTk5C/xuSVtSGIYdO3asvv4jp9NVWIiUlpKFhffUdi13dju0tsKpU8wLFwiRSLh79/59+/bJZvuRFyuY2THH4/GcP3++paX5qy8/t93+XwzKel5B5iUH8jaCUrRSngzdXrg0BDoMdAZ2rwmnADanqX7/0svFxcVpaWmhXh3tAXw+X3Nz8/Hjx3S6DjYb2boVSkrIoiIIn8lyj8tqhc8+g6Ym5oULJIPBLCjYtndvWUFBQVAOYS9JdtzNaDRqtVpt+1fa9q8dTjcaxUiVQrqMTE+AHPl8D9gTgCBhyApXTXDVBN8Pc3qNPjJAJcqk+VsL8vPz8/LyYmPDtY6Ndi+73d7W1tbY+HF7uxbHic2bmdnZeE4ObNsGaNgPS/R4oLMTtFrQaiN/+MHH4bA1mvwdO14pLi5efd8cql9nybNjDkmSer3+ypUr3d1dXZe/H7puDAQoUSxbJQmoxIRSBCopJIvny0TDHEXByC0YMN+5MCt74Cbh9Qe4aFT6M+kZmVkZGRlZWVlxcfT+0zLmdDq1Wq1Op/vmmy+HhkwcDiM9PUKtxtVqUKvv1HmEXCAABgN0d0N3N3R1sXt7cZKE1FSFRlOg0Whyc3OXaNvut8uO+0xNTfX09PT09GAYNtCvNwxe80x7AUAqYCcKKFksniAEmQBka0AmAEkMMEK3/+DywogNRmxgssGIDUZuRYzcZl2zEB4vCQBS8drkjSmqlLSUlBS1Wq1QKOi9kieS2WzW6XSXL1/u6rrU14fhOMnnM9PSELkcVyhAqQS5HOLjl/wrOUmCyQQGAwwOwtAQGAzs/v6Ay0WsWsXetCk1IyM7KysrNzdXcHeB+tIIWXbch6Iok8mEYRiGYUajccQ0PGL6afSGxevzAwCCgJDHFnCRtVwyjksIuRDHA14U8J+CaA6gHIjmAHcVrI6CCAT4Cyi29+Iw44cZP7i84PaCwwNuL7i8MDUDk1Mw6QSrM8LmZk44kXHHnYwAAOEavkwWL0tMkskSkpKSVCqVUqkM7nMgbVnwer29vb3d3d0DAwNDQz9iGDY56QAANjtCLGZJJNT69X6xGCQSiIkBPh/4fODxgM8HDucR82KdTpieBodj/rLb4cYNGBtDzGbW6CiMj+MEQQGARCJUKJLl8o2pqalqtVqlUrGCWHO0AOGSHb/EarWaTCaz2TwxMWGz2axW6+TEuG3SarVanE6Xw+l+yL1MBoJGzZcMECTlmn7YwXhOJJuLPiVYEytcG7dOvF4gEAiFwnXr1gmFQplMlpCQQBdn0H6Jw+EYHBwcHh42m81jY2Ojoyaz2WSxWO32KRx/wMQUBAEej3XX7Q/u+IyMZMXEcCUSiUQik0rjpVKpRCLZsGGDXC5HQ/3TS7hnxyO5XC632+1yuVwul8PhIEly6uf+aZ/PNz09P6wJQZC5ChMWixUdHc3hcFAURVGUx+OhKPobxzZthXC73Y6fzczMzE5IuvuNCgCz7Qqzb1EOh8Pn8/l8Po/HC+ePq2WfHTQaLSToX/VoNNpi0NlBo9EWg84OGo22GP8HK+JX3lTrfYIAAAAASUVORK5CYII=", "text/plain": [ "" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } diff --git a/langgraph/checkpoint/aiosqlite.py b/langgraph/checkpoint/aiosqlite.py index 8d9d3fbc0..7d3101bf2 100644 --- a/langgraph/checkpoint/aiosqlite.py +++ b/langgraph/checkpoint/aiosqlite.py @@ -75,8 +75,8 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): async with self.conn.execute( "SELECT checkpoint, parent_ts FROM checkpoints WHERE thread_id = ? AND thread_ts = ?", ( - config["configurable"]["thread_id"], - config["configurable"]["thread_ts"], + str(config["configurable"]["thread_id"]), + str(config["configurable"]["thread_ts"]), ), ) as cursor: if value := await cursor.fetchone(): @@ -95,7 +95,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): else: async with self.conn.execute( "SELECT thread_id, thread_ts, parent_ts, checkpoint FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC LIMIT 1", - (config["configurable"]["thread_id"],), + (str(config["configurable"]["thread_id"]),), ) as cursor: if value := await cursor.fetchone(): return CheckpointTuple( @@ -120,7 +120,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): await self.setup() async with self.conn.execute( "SELECT thread_id, thread_ts, parent_ts, checkpoint FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC", - (config["configurable"]["thread_id"],), + (str(config["configurable"]["thread_id"]),), ) as cursor: async for thread_id, thread_ts, parent_ts, value in cursor: yield CheckpointTuple( @@ -138,7 +138,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): async with self.conn.execute( "INSERT OR REPLACE INTO checkpoints (thread_id, thread_ts, parent_ts, checkpoint) VALUES (?, ?, ?, ?)", ( - config["configurable"]["thread_id"], + str(config["configurable"]["thread_id"]), checkpoint["ts"], config["configurable"].get("thread_ts"), self.serde.dumps(checkpoint), diff --git a/langgraph/checkpoint/sqlite.py b/langgraph/checkpoint/sqlite.py index 0378b80c6..578c42d8d 100644 --- a/langgraph/checkpoint/sqlite.py +++ b/langgraph/checkpoint/sqlite.py @@ -93,8 +93,8 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): cur.execute( "SELECT checkpoint, parent_ts FROM checkpoints WHERE thread_id = ? AND thread_ts = ?", ( - config["configurable"]["thread_id"], - config["configurable"]["thread_ts"], + str(config["configurable"]["thread_id"]), + str(config["configurable"]["thread_ts"]), ), ) if value := cur.fetchone(): @@ -113,7 +113,7 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): else: cur.execute( "SELECT thread_id, thread_ts, parent_ts, checkpoint FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC LIMIT 1", - (config["configurable"]["thread_id"],), + (str(config["configurable"]["thread_id"]),), ) if value := cur.fetchone(): return CheckpointTuple( @@ -138,7 +138,7 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): with self.cursor(transaction=False) as cur: cur.execute( "SELECT thread_id, thread_ts, parent_ts, checkpoint FROM checkpoints WHERE thread_id = ? ORDER BY thread_ts DESC", - (config["configurable"]["thread_id"],), + (str(config["configurable"]["thread_id"]),), ) for thread_id, thread_ts, parent_ts, value in cur: yield CheckpointTuple( @@ -159,7 +159,7 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): cur.execute( "INSERT OR REPLACE INTO checkpoints (thread_id, thread_ts, parent_ts, checkpoint) VALUES (?, ?, ?, ?)", ( - config["configurable"]["thread_id"], + str(config["configurable"]["thread_id"]), checkpoint["ts"], config["configurable"].get("thread_ts"), self.serde.dumps(checkpoint), diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index 7291b4e15..59fa3b874 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -1,15 +1,19 @@ import logging -from collections import Counter, defaultdict +from collections import defaultdict from typing import ( Any, Awaitable, Callable, Dict, + Literal, NamedTuple, Optional, Sequence, Union, cast, + get_args, + get_origin, + get_type_hints, ) from langchain_core.runnables import Runnable @@ -24,6 +28,7 @@ from langgraph.checkpoint import BaseCheckpointSaver from langgraph.constants import TAG_HIDDEN from langgraph.pregel import Channel, Pregel from langgraph.pregel.read import PregelNode +from langgraph.pregel.types import All from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry from langgraph.utils import DrawableGraph, RunnableCallable, coerce_to_runnable @@ -142,7 +147,7 @@ class Graph: Callable[..., Awaitable[Union[str, list[str]]]], Runnable[Any, Union[str, list[str]]], ], - path_map: Optional[dict[str, str]] = None, + path_map: Optional[Union[dict[str, str], list[str]]] = None, then: Optional[str] = None, ) -> None: """Add a conditional edge from the starting node to any number of destination nodes. @@ -166,6 +171,14 @@ class Graph: "Adding an edge to a graph that has already been compiled. This will " "not be reflected in the compiled graph." ) + # coerce path_map to a dictionary + if isinstance(path_map, dict): + pass + elif isinstance(path_map, list): + path_map = {name: name for name in path_map} + elif rtn_type := get_type_hints(path).get("return"): + if get_origin(rtn_type) is Literal: + path_map = {name: name for name in get_args(rtn_type)} # find a name for the condition path = coerce_to_runnable(path, name=None, trace=True) name = path.name or "condition" @@ -283,8 +296,8 @@ class Graph: def compile( self, checkpointer: Optional[BaseCheckpointSaver] = None, - interrupt_before: Optional[Sequence[str]] = None, - interrupt_after: Optional[Sequence[str]] = None, + interrupt_before: Optional[Union[All, Sequence[str]]] = None, + interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, ) -> "CompiledGraph": # assign default values @@ -292,11 +305,16 @@ class Graph: interrupt_after = interrupt_after or [] # validate the graph - self.validate(interrupt=interrupt_before + interrupt_after) + self.validate( + interrupt=(interrupt_before if interrupt_before != "*" else []) + + interrupt_after + if interrupt_after != "*" + else [] + ) # create empty compiled graph compiled = CompiledGraph( - graph=self, + builder=self, nodes={}, channels={START: EphemeralValue(Any), END: EphemeralValue(Any)}, input_channels=START, @@ -326,7 +344,7 @@ class Graph: class CompiledGraph(Pregel): - graph: Graph + builder: Graph def attach_node(self, key: str, node: Runnable) -> None: self.channels[key] = EphemeralValue(Any) @@ -378,7 +396,6 @@ class CompiledGraph(Pregel): config: Optional[RunnableConfig] = None, *, xray: Union[int, bool] = False, - add_condition_nodes: bool = True, ) -> DrawableGraph: """Returns a drawable representation of the computation graph.""" graph = DrawableGraph() @@ -389,7 +406,7 @@ class CompiledGraph(Pregel): END: graph.add_node(self.get_output_schema(config), END) } - for key, node in self.graph.nodes.items(): + for key, node in self.builder.nodes.items(): if xray: subgraph = ( node.get_graph( @@ -413,40 +430,28 @@ class CompiledGraph(Pregel): n = graph.add_node(node, key) start_nodes[key] = n end_nodes[key] = n - for start, end in sorted(self.graph._all_edges): + for start, end in sorted(self.builder._all_edges): graph.add_edge(start_nodes[start], end_nodes[end]) - branches_by_name = Counter( - name for _, branches in self.graph.branches.items() for name in branches - ) - for start, branches in self.graph.branches.items(): + for start, branches in self.builder.branches.items(): default_ends = { - **{k: k for k in self.graph.nodes if k != start}, + **{k: k for k in self.builder.nodes if k != start}, END: END, } - for name, branch in branches.items(): + for _, branch in branches.items(): if branch.ends is not None: ends = branch.ends elif branch.then is not None: ends = {k: k for k in default_ends if k not in (END, branch.then)} else: ends = default_ends - - if add_condition_nodes is True: - cond = graph.add_node( - branch.path, - f"{start}_{name}" if branches_by_name[name] > 1 else name, + for label, end in ends.items(): + graph.add_edge( + start_nodes[start], + end_nodes[end], + label if label != end else None, + conditional=True, ) - graph.add_edge(start_nodes[start], cond) - for label, end in ends.items(): - graph.add_edge(cond, end_nodes[end], label, conditional=True) - if branch.then is not None: - graph.add_edge(start_nodes[end], end_nodes[branch.then]) - else: - for label, end in ends.items(): - graph.add_edge( - start_nodes[start], end_nodes[end], label, conditional=True - ) - if branch.then is not None: - graph.add_edge(start_nodes[end], end_nodes[branch.then]) + if branch.then is not None: + graph.add_edge(start_nodes[end], end_nodes[branch.then]) return graph diff --git a/langgraph/graph/state.py b/langgraph/graph/state.py index d5ac32346..5e7bfe8b1 100644 --- a/langgraph/graph/state.py +++ b/langgraph/graph/state.py @@ -16,6 +16,7 @@ from langgraph.checkpoint import BaseCheckpointSaver from langgraph.constants import TAG_HIDDEN from langgraph.graph.graph import END, START, Branch, CompiledGraph, Graph from langgraph.pregel.read import ChannelRead, PregelNode +from langgraph.pregel.types import All from langgraph.pregel.write import SKIP_WRITE, ChannelWrite, ChannelWriteEntry from langgraph.utils import RunnableCallable @@ -100,8 +101,8 @@ class StateGraph(Graph): def compile( self, checkpointer: Optional[BaseCheckpointSaver] = None, - interrupt_before: Optional[Sequence[str]] = None, - interrupt_after: Optional[Sequence[str]] = None, + interrupt_before: Optional[Union[All, Sequence[str]]] = None, + interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: bool = False, ) -> CompiledGraph: """Compiles the state graph into a `CompiledGraph` object. @@ -120,14 +121,19 @@ class StateGraph(Graph): interrupt_after = interrupt_after or [] # validate the graph - self.validate(interrupt=interrupt_before + interrupt_after) + self.validate( + interrupt=(interrupt_before if interrupt_before != "*" else []) + + interrupt_after + if interrupt_after != "*" + else [] + ) # prepare output channels state_keys = list(self.channels) output_channels = state_keys[0] if state_keys == ["__root__"] else state_keys compiled = CompiledStateGraph( - graph=self, + builder=self, nodes={}, channels={**self.channels, START: EphemeralValue(self.schema)}, input_channels=START, @@ -159,7 +165,7 @@ class StateGraph(Graph): class CompiledStateGraph(CompiledGraph): - graph: StateGraph + builder: StateGraph def attach_node(self, key: str, node: Optional[Runnable]) -> None: def _get_state_key(input: dict, config: RunnableConfig, *, key: str) -> Any: @@ -170,7 +176,7 @@ class CompiledStateGraph(CompiledGraph): else: return input.get(key, SKIP_WRITE) - state_keys = list(self.graph.channels) + state_keys = list(self.builder.channels) # state updaters state_write_entries = [ ( @@ -210,7 +216,7 @@ class CompiledStateGraph(CompiledGraph): mapper=( None if state_keys == ["__root__"] - else partial(_coerce_state, self.graph.schema) + else partial(_coerce_state, self.builder.schema) ), writers=[ # publish to this channel and state keys @@ -265,13 +271,13 @@ class CompiledStateGraph(CompiledGraph): return ChannelWrite(writes, tags=[TAG_HIDDEN]) # attach branch publisher - self.nodes[start] |= branch.run(branch_writer, _get_state_reader(self.graph)) + self.nodes[start] |= branch.run(branch_writer, _get_state_reader(self.builder)) # attach branch subscribers ends = ( branch.ends.values() if branch.ends - else [node for node in self.graph.nodes if node != branch.then] + else [node for node in self.builder.nodes if node != branch.then] ) for end in ends: if end != END: diff --git a/langgraph/pregel/__init__.py b/langgraph/pregel/__init__.py index 5d5f90633..e02de7172 100644 --- a/langgraph/pregel/__init__.py +++ b/langgraph/pregel/__init__.py @@ -54,7 +54,6 @@ from langgraph.channels.base import ( InvalidUpdateError, create_checkpoint, ) -from langgraph.channels.last_value import LastValue from langgraph.checkpoint.base import ( BaseCheckpointSaver, Checkpoint, @@ -66,8 +65,12 @@ from langgraph.constants import ( CONFIG_KEY_READ, CONFIG_KEY_SEND, INTERRUPT, + TAG_HIDDEN, ) from langgraph.pregel.debug import ( + map_debug_checkpoint, + map_debug_task_results, + map_debug_tasks, print_step_checkpoint, print_step_tasks, print_step_writes, @@ -82,6 +85,7 @@ from langgraph.pregel.io import ( from langgraph.pregel.log import logger from langgraph.pregel.read import PregelNode from langgraph.pregel.types import ( + All, PregelExecutableTask, PregelTaskDescription, StateSnapshot, @@ -175,7 +179,7 @@ class Channel: ) -StreamMode = Literal["values", "updates"] +StreamMode = Literal["values", "updates", "debug"] class Pregel( @@ -185,23 +189,21 @@ class Pregel( channels: Mapping[str, BaseChannel] = Field(default_factory=dict) - default_channel_cls: Type[BaseChannel] = Field(default=LastValue) - auto_validate: bool = True stream_mode: StreamMode = "values" - output_channels: Union[str, Sequence[str]] = "output" + output_channels: Union[str, Sequence[str]] """Channels to output, defaults to channel named 'output'.""" stream_channels: Optional[Union[str, Sequence[str]]] = None """Channels to stream, defaults to all channels not in reserved channels""" - interrupt_after_nodes: Sequence[str] = Field(default_factory=list) + interrupt_after_nodes: Union[All, Sequence[str]] = Field(default_factory=list) - interrupt_before_nodes: Sequence[str] = Field(default_factory=list) + interrupt_before_nodes: Union[All, Sequence[str]] = Field(default_factory=list) - input_channels: Union[str, Sequence[str]] = "input" + input_channels: Union[str, Sequence[str]] step_timeout: Optional[float] = None @@ -214,6 +216,11 @@ class Pregel( class Config: arbitrary_types_allowed = True + @classmethod + def is_lc_serializable(cls) -> bool: + """Return whether the graph can be serialized by Langchain.""" + return True + @root_validator(skip_on_failure=True) def validate_on_init(cls, values: dict[str, Any]) -> dict[str, Any]: if not values["auto_validate"]: @@ -226,7 +233,6 @@ class Pregel( values["stream_channels"], values["interrupt_after_nodes"], values["interrupt_before_nodes"], - values["default_channel_cls"], ) if values["interrupt_after_nodes"] or values["interrupt_before_nodes"]: if not values["checkpointer"]: @@ -242,7 +248,6 @@ class Pregel( self.stream_channels, self.interrupt_after_nodes, self.interrupt_before_nodes, - self.default_channel_cls, ) if self.interrupt_after_nodes or self.interrupt_before_nodes: if not self.checkpointer: @@ -302,12 +307,15 @@ class Pregel( @property def stream_channels_list(self) -> Sequence[str]: + stream_channels = self.stream_channels_asis return ( - [self.stream_channels] - if isinstance(self.stream_channels, str) - else self.stream_channels or [k for k in self.channels] + [stream_channels] if isinstance(stream_channels, str) else stream_channels ) + @property + def stream_channels_asis(self) -> Union[str, Sequence[str]]: + return self.stream_channels or [k for k in self.channels] + def get_state(self, config: RunnableConfig) -> StateSnapshot: """Get the current state of the graph.""" if not self.checkpointer: @@ -320,11 +328,8 @@ class Pregel( _, next_tasks = _prepare_next_tasks( checkpoint, self.nodes, channels, for_execution=False ) - values = read_channels(channels, self.stream_channels_list) return StateSnapshot( - values.get(self.stream_channels, None) - if isinstance(self.stream_channels, str) - else values, + read_channels(channels, self.stream_channels_asis), tuple(name for name, _ in next_tasks), config, ) @@ -341,11 +346,8 @@ class Pregel( _, next_tasks = _prepare_next_tasks( checkpoint, self.nodes, channels, for_execution=False ) - values = read_channels(channels, self.stream_channels_list) return StateSnapshot( - values.get(self.stream_channels, None) - if isinstance(self.stream_channels, str) - else values, + read_channels(channels, self.stream_channels_asis), tuple(name for name, _ in next_tasks), config, ) @@ -360,11 +362,8 @@ class Pregel( _, next_tasks = _prepare_next_tasks( checkpoint, self.nodes, channels, for_execution=False ) - values = read_channels(channels, self.stream_channels_list) yield StateSnapshot( - values.get(self.stream_channels, None) - if isinstance(self.stream_channels, str) - else values, + read_channels(channels, self.stream_channels_asis), tuple(name for name, _ in next_tasks), config, parent_config, @@ -382,11 +381,8 @@ class Pregel( _, next_tasks = _prepare_next_tasks( checkpoint, self.nodes, channels, for_execution=False ) - values = read_channels(channels, self.stream_channels_list) yield StateSnapshot( - values.get(self.stream_channels, None) - if isinstance(self.stream_channels, str) - else values, + read_channels(channels, self.stream_channels_asis), tuple(name for name, _ in next_tasks), config, parent_config, @@ -434,6 +430,8 @@ class Pregel( values, RunnableSequence(*writers) if len(writers) > 1 else writers[0], deque(), + None, + [INTERRUPT], ) # execute task task.proc.invoke( @@ -494,6 +492,8 @@ class Pregel( values, RunnableSequence(*writers) if len(writers) > 1 else writers[0], deque(), + None, + [INTERRUPT], ) # execute task await task.proc.ainvoke( @@ -523,8 +523,8 @@ class Pregel( stream_mode: Optional[StreamMode] = None, input_keys: Optional[Union[str, Sequence[str]]] = None, output_keys: Optional[Union[str, Sequence[str]]] = None, - interrupt_before: Optional[Sequence[str]] = None, - interrupt_after: Optional[Sequence[str]] = None, + interrupt_before: Optional[Union[All, Sequence[str]]] = None, + interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, ) -> tuple[ bool, @@ -536,11 +536,7 @@ class Pregel( ]: debug = debug if debug is not None else self.debug if output_keys is None: - output_keys = ( - [chan for chan in self.channels] - if self.stream_channels is None - else self.stream_channels - ) + output_keys = self.stream_channels_asis else: validate_keys(output_keys, self.channels) if input_keys is None: @@ -570,8 +566,8 @@ class Pregel( stream_mode: Optional[StreamMode] = None, output_keys: Optional[Union[str, Sequence[str]]] = None, input_keys: Optional[Union[str, Sequence[str]]] = None, - interrupt_before: Optional[Sequence[str]] = None, - interrupt_after: Optional[Sequence[str]] = None, + interrupt_before: Optional[Union[All, Sequence[str]]] = None, + interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, ) -> Iterator[Union[dict[str, Any], Any]]: """Stream graph steps for a single input.""" @@ -671,6 +667,9 @@ class Pregel( if debug: print_step_tasks(step, next_tasks) + if stream_mode == "debug": + for chunk in map_debug_tasks(step, next_tasks): + yield chunk # prepare tasks with config tasks_w_config = [ @@ -690,7 +689,7 @@ class Pregel( }, ), ) - for name, input, proc, writes, proc_config in next_tasks + for name, input, proc, writes, proc_config, _ in next_tasks ] futures = [ @@ -711,7 +710,7 @@ class Pregel( # combine pending writes from all tasks pending_writes = deque[tuple[str, Any]]() - for _, _, _, writes, _ in next_tasks: + for _, _, _, writes, _, _ in next_tasks: pending_writes.extend(writes) if debug: @@ -730,6 +729,10 @@ class Pregel( yield from map_output_values( output_keys, pending_writes, channels ) + elif stream_mode == "debug": + yield from map_debug_task_results( + step, next_tasks, self.stream_channels_list + ) else: yield from map_output_updates(output_keys, next_tasks) @@ -741,6 +744,17 @@ class Pregel( checkpoint_config = self.checkpointer.put( checkpoint_config, checkpoint ) + if stream_mode == "debug": + yield map_debug_checkpoint( + step, + checkpoint_config, + channels, + self.stream_channels_asis, + ) + elif stream_mode == "debug": + yield map_debug_checkpoint( + step, None, channels, self.stream_channels_asis + ) # after execution, check if we should interrupt if _should_interrupt( @@ -760,7 +774,20 @@ class Pregel( and self.checkpointer.at == CheckpointAt.END_OF_RUN ): checkpoint = create_checkpoint(checkpoint, channels) - self.checkpointer.put(checkpoint_config, checkpoint) + checkpoint_config = self.checkpointer.put( + checkpoint_config, checkpoint + ) + if stream_mode == "debug": + yield map_debug_checkpoint( + step, + checkpoint_config, + channels, + self.stream_channels_asis, + ) + elif self.checkpointer is None and stream_mode == "debug": + yield map_debug_checkpoint( + step, None, channels, self.stream_channels_asis + ) except BaseException as e: run_manager.on_chain_error(e) raise @@ -780,8 +807,8 @@ class Pregel( stream_mode: Optional[StreamMode] = None, output_keys: Optional[Union[str, Sequence[str]]] = None, input_keys: Optional[Union[str, Sequence[str]]] = None, - interrupt_before: Optional[Sequence[str]] = None, - interrupt_after: Optional[Sequence[str]] = None, + interrupt_before: Optional[Union[All, Sequence[str]]] = None, + interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, ) -> AsyncIterator[Union[dict[str, Any], Any]]: config = ensure_config(config) @@ -889,6 +916,9 @@ class Pregel( if debug: print_step_tasks(step, next_tasks) + if stream_mode == "debug": + for chunk in map_debug_tasks(step, next_tasks): + yield chunk # prepare tasks with config tasks_w_config = [ @@ -908,7 +938,7 @@ class Pregel( }, ), ) - for name, input, proc, writes, proc_config in next_tasks + for name, input, proc, writes, proc_config, _ in next_tasks ] futures = ( @@ -936,7 +966,7 @@ class Pregel( # combine pending writes from all tasks pending_writes = deque[tuple[str, Any]]() - for _, _, _, writes, _ in next_tasks: + for _, _, _, writes, _, _ in next_tasks: pending_writes.extend(writes) if debug: @@ -956,6 +986,11 @@ class Pregel( output_keys, pending_writes, channels ): yield chunk + elif stream_mode == "debug": + for chunk in map_debug_task_results( + step, next_tasks, self.stream_channels_list + ): + yield chunk else: for chunk in map_output_updates(output_keys, next_tasks): yield chunk @@ -968,6 +1003,17 @@ class Pregel( checkpoint_config = await self.checkpointer.aput( checkpoint_config, checkpoint ) + if stream_mode == "debug": + yield map_debug_checkpoint( + step, + checkpoint_config, + channels, + self.stream_channels_asis, + ) + elif stream_mode == "debug": + yield map_debug_checkpoint( + step, None, channels, self.stream_channels_asis + ) # after execution, check if we should interrupt if _should_interrupt( @@ -987,7 +1033,17 @@ class Pregel( and self.checkpointer.at == CheckpointAt.END_OF_RUN ): checkpoint = create_checkpoint(checkpoint, channels) - await self.checkpointer.aput(checkpoint_config, checkpoint) + checkpoint_config = await self.checkpointer.aput( + checkpoint_config, checkpoint + ) + if stream_mode == "debug": + yield map_debug_checkpoint( + step, checkpoint_config, channels, self.stream_channels_asis + ) + elif self.checkpointer is None and stream_mode == "debug": + yield map_debug_checkpoint( + step, None, channels, self.stream_channels_asis + ) except BaseException as e: await run_manager.on_chain_error(e) raise @@ -1007,8 +1063,8 @@ class Pregel( stream_mode: StreamMode = "values", output_keys: Optional[Union[str, Sequence[str]]] = None, input_keys: Optional[Union[str, Sequence[str]]] = None, - interrupt_before_nodes: Optional[Sequence[str]] = None, - interrupt_after_nodes: Optional[Sequence[str]] = None, + interrupt_before: Optional[Union[All, Sequence[str]]] = None, + interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: @@ -1020,8 +1076,8 @@ class Pregel( stream_mode: Optional[str]. The stream mode for the graph run. Default is "values". output_keys: Optional. The output keys to retrieve from the graph run. input_keys: Optional. The input keys to provide for the graph run. - interrupt_before_nodes: Optional. The nodes to interrupt the graph run before. - interrupt_after_nodes: Optional. The nodes to interrupt the graph run after. + interrupt_before: Optional. The nodes to interrupt the graph run before. + interrupt_after: Optional. The nodes to interrupt the graph run after. debug: Optional. Enable debug mode for the graph run. **kwargs: Additional keyword arguments to pass to the graph run. @@ -1040,8 +1096,8 @@ class Pregel( stream_mode=stream_mode, output_keys=output_keys, input_keys=input_keys, - interrupt_before=interrupt_before_nodes, - interrupt_after=interrupt_after_nodes, + interrupt_before=interrupt_before, + interrupt_after=interrupt_after, debug=debug, **kwargs, ): @@ -1062,8 +1118,8 @@ class Pregel( stream_mode: StreamMode = "values", output_keys: Optional[Union[str, Sequence[str]]] = None, input_keys: Optional[Union[str, Sequence[str]]] = None, - interrupt_before_nodes: Optional[Sequence[str]] = None, - interrupt_after_nodes: Optional[Sequence[str]] = None, + interrupt_before: Optional[Union[All, Sequence[str]]] = None, + interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, **kwargs: Any, ) -> Union[dict[str, Any], Any]: @@ -1075,8 +1131,8 @@ class Pregel( stream_mode: Optional. The stream mode for the computation. Default is "values". output_keys: Optional. The output keys to include in the result. Default is None. input_keys: Optional. The input keys to include in the result. Default is None. - interrupt_before_nodes: Optional. The nodes to interrupt before. Default is None. - interrupt_after_nodes: Optional. The nodes to interrupt after. Default is None. + interrupt_before: Optional. The nodes to interrupt before. Default is None. + interrupt_after: Optional. The nodes to interrupt after. Default is None. debug: Optional. Whether to enable debug mode. Default is None. **kwargs: Additional keyword arguments. @@ -1096,8 +1152,8 @@ class Pregel( stream_mode=stream_mode, output_keys=output_keys, input_keys=input_keys, - interrupt_before=interrupt_before_nodes, - interrupt_after=interrupt_after_nodes, + interrupt_before=interrupt_before, + interrupt_after=interrupt_after, debug=debug, **kwargs, ): @@ -1137,7 +1193,7 @@ def _panic_or_proceed( def _should_interrupt( checkpoint: Checkpoint, - interrupt_nodes: Sequence[str], + interrupt_nodes: Union[All, Sequence[str]], snapshot_channels: Sequence[str], tasks: list[PregelExecutableTask], ) -> bool: @@ -1150,7 +1206,15 @@ def _should_interrupt( for chan in snapshot_channels ) # and any channel written to is in interrupt_nodes list - and any(node for node, _, _, _, _ in tasks if node in interrupt_nodes) + and any( + node + for node, _, _, _, config, _ in tasks + if ( + (not config or TAG_HIDDEN not in config.get("tags")) + if interrupt_nodes == "*" + else node in interrupt_nodes + ) + ) ) @@ -1240,13 +1304,14 @@ def _prepare_next_tasks( for name, proc in processes.items(): seen = checkpoint["versions_seen"][name] # If any of the channels read by this process were updated - if any( - checkpoint["channel_versions"][chan] > seen[chan] + if triggers := [ + chan for chan in proc.triggers if not isinstance( read_channel(channels, chan, return_exception=True), EmptyChannelError ) - ): + and checkpoint["channel_versions"][chan] > seen[chan] + ]: # If all trigger channels subscribed by this process are not empty # then invoke the process with the values of all non-empty channels if isinstance(proc.channels, dict): @@ -1287,7 +1352,9 @@ def _prepare_next_tasks( if for_execution: if node := proc.get_node(): tasks.append( - PregelExecutableTask(name, val, node, deque(), proc.config) + PregelExecutableTask( + name, val, node, deque(), proc.config, triggers + ) ) else: tasks.append(PregelTaskDescription(name, val)) diff --git a/langgraph/pregel/debug.py b/langgraph/pregel/debug.py index 0857b3074..5fd548344 100644 --- a/langgraph/pregel/debug.py +++ b/langgraph/pregel/debug.py @@ -1,13 +1,124 @@ +import json from collections import defaultdict +from datetime import datetime, timezone from pprint import pformat -from typing import Any, Iterator, Mapping, Sequence +from typing import Any, Iterator, Literal, Mapping, Optional, Sequence, TypedDict, Union +from uuid import UUID, uuid5 +from langchain_core.runnables.config import RunnableConfig from langchain_core.utils.input import get_bolded_text, get_colored_text -from langgraph.channels.base import BaseChannel, EmptyChannelError +from langgraph.channels.base import BaseChannel +from langgraph.constants import TAG_HIDDEN +from langgraph.pregel.io import read_channels from langgraph.pregel.types import PregelExecutableTask +class TaskPayload(TypedDict): + id: str + name: str + input: Any + triggers: list[str] + + +class TaskResultPayload(TypedDict): + id: str + result: list[tuple[str, Any]] + + +class CheckpointPayload(TypedDict): + config: Optional[RunnableConfig] + values: dict[str, Any] + + +class DebugOutputBase(TypedDict): + timestamp: str + step: int + type: str + payload: dict[str, Any] + + +class DebugOutputTask(DebugOutputBase): + type: Literal["task"] + payload: TaskPayload + + +class DebugOutputTaskResult(DebugOutputBase): + type: Literal["task_result"] + payload: TaskResultPayload + + +class DebugOutputCheckpoint(DebugOutputBase): + type: Literal["checkpoint"] + payload: CheckpointPayload + + +DebugOutput = Union[DebugOutputTask, DebugOutputTaskResult, DebugOutputCheckpoint] + + +TASK_NAMESPACE = UUID("6ba7b831-9dad-11d1-80b4-00c04fd430c8") + + +def map_debug_tasks( + step: int, tasks: list[PregelExecutableTask] +) -> Iterator[DebugOutputTask]: + ts = datetime.now(timezone.utc).isoformat() + for name, input, _, _, config, triggers in tasks: + if config is not None and TAG_HIDDEN in config.get("tags", []): + continue + + yield { + "type": "task", + "timestamp": ts, + "step": step, + "payload": { + "id": str(uuid5(TASK_NAMESPACE, json.dumps((name, step)))), + "name": name, + "input": input, + "triggers": triggers, + }, + } + + +def map_debug_task_results( + step: int, + tasks: list[PregelExecutableTask], + stream_channels_list: Sequence[str], +) -> Iterator[DebugOutputTaskResult]: + ts = datetime.now(timezone.utc).isoformat() + for name, _, _, writes, config, _ in tasks: + if config is not None and TAG_HIDDEN in config.get("tags", []): + continue + + yield { + "type": "task_result", + "timestamp": ts, + "step": step, + "payload": { + "id": str(uuid5(TASK_NAMESPACE, json.dumps((name, step)))), + "result": [w for w in writes if w[0] in stream_channels_list], + }, + } + + +def map_debug_checkpoint( + step: int, + config: RunnableConfig, + channels: Mapping[str, BaseChannel], + stream_channels: Union[str, Sequence[str]], +) -> DebugOutputCheckpoint: + ts = datetime.now(timezone.utc).isoformat() + return { + "type": "checkpoint", + "timestamp": ts, + "step": step, + "payload": { + "config": config, + "values": read_channels(channels, stream_channels), + }, + } + + def print_step_tasks(step: int, next_tasks: list[PregelExecutableTask]) -> None: n_tasks = len(next_tasks) print( @@ -17,7 +128,7 @@ def print_step_tasks(step: int, next_tasks: list[PregelExecutableTask]) -> None: ) + "\n".join( f"- {get_colored_text(name, 'green')} -> {pformat(val)}" - for name, val, _, _, _ in next_tasks + for name, val, _, _, _, _ in next_tasks ) ) @@ -47,16 +158,5 @@ def print_step_checkpoint( print( f"{get_colored_text(f'[{step}:checkpoint]', color='blue')} " + get_bolded_text(f"State at the end of step {step}:\n") - + pformat( - {name: val for name, val in _read_channels(channels) if name in whitelist}, - depth=3, - ) + + pformat(read_channels(channels, whitelist), depth=3) ) - - -def _read_channels(channels: Mapping[str, BaseChannel]) -> Iterator[tuple[str, Any]]: - for name, channel in channels.items(): - try: - yield (name, channel.get()) - except EmptyChannelError: - pass diff --git a/langgraph/pregel/io.py b/langgraph/pregel/io.py index 7dfc738e5..67382fc77 100644 --- a/langgraph/pregel/io.py +++ b/langgraph/pregel/io.py @@ -105,7 +105,7 @@ def map_output_updates( if updated := AddableUpdatesDict( { node: value - for node, _, _, writes, _ in output_tasks + for node, _, _, writes, _, _ in output_tasks for chan, value in writes if chan == output_channels } @@ -115,7 +115,7 @@ def map_output_updates( if updated := AddableUpdatesDict( { node: {chan: value for chan, value in writes if chan in output_channels} - for node, _, _, writes, _ in output_tasks + for node, _, _, writes, _, _ in output_tasks if any(chan in output_channels for chan, _ in writes) } ): diff --git a/langgraph/pregel/types.py b/langgraph/pregel/types.py index 8ebe52e99..7c6075c98 100644 --- a/langgraph/pregel/types.py +++ b/langgraph/pregel/types.py @@ -1,5 +1,5 @@ from collections import deque -from typing import Any, NamedTuple, Optional, Union +from typing import Any, Literal, NamedTuple, Optional, Union from langchain_core.runnables import Runnable, RunnableConfig @@ -14,7 +14,8 @@ class PregelExecutableTask(NamedTuple): input: Any proc: Runnable writes: deque[tuple[str, Any]] - config: Optional[RunnableConfig] = None + config: Optional[RunnableConfig] + triggers: list[str] class StateSnapshot(NamedTuple): @@ -26,3 +27,6 @@ class StateSnapshot(NamedTuple): """Config used to fetch this snapshot""" parent_config: Optional[RunnableConfig] = None """Config used to fetch the parent snapshot, if any""" + + +All = Literal["*"] diff --git a/langgraph/pregel/validate.py b/langgraph/pregel/validate.py index 69f29cfd4..ad4959657 100644 --- a/langgraph/pregel/validate.py +++ b/langgraph/pregel/validate.py @@ -1,8 +1,9 @@ -from typing import Any, Mapping, Optional, Sequence, Type, Union +from typing import Mapping, Optional, Sequence, Union from langgraph.channels.base import BaseChannel from langgraph.constants import INTERRUPT from langgraph.pregel.read import PregelNode +from langgraph.pregel.types import All def validate_graph( @@ -11,10 +12,13 @@ def validate_graph( input_channels: Union[str, Sequence[str]], output_channels: Union[str, Sequence[str]], stream_channels: Optional[Union[str, Sequence[str]]], - interrupt_after_nodes: Sequence[str], - interrupt_before_nodes: Sequence[str], - default_channel_cls: Type[BaseChannel], + interrupt_after_nodes: Union[All, Sequence[str]], + interrupt_before_nodes: Union[All, Sequence[str]], ) -> None: + for chan in channels: + if chan == INTERRUPT: + raise ValueError(f"Channel name {INTERRUPT} is reserved") + subscribed_channels = set[str]() for name, node in nodes.items(): if name == INTERRUPT: @@ -28,11 +32,11 @@ def validate_graph( for chan in subscribed_channels: if chan not in channels: - channels[chan] = default_channel_cls(Any) # type: ignore[arg-type] + raise ValueError(f"Subscribed channel '{chan}' not in 'channels'") if isinstance(input_channels, str): if input_channels not in channels: - channels[input_channels] = default_channel_cls(Any) # type: ignore[arg-type] + raise ValueError(f"Input channel '{input_channels}' not in 'channels'") if input_channels not in subscribed_channels: raise ValueError( f"Input channel {input_channels} is not subscribed to by any node" @@ -40,7 +44,7 @@ def validate_graph( else: for chan in input_channels: if chan not in channels: - channels[chan] = default_channel_cls(Any) # type: ignore[arg-type] + raise ValueError(f"Input channel '{chan}' not in 'channels'") if all(chan not in subscribed_channels for chan in input_channels): raise ValueError( f"None of the input channels {input_channels} are subscribed to by any node" @@ -58,14 +62,16 @@ def validate_graph( for chan in all_output_channels: if chan not in channels: - channels[chan] = default_channel_cls(Any) # type: ignore[arg-type] + raise ValueError(f"Output channel '{chan}' not in 'channels'") - for node in interrupt_after_nodes: - if node not in nodes: - raise ValueError(f"Node {node} not in nodes") - for node in interrupt_before_nodes: - if node not in nodes: - raise ValueError(f"Node {node} not in nodes") + if interrupt_after_nodes != "*": + for node in interrupt_after_nodes: + if node not in nodes: + raise ValueError(f"Node {node} not in nodes") + if interrupt_before_nodes != "*": + for node in interrupt_before_nodes: + if node not in nodes: + raise ValueError(f"Node {node} not in nodes") def validate_keys( diff --git a/langgraph/serde/jsonplus.py b/langgraph/serde/jsonplus.py index 9fe4b9d3c..c6aa15184 100644 --- a/langgraph/serde/jsonplus.py +++ b/langgraph/serde/jsonplus.py @@ -38,7 +38,13 @@ class JsonPlusSerializer(SerializerProtocol): if isinstance(obj, Serializable): return obj.to_json() elif isinstance(obj, (BaseModel, LcBaseModel)): - return self._encode_constructor_args(obj.__class__, kwargs=obj.dict()) + # prefer non-deprecated method if available + if hasattr(obj, "model_dump"): + return self._encode_constructor_args( + obj.__class__, kwargs=obj.model_dump() + ) + else: + return self._encode_constructor_args(obj.__class__, kwargs=obj.dict()) elif isinstance(obj, UUID): return self._encode_constructor_args(UUID, args=[obj.hex]) elif isinstance(obj, (set, frozenset)): diff --git a/pyproject.toml b/pyproject.toml index 46a20c711..91823aedd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.0.39" +version = "0.0.40" description = "langgraph" authors = [] license = "MIT" diff --git a/tests/__snapshots__/test_pregel.ambr b/tests/__snapshots__/test_pregel.ambr index baef3d081..b5bc325f6 100644 --- a/tests/__snapshots__/test_pregel.ambr +++ b/tests/__snapshots__/test_pregel.ambr @@ -1,97 +1,17 @@ # serializer version: 1 # name: test_branch_then[end_of_run] ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------+ - | prepare | - +---------+ - * - * - * - +-----------+ - | condition | - +-----------+ - .. .. - .. .. - .. .. - +---------------+ +---------------+ - | tool_two_slow | | tool_two_fast | - +---------------+ +---------------+ - ** ** - ** ** - ** ** - +--------+ - | finish | - +--------+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + ''' # --- # name: test_branch_then[end_of_run].1 - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------+ - | prepare | - +---------+. - .. .. - .. .. - .. .. - +---------------+ +---------------+ - | tool_two_slow | | tool_two_fast | - +---------------+ +---------------+ - ** ** - ** ** - ** ** - +--------+ - | finish | - +--------+ - * - * - * - +---------+ - | __end__ | - +---------+ - ''' -# --- -# name: test_branch_then[end_of_run].2 - ''' - %%{init: {'flowchart': {'curve': 'linear'}}}%% - graph TD; - __start__[__start__]:::startclass; - __end__[__end__]:::endclass; - prepare([prepare]):::otherclass; - tool_two_slow([tool_two_slow]):::otherclass; - tool_two_fast([tool_two_fast]):::otherclass; - finish([finish]):::otherclass; - condition([condition]):::otherclass; - __start__ --> prepare; - finish --> __end__; - prepare --> condition; - condition -. tool_two_slow .-> tool_two_slow; - tool_two_slow --> finish; - condition -. tool_two_fast .-> tool_two_fast; - tool_two_fast --> finish; - classDef startclass fill:#ffdfba; - classDef endclass fill:#baffc9; - classDef otherclass fill:#fad7de; - - ''' -# --- -# name: test_branch_then[end_of_run].3 ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% graph TD; @@ -103,9 +23,9 @@ finish([finish]):::otherclass; __start__ --> prepare; finish --> __end__; - prepare -. tool_two_slow .-> tool_two_slow; + prepare -.-> tool_two_slow; tool_two_slow --> finish; - prepare -. tool_two_fast .-> tool_two_fast; + prepare -.-> tool_two_fast; tool_two_fast --> finish; classDef startclass fill:#ffdfba; classDef endclass fill:#baffc9; @@ -115,97 +35,17 @@ # --- # name: test_branch_then[end_of_step] ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------+ - | prepare | - +---------+ - * - * - * - +-----------+ - | condition | - +-----------+ - .. .. - .. .. - .. .. - +---------------+ +---------------+ - | tool_two_slow | | tool_two_fast | - +---------------+ +---------------+ - ** ** - ** ** - ** ** - +--------+ - | finish | - +--------+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> prepare; + finish --> __end__; + prepare -.-> tool_two_slow; + tool_two_slow --> finish; + prepare -.-> tool_two_fast; + tool_two_fast --> finish; + ''' # --- # name: test_branch_then[end_of_step].1 - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------+ - | prepare | - +---------+. - .. .. - .. .. - .. .. - +---------------+ +---------------+ - | tool_two_slow | | tool_two_fast | - +---------------+ +---------------+ - ** ** - ** ** - ** ** - +--------+ - | finish | - +--------+ - * - * - * - +---------+ - | __end__ | - +---------+ - ''' -# --- -# name: test_branch_then[end_of_step].2 - ''' - %%{init: {'flowchart': {'curve': 'linear'}}}%% - graph TD; - __start__[__start__]:::startclass; - __end__[__end__]:::endclass; - prepare([prepare]):::otherclass; - tool_two_slow([tool_two_slow]):::otherclass; - tool_two_fast([tool_two_fast]):::otherclass; - finish([finish]):::otherclass; - condition([condition]):::otherclass; - __start__ --> prepare; - finish --> __end__; - prepare --> condition; - condition -. tool_two_slow .-> tool_two_slow; - tool_two_slow --> finish; - condition -. tool_two_fast .-> tool_two_fast; - tool_two_fast --> finish; - classDef startclass fill:#ffdfba; - classDef endclass fill:#baffc9; - classDef otherclass fill:#fad7de; - - ''' -# --- -# name: test_branch_then[end_of_step].3 ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% graph TD; @@ -217,9 +57,9 @@ finish([finish]):::otherclass; __start__ --> prepare; finish --> __end__; - prepare -. tool_two_slow .-> tool_two_slow; + prepare -.-> tool_two_slow; tool_two_slow --> finish; - prepare -. tool_two_fast .-> tool_two_fast; + prepare -.-> tool_two_fast; tool_two_fast --> finish; classDef startclass fill:#ffdfba; classDef endclass fill:#baffc9; @@ -270,30 +110,6 @@ ], "name": "right" } - }, - { - "id": "should_start", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_start" - } - }, - { - "id": "condition", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "RunnableCallable" - } } ], "edges": [ @@ -303,28 +119,19 @@ }, { "source": "__start__", - "target": "should_start" - }, - { - "source": "should_start", "target": "left", "data": "go-left", "conditional": true }, { - "source": "should_start", + "source": "__start__", "target": "right", "data": "go-right", "conditional": true }, { "source": "left", - "target": "condition" - }, - { - "source": "condition", "target": "__end__", - "data": "__end__", "conditional": true } ] @@ -333,33 +140,12 @@ # --- # name: test_conditional_entrypoint_graph.3 ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +--------------+ - | should_start | - +--------------+ - . .. - .. . - . .. - +------+ . - | left | . - +------+ . - * . - * . - * . - +-----------+ +-------+ - | condition | | right | - +-----------+ +-------+ - . * - .. ** - . * - +---------+ - | __end__ | - +---------+ + graph TD; + right --> __end__; + __start__ -. go-left .-> left; + __start__ -. go-right .-> right; + left -.-> __end__; + ''' # --- # name: test_conditional_entrypoint_graph_state @@ -405,30 +191,6 @@ ], "name": "right" } - }, - { - "id": "should_start", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_start" - } - }, - { - "id": "condition", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "RunnableCallable" - } } ], "edges": [ @@ -438,28 +200,19 @@ }, { "source": "__start__", - "target": "should_start" - }, - { - "source": "should_start", "target": "left", "data": "go-left", "conditional": true }, { - "source": "should_start", + "source": "__start__", "target": "right", "data": "go-right", "conditional": true }, { "source": "left", - "target": "condition" - }, - { - "source": "condition", "target": "__end__", - "data": "__end__", "conditional": true } ] @@ -468,33 +221,12 @@ # --- # name: test_conditional_entrypoint_graph_state.3 ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +--------------+ - | should_start | - +--------------+ - . .. - .. . - . .. - +------+ . - | left | . - +------+ . - * . - * . - * . - +-----------+ +-------+ - | condition | | right | - +-----------+ +-------+ - . * - .. ** - . * - +---------+ - | __end__ | - +---------+ + graph TD; + right --> __end__; + __start__ -. go-left .-> left; + __start__ -. go-right .-> right; + left -.-> __end__; + ''' # --- # name: test_conditional_graph[end_of_run] @@ -535,18 +267,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -560,16 +280,12 @@ }, { "source": "agent", - "target": "should_continue" - }, - { - "source": "should_continue", "target": "tools", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": "agent", "target": "__end__", "data": "exit", "conditional": true @@ -579,31 +295,6 @@ ''' # --- # name: test_conditional_graph[end_of_run].1 - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | agent | - +-------+* - *** *** - * * - ** *** - +-----------------+ * - | should_continue | * - +-----------------+. * - . ..... * - . ... * - . ... * - +---------+ +-------+ - | __end__ | | tools | - +---------+ +-------+ - ''' -# --- -# name: test_conditional_graph[end_of_run].2 ''' graph TD; __start__ --> agent; @@ -613,7 +304,7 @@ ''' # --- -# name: test_conditional_graph[end_of_run].3 +# name: test_conditional_graph[end_of_run].2 ''' { "nodes": [ @@ -700,18 +391,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -749,16 +428,12 @@ }, { "source": 3, - "target": "should_continue" - }, - { - "source": "should_continue", "target": "tools", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": 3, "target": "__end__", "data": "exit", "conditional": true @@ -767,56 +442,7 @@ } ''' # --- -# name: test_conditional_graph[end_of_run].4 - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +------------------------------+ - | ParallelInput | - +------------------------------+ - ***** * ***** - ****** * **** - *** * ***** - +----------------+ * *** - | PromptTemplate | * * - +----------------+ * * - * * * - * * * - * * * - +----------------------+ * * - | FakeStreamingListLLM | * * - +----------------------+ * * - * * * - * * * - * * * - +----------------------+ +-------------+ * - | Lambda(agent_parser) | | Passthrough | * - +----------------------+ +-------------+ * - *** *** * - ** ** * - ** ** * - +-------------------------------+ * - | ParallelOutput | * - +-------------------------------+ * - * * - * * - * * - +-----------------+ * - | should_continue |... * - +-----------------+ ......... * - . ........ * - . ......... * - . ..... * - +---------+ +-------+ - | __end__ | | tools | - +---------+ +-------+ - ''' -# --- -# name: test_conditional_graph[end_of_run].5 +# name: test_conditional_graph[end_of_run].3 ''' graph TD; PromptTemplate --> FakeStreamingListLLM; @@ -870,18 +496,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -895,16 +509,12 @@ }, { "source": "agent", - "target": "should_continue" - }, - { - "source": "should_continue", "target": "tools", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": "agent", "target": "__end__", "data": "exit", "conditional": true @@ -914,31 +524,6 @@ ''' # --- # name: test_conditional_graph[end_of_step].1 - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | agent | - +-------+* - *** *** - * * - ** *** - +-----------------+ * - | should_continue | * - +-----------------+. * - . ..... * - . ... * - . ... * - +---------+ +-------+ - | __end__ | | tools | - +---------+ +-------+ - ''' -# --- -# name: test_conditional_graph[end_of_step].2 ''' graph TD; __start__ --> agent; @@ -948,7 +533,7 @@ ''' # --- -# name: test_conditional_graph[end_of_step].3 +# name: test_conditional_graph[end_of_step].2 ''' { "nodes": [ @@ -1035,18 +620,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -1084,16 +657,12 @@ }, { "source": 3, - "target": "should_continue" - }, - { - "source": "should_continue", "target": "tools", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": 3, "target": "__end__", "data": "exit", "conditional": true @@ -1102,56 +671,7 @@ } ''' # --- -# name: test_conditional_graph[end_of_step].4 - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +------------------------------+ - | ParallelInput | - +------------------------------+ - ***** * ***** - ****** * **** - *** * ***** - +----------------+ * *** - | PromptTemplate | * * - +----------------+ * * - * * * - * * * - * * * - +----------------------+ * * - | FakeStreamingListLLM | * * - +----------------------+ * * - * * * - * * * - * * * - +----------------------+ +-------------+ * - | Lambda(agent_parser) | | Passthrough | * - +----------------------+ +-------------+ * - *** *** * - ** ** * - ** ** * - +-------------------------------+ * - | ParallelOutput | * - +-------------------------------+ * - * * - * * - * * - +-----------------+ * - | should_continue |... * - +-----------------+ ......... * - . ........ * - . ......... * - . ..... * - +---------+ +-------+ - | __end__ | | tools | - +---------+ +-------+ - ''' -# --- -# name: test_conditional_graph[end_of_step].5 +# name: test_conditional_graph[end_of_step].3 ''' graph TD; PromptTemplate --> FakeStreamingListLLM; @@ -1167,13 +687,13 @@ ''' # --- -# name: test_conditional_graph_state[end_of_run] +# name: test_conditional_state_graph[end_of_run] '{"title": "LangGraphInput", "$ref": "#/definitions/AgentState", "definitions": {"AgentAction": {"title": "AgentAction", "description": "A full description of an action for an ActionAgent to execute.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "The final return value of an ActionAgent.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}, "AgentState": {"title": "AgentState", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}}}}' # --- -# name: test_conditional_graph_state[end_of_run].1 +# name: test_conditional_state_graph[end_of_run].1 '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "A full description of an action for an ActionAgent to execute.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "The final return value of an ActionAgent.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- -# name: test_conditional_graph_state[end_of_run].2 +# name: test_conditional_state_graph[end_of_run].2 ''' { "nodes": [ @@ -1211,18 +731,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -1236,16 +744,12 @@ }, { "source": "agent", - "target": "should_continue" - }, - { - "source": "should_continue", "target": "tools", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": "agent", "target": "__end__", "data": "exit", "conditional": true @@ -1254,38 +758,23 @@ } ''' # --- -# name: test_conditional_graph_state[end_of_run].3 +# name: test_conditional_state_graph[end_of_run].3 ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | agent | - +-------+* - *** *** - * * - ** *** - +-----------------+ * - | should_continue | * - +-----------------+. * - . ..... * - . ... * - . ... * - +---------+ +-------+ - | __end__ | | tools | - +---------+ +-------+ + graph TD; + __start__ --> agent; + tools --> agent; + agent -. continue .-> tools; + agent -. exit .-> __end__; + ''' # --- -# name: test_conditional_graph_state[end_of_step] +# name: test_conditional_state_graph[end_of_step] '{"title": "LangGraphInput", "$ref": "#/definitions/AgentState", "definitions": {"AgentAction": {"title": "AgentAction", "description": "A full description of an action for an ActionAgent to execute.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "The final return value of an ActionAgent.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}, "AgentState": {"title": "AgentState", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}}}}' # --- -# name: test_conditional_graph_state[end_of_step].1 +# name: test_conditional_state_graph[end_of_step].1 '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "A full description of an action for an ActionAgent to execute.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "The final return value of an ActionAgent.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- -# name: test_conditional_graph_state[end_of_step].2 +# name: test_conditional_state_graph[end_of_step].2 ''' { "nodes": [ @@ -1323,18 +812,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -1348,16 +825,12 @@ }, { "source": "agent", - "target": "should_continue" - }, - { - "source": "should_continue", "target": "tools", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": "agent", "target": "__end__", "data": "exit", "conditional": true @@ -1366,251 +839,92 @@ } ''' # --- -# name: test_conditional_graph_state[end_of_step].3 +# name: test_conditional_state_graph[end_of_step].3 ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | agent | - +-------+* - *** *** - * * - ** *** - +-----------------+ * - | should_continue | * - +-----------------+. * - . ..... * - . ... * - . ... * - +---------+ +-------+ - | __end__ | | tools | - +---------+ +-------+ + graph TD; + __start__ --> agent; + tools --> agent; + agent -. continue .-> tools; + agent -. exit .-> __end__; + ''' # --- # name: test_in_one_fan_out_state_graph_waiting_edge[end_of_run] ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------------+ - | rewrite_query | - +---------------+ - *** *** - * * - ** *** - +--------------+ * - | analyzer_one | * - +--------------+ * - * * - * * - * * - +---------------+ +---------------+ - | retriever_one | | retriever_two | - +---------------+ +---------------+ - *** *** - * * - ** ** - +----+ - | qa | - +----+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + ''' # --- # name: test_in_one_fan_out_state_graph_waiting_edge[end_of_step] ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------------+ - | rewrite_query | - +---------------+ - *** *** - * * - ** *** - +--------------+ * - | analyzer_one | * - +--------------+ * - * * - * * - * * - +---------------+ +---------------+ - | retriever_one | | retriever_two | - +---------------+ +---------------+ - *** *** - * * - ** ** - +----+ - | qa | - +----+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query --> retriever_two; + ''' # --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[end_of_run] ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------------+ - | rewrite_query | - +---------------+ - *** *** - * * - ** ** - +--------------+ +---------+ - | analyzer_one | | decider | - +--------------+ +---------+ - * . - * . - * . - +---------------+ +---------------+ - | retriever_one | | retriever_two | - +---------------+ +---------------+ - *** *** - * * - ** ** - +----+ - | qa | - +----+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + ''' # --- # name: test_in_one_fan_out_state_graph_waiting_edge_custom_state_class[end_of_step] ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------------+ - | rewrite_query | - +---------------+ - *** *** - * * - ** ** - +--------------+ +---------+ - | analyzer_one | | decider | - +--------------+ +---------+ - * . - * . - * . - +---------------+ +---------------+ - | retriever_one | | retriever_two | - +---------------+ +---------------+ - *** *** - * * - ** ** - +----+ - | qa | - +----+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + ''' # --- # name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[end_of_run] ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------------+ - | rewrite_query | - +---------------+ - *** *** - * * - ** ** - +--------------+ +-----------+ - | analyzer_one | | condition | - +--------------+ +-----------+ - * . - * . - * . - +---------------+ +---------------+ - | retriever_one | | retriever_two | - +---------------+ +---------------+ - *** *** - * * - ** ** - +----+ - | qa | - +----+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + ''' # --- # name: test_in_one_fan_out_state_graph_waiting_edge_via_branch[end_of_step] ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +---------------+ - | rewrite_query | - +---------------+ - *** *** - * * - ** ** - +--------------+ +-----------+ - | analyzer_one | | condition | - +--------------+ +-----------+ - * . - * . - * . - +---------------+ +---------------+ - | retriever_one | | retriever_two | - +---------------+ +---------------+ - *** *** - * * - ** ** - +----+ - | qa | - +----+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> rewrite_query; + analyzer_one --> retriever_one; + qa --> __end__; + retriever_one --> qa; + retriever_two --> qa; + rewrite_query --> analyzer_one; + rewrite_query -.-> retriever_two; + ''' # --- # name: test_message_graph[end_of_run] @@ -1657,18 +971,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -1682,16 +984,12 @@ }, { "source": "agent", - "target": "should_continue" - }, - { - "source": "should_continue", "target": "action", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": "agent", "target": "__end__", "data": "end", "conditional": true @@ -1702,27 +1000,12 @@ # --- # name: test_message_graph[end_of_run].3 ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | agent | - +-------+* - *** *** - * * - ** *** - +-----------------+ * - | should_continue | * - +-----------------+. * - . ..... * - . ... * - . ... * - +---------+ +--------+ - | __end__ | | action | - +---------+ +--------+ + graph TD; + __start__ --> agent; + action --> agent; + agent -. continue .-> action; + agent -. end .-> __end__; + ''' # --- # name: test_message_graph[end_of_step] @@ -1769,18 +1052,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -1794,16 +1065,12 @@ }, { "source": "agent", - "target": "should_continue" - }, - { - "source": "should_continue", "target": "action", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": "agent", "target": "__end__", "data": "end", "conditional": true @@ -1814,52 +1081,21 @@ # --- # name: test_message_graph[end_of_step].3 ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | agent | - +-------+* - *** *** - * * - ** *** - +-----------------+ * - | should_continue | * - +-----------------+. * - . ..... * - . ... * - . ... * - +---------+ +--------+ - | __end__ | | action | - +---------+ +--------+ + graph TD; + __start__ --> agent; + action --> agent; + agent -. continue .-> action; + agent -. end .-> __end__; + ''' # --- # name: test_nested_graph ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | inner | - +-------+ - * - * - * - +------+ - | side | - +------+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> inner; + inner --> side; + side --> __end__; + ''' # --- # name: test_nested_graph.1 @@ -1884,8 +1120,7 @@ 'edges': list([ dict({ 'conditional': True, - 'data': 'tool_two_slow', - 'source': 'tool_two:condition', + 'source': 'tool_two:__start__', 'target': 'tool_two:tool_two_slow', }), dict({ @@ -1894,22 +1129,16 @@ }), dict({ 'conditional': True, - 'data': 'tool_two_fast', - 'source': 'tool_two:condition', + 'source': 'tool_two:__start__', 'target': 'tool_two:tool_two_fast', }), dict({ 'source': 'tool_two:tool_two_fast', 'target': 'tool_two:__end__', }), - dict({ - 'source': '__start__', - 'target': 'condition', - }), dict({ 'conditional': True, - 'data': 'tool_one', - 'source': 'condition', + 'source': '__start__', 'target': 'tool_one', }), dict({ @@ -1918,9 +1147,8 @@ }), dict({ 'conditional': True, - 'data': 'tool_two', - 'source': 'condition', - 'target': 'tool_two:condition', + 'source': '__start__', + 'target': 'tool_two:__start__', }), dict({ 'source': 'tool_two:__end__', @@ -1928,8 +1156,7 @@ }), dict({ 'conditional': True, - 'data': 'tool_three', - 'source': 'condition', + 'source': '__start__', 'target': 'tool_three', }), dict({ @@ -1960,6 +1187,11 @@ 'id': 'tool_one', 'type': 'runnable', }), + dict({ + 'data': 'tool_two:__start__', + 'id': 'tool_two:__start__', + 'type': 'schema', + }), dict({ 'data': 'tool_two:__end__', 'id': 'tool_two:__end__', @@ -1989,18 +1221,6 @@ 'id': 'tool_two:tool_two_fast', 'type': 'runnable', }), - dict({ - 'data': dict({ - 'id': list([ - 'langgraph', - 'utils', - 'RunnableCallable', - ]), - 'name': 'RunnableCallable', - }), - 'id': 'tool_two:condition', - 'type': 'runnable', - }), dict({ 'data': dict({ 'id': list([ @@ -2013,71 +1233,32 @@ 'id': 'tool_three', 'type': 'runnable', }), - dict({ - 'data': dict({ - 'id': list([ - 'langgraph', - 'utils', - 'RunnableCallable', - ]), - 'name': 'RunnableCallable', - }), - 'id': 'condition', - 'type': 'runnable', - }), ]), }) # --- # name: test_nested_graph_xray.1 - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-----------+ - | condition | - ..+-----------+... - ..... . ..... - ... . ... - ... . ... - +----------+ +----------+ +------------+ - | tool_one |* | tool_two | | tool_three | - +----------+ *** +----------+ **+------------+ - ***** * ***** - *** * *** - *** * *** - +---------+ - | __end__ | - +---------+ - ''' -# --- -# name: test_nested_graph_xray.2 ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% graph TD; __start__[__start__]:::startclass; __end__[__end__]:::endclass; tool_one([tool_one]):::otherclass; + tool_two___start__([__start__]):::otherclass; tool_two___end__([__end__]):::otherclass; tool_two_tool_two_slow([tool_two_slow]):::otherclass; tool_two_tool_two_fast([tool_two_fast]):::otherclass; - tool_two_condition([condition]):::otherclass; tool_three([tool_three]):::otherclass; - condition([condition]):::otherclass; subgraph tool_two - tool_two_condition -. tool_two_slow .-> tool_two_tool_two_slow; + tool_two___start__ -.-> tool_two_tool_two_slow; tool_two_tool_two_slow --> tool_two___end__; - tool_two_condition -. tool_two_fast .-> tool_two_tool_two_fast; + tool_two___start__ -.-> tool_two_tool_two_fast; tool_two_tool_two_fast --> tool_two___end__; end - __start__ --> condition; - condition -. tool_one .-> tool_one; + __start__ -.-> tool_one; tool_one --> __end__; - condition -. tool_two .-> tool_two_condition; + __start__ -.-> tool_two___start__; tool_two___end__ --> __end__; - condition -. tool_three .-> tool_three; + __start__ -.-> tool_three; tool_three --> __end__; classDef startclass fill:#ffdfba; classDef endclass fill:#baffc9; @@ -2130,18 +1311,6 @@ ], "name": "call_tool" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -2155,16 +1324,12 @@ }, { "source": "agent", - "target": "should_continue" - }, - { - "source": "should_continue", "target": "action", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": "agent", "target": "__end__", "data": "end", "conditional": true @@ -2175,27 +1340,12 @@ # --- # name: test_prebuilt_chat.3 ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | agent | - +-------+* - *** *** - * * - ** *** - +-----------------+ * - | should_continue | * - +-----------------+. * - . ..... * - . ... * - . ... * - +---------+ +--------+ - | __end__ | | action | - +---------+ +--------+ + graph TD; + __start__ --> agent; + action --> agent; + agent -. continue .-> action; + agent -. end .-> __end__; + ''' # --- # name: test_prebuilt_tool_chat @@ -2243,18 +1393,6 @@ ], "name": "tools" } - }, - { - "id": "should_continue", - "type": "runnable", - "data": { - "id": [ - "langgraph", - "utils", - "RunnableCallable" - ], - "name": "should_continue" - } } ], "edges": [ @@ -2268,16 +1406,12 @@ }, { "source": "agent", - "target": "should_continue" - }, - { - "source": "should_continue", "target": "action", "data": "continue", "conditional": true }, { - "source": "should_continue", + "source": "agent", "target": "__end__", "data": "end", "conditional": true @@ -2288,123 +1422,41 @@ # --- # name: test_prebuilt_tool_chat.3 ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-------+ - | agent | - +-------+* - *** *** - * * - ** *** - +-----------------+ * - | should_continue | * - +-----------------+. * - . ..... * - . ... * - . ... * - +---------+ +--------+ - | __end__ | | action | - +---------+ +--------+ + graph TD; + __start__ --> agent; + action --> agent; + agent -. continue .-> action; + agent -. end .-> __end__; + ''' # --- # name: test_repeat_condition ''' graph TD; __start__ --> Researcher; - Researcher --> Researcher_router; - Researcher_router -. continue .-> Chart_Generator; - Researcher_router -. call_tool .-> Call_Tool; - Researcher_router -. end .-> __end__; - Chart_Generator --> Chart_Generator_router; - Chart_Generator_router -. continue .-> Researcher; - Chart_Generator_router -. call_tool .-> Call_Tool; - Chart_Generator_router -. end .-> __end__; - Call_Tool --> condition; - condition -. Researcher .-> Researcher; - condition -. Chart Generator .-> Chart_Generator; + Researcher -. continue .-> Chart_Generator; + Researcher -. call_tool .-> Call_Tool; + Researcher -. end .-> __end__; + Chart_Generator -. continue .-> Researcher; + Chart_Generator -. call_tool .-> Call_Tool; + Chart_Generator -. end .-> __end__; + Call_Tool -.-> Researcher; + Call_Tool -.-> Chart_Generator; ''' # --- # name: test_simple_multi_edge ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +----+ - | up | - +----+ - ** ** - * * - * * - +------+ * - | side | * - +------+ * - ** ** - * * - * * - +------+ - | down | - +------+ - * - * - * - +---------+ - | __end__ | - +---------+ + graph TD; + __start__ --> up; + down --> __end__; + side --> down; + up --> down; + up --> side; + ''' # --- # name: test_start_branch_then[end_of_run] - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-----------+ - | condition | - +-----------+ - .. .. - .. .. - .. .. - +---------------+ +---------------+ - | tool_two_slow | | tool_two_fast | - +---------------+ +---------------+ - ** ** - ** ** - ** ** - +---------+ - | __end__ | - +---------+ - ''' -# --- -# name: test_start_branch_then[end_of_run].1 - ''' - +-----------+ - | __start__ | - +-----------+ - .. .. - .. .. - .. .. - +---------------+ +---------------+ - | tool_two_slow | | tool_two_fast | - +---------------+ +---------------+ - ** ** - ** ** - ** ** - +---------+ - | __end__ | - +---------+ - ''' -# --- -# name: test_start_branch_then[end_of_run].2 ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% graph TD; @@ -2412,29 +1464,9 @@ __end__[__end__]:::endclass; tool_two_slow([tool_two_slow]):::otherclass; tool_two_fast([tool_two_fast]):::otherclass; - condition([condition]):::otherclass; - __start__ --> condition; - condition -. tool_two_slow .-> tool_two_slow; + __start__ -.-> tool_two_slow; tool_two_slow --> __end__; - condition -. tool_two_fast .-> tool_two_fast; - tool_two_fast --> __end__; - classDef startclass fill:#ffdfba; - classDef endclass fill:#baffc9; - classDef otherclass fill:#fad7de; - - ''' -# --- -# name: test_start_branch_then[end_of_run].3 - ''' - %%{init: {'flowchart': {'curve': 'linear'}}}%% - graph TD; - __start__[__start__]:::startclass; - __end__[__end__]:::endclass; - tool_two_slow([tool_two_slow]):::otherclass; - tool_two_fast([tool_two_fast]):::otherclass; - __start__ -. tool_two_slow .-> tool_two_slow; - tool_two_slow --> __end__; - __start__ -. tool_two_fast .-> tool_two_fast; + __start__ -.-> tool_two_fast; tool_two_fast --> __end__; classDef startclass fill:#ffdfba; classDef endclass fill:#baffc9; @@ -2443,50 +1475,6 @@ ''' # --- # name: test_start_branch_then[end_of_step] - ''' - +-----------+ - | __start__ | - +-----------+ - * - * - * - +-----------+ - | condition | - +-----------+ - .. .. - .. .. - .. .. - +---------------+ +---------------+ - | tool_two_slow | | tool_two_fast | - +---------------+ +---------------+ - ** ** - ** ** - ** ** - +---------+ - | __end__ | - +---------+ - ''' -# --- -# name: test_start_branch_then[end_of_step].1 - ''' - +-----------+ - | __start__ | - +-----------+ - .. .. - .. .. - .. .. - +---------------+ +---------------+ - | tool_two_slow | | tool_two_fast | - +---------------+ +---------------+ - ** ** - ** ** - ** ** - +---------+ - | __end__ | - +---------+ - ''' -# --- -# name: test_start_branch_then[end_of_step].2 ''' %%{init: {'flowchart': {'curve': 'linear'}}}%% graph TD; @@ -2494,29 +1482,9 @@ __end__[__end__]:::endclass; tool_two_slow([tool_two_slow]):::otherclass; tool_two_fast([tool_two_fast]):::otherclass; - condition([condition]):::otherclass; - __start__ --> condition; - condition -. tool_two_slow .-> tool_two_slow; + __start__ -.-> tool_two_slow; tool_two_slow --> __end__; - condition -. tool_two_fast .-> tool_two_fast; - tool_two_fast --> __end__; - classDef startclass fill:#ffdfba; - classDef endclass fill:#baffc9; - classDef otherclass fill:#fad7de; - - ''' -# --- -# name: test_start_branch_then[end_of_step].3 - ''' - %%{init: {'flowchart': {'curve': 'linear'}}}%% - graph TD; - __start__[__start__]:::startclass; - __end__[__end__]:::endclass; - tool_two_slow([tool_two_slow]):::otherclass; - tool_two_fast([tool_two_fast]):::otherclass; - __start__ -. tool_two_slow .-> tool_two_slow; - tool_two_slow --> __end__; - __start__ -. tool_two_fast .-> tool_two_fast; + __start__ -.-> tool_two_fast; tool_two_fast --> __end__; classDef startclass fill:#ffdfba; classDef endclass fill:#baffc9; diff --git a/tests/__snapshots__/test_pregel_async.ambr b/tests/__snapshots__/test_pregel_async.ambr index 709726945..2d68a82a5 100644 --- a/tests/__snapshots__/test_pregel_async.ambr +++ b/tests/__snapshots__/test_pregel_async.ambr @@ -10,12 +10,12 @@ +---------------+ | rewrite_query | +---------------+ - *** *** - * * - ** ** - +--------------+ +---------+ - | analyzer_one | | decider | - +--------------+ +---------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . * . * . * . @@ -47,12 +47,12 @@ +---------------+ | rewrite_query | +---------------+ - *** *** - * * - ** ** - +--------------+ +---------+ - | analyzer_one | | decider | - +--------------+ +---------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . * . * . * . @@ -84,12 +84,12 @@ +---------------+ | rewrite_query | +---------------+ - *** *** - * * - ** ** - +--------------+ +-----------+ - | analyzer_one | | condition | - +--------------+ +-----------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . * . * . * . @@ -121,12 +121,12 @@ +---------------+ | rewrite_query | +---------------+ - *** *** - * * - ** ** - +--------------+ +-----------+ - | analyzer_one | | condition | - +--------------+ +-----------+ + *** ... + * . + ** ... + +--------------+ . + | analyzer_one | . + +--------------+ . * . * . * . diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 631f666b6..5aac3541b 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -4,7 +4,7 @@ import time import warnings from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager -from typing import Annotated, Any, Generator, Optional, TypedDict, Union +from typing import Annotated, Any, Generator, Literal, Optional, TypedDict, Union import pytest from langchain_core.runnables import RunnableLambda, RunnablePassthrough @@ -168,17 +168,6 @@ def test_invoke_single_process_in_out_falsy_values(falsy_value: Any) -> None: assert gapp.invoke(1) == falsy_value -def test_invoke_single_process_in_out_implicit_channels(mocker: MockerFixture) -> None: - add_one = mocker.Mock(side_effect=lambda x: x + 1) - chain = Channel.subscribe_to("input") | add_one | Channel.write_to("output") - - app = Pregel(nodes={"one": chain}) - - assert app.input_schema.schema() == {"title": "LangGraphInput"} - assert app.output_schema.schema() == {"title": "LangGraphOutput"} - assert app.invoke(2) == 3 - - def test_invoke_single_process_in_write_kwargs(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) chain = ( @@ -188,17 +177,25 @@ def test_invoke_single_process_in_write_kwargs(mocker: MockerFixture) -> None: ) app = Pregel( - nodes={"one": chain}, output_channels=["output", "fixed", "output_plus_one"] + nodes={"one": chain}, + channels={ + "input": LastValue(int), + "output": LastValue(int), + "fixed": LastValue(int), + "output_plus_one": LastValue(int), + }, + output_channels=["output", "fixed", "output_plus_one"], + input_channels="input", ) - assert app.input_schema.schema() == {"title": "LangGraphInput"} + assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} assert app.output_schema.schema() == { "title": "LangGraphOutput", "type": "object", "properties": { - "output": {"title": "Output"}, - "fixed": {"title": "Fixed"}, - "output_plus_one": {"title": "Output Plus One"}, + "output": {"title": "Output", "type": "integer"}, + "fixed": {"title": "Fixed", "type": "integer"}, + "output_plus_one": {"title": "Output Plus One", "type": "integer"}, }, } assert app.invoke(2) == {"output": 3, "fixed": 5, "output_plus_one": 4} @@ -209,17 +206,17 @@ def test_invoke_single_process_in_out_dict(mocker: MockerFixture) -> None: chain = Channel.subscribe_to("input") | add_one | Channel.write_to("output") app = Pregel( - nodes={ - "one": chain, - }, + nodes={"one": chain}, + channels={"input": LastValue(int), "output": LastValue(int)}, + input_channels="input", output_channels=["output"], ) - assert app.input_schema.schema() == {"title": "LangGraphInput"} + assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} assert app.output_schema.schema() == { "title": "LangGraphOutput", "type": "object", - "properties": {"output": {"title": "Output"}}, + "properties": {"output": {"title": "Output", "type": "integer"}}, } assert app.invoke(2) == {"output": 3} @@ -229,9 +226,8 @@ def test_invoke_single_process_in_dict_out_dict(mocker: MockerFixture) -> None: chain = Channel.subscribe_to("input") | add_one | Channel.write_to("output") app = Pregel( - nodes={ - "one": chain, - }, + nodes={"one": chain}, + channels={"input": LastValue(int), "output": LastValue(int)}, input_channels=["input"], output_channels=["output"], ) @@ -239,12 +235,12 @@ def test_invoke_single_process_in_dict_out_dict(mocker: MockerFixture) -> None: assert app.input_schema.schema() == { "title": "LangGraphInput", "type": "object", - "properties": {"input": {"title": "Input"}}, + "properties": {"input": {"title": "Input", "type": "integer"}}, } assert app.output_schema.schema() == { "title": "LangGraphOutput", "type": "object", - "properties": {"output": {"title": "Output"}}, + "properties": {"output": {"title": "Output", "type": "integer"}}, } assert app.invoke({"input": 2}) == {"output": 3} @@ -256,6 +252,13 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: app = Pregel( nodes={"one": one, "two": two}, + channels={ + "inbox": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", ) assert app.invoke(2) == 4 @@ -302,6 +305,13 @@ def test_invoke_two_processes_in_out_interrupt( memory = MemorySaverAssertImmutable(at=checkpoint_at) app = Pregel( nodes={"one": one, "two": two}, + channels={ + "inbox": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", checkpointer=memory, interrupt_after_nodes=["one"], ) @@ -357,9 +367,14 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: app = Pregel( nodes={"one": one, "two": two}, - channels={"inbox": Topic(int)}, + channels={ + "inbox": Topic(int), + "output": LastValue(int), + "input": LastValue(int), + }, input_channels=["input", "inbox"], stream_channels=["output", "inbox"], + output_channels=["output"], ) # [12 + 1, 2 + 1 + 1] @@ -384,6 +399,86 @@ def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: {"inbox": [3], "output": 13}, {"inbox": [], "output": 4}, ] + assert [*app.stream({"input": 2, "inbox": 12}, stream_mode="debug")] == [ + { + "type": "task", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "id": "7a3cc398-2e02-5023-ad7b-e4848d3b67fa", + "name": "one", + "input": 2, + "triggers": ["input"], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "id": "34e90af0-f97e-54e0-a159-691da37f175f", + "name": "two", + "input": [12], + "triggers": ["inbox"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "id": "7a3cc398-2e02-5023-ad7b-e4848d3b67fa", + "result": [("inbox", 3)], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "id": "34e90af0-f97e-54e0-a159-691da37f175f", + "result": [("output", 13)], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": {"config": None, "values": {"output": 13, "inbox": [3]}}, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "cf7cf374-2a2a-556f-8561-91737af89d2f", + "name": "two", + "input": [3], + "triggers": ["inbox"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "cf7cf374-2a2a-556f-8561-91737af89d2f", + "result": [("output", 4)], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": {"config": None, "values": {"output": 4, "inbox": []}}, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": {"config": None, "values": {"output": 4, "inbox": []}}, + }, + ] def test_batch_two_processes_in_out() -> None: @@ -394,7 +489,16 @@ def test_batch_two_processes_in_out() -> None: one = Channel.subscribe_to("input") | add_one_with_delay | Channel.write_to("one") two = Channel.subscribe_to("one") | add_one_with_delay | Channel.write_to("output") - app = Pregel(nodes={"one": one, "two": two}) + app = Pregel( + nodes={"one": one, "two": two}, + channels={ + "one": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", + ) assert app.batch([3, 2, 1, 3, 5]) == [5, 4, 3, 5, 7] assert app.batch([3, 2, 1, 3, 5], output_keys=["output"]) == [ @@ -427,7 +531,13 @@ def test_invoke_many_processes_in_out(mocker: MockerFixture) -> None: ) nodes["last"] = Channel.subscribe_to(str(i)) | add_one | Channel.write_to("output") - app = Pregel(nodes=nodes) + app = Pregel( + nodes=nodes, + channels={str(i): LastValue(int) for i in range(-1, test_size - 2)} + | {"input": LastValue(int), "output": LastValue(int)}, + input_channels="input", + output_channels="output", + ) for _ in range(10): assert app.invoke(2, {"recursion_limit": test_size}) == 2 + test_size @@ -449,7 +559,13 @@ def test_batch_many_processes_in_out(mocker: MockerFixture) -> None: ) nodes["last"] = Channel.subscribe_to(str(i)) | add_one | Channel.write_to("output") - app = Pregel(nodes=nodes) + app = Pregel( + nodes=nodes, + channels={str(i): LastValue(int) for i in range(-1, test_size - 2)} + | {"input": LastValue(int), "output": LastValue(int)}, + input_channels="input", + output_channels="output", + ) for _ in range(3): assert app.batch([2, 1, 3, 4, 5], {"recursion_limit": test_size}) == [ @@ -476,7 +592,12 @@ def test_invoke_two_processes_two_in_two_out_invalid(mocker: MockerFixture) -> N one = Channel.subscribe_to("input") | add_one | Channel.write_to("output") two = Channel.subscribe_to("input") | add_one | Channel.write_to("output") - app = Pregel(nodes={"one": one, "two": two}) + app = Pregel( + nodes={"one": one, "two": two}, + channels={"output": LastValue(int), "input": LastValue(int)}, + input_channels="input", + output_channels="output", + ) with pytest.raises(InvalidUpdateError): # LastValue channels can only be updated once per iteration @@ -491,7 +612,12 @@ def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) -> Non app = Pregel( nodes={"one": one, "two": two}, - channels={"output": Topic(int)}, + channels={ + "input": LastValue(int), + "output": Topic(int), + }, + input_channels="input", + output_channels="output", ) # An Inbox channel accumulates updates into a sequence @@ -520,7 +646,13 @@ def test_invoke_checkpoint(mocker: MockerFixture, checkpoint_at: CheckpointAt) - app = Pregel( nodes={"one": one}, - channels={"total": BinaryOperatorAggregate(int, operator.add)}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", checkpointer=memory, ) @@ -575,7 +707,13 @@ def test_invoke_checkpoint_sqlite( memory.at = checkpoint_at app = Pregel( nodes={"one": one}, - channels={"total": BinaryOperatorAggregate(int, operator.add)}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", checkpointer=memory, ) @@ -663,7 +801,13 @@ def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) -> None "chain_three": chain_three, "chain_four": chain_four, }, - channels={"inbox": Topic(int)}, + channels={ + "inbox": Topic(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", ) # Then invoke app @@ -676,14 +820,20 @@ def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) -> None assert [*executor.map(app.invoke, [2] * 100)] == [[13, 13]] * 100 -def test_invoke_join_then_call_other_app(mocker: MockerFixture) -> None: +def test_invoke_join_then_call_other_pregel(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) add_10_each = mocker.Mock(side_effect=lambda x: [y + 10 for y in x]) inner_app = Pregel( nodes={ "one": Channel.subscribe_to("input") | add_one | Channel.write_to("output") - } + }, + channels={ + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", ) one = ( @@ -705,7 +855,14 @@ def test_invoke_join_then_call_other_app(mocker: MockerFixture) -> None: "two": two, "chain_three": chain_three, }, - channels={"inbox_one": Topic(int)}, + channels={ + "inbox_one": Topic(int), + "outbox_one": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", ) for _ in range(10): @@ -725,7 +882,17 @@ def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> None: ) two = Channel.subscribe_to("between") | add_one | Channel.write_to("output") - app = Pregel(nodes={"one": one, "two": two}, stream_channels=["output", "between"]) + app = Pregel( + nodes={"one": one, "two": two}, + channels={ + "input": LastValue(int), + "between": LastValue(int), + "output": LastValue(int), + }, + stream_channels=["output", "between"], + input_channels="input", + output_channels="output", + ) assert [c for c in app.stream(2, stream_mode="updates")] == [ {"one": {"between": 3, "output": 3}}, @@ -742,7 +909,16 @@ def test_invoke_two_processes_no_out(mocker: MockerFixture) -> None: one = Channel.subscribe_to("input") | add_one | Channel.write_to("between") two = Channel.subscribe_to("between") | add_one - app = Pregel(nodes={"one": one, "two": two}) + app = Pregel( + nodes={"one": one, "two": two}, + channels={ + "input": LastValue(int), + "between": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + ) # It finishes executing (once no more messages being published) # but returns nothing, as nothing was published to OUT topic @@ -784,7 +960,10 @@ def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: channels={ "inbox": Topic(int), "ctx": Context(an_int, typ=int), + "output": LastValue(int), + "input": LastValue(int), }, + input_channels="input", output_channels=["inbox", "output"], stream_channels=["inbox", "output"], ) @@ -882,19 +1061,9 @@ def test_conditional_graph( app = workflow.compile() assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_ascii() == snapshot - assert ( - app.get_graph(add_condition_nodes=False).draw_mermaid(with_styles=False) - == snapshot - ) + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert json.dumps(app.get_graph(xray=True).to_json(), indent=2) == snapshot - assert app.get_graph(xray=True).draw_ascii() == snapshot - assert ( - app.get_graph(xray=True, add_condition_nodes=False).draw_mermaid( - with_styles=False - ) - == snapshot - ) + assert app.get_graph(xray=True).draw_mermaid(with_styles=False) == snapshot assert app.invoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", @@ -1470,7 +1639,7 @@ def test_conditional_entrypoint_graph(snapshot: SnapshotAssertion) -> None: assert app.get_input_schema().schema_json() == snapshot assert app.get_output_schema().schema_json() == snapshot assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert ( app.invoke("what is weather in sf", debug=True) @@ -1485,7 +1654,7 @@ def test_conditional_entrypoint_graph(snapshot: SnapshotAssertion) -> None: @pytest.mark.parametrize( "checkpoint_at", [CheckpointAt.END_OF_RUN, CheckpointAt.END_OF_STEP] ) -def test_conditional_graph_state( +def test_conditional_state_graph( snapshot: SnapshotAssertion, checkpoint_at: CheckpointAt ) -> None: from langchain.llms.fake import FakeStreamingListLLM @@ -1570,7 +1739,7 @@ def test_conditional_graph_state( assert app.get_input_schema().schema_json() == snapshot assert app.get_output_schema().schema_json() == snapshot assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", @@ -1883,6 +2052,182 @@ def test_conditional_graph_state( config=app_w_interrupt.checkpointer.get_tuple(config).config, ) + # test w interrupt before all + app_w_interrupt = workflow.compile( + checkpointer=MemorySaverAssertImmutable(at=checkpoint_at), + interrupt_before="*", + debug=True, + ) + config = {"configurable": {"thread_id": "3"}} + llm.i = 0 # reset the llm + + assert [ + c for c in app_w_interrupt.stream({"input": "what is weather in sf"}, config) + ] == [] + + assert app_w_interrupt.get_state(config) == StateSnapshot( + values={ + "input": "what is weather in sf", + "intermediate_steps": [], + }, + next=("agent",), + config=app_w_interrupt.checkpointer.get_tuple(config).config, + ) + + assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + } + }, + ] + + assert app_w_interrupt.get_state(config) == StateSnapshot( + values={ + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + "intermediate_steps": [], + }, + next=("tools",), + config=app_w_interrupt.checkpointer.get_tuple(config).config, + ) + + assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "tools": { + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ) + ], + } + }, + ] + + assert app_w_interrupt.get_state(config) == StateSnapshot( + values={ + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ) + ], + }, + next=("agent",), + config=app_w_interrupt.checkpointer.get_tuple(config).config, + ) + + assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] + + # test w interrupt after all + app_w_interrupt = workflow.compile( + checkpointer=MemorySaverAssertImmutable(at=checkpoint_at), + interrupt_after="*", + ) + config = {"configurable": {"thread_id": "4"}} + llm.i = 0 # reset the llm + + assert [ + c for c in app_w_interrupt.stream({"input": "what is weather in sf"}, config) + ] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + } + }, + ] + + assert app_w_interrupt.get_state(config) == StateSnapshot( + values={ + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + "intermediate_steps": [], + }, + next=("tools",), + config=app_w_interrupt.checkpointer.get_tuple(config).config, + ) + + assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "tools": { + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ) + ], + } + }, + ] + + assert app_w_interrupt.get_state(config) == StateSnapshot( + values={ + "input": "what is weather in sf", + "agent_outcome": AgentAction( + tool="search_api", tool_input="query", log="tool:search_api:query" + ), + "intermediate_steps": [ + ( + AgentAction( + tool="search_api", + tool_input="query", + log="tool:search_api:query", + ), + "result for query", + ) + ], + }, + next=("agent",), + config=app_w_interrupt.checkpointer.get_tuple(config).config, + ) + + assert [c for c in app_w_interrupt.stream(None, config)] == [ + { + "agent": { + "agent_outcome": AgentAction( + tool="search_api", + tool_input="another", + log="tool:search_api:another", + ), + } + }, + ] + def test_conditional_entrypoint_graph_state(snapshot: SnapshotAssertion) -> None: class AgentState(TypedDict, total=False): @@ -1922,7 +2267,7 @@ def test_conditional_entrypoint_graph_state(snapshot: SnapshotAssertion) -> None assert app.get_input_schema().schema_json() == snapshot assert app.get_output_schema().schema_json() == snapshot assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke({"input": "what is weather in sf"}) == { "input": "what is weather in sf", @@ -1988,7 +2333,7 @@ def test_prebuilt_tool_chat(snapshot: SnapshotAssertion) -> None: assert app.get_input_schema().schema_json() == snapshot assert app.get_output_schema().schema_json() == snapshot assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke( {"messages": [HumanMessage(content="what is weather in sf")]} @@ -2251,7 +2596,7 @@ def test_prebuilt_chat(snapshot: SnapshotAssertion) -> None: assert app.get_input_schema().schema_json() == snapshot assert app.get_output_schema().schema_json() == snapshot assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke( {"messages": [HumanMessage(content="what is weather in sf")]} @@ -2466,7 +2811,7 @@ def test_message_graph( assert app.get_input_schema().schema_json() == snapshot assert app.get_output_schema().schema_json() == snapshot assert json.dumps(app.get_graph().to_json(), indent=2) == snapshot - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke(HumanMessage(content="what is weather in sf")) == [ HumanMessage( @@ -3031,10 +3376,7 @@ def test_start_branch_then( lambda s: "tool_two_slow" if s["market"] == "DE" else "tool_two_fast", then=END ) tool_two = tool_two_graph.compile() - assert tool_two.get_graph().draw_ascii() == snapshot - assert tool_two.get_graph(add_condition_nodes=False).draw_ascii() == snapshot assert tool_two.get_graph().draw_mermaid() == snapshot - assert tool_two.get_graph(add_condition_nodes=False).draw_mermaid() == snapshot assert tool_two.invoke({"my_key": "value", "market": "DE"}) == { "my_key": "value slow", @@ -3115,6 +3457,7 @@ def test_branch_then(snapshot: SnapshotAssertion, checkpoint_at: CheckpointAt) - invalid_graph.add_conditional_edges( source="prepare", path=lambda s: "tool_two_slow" if s["market"] == "DE" else "tool_two_fast", + path_map=["tool_two_slow", "tool_two_fast"], ) invalid_graph.add_node("prepare", lambda s: {"my_key": " prepared"}) invalid_graph.add_node("tool_two_slow", lambda s: {"my_key": " slow"}) @@ -3136,10 +3479,8 @@ def test_branch_then(snapshot: SnapshotAssertion, checkpoint_at: CheckpointAt) - tool_two_graph.add_node("tool_two_fast", lambda s: {"my_key": " fast"}) tool_two_graph.add_node("finish", lambda s: {"my_key": " finished"}) tool_two = tool_two_graph.compile() - assert tool_two.get_graph().draw_ascii() == snapshot - assert tool_two.get_graph(add_condition_nodes=False).draw_ascii() == snapshot + assert tool_two.get_graph().draw_mermaid(with_styles=False) == snapshot assert tool_two.get_graph().draw_mermaid() == snapshot - assert tool_two.get_graph(add_condition_nodes=False).draw_mermaid() == snapshot assert tool_two.invoke({"my_key": "value", "market": "DE"}, debug=1) == { "my_key": "value prepared slow finished", @@ -3152,6 +3493,248 @@ def test_branch_then(snapshot: SnapshotAssertion, checkpoint_at: CheckpointAt) - with SqliteSaver.from_conn_string(":memory:") as saver: saver.at = checkpoint_at + + # test stream_mode=debug + tool_two = tool_two_graph.compile(checkpointer=saver) + thread10 = {"configurable": {"thread_id": "10"}} + if checkpoint_at is CheckpointAt.END_OF_RUN: + assert [ + *tool_two.stream( + {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": None, + "values": {"my_key": "value", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "e7879e70-6335-5867-9ec6-957fbb3da6fa", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "e7879e70-6335-5867-9ec6-957fbb3da6fa", + "result": [("my_key", " prepared")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": None, + "values": {"my_key": "value prepared", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "122f31bd-0e14-5b8f-91e7-4f241047a3fd", + "name": "tool_two_slow", + "input": {"my_key": "value prepared", "market": "DE"}, + "triggers": ["branch:prepare:condition:tool_two_slow"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "122f31bd-0e14-5b8f-91e7-4f241047a3fd", + "result": [("my_key", " slow")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "config": None, + "values": {"my_key": "value prepared slow", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "48a16051-2c14-5ff5-9cfe-e8c7c32d5c83", + "name": "finish", + "input": {"my_key": "value prepared slow", "market": "DE"}, + "triggers": ["branch:prepare:condition:then"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "48a16051-2c14-5ff5-9cfe-e8c7c32d5c83", + "result": [("my_key", " finished")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "config": None, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 4, + "payload": { + "config": { + "configurable": { + "thread_id": "10", + "thread_ts": AnyStr(), + } + }, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + }, + }, + ] + else: + assert [ + *tool_two.stream( + {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "configurable": {"thread_id": "10", "thread_ts": AnyStr()} + }, + "values": {"my_key": "value", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "e7879e70-6335-5867-9ec6-957fbb3da6fa", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "e7879e70-6335-5867-9ec6-957fbb3da6fa", + "result": [("my_key", " prepared")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "configurable": {"thread_id": "10", "thread_ts": AnyStr()} + }, + "values": {"my_key": "value prepared", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "122f31bd-0e14-5b8f-91e7-4f241047a3fd", + "name": "tool_two_slow", + "input": {"my_key": "value prepared", "market": "DE"}, + "triggers": ["branch:prepare:condition:tool_two_slow"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "122f31bd-0e14-5b8f-91e7-4f241047a3fd", + "result": [("my_key", " slow")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "config": { + "configurable": {"thread_id": "10", "thread_ts": AnyStr()} + }, + "values": {"my_key": "value prepared slow", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "48a16051-2c14-5ff5-9cfe-e8c7c32d5c83", + "name": "finish", + "input": {"my_key": "value prepared slow", "market": "DE"}, + "triggers": ["branch:prepare:condition:then"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "48a16051-2c14-5ff5-9cfe-e8c7c32d5c83", + "result": [("my_key", " finished")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "config": { + "configurable": {"thread_id": "10", "thread_ts": AnyStr()} + }, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + }, + }, + ] + tool_two = tool_two_graph.compile( checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] ) @@ -3311,7 +3894,7 @@ def test_in_one_fan_out_state_graph_waiting_edge( app = workflow.compile() - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke({"query": "what is weather in sf"}) == { "query": "analyzed: query: what is weather in sf", @@ -3387,6 +3970,9 @@ def test_in_one_fan_out_state_graph_waiting_edge_via_branch( def qa(data: State) -> State: return {"answer": ",".join(data["docs"])} + def rewrite_query_then(data: State) -> Literal["retriever_two"]: + return "retriever_two" + workflow = StateGraph(State) workflow.add_node("rewrite_query", rewrite_query) @@ -3398,15 +3984,13 @@ def test_in_one_fan_out_state_graph_waiting_edge_via_branch( workflow.set_entry_point("rewrite_query") workflow.add_edge("rewrite_query", "analyzer_one") workflow.add_edge("analyzer_one", "retriever_one") - workflow.add_conditional_edges( - "rewrite_query", lambda _: "retriever_two", {"retriever_two": "retriever_two"} - ) + workflow.add_conditional_edges("rewrite_query", rewrite_query_then) workflow.add_edge(["retriever_one", "retriever_two"], "qa") workflow.set_finish_point("qa") app = workflow.compile() - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke({"query": "what is weather in sf"}, debug=True) == { "query": "analyzed: query: what is weather in sf", @@ -3507,7 +4091,7 @@ def test_in_one_fan_out_state_graph_waiting_edge_custom_state_class( app = workflow.compile() - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot with pytest.raises(ValidationError): app.invoke({"query": {}}) @@ -3844,7 +4428,7 @@ def test_simple_multi_edge(snapshot: SnapshotAssertion) -> None: app = graph.compile() - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.invoke({"my_key": "my_value"}) == {"my_key": "my_value"} @@ -3873,7 +4457,6 @@ def test_nested_graph_xray(snapshot: SnapshotAssertion) -> None: app = graph.compile() assert app.get_graph(xray=True).to_json() == snapshot - assert app.get_graph().draw_ascii() == snapshot assert app.get_graph(xray=True).draw_mermaid() == snapshot @@ -3911,7 +4494,7 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None: app = graph.compile() - assert app.get_graph().draw_ascii() == snapshot + assert app.get_graph().draw_mermaid(with_styles=False) == snapshot assert app.get_graph(xray=True).draw_mermaid() == snapshot assert app.invoke( {"my_key": "my value", "never_called": never_called}, debug=True @@ -3942,6 +4525,17 @@ def test_nested_graph(snapshot: SnapshotAssertion) -> None: }, ] + chain = app | RunnablePassthrough() + + assert chain.invoke({"my_key": "my value", "never_called": never_called}) == { + "my_key": "my value there and back again", + "never_called": never_called, + } + assert [*chain.stream({"my_key": "my value", "never_called": never_called})] == [ + {"inner": {"my_key": "my value there"}}, + {"side": {"my_key": "my value there and back again"}}, + ] + def test_repeat_condition(snapshot: SnapshotAssertion) -> None: class AgentState(TypedDict): diff --git a/tests/test_pregel_async.py b/tests/test_pregel_async.py index 1196de6d6..ff099b3e2 100644 --- a/tests/test_pregel_async.py +++ b/tests/test_pregel_async.py @@ -38,6 +38,57 @@ from tests.any_str import AnyStr from tests.memory_assert import MemorySaverAssertImmutable +async def test_node_cancellation_on_external_cancel() -> None: + inner_task_cancelled = False + + async def awhile(input: Any) -> None: + try: + await asyncio.sleep(1) + except asyncio.CancelledError: + nonlocal inner_task_cancelled + inner_task_cancelled = True + raise + + builder = Graph() + builder.add_node("agent", awhile) + builder.set_entry_point("agent") + builder.set_finish_point("agent") + + graph = builder.compile() + + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(graph.ainvoke(1), 0.5) + + assert inner_task_cancelled + + +async def test_node_cancellation_on_other_node_exception() -> None: + inner_task_cancelled = False + + async def awhile(input: Any) -> None: + try: + await asyncio.sleep(1) + except asyncio.CancelledError: + nonlocal inner_task_cancelled + inner_task_cancelled = True + raise + + async def iambad(input: Any) -> None: + raise ValueError("I am bad") + + builder = Graph() + builder.add_node("agent", awhile) + builder.add_node("bad", iambad) + builder.set_conditional_entry_point(lambda _: ["agent", "bad"], then=END) + + graph = builder.compile() + + with pytest.raises(ValueError, match="I am bad"): + await graph.ainvoke(1) + + assert inner_task_cancelled + + async def test_invoke_single_process_in_out(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) chain = Channel.subscribe_to("input") | add_one | Channel.write_to("output") @@ -80,19 +131,6 @@ async def test_invoke_single_process_in_out_falsy_values(falsy_value: Any) -> No assert falsy_value == await gapp.ainvoke(1) -async def test_invoke_single_process_in_out_implicit_channels( - mocker: MockerFixture, -) -> None: - add_one = mocker.Mock(side_effect=lambda x: x + 1) - chain = Channel.subscribe_to("input") | add_one | Channel.write_to("output") - - app = Pregel(nodes={"one": chain}) - - assert app.input_schema.schema() == {"title": "LangGraphInput"} - assert app.output_schema.schema() == {"title": "LangGraphOutput"} - assert await app.ainvoke(2) == 3 - - async def test_invoke_single_process_in_write_kwargs(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) chain = ( @@ -102,17 +140,25 @@ async def test_invoke_single_process_in_write_kwargs(mocker: MockerFixture) -> N ) app = Pregel( - nodes={"one": chain}, output_channels=["output", "fixed", "output_plus_one"] + nodes={"one": chain}, + channels={ + "input": LastValue(int), + "output": LastValue(int), + "fixed": LastValue(int), + "output_plus_one": LastValue(int), + }, + output_channels=["output", "fixed", "output_plus_one"], + input_channels="input", ) - assert app.input_schema.schema() == {"title": "LangGraphInput"} + assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} assert app.output_schema.schema() == { "title": "LangGraphOutput", "type": "object", "properties": { - "output": {"title": "Output"}, - "fixed": {"title": "Fixed"}, - "output_plus_one": {"title": "Output Plus One"}, + "output": {"title": "Output", "type": "integer"}, + "fixed": {"title": "Fixed", "type": "integer"}, + "output_plus_one": {"title": "Output Plus One", "type": "integer"}, }, } assert await app.ainvoke(2) == {"output": 3, "fixed": 5, "output_plus_one": 4} @@ -124,14 +170,16 @@ async def test_invoke_single_process_in_out_dict(mocker: MockerFixture) -> None: app = Pregel( nodes={"one": chain}, + channels={"input": LastValue(int), "output": LastValue(int)}, + input_channels="input", output_channels=["output"], ) - assert app.input_schema.schema() == {"title": "LangGraphInput"} + assert app.input_schema.schema() == {"title": "LangGraphInput", "type": "integer"} assert app.output_schema.schema() == { "title": "LangGraphOutput", "type": "object", - "properties": {"output": {"title": "Output"}}, + "properties": {"output": {"title": "Output", "type": "integer"}}, } assert await app.ainvoke(2) == {"output": 3} @@ -141,9 +189,8 @@ async def test_invoke_single_process_in_dict_out_dict(mocker: MockerFixture) -> chain = Channel.subscribe_to("input") | add_one | Channel.write_to("output") app = Pregel( - nodes={ - "one": chain, - }, + nodes={"one": chain}, + channels={"input": LastValue(int), "output": LastValue(int)}, input_channels=["input"], output_channels=["output"], ) @@ -151,12 +198,12 @@ async def test_invoke_single_process_in_dict_out_dict(mocker: MockerFixture) -> assert app.input_schema.schema() == { "title": "LangGraphInput", "type": "object", - "properties": {"input": {"title": "Input"}}, + "properties": {"input": {"title": "Input", "type": "integer"}}, } assert app.output_schema.schema() == { "title": "LangGraphOutput", "type": "object", - "properties": {"output": {"title": "Output"}}, + "properties": {"output": {"title": "Output", "type": "integer"}}, } assert await app.ainvoke({"input": 2}) == {"output": 3} @@ -166,7 +213,17 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: one = Channel.subscribe_to("input") | add_one | Channel.write_to("inbox") two = Channel.subscribe_to("inbox") | add_one | Channel.write_to("output") - app = Pregel(nodes={"one": one, "two": two}, stream_channels=["inbox", "output"]) + app = Pregel( + nodes={"one": one, "two": two}, + channels={ + "inbox": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", + stream_channels=["inbox", "output"], + ) assert await app.ainvoke(2) == 4 @@ -226,6 +283,13 @@ async def test_invoke_two_processes_in_out_interrupt( memory = MemorySaverAssertImmutable(at=checkpoint_at) app = Pregel( nodes={"one": one, "two": two}, + channels={ + "inbox": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", checkpointer=memory, interrupt_after_nodes=["one"], ) @@ -281,9 +345,14 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: app = Pregel( nodes={"one": one, "two": two}, - channels={"inbox": Topic(int)}, + channels={ + "inbox": Topic(int), + "output": LastValue(int), + "input": LastValue(int), + }, input_channels=["input", "inbox"], - stream_channels=["inbox", "output"], + stream_channels=["output", "inbox"], + output_channels=["output"], ) # [12 + 1, 2 + 1 + 1] @@ -310,6 +379,88 @@ async def test_invoke_two_processes_in_dict_out(mocker: MockerFixture) -> None: {"inbox": [3], "output": 13}, {"inbox": [], "output": 4}, ] + assert [ + c async for c in app.astream({"input": 2, "inbox": 12}, stream_mode="debug") + ] == [ + { + "type": "task", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "id": "7a3cc398-2e02-5023-ad7b-e4848d3b67fa", + "name": "one", + "input": 2, + "triggers": ["input"], + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "id": "34e90af0-f97e-54e0-a159-691da37f175f", + "name": "two", + "input": [12], + "triggers": ["inbox"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "id": "7a3cc398-2e02-5023-ad7b-e4848d3b67fa", + "result": [("inbox", 3)], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "id": "34e90af0-f97e-54e0-a159-691da37f175f", + "result": [("output", 13)], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": {"config": None, "values": {"output": 13, "inbox": [3]}}, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "cf7cf374-2a2a-556f-8561-91737af89d2f", + "name": "two", + "input": [3], + "triggers": ["inbox"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "cf7cf374-2a2a-556f-8561-91737af89d2f", + "result": [("output", 4)], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": {"config": None, "values": {"output": 4, "inbox": []}}, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": {"config": None, "values": {"output": 4, "inbox": []}}, + }, + ] async def test_batch_two_processes_in_out() -> None: @@ -322,7 +473,13 @@ async def test_batch_two_processes_in_out() -> None: app = Pregel( nodes={"one": one, "two": two}, - channels={"one": LastValue(int)}, + channels={ + "one": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", ) assert await app.abatch([3, 2, 1, 3, 5]) == [5, 4, 3, 5, 7] @@ -356,7 +513,13 @@ async def test_invoke_many_processes_in_out(mocker: MockerFixture) -> None: ) nodes["last"] = Channel.subscribe_to(str(i)) | add_one | Channel.write_to("output") - app = Pregel(nodes=nodes) + app = Pregel( + nodes=nodes, + channels={str(i): LastValue(int) for i in range(-1, test_size - 2)} + | {"input": LastValue(int), "output": LastValue(int)}, + input_channels="input", + output_channels="output", + ) # No state is left over from previous invocations for _ in range(10): @@ -379,7 +542,13 @@ async def test_batch_many_processes_in_out(mocker: MockerFixture) -> None: ) nodes["last"] = Channel.subscribe_to(str(i)) | add_one | Channel.write_to("output") - app = Pregel(nodes=nodes) + app = Pregel( + nodes=nodes, + channels={str(i): LastValue(int) for i in range(-1, test_size - 2)} + | {"input": LastValue(int), "output": LastValue(int)}, + input_channels="input", + output_channels="output", + ) # No state is left over from previous invocations for _ in range(3): @@ -409,7 +578,12 @@ async def test_invoke_two_processes_two_in_two_out_invalid( one = Channel.subscribe_to("input") | add_one | Channel.write_to("output") two = Channel.subscribe_to("input") | add_one | Channel.write_to("output") - app = Pregel(nodes={"one": one, "two": two}) + app = Pregel( + nodes={"one": one, "two": two}, + channels={"output": LastValue(int), "input": LastValue(int)}, + input_channels="input", + output_channels="output", + ) with pytest.raises(InvalidUpdateError): # LastValue channels can only be updated once per iteration @@ -424,7 +598,12 @@ async def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) app = Pregel( nodes={"one": one, "two": two}, - channels={"output": Topic(int)}, + channels={ + "input": LastValue(int), + "output": Topic(int), + }, + input_channels="input", + output_channels="output", ) # An Topic channel accumulates updates into a sequence @@ -455,7 +634,13 @@ async def test_invoke_checkpoint( app = Pregel( nodes={"one": one}, - channels={"total": BinaryOperatorAggregate(int, operator.add)}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", checkpointer=memory, ) @@ -510,7 +695,13 @@ async def test_invoke_checkpoint_aiosqlite( memory.at = checkpoint_at app = Pregel( nodes={"one": one}, - channels={"total": BinaryOperatorAggregate(int, operator.add)}, + channels={ + "total": BinaryOperatorAggregate(int, operator.add), + "input": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", checkpointer=memory, debug=True, ) @@ -605,7 +796,13 @@ async def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) - "chain_three": chain_three, "chain_four": chain_four, }, - channels={"inbox": Topic(int)}, + channels={ + "inbox": Topic(int), + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", ) # Then invoke app @@ -619,14 +816,20 @@ async def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) - ] -async def test_invoke_join_then_call_other_pubsub(mocker: MockerFixture) -> None: +async def test_invoke_join_then_call_other_pregel(mocker: MockerFixture) -> None: add_one = mocker.Mock(side_effect=lambda x: x + 1) add_10_each = mocker.Mock(side_effect=lambda x: [y + 10 for y in x]) inner_app = Pregel( nodes={ "one": Channel.subscribe_to("input") | add_one | Channel.write_to("output") - } + }, + channels={ + "output": LastValue(int), + "input": LastValue(int), + }, + input_channels="input", + output_channels="output", ) one = ( @@ -651,7 +854,11 @@ async def test_invoke_join_then_call_other_pubsub(mocker: MockerFixture) -> None channels={ "inbox_one": Topic(int), "outbox_one": LastValue(int), + "output": LastValue(int), + "input": LastValue(int), }, + input_channels="input", + output_channels="output", ) # Then invoke pubsub @@ -673,7 +880,17 @@ async def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> Non ) two = Channel.subscribe_to("between") | add_one | Channel.write_to("output") - app = Pregel(nodes={"one": one, "two": two}, stream_channels=["output", "between"]) + app = Pregel( + nodes={"one": one, "two": two}, + channels={ + "input": LastValue(int), + "between": LastValue(int), + "output": LastValue(int), + }, + stream_channels=["output", "between"], + input_channels="input", + output_channels="output", + ) # Then invoke pubsub assert [c async for c in app.astream(2)] == [ @@ -687,7 +904,16 @@ async def test_invoke_two_processes_no_out(mocker: MockerFixture) -> None: one = Channel.subscribe_to("input") | add_one | Channel.write_to("between") two = Channel.subscribe_to("between") | add_one - app = Pregel(nodes={"one": one, "two": two}) + app = Pregel( + nodes={"one": one, "two": two}, + channels={ + "input": LastValue(int), + "between": LastValue(int), + "output": LastValue(int), + }, + input_channels="input", + output_channels="output", + ) # It finishes executing (once no more messages being published) # but returns nothing, as nothing was published to "output" topic @@ -727,9 +953,12 @@ async def test_channel_enter_exit_timing(mocker: MockerFixture) -> None: app = Pregel( nodes={"one": one, "two": two}, channels={ + "input": LastValue(int), + "output": LastValue(int), "inbox": Topic(int), "ctx": Context(an_int, an_int_async, typ=int), }, + input_channels="input", output_channels=["inbox", "output"], stream_channels=["inbox", "output"], ) @@ -2798,6 +3027,250 @@ async def test_branch_then( async with AsyncSqliteSaver.from_conn_string(":memory:") as saver: saver.at = checkpoint_at + + # test stream_mode=debug + tool_two = tool_two_graph.compile(checkpointer=saver) + thread10 = {"configurable": {"thread_id": "10"}} + if checkpoint_at is CheckpointAt.END_OF_RUN: + assert [ + c + async for c in tool_two.astream( + {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": None, + "values": {"my_key": "value", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "e7879e70-6335-5867-9ec6-957fbb3da6fa", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "e7879e70-6335-5867-9ec6-957fbb3da6fa", + "result": [("my_key", " prepared")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": None, + "values": {"my_key": "value prepared", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "122f31bd-0e14-5b8f-91e7-4f241047a3fd", + "name": "tool_two_slow", + "input": {"my_key": "value prepared", "market": "DE"}, + "triggers": ["branch:prepare:condition:tool_two_slow"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "122f31bd-0e14-5b8f-91e7-4f241047a3fd", + "result": [("my_key", " slow")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "config": None, + "values": {"my_key": "value prepared slow", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "48a16051-2c14-5ff5-9cfe-e8c7c32d5c83", + "name": "finish", + "input": {"my_key": "value prepared slow", "market": "DE"}, + "triggers": ["branch:prepare:condition:then"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "48a16051-2c14-5ff5-9cfe-e8c7c32d5c83", + "result": [("my_key", " finished")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "config": None, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 4, + "payload": { + "config": { + "configurable": { + "thread_id": "10", + "thread_ts": AnyStr(), + } + }, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + }, + }, + ] + else: + assert [ + c + async for c in tool_two.astream( + {"my_key": "value", "market": "DE"}, thread10, stream_mode="debug" + ) + ] == [ + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 0, + "payload": { + "config": { + "configurable": {"thread_id": "10", "thread_ts": AnyStr()} + }, + "values": {"my_key": "value", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "e7879e70-6335-5867-9ec6-957fbb3da6fa", + "name": "prepare", + "input": {"my_key": "value", "market": "DE"}, + "triggers": ["start:prepare"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "id": "e7879e70-6335-5867-9ec6-957fbb3da6fa", + "result": [("my_key", " prepared")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 1, + "payload": { + "config": { + "configurable": {"thread_id": "10", "thread_ts": AnyStr()} + }, + "values": {"my_key": "value prepared", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "122f31bd-0e14-5b8f-91e7-4f241047a3fd", + "name": "tool_two_slow", + "input": {"my_key": "value prepared", "market": "DE"}, + "triggers": ["branch:prepare:condition:tool_two_slow"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "id": "122f31bd-0e14-5b8f-91e7-4f241047a3fd", + "result": [("my_key", " slow")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 2, + "payload": { + "config": { + "configurable": {"thread_id": "10", "thread_ts": AnyStr()} + }, + "values": {"my_key": "value prepared slow", "market": "DE"}, + }, + }, + { + "type": "task", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "48a16051-2c14-5ff5-9cfe-e8c7c32d5c83", + "name": "finish", + "input": {"my_key": "value prepared slow", "market": "DE"}, + "triggers": ["branch:prepare:condition:then"], + }, + }, + { + "type": "task_result", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "id": "48a16051-2c14-5ff5-9cfe-e8c7c32d5c83", + "result": [("my_key", " finished")], + }, + }, + { + "type": "checkpoint", + "timestamp": AnyStr(), + "step": 3, + "payload": { + "config": { + "configurable": {"thread_id": "10", "thread_ts": AnyStr()} + }, + "values": { + "my_key": "value prepared slow finished", + "market": "DE", + }, + }, + }, + ] + tool_two = tool_two_graph.compile( checkpointer=saver, interrupt_before=["tool_two_fast", "tool_two_slow"] ) @@ -3563,3 +4036,36 @@ async def test_nested_graph(snapshot: SnapshotAssertion) -> None: ] } assert times_called == 1 + + chain = app | RunnablePassthrough() + + assert await chain.ainvoke( + {"my_key": "my value", "never_called": never_called} + ) == { + "my_key": "my value there and back again", + "never_called": never_called, + } + assert [ + chunk + async for chunk in chain.astream( + {"my_key": "my value", "never_called": never_called} + ) + ] == [ + {"inner": {"my_key": "my value there"}}, + {"side": {"my_key": "my value there and back again"}}, + ] + times_called = 0 + async for event in chain.astream_events( + {"my_key": "my value", "never_called": never_called}, + version="v1", + config={"run_id": UUID(int=0)}, + ): + if event["event"] == "on_chain_end" and event["run_id"] == str(UUID(int=0)): + times_called += 1 + assert event["data"] == { + "output": [ + {"inner": {"my_key": "my value there"}}, + {"side": {"my_key": "my value there and back again"}}, + ] + } + assert times_called == 1