Spaces:
Sleeping
Sleeping
Denis Mbugua commited on
Commit ·
e02441f
1
Parent(s): cbec75c
init
Browse files- .gitignore +15 -0
- 1_lab1.ipynb +325 -0
- 2_lab2.ipynb +491 -0
- 3_lab3.ipynb +568 -0
- 4_lab4.ipynb +580 -0
- app.py +76 -0
- sandbox/dinner.md +24 -0
- sandbox/kenya_presidents_and_economic_changes.md +54 -0
- sidekick.py +249 -0
- sidekick_tools.py +55 -0
.gitignore
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# .gitignore
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.venv/
|
| 5 |
+
*.db # Important: Exclude database files like George.db, Warren.db, accounts.db
|
| 6 |
+
*.txt # Exclude temporary files
|
| 7 |
+
/memory/ # Exclude the entire memory directory if it's large/temporary
|
| 8 |
+
memory.db
|
| 9 |
+
memory.*
|
| 10 |
+
community_contributions/
|
| 11 |
+
|
| 12 |
+
# Ignore all sqlite DB files
|
| 13 |
+
*.db
|
| 14 |
+
*.db-wal
|
| 15 |
+
*.db-shm
|
1_lab1.ipynb
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"## Welcome back to Python Notebooks!\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"Didja miss me??\n",
|
| 10 |
+
"\n",
|
| 11 |
+
"### And welcome to Week 4, Day 2 - introducing LangGraph!"
|
| 12 |
+
]
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"cell_type": "code",
|
| 16 |
+
"execution_count": null,
|
| 17 |
+
"metadata": {},
|
| 18 |
+
"outputs": [],
|
| 19 |
+
"source": [
|
| 20 |
+
"from typing import Annotated\n",
|
| 21 |
+
"from langgraph.graph import StateGraph, START, END\n",
|
| 22 |
+
"from langgraph.graph.message import add_messages\n",
|
| 23 |
+
"from dotenv import load_dotenv\n",
|
| 24 |
+
"from IPython.display import Image, display\n",
|
| 25 |
+
"import gradio as gr\n",
|
| 26 |
+
"from langgraph.graph import StateGraph\n",
|
| 27 |
+
"from langgraph.graph.message import add_messages\n",
|
| 28 |
+
"from langchain_openai import ChatOpenAI\n",
|
| 29 |
+
"from pydantic import BaseModel\n",
|
| 30 |
+
"import random\n"
|
| 31 |
+
]
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"cell_type": "code",
|
| 35 |
+
"execution_count": null,
|
| 36 |
+
"metadata": {},
|
| 37 |
+
"outputs": [],
|
| 38 |
+
"source": [
|
| 39 |
+
"# Some useful constants\n",
|
| 40 |
+
"\n",
|
| 41 |
+
"nouns = [\"Cabbages\", \"Unicorns\", \"Toasters\", \"Penguins\", \"Bananas\", \"Zombies\", \"Rainbows\", \"Eels\", \"Pickles\", \"Muffins\"]\n",
|
| 42 |
+
"adjectives = [\"outrageous\", \"smelly\", \"pedantic\", \"existential\", \"moody\", \"sparkly\", \"untrustworthy\", \"sarcastic\", \"squishy\", \"haunted\"]"
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"cell_type": "code",
|
| 47 |
+
"execution_count": null,
|
| 48 |
+
"metadata": {},
|
| 49 |
+
"outputs": [],
|
| 50 |
+
"source": [
|
| 51 |
+
"# Our favorite first step! Crew was doing this for us, by the way.\n",
|
| 52 |
+
"load_dotenv(override=True)\n"
|
| 53 |
+
]
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"cell_type": "code",
|
| 57 |
+
"execution_count": null,
|
| 58 |
+
"metadata": {},
|
| 59 |
+
"outputs": [],
|
| 60 |
+
"source": [
|
| 61 |
+
"def shout(text: Annotated[str, \"something to be shouted\"]) -> str:\n",
|
| 62 |
+
" print(text.upper())\n",
|
| 63 |
+
" return text.upper()\n",
|
| 64 |
+
"\n",
|
| 65 |
+
"shout(\"hello\")"
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"cell_type": "markdown",
|
| 70 |
+
"metadata": {},
|
| 71 |
+
"source": [
|
| 72 |
+
"### Step 1: Define the State object\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"You can use any python object; but it's most common to use a TypedDict or a Pydantic BaseModel."
|
| 75 |
+
]
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"cell_type": "code",
|
| 79 |
+
"execution_count": null,
|
| 80 |
+
"metadata": {},
|
| 81 |
+
"outputs": [],
|
| 82 |
+
"source": [
|
| 83 |
+
"\n",
|
| 84 |
+
"class State(BaseModel):\n",
|
| 85 |
+
" \n",
|
| 86 |
+
" messages: Annotated[list, add_messages]\n"
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"cell_type": "markdown",
|
| 91 |
+
"metadata": {},
|
| 92 |
+
"source": [
|
| 93 |
+
"### Step 2: Start the Graph Builder with this State class"
|
| 94 |
+
]
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"cell_type": "code",
|
| 98 |
+
"execution_count": null,
|
| 99 |
+
"metadata": {},
|
| 100 |
+
"outputs": [],
|
| 101 |
+
"source": [
|
| 102 |
+
"graph_builder = StateGraph(State)"
|
| 103 |
+
]
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"cell_type": "markdown",
|
| 107 |
+
"metadata": {},
|
| 108 |
+
"source": [
|
| 109 |
+
"### Step 3: Create a Node\n",
|
| 110 |
+
"\n",
|
| 111 |
+
"A node can be any python function.\n",
|
| 112 |
+
"\n",
|
| 113 |
+
"The reducer that we set before gets automatically called to combine this response with previous responses\n"
|
| 114 |
+
]
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"cell_type": "code",
|
| 118 |
+
"execution_count": null,
|
| 119 |
+
"metadata": {},
|
| 120 |
+
"outputs": [],
|
| 121 |
+
"source": [
|
| 122 |
+
"def our_first_node(old_state: State) -> State:\n",
|
| 123 |
+
"\n",
|
| 124 |
+
" reply = f\"{random.choice(nouns)} are {random.choice(adjectives)}\"\n",
|
| 125 |
+
" messages = [{\"role\": \"assistant\", \"content\": reply}]\n",
|
| 126 |
+
"\n",
|
| 127 |
+
" new_state = State(messages=messages)\n",
|
| 128 |
+
"\n",
|
| 129 |
+
" return new_state\n",
|
| 130 |
+
"\n",
|
| 131 |
+
"graph_builder.add_node(\"first_node\", our_first_node)"
|
| 132 |
+
]
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"cell_type": "markdown",
|
| 136 |
+
"metadata": {},
|
| 137 |
+
"source": [
|
| 138 |
+
"### Step 4: Create Edges"
|
| 139 |
+
]
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"cell_type": "code",
|
| 143 |
+
"execution_count": null,
|
| 144 |
+
"metadata": {},
|
| 145 |
+
"outputs": [],
|
| 146 |
+
"source": [
|
| 147 |
+
"graph_builder.add_edge(START, \"first_node\")\n",
|
| 148 |
+
"graph_builder.add_edge(\"first_node\", END)"
|
| 149 |
+
]
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"cell_type": "markdown",
|
| 153 |
+
"metadata": {},
|
| 154 |
+
"source": [
|
| 155 |
+
"### Step 5: Compile the Graph"
|
| 156 |
+
]
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"cell_type": "code",
|
| 160 |
+
"execution_count": null,
|
| 161 |
+
"metadata": {},
|
| 162 |
+
"outputs": [],
|
| 163 |
+
"source": [
|
| 164 |
+
"graph = graph_builder.compile()"
|
| 165 |
+
]
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"cell_type": "code",
|
| 169 |
+
"execution_count": null,
|
| 170 |
+
"metadata": {},
|
| 171 |
+
"outputs": [],
|
| 172 |
+
"source": [
|
| 173 |
+
"display(Image(graph.get_graph().draw_mermaid_png()))"
|
| 174 |
+
]
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"cell_type": "markdown",
|
| 178 |
+
"metadata": {},
|
| 179 |
+
"source": [
|
| 180 |
+
"### That's it! Showtime!"
|
| 181 |
+
]
|
| 182 |
+
},
|
| 183 |
+
{
|
| 184 |
+
"cell_type": "code",
|
| 185 |
+
"execution_count": null,
|
| 186 |
+
"metadata": {},
|
| 187 |
+
"outputs": [],
|
| 188 |
+
"source": [
|
| 189 |
+
"def chat(user_input: str, history):\n",
|
| 190 |
+
" message = {\"role\": \"user\", \"content\": user_input}\n",
|
| 191 |
+
" messages = [message]\n",
|
| 192 |
+
" state = State(messages=messages)\n",
|
| 193 |
+
" result = graph.invoke(state)\n",
|
| 194 |
+
" print(result)\n",
|
| 195 |
+
" return result[\"messages\"][-1].content\n",
|
| 196 |
+
"\n",
|
| 197 |
+
"\n",
|
| 198 |
+
"gr.ChatInterface(chat, type=\"messages\").launch()"
|
| 199 |
+
]
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
"cell_type": "markdown",
|
| 203 |
+
"metadata": {},
|
| 204 |
+
"source": [
|
| 205 |
+
"### But why did I show you that?\n",
|
| 206 |
+
"\n",
|
| 207 |
+
"To make the point that LangGraph is all about python functions - it doesn't need to involve LLMs!!\n",
|
| 208 |
+
"\n",
|
| 209 |
+
"Now we'll do the 5 steps again, but in 1 shot:"
|
| 210 |
+
]
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"cell_type": "code",
|
| 214 |
+
"execution_count": null,
|
| 215 |
+
"metadata": {},
|
| 216 |
+
"outputs": [],
|
| 217 |
+
"source": [
|
| 218 |
+
"# Step 1: Define the State object\n",
|
| 219 |
+
"class State(BaseModel):\n",
|
| 220 |
+
" messages: Annotated[list, add_messages]\n"
|
| 221 |
+
]
|
| 222 |
+
},
|
| 223 |
+
{
|
| 224 |
+
"cell_type": "code",
|
| 225 |
+
"execution_count": null,
|
| 226 |
+
"metadata": {},
|
| 227 |
+
"outputs": [],
|
| 228 |
+
"source": [
|
| 229 |
+
"# Step 2: Start the Graph Builder with this State class\n",
|
| 230 |
+
"graph_builder = StateGraph(State)\n"
|
| 231 |
+
]
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
"cell_type": "code",
|
| 235 |
+
"execution_count": null,
|
| 236 |
+
"metadata": {},
|
| 237 |
+
"outputs": [],
|
| 238 |
+
"source": [
|
| 239 |
+
"# Step 3: Create a Node\n",
|
| 240 |
+
"\n",
|
| 241 |
+
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
| 242 |
+
"\n",
|
| 243 |
+
"def chatbot_node(old_state: State) -> State:\n",
|
| 244 |
+
" response = llm.invoke(old_state.messages)\n",
|
| 245 |
+
" new_state = State(messages=[response])\n",
|
| 246 |
+
" return new_state\n",
|
| 247 |
+
"\n",
|
| 248 |
+
"graph_builder.add_node(\"chatbot\", chatbot_node)"
|
| 249 |
+
]
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"cell_type": "code",
|
| 253 |
+
"execution_count": null,
|
| 254 |
+
"metadata": {},
|
| 255 |
+
"outputs": [],
|
| 256 |
+
"source": [
|
| 257 |
+
"# Step 4: Create Edges\n",
|
| 258 |
+
"graph_builder.add_edge(START, \"chatbot\")\n",
|
| 259 |
+
"graph_builder.add_edge(\"chatbot\", END)"
|
| 260 |
+
]
|
| 261 |
+
},
|
| 262 |
+
{
|
| 263 |
+
"cell_type": "code",
|
| 264 |
+
"execution_count": null,
|
| 265 |
+
"metadata": {},
|
| 266 |
+
"outputs": [],
|
| 267 |
+
"source": [
|
| 268 |
+
"# Step 5: Compile the Graph\n",
|
| 269 |
+
"graph = graph_builder.compile()\n",
|
| 270 |
+
"display(Image(graph.get_graph().draw_mermaid_png()))"
|
| 271 |
+
]
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"cell_type": "markdown",
|
| 275 |
+
"metadata": {},
|
| 276 |
+
"source": [
|
| 277 |
+
"### That's it! And, let's do this:"
|
| 278 |
+
]
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"cell_type": "code",
|
| 282 |
+
"execution_count": null,
|
| 283 |
+
"metadata": {},
|
| 284 |
+
"outputs": [],
|
| 285 |
+
"source": [
|
| 286 |
+
"def chat(user_input: str, history):\n",
|
| 287 |
+
" initial_state = State(messages=[{\"role\": \"user\", \"content\": user_input}])\n",
|
| 288 |
+
" result = graph.invoke(initial_state)\n",
|
| 289 |
+
" print(result)\n",
|
| 290 |
+
" return result['messages'][-1].content\n",
|
| 291 |
+
"\n",
|
| 292 |
+
"\n",
|
| 293 |
+
"gr.ChatInterface(chat, type=\"messages\").launch()"
|
| 294 |
+
]
|
| 295 |
+
},
|
| 296 |
+
{
|
| 297 |
+
"cell_type": "code",
|
| 298 |
+
"execution_count": null,
|
| 299 |
+
"metadata": {},
|
| 300 |
+
"outputs": [],
|
| 301 |
+
"source": []
|
| 302 |
+
}
|
| 303 |
+
],
|
| 304 |
+
"metadata": {
|
| 305 |
+
"kernelspec": {
|
| 306 |
+
"display_name": ".venv",
|
| 307 |
+
"language": "python",
|
| 308 |
+
"name": "python3"
|
| 309 |
+
},
|
| 310 |
+
"language_info": {
|
| 311 |
+
"codemirror_mode": {
|
| 312 |
+
"name": "ipython",
|
| 313 |
+
"version": 3
|
| 314 |
+
},
|
| 315 |
+
"file_extension": ".py",
|
| 316 |
+
"mimetype": "text/x-python",
|
| 317 |
+
"name": "python",
|
| 318 |
+
"nbconvert_exporter": "python",
|
| 319 |
+
"pygments_lexer": "ipython3",
|
| 320 |
+
"version": "3.12.3"
|
| 321 |
+
}
|
| 322 |
+
},
|
| 323 |
+
"nbformat": 4,
|
| 324 |
+
"nbformat_minor": 2
|
| 325 |
+
}
|
2_lab2.ipynb
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"### And welcome to Week 4, Day 3 - more LangGraph.."
|
| 8 |
+
]
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"cell_type": "code",
|
| 12 |
+
"execution_count": null,
|
| 13 |
+
"metadata": {},
|
| 14 |
+
"outputs": [],
|
| 15 |
+
"source": [
|
| 16 |
+
"from typing import Annotated\n",
|
| 17 |
+
"from langgraph.graph import StateGraph, START\n",
|
| 18 |
+
"from langgraph.graph.message import add_messages\n",
|
| 19 |
+
"from dotenv import load_dotenv\n",
|
| 20 |
+
"from IPython.display import Image, display\n",
|
| 21 |
+
"import gradio as gr\n",
|
| 22 |
+
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
| 23 |
+
"import requests\n",
|
| 24 |
+
"import os\n",
|
| 25 |
+
"from langchain_openai import ChatOpenAI\n",
|
| 26 |
+
"from typing import TypedDict\n"
|
| 27 |
+
]
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"cell_type": "code",
|
| 31 |
+
"execution_count": null,
|
| 32 |
+
"metadata": {},
|
| 33 |
+
"outputs": [],
|
| 34 |
+
"source": [
|
| 35 |
+
"# Our favorite first step! Crew was doing this for us, by the way.\n",
|
| 36 |
+
"load_dotenv(override=True)\n"
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"cell_type": "markdown",
|
| 41 |
+
"metadata": {},
|
| 42 |
+
"source": [
|
| 43 |
+
"### First, let's go set up LangSmith!\n",
|
| 44 |
+
"\n",
|
| 45 |
+
"https://smith.langchain.com/"
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"cell_type": "markdown",
|
| 50 |
+
"metadata": {},
|
| 51 |
+
"source": [
|
| 52 |
+
"### Next, here is a useful function in LangChain community:"
|
| 53 |
+
]
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"cell_type": "code",
|
| 57 |
+
"execution_count": null,
|
| 58 |
+
"metadata": {},
|
| 59 |
+
"outputs": [],
|
| 60 |
+
"source": [
|
| 61 |
+
"from langchain_community.utilities import GoogleSerperAPIWrapper\n",
|
| 62 |
+
"\n",
|
| 63 |
+
"serper = GoogleSerperAPIWrapper()\n",
|
| 64 |
+
"serper.run(\"What is the capital of France?\")"
|
| 65 |
+
]
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"cell_type": "markdown",
|
| 69 |
+
"metadata": {},
|
| 70 |
+
"source": [
|
| 71 |
+
"### Now here is a LangChain wrapper class for converting functions into Tools"
|
| 72 |
+
]
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"cell_type": "code",
|
| 76 |
+
"execution_count": null,
|
| 77 |
+
"metadata": {},
|
| 78 |
+
"outputs": [],
|
| 79 |
+
"source": [
|
| 80 |
+
"from langchain.agents import Tool\n",
|
| 81 |
+
"\n",
|
| 82 |
+
"tool_search =Tool(\n",
|
| 83 |
+
" name=\"search\",\n",
|
| 84 |
+
" func=serper.run,\n",
|
| 85 |
+
" description=\"Useful for when you need more information from an online search\"\n",
|
| 86 |
+
" )\n",
|
| 87 |
+
"\n"
|
| 88 |
+
]
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"cell_type": "markdown",
|
| 92 |
+
"metadata": {},
|
| 93 |
+
"source": [
|
| 94 |
+
"### Now we can try out the tool the langchain way"
|
| 95 |
+
]
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"cell_type": "code",
|
| 99 |
+
"execution_count": null,
|
| 100 |
+
"metadata": {},
|
| 101 |
+
"outputs": [],
|
| 102 |
+
"source": [
|
| 103 |
+
"tool_search.invoke(\"What is the capital of France?\")"
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"cell_type": "markdown",
|
| 108 |
+
"metadata": {},
|
| 109 |
+
"source": [
|
| 110 |
+
"### And now let's write a tool ourselves\n",
|
| 111 |
+
"\n",
|
| 112 |
+
"We'll pick a familiar one"
|
| 113 |
+
]
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"cell_type": "code",
|
| 117 |
+
"execution_count": null,
|
| 118 |
+
"metadata": {},
|
| 119 |
+
"outputs": [],
|
| 120 |
+
"source": [
|
| 121 |
+
"pushover_token = os.getenv(\"PUSHOVER_TOKEN\")\n",
|
| 122 |
+
"pushover_user = os.getenv(\"PUSHOVER_USER\")\n",
|
| 123 |
+
"pushover_url = \"https://api.pushover.net/1/messages.json\"\n",
|
| 124 |
+
"\n",
|
| 125 |
+
"def push(text: str):\n",
|
| 126 |
+
" \"\"\"Send a push notification to the user\"\"\"\n",
|
| 127 |
+
" requests.post(pushover_url, data = {\"token\": pushover_token, \"user\": pushover_user, \"message\": text})"
|
| 128 |
+
]
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"cell_type": "code",
|
| 132 |
+
"execution_count": null,
|
| 133 |
+
"metadata": {},
|
| 134 |
+
"outputs": [],
|
| 135 |
+
"source": [
|
| 136 |
+
"tool_push = Tool(\n",
|
| 137 |
+
" name=\"send_push_notification\",\n",
|
| 138 |
+
" func=push,\n",
|
| 139 |
+
" description=\"useful for when you want to send a push notification\"\n",
|
| 140 |
+
" )\n",
|
| 141 |
+
"\n",
|
| 142 |
+
"tool_push.invoke(\"Hello, me\")"
|
| 143 |
+
]
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"cell_type": "markdown",
|
| 147 |
+
"metadata": {},
|
| 148 |
+
"source": [
|
| 149 |
+
"### Back to the Graph from yesterday\n",
|
| 150 |
+
"\n",
|
| 151 |
+
"One small change - using TypedDict instead of BaseModel for the State object\n",
|
| 152 |
+
"\n",
|
| 153 |
+
"When we implement tools, we always need to make 2 changes to the code:\n",
|
| 154 |
+
"\n",
|
| 155 |
+
"1. Changes to provide the tools to OpenAI in json when we make the call\n",
|
| 156 |
+
"\n",
|
| 157 |
+
"2. Changes to handle the results back: look for the model staying that the finish_reason==\"tool_calls\" and then retrieve the call, run the function, provide the results."
|
| 158 |
+
]
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"cell_type": "markdown",
|
| 162 |
+
"metadata": {},
|
| 163 |
+
"source": [
|
| 164 |
+
"### Bring them together"
|
| 165 |
+
]
|
| 166 |
+
},
|
| 167 |
+
{
|
| 168 |
+
"cell_type": "code",
|
| 169 |
+
"execution_count": null,
|
| 170 |
+
"metadata": {},
|
| 171 |
+
"outputs": [],
|
| 172 |
+
"source": [
|
| 173 |
+
"tools = [tool_search, tool_push]"
|
| 174 |
+
]
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"cell_type": "code",
|
| 178 |
+
"execution_count": null,
|
| 179 |
+
"metadata": {},
|
| 180 |
+
"outputs": [],
|
| 181 |
+
"source": [
|
| 182 |
+
"# Step 1: Define the State object\n",
|
| 183 |
+
"class State(TypedDict):\n",
|
| 184 |
+
" messages: Annotated[list, add_messages]"
|
| 185 |
+
]
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"cell_type": "code",
|
| 189 |
+
"execution_count": null,
|
| 190 |
+
"metadata": {},
|
| 191 |
+
"outputs": [],
|
| 192 |
+
"source": [
|
| 193 |
+
"# Step 2: Start the Graph Builder with this State class\n",
|
| 194 |
+
"graph_builder = StateGraph(State)"
|
| 195 |
+
]
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"cell_type": "code",
|
| 199 |
+
"execution_count": null,
|
| 200 |
+
"metadata": {},
|
| 201 |
+
"outputs": [],
|
| 202 |
+
"source": [
|
| 203 |
+
"# This is different:\n",
|
| 204 |
+
"\n",
|
| 205 |
+
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
| 206 |
+
"llm_with_tools = llm.bind_tools(tools)"
|
| 207 |
+
]
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"cell_type": "code",
|
| 211 |
+
"execution_count": null,
|
| 212 |
+
"metadata": {},
|
| 213 |
+
"outputs": [],
|
| 214 |
+
"source": [
|
| 215 |
+
"# Step 3: Create a Node\n",
|
| 216 |
+
"\n",
|
| 217 |
+
"\n",
|
| 218 |
+
"def chatbot(state: State):\n",
|
| 219 |
+
" return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
|
| 220 |
+
"\n",
|
| 221 |
+
"graph_builder.add_node(\"chatbot\", chatbot)\n",
|
| 222 |
+
"graph_builder.add_node(\"tools\", ToolNode(tools=tools))"
|
| 223 |
+
]
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"cell_type": "code",
|
| 227 |
+
"execution_count": null,
|
| 228 |
+
"metadata": {},
|
| 229 |
+
"outputs": [],
|
| 230 |
+
"source": [
|
| 231 |
+
"# Step 4: Create Edges\n",
|
| 232 |
+
"\n",
|
| 233 |
+
"\n",
|
| 234 |
+
"graph_builder.add_conditional_edges( \"chatbot\", tools_condition, \"tools\")\n",
|
| 235 |
+
"\n",
|
| 236 |
+
"# Any time a tool is called, we return to the chatbot to decide the next step\n",
|
| 237 |
+
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
| 238 |
+
"graph_builder.add_edge(START, \"chatbot\")"
|
| 239 |
+
]
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"cell_type": "code",
|
| 243 |
+
"execution_count": null,
|
| 244 |
+
"metadata": {},
|
| 245 |
+
"outputs": [],
|
| 246 |
+
"source": [
|
| 247 |
+
"# Step 5: Compile the Graph\n",
|
| 248 |
+
"graph = graph_builder.compile()\n",
|
| 249 |
+
"display(Image(graph.get_graph().draw_mermaid_png()))"
|
| 250 |
+
]
|
| 251 |
+
},
|
| 252 |
+
{
|
| 253 |
+
"cell_type": "markdown",
|
| 254 |
+
"metadata": {},
|
| 255 |
+
"source": [
|
| 256 |
+
"### That's it! And, let's do this:"
|
| 257 |
+
]
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"cell_type": "code",
|
| 261 |
+
"execution_count": null,
|
| 262 |
+
"metadata": {},
|
| 263 |
+
"outputs": [],
|
| 264 |
+
"source": [
|
| 265 |
+
"def chat(user_input: str, history):\n",
|
| 266 |
+
" result = graph.invoke({\"messages\": [{\"role\": \"user\", \"content\": user_input}]})\n",
|
| 267 |
+
" return result[\"messages\"][-1].content\n",
|
| 268 |
+
"\n",
|
| 269 |
+
"\n",
|
| 270 |
+
"gr.ChatInterface(chat, type=\"messages\").launch()"
|
| 271 |
+
]
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"cell_type": "markdown",
|
| 275 |
+
"metadata": {},
|
| 276 |
+
"source": [
|
| 277 |
+
"## OK it's time to add Memory!\n",
|
| 278 |
+
"\n",
|
| 279 |
+
"### BUT WAIT!\n",
|
| 280 |
+
"\n",
|
| 281 |
+
"We have this whole Graph maintaining the state and appending to the state.\n",
|
| 282 |
+
"\n",
|
| 283 |
+
"Why isn't this handling memory?\n",
|
| 284 |
+
"\n",
|
| 285 |
+
"### This is a crucial point for understanding LangGraph\n",
|
| 286 |
+
"\n",
|
| 287 |
+
"> A super-step can be considered a single iteration over the graph nodes. Nodes that run in parallel are part of the same super-step, while nodes that run sequentially belong to separate super-steps.\n",
|
| 288 |
+
"\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"One \"Super-Step\" of the graph represents one invocation of passing messages between agents.\n",
|
| 291 |
+
"\n",
|
| 292 |
+
"In idomatic LangGraph, you call invoke to run your graph for each super-step; for each interaction.\n",
|
| 293 |
+
"\n",
|
| 294 |
+
"The reducer handles state updates automatically within one super-step, but not between them.\n",
|
| 295 |
+
"\n",
|
| 296 |
+
"That is what checkpointing achieves."
|
| 297 |
+
]
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"cell_type": "code",
|
| 301 |
+
"execution_count": null,
|
| 302 |
+
"metadata": {},
|
| 303 |
+
"outputs": [],
|
| 304 |
+
"source": [
|
| 305 |
+
"from langgraph.checkpoint.memory import MemorySaver\n",
|
| 306 |
+
"\n",
|
| 307 |
+
"memory = MemorySaver()"
|
| 308 |
+
]
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
"cell_type": "code",
|
| 312 |
+
"execution_count": null,
|
| 313 |
+
"metadata": {},
|
| 314 |
+
"outputs": [],
|
| 315 |
+
"source": [
|
| 316 |
+
"# Steps 1 and 2\n",
|
| 317 |
+
"graph_builder = StateGraph(State)\n",
|
| 318 |
+
"\n",
|
| 319 |
+
"\n",
|
| 320 |
+
"# Step 3\n",
|
| 321 |
+
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
| 322 |
+
"llm_with_tools = llm.bind_tools(tools)\n",
|
| 323 |
+
"\n",
|
| 324 |
+
"def chatbot(state: State):\n",
|
| 325 |
+
" print(state)\n",
|
| 326 |
+
" return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
|
| 327 |
+
"\n",
|
| 328 |
+
"graph_builder.add_node(\"chatbot\", chatbot)\n",
|
| 329 |
+
"graph_builder.add_node(\"tools\", ToolNode(tools=tools))\n",
|
| 330 |
+
"\n",
|
| 331 |
+
"# Step 4\n",
|
| 332 |
+
"graph_builder.add_conditional_edges( \"chatbot\", tools_condition, \"tools\")\n",
|
| 333 |
+
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
| 334 |
+
"graph_builder.add_edge(START, \"chatbot\")\n",
|
| 335 |
+
"\n",
|
| 336 |
+
"# Step 5\n",
|
| 337 |
+
"graph = graph_builder.compile(checkpointer=memory)\n",
|
| 338 |
+
"display(Image(graph.get_graph().draw_mermaid_png()))"
|
| 339 |
+
]
|
| 340 |
+
},
|
| 341 |
+
{
|
| 342 |
+
"cell_type": "code",
|
| 343 |
+
"execution_count": null,
|
| 344 |
+
"metadata": {},
|
| 345 |
+
"outputs": [],
|
| 346 |
+
"source": [
|
| 347 |
+
"config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
|
| 348 |
+
"\n",
|
| 349 |
+
"def chat(user_input: str, history):\n",
|
| 350 |
+
" result = graph.invoke({\"messages\": [{\"role\": \"user\", \"content\": user_input}]}, config=config)\n",
|
| 351 |
+
" return result[\"messages\"][-1].content\n",
|
| 352 |
+
"\n",
|
| 353 |
+
"\n",
|
| 354 |
+
"gr.ChatInterface(chat, type=\"messages\").launch()"
|
| 355 |
+
]
|
| 356 |
+
},
|
| 357 |
+
{
|
| 358 |
+
"cell_type": "code",
|
| 359 |
+
"execution_count": null,
|
| 360 |
+
"metadata": {},
|
| 361 |
+
"outputs": [],
|
| 362 |
+
"source": [
|
| 363 |
+
"graph.get_state(config)"
|
| 364 |
+
]
|
| 365 |
+
},
|
| 366 |
+
{
|
| 367 |
+
"cell_type": "code",
|
| 368 |
+
"execution_count": null,
|
| 369 |
+
"metadata": {},
|
| 370 |
+
"outputs": [],
|
| 371 |
+
"source": [
|
| 372 |
+
"# Most recent first\n",
|
| 373 |
+
"\n",
|
| 374 |
+
"list(graph.get_state_history(config))"
|
| 375 |
+
]
|
| 376 |
+
},
|
| 377 |
+
{
|
| 378 |
+
"cell_type": "markdown",
|
| 379 |
+
"metadata": {},
|
| 380 |
+
"source": [
|
| 381 |
+
"### LangGraph gives you tools to set the state back to a prior point in time, to branch off:\n",
|
| 382 |
+
"\n",
|
| 383 |
+
"```\n",
|
| 384 |
+
"config = {\"configurable\": {\"thread_id\": \"1\", \"checkpoint_id\": ...}}\n",
|
| 385 |
+
"graph.invoke(None, config=config)\n",
|
| 386 |
+
"```\n",
|
| 387 |
+
"\n",
|
| 388 |
+
"And this allows you to build stable systems that can be recovered and rerun from any prior checkpoint."
|
| 389 |
+
]
|
| 390 |
+
},
|
| 391 |
+
{
|
| 392 |
+
"cell_type": "markdown",
|
| 393 |
+
"metadata": {},
|
| 394 |
+
"source": [
|
| 395 |
+
"### And now let's store in SQL\n",
|
| 396 |
+
"\n",
|
| 397 |
+
"### And this is the power of LangGraph."
|
| 398 |
+
]
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
"cell_type": "code",
|
| 402 |
+
"execution_count": null,
|
| 403 |
+
"metadata": {},
|
| 404 |
+
"outputs": [],
|
| 405 |
+
"source": [
|
| 406 |
+
"import sqlite3\n",
|
| 407 |
+
"from langgraph.checkpoint.sqlite import SqliteSaver\n",
|
| 408 |
+
"\n",
|
| 409 |
+
"db_path = \"memory.db\"\n",
|
| 410 |
+
"conn = sqlite3.connect(db_path, check_same_thread=False)\n",
|
| 411 |
+
"sql_memory = SqliteSaver(conn)"
|
| 412 |
+
]
|
| 413 |
+
},
|
| 414 |
+
{
|
| 415 |
+
"cell_type": "code",
|
| 416 |
+
"execution_count": null,
|
| 417 |
+
"metadata": {},
|
| 418 |
+
"outputs": [],
|
| 419 |
+
"source": [
|
| 420 |
+
"# Steps 1 and 2\n",
|
| 421 |
+
"graph_builder = StateGraph(State)\n",
|
| 422 |
+
"\n",
|
| 423 |
+
"\n",
|
| 424 |
+
"# Step 3\n",
|
| 425 |
+
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
| 426 |
+
"llm_with_tools = llm.bind_tools(tools)\n",
|
| 427 |
+
"\n",
|
| 428 |
+
"def chatbot(state: State):\n",
|
| 429 |
+
" print(state)\n",
|
| 430 |
+
" return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n",
|
| 431 |
+
"\n",
|
| 432 |
+
"graph_builder.add_node(\"chatbot\", chatbot)\n",
|
| 433 |
+
"graph_builder.add_node(\"tools\", ToolNode(tools=tools))\n",
|
| 434 |
+
"\n",
|
| 435 |
+
"# Step 4\n",
|
| 436 |
+
"graph_builder.add_conditional_edges( \"chatbot\", tools_condition, \"tools\")\n",
|
| 437 |
+
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
| 438 |
+
"graph_builder.add_edge(START, \"chatbot\")\n",
|
| 439 |
+
"\n",
|
| 440 |
+
"# Step 5\n",
|
| 441 |
+
"graph = graph_builder.compile(checkpointer=sql_memory)\n",
|
| 442 |
+
"display(Image(graph.get_graph().draw_mermaid_png()))\n",
|
| 443 |
+
" "
|
| 444 |
+
]
|
| 445 |
+
},
|
| 446 |
+
{
|
| 447 |
+
"cell_type": "code",
|
| 448 |
+
"execution_count": null,
|
| 449 |
+
"metadata": {},
|
| 450 |
+
"outputs": [],
|
| 451 |
+
"source": [
|
| 452 |
+
"config = {\"configurable\": {\"thread_id\": \"3\"}}\n",
|
| 453 |
+
"\n",
|
| 454 |
+
"def chat(user_input: str, history):\n",
|
| 455 |
+
" result = graph.invoke({\"messages\": [{\"role\": \"user\", \"content\": user_input}]}, config=config)\n",
|
| 456 |
+
" return result[\"messages\"][-1].content\n",
|
| 457 |
+
"\n",
|
| 458 |
+
"\n",
|
| 459 |
+
"gr.ChatInterface(chat, type=\"messages\").launch()"
|
| 460 |
+
]
|
| 461 |
+
},
|
| 462 |
+
{
|
| 463 |
+
"cell_type": "code",
|
| 464 |
+
"execution_count": null,
|
| 465 |
+
"metadata": {},
|
| 466 |
+
"outputs": [],
|
| 467 |
+
"source": []
|
| 468 |
+
}
|
| 469 |
+
],
|
| 470 |
+
"metadata": {
|
| 471 |
+
"kernelspec": {
|
| 472 |
+
"display_name": ".venv",
|
| 473 |
+
"language": "python",
|
| 474 |
+
"name": "python3"
|
| 475 |
+
},
|
| 476 |
+
"language_info": {
|
| 477 |
+
"codemirror_mode": {
|
| 478 |
+
"name": "ipython",
|
| 479 |
+
"version": 3
|
| 480 |
+
},
|
| 481 |
+
"file_extension": ".py",
|
| 482 |
+
"mimetype": "text/x-python",
|
| 483 |
+
"name": "python",
|
| 484 |
+
"nbconvert_exporter": "python",
|
| 485 |
+
"pygments_lexer": "ipython3",
|
| 486 |
+
"version": "3.12.3"
|
| 487 |
+
}
|
| 488 |
+
},
|
| 489 |
+
"nbformat": 4,
|
| 490 |
+
"nbformat_minor": 2
|
| 491 |
+
}
|
3_lab3.ipynb
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"## Welcome to Week 4, Day 4\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"This is the start of an AWESOME project! Really simple and very effective."
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"cell_type": "code",
|
| 14 |
+
"execution_count": 1,
|
| 15 |
+
"metadata": {},
|
| 16 |
+
"outputs": [],
|
| 17 |
+
"source": [
|
| 18 |
+
"from typing import Annotated\n",
|
| 19 |
+
"from typing_extensions import TypedDict\n",
|
| 20 |
+
"from langgraph.graph import StateGraph, START\n",
|
| 21 |
+
"from langgraph.graph.message import add_messages\n",
|
| 22 |
+
"from dotenv import load_dotenv\n",
|
| 23 |
+
"from IPython.display import Image, display\n",
|
| 24 |
+
"import gradio as gr\n",
|
| 25 |
+
"from langgraph.prebuilt import ToolNode, tools_condition\n",
|
| 26 |
+
"import requests\n",
|
| 27 |
+
"import os\n",
|
| 28 |
+
"from langchain.agents import Tool\n",
|
| 29 |
+
"\n",
|
| 30 |
+
"from langchain_openai import ChatOpenAI\n",
|
| 31 |
+
"from langgraph.checkpoint.memory import MemorySaver"
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"cell_type": "code",
|
| 36 |
+
"execution_count": 2,
|
| 37 |
+
"metadata": {},
|
| 38 |
+
"outputs": [
|
| 39 |
+
{
|
| 40 |
+
"data": {
|
| 41 |
+
"text/plain": [
|
| 42 |
+
"True"
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
"execution_count": 2,
|
| 46 |
+
"metadata": {},
|
| 47 |
+
"output_type": "execute_result"
|
| 48 |
+
}
|
| 49 |
+
],
|
| 50 |
+
"source": [
|
| 51 |
+
"load_dotenv(override=True)"
|
| 52 |
+
]
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"cell_type": "markdown",
|
| 56 |
+
"metadata": {},
|
| 57 |
+
"source": [
|
| 58 |
+
"### Asynchronous LangGraph\n",
|
| 59 |
+
"\n",
|
| 60 |
+
"To run a tool: \n",
|
| 61 |
+
"Sync: `tool.run(inputs)` \n",
|
| 62 |
+
"Async: `await tool.arun(inputs)`\n",
|
| 63 |
+
"\n",
|
| 64 |
+
"To invoke the graph: \n",
|
| 65 |
+
"Sync: `graph.invoke(state)` \n",
|
| 66 |
+
"Async: `await graph.ainvoke(state)`"
|
| 67 |
+
]
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"cell_type": "code",
|
| 71 |
+
"execution_count": 3,
|
| 72 |
+
"metadata": {},
|
| 73 |
+
"outputs": [],
|
| 74 |
+
"source": [
|
| 75 |
+
"class State(TypedDict):\n",
|
| 76 |
+
" \n",
|
| 77 |
+
" messages: Annotated[list, add_messages]\n",
|
| 78 |
+
"\n",
|
| 79 |
+
"\n",
|
| 80 |
+
"graph_builder = StateGraph(State)"
|
| 81 |
+
]
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"cell_type": "code",
|
| 85 |
+
"execution_count": 4,
|
| 86 |
+
"metadata": {},
|
| 87 |
+
"outputs": [],
|
| 88 |
+
"source": [
|
| 89 |
+
"pushover_token = os.getenv(\"PUSHOVER_TOKEN\")\n",
|
| 90 |
+
"pushover_user = os.getenv(\"PUSHOVER_USER\")\n",
|
| 91 |
+
"pushover_url = \"https://api.pushover.net/1/messages.json\"\n",
|
| 92 |
+
"\n",
|
| 93 |
+
"def push(text: str):\n",
|
| 94 |
+
" \"\"\"Send a push notification to the user\"\"\"\n",
|
| 95 |
+
" requests.post(pushover_url, data = {\"token\": pushover_token, \"user\": pushover_user, \"message\": text})\n",
|
| 96 |
+
"\n",
|
| 97 |
+
"tool_push = Tool(\n",
|
| 98 |
+
" name=\"send_push_notification\",\n",
|
| 99 |
+
" func=push,\n",
|
| 100 |
+
" description=\"useful for when you want to send a push notification\"\n",
|
| 101 |
+
" )"
|
| 102 |
+
]
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
"cell_type": "markdown",
|
| 106 |
+
"metadata": {},
|
| 107 |
+
"source": [
|
| 108 |
+
"## Extra installation step - if you don't have Node and Playwright on your computer\n",
|
| 109 |
+
"\n",
|
| 110 |
+
"Next, you need to install NodeJS and Playwright on your computer if you don't already have them. Please see instructions here:\n",
|
| 111 |
+
"\n",
|
| 112 |
+
"[Node and Playwright setup](../setup/SETUP-node.md)"
|
| 113 |
+
]
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"cell_type": "markdown",
|
| 117 |
+
"metadata": {},
|
| 118 |
+
"source": [
|
| 119 |
+
"## And now - after Installing Playwright, a heads up for Windows PC Users:\n",
|
| 120 |
+
"\n",
|
| 121 |
+
"While executing the next few cells, you might hit a problem with the Playwright browser raising a NotImplementedError.\n",
|
| 122 |
+
"\n",
|
| 123 |
+
"This should work when we move to python modules, but it can cause problems in Windows in a notebook.\n",
|
| 124 |
+
"\n",
|
| 125 |
+
"If you it this error and would like to run the notebook, you need to make a small change which seems quite hacky! You need to do this AFTER installing Playwright (prior cells)\n",
|
| 126 |
+
"\n",
|
| 127 |
+
"1. Right click in `.venv` in the File Explorer on the left and select \"Find in folder\"\n",
|
| 128 |
+
"2. Search for `asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())` \n",
|
| 129 |
+
"3. That code should be found in a line of code in a file called `kernelapp.py`\n",
|
| 130 |
+
"4. Comment out the entire else clause that this line is a part of - see the fragment below. Be sure to have the \"pass\" statement after the ImportError line.\n",
|
| 131 |
+
"5. Restart the kernel by pressing the \"Restart\" button above\n",
|
| 132 |
+
"\n",
|
| 133 |
+
"```python\n",
|
| 134 |
+
" if sys.platform.startswith(\"win\") and sys.version_info >= (3, 8):\n",
|
| 135 |
+
" import asyncio\n",
|
| 136 |
+
" \n",
|
| 137 |
+
" try:\n",
|
| 138 |
+
" from asyncio import WindowsProactorEventLoopPolicy, WindowsSelectorEventLoopPolicy\n",
|
| 139 |
+
" except ImportError:\n",
|
| 140 |
+
" pass\n",
|
| 141 |
+
" # not affected\n",
|
| 142 |
+
" # else:\n",
|
| 143 |
+
" # if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy:\n",
|
| 144 |
+
" # WindowsProactorEventLoopPolicy is not compatible with tornado 6\n",
|
| 145 |
+
" # fallback to the pre-3.8 default of Selector\n",
|
| 146 |
+
" # asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())\n",
|
| 147 |
+
"```\n",
|
| 148 |
+
"\n",
|
| 149 |
+
"Thank you to student Nicolas for finding this, and to Kalyan, Yaki, Zibin and Bhaskar for confirming that this worked for them! And to Vladislav for the extra pointers.\n",
|
| 150 |
+
"\n",
|
| 151 |
+
"As an alternative, you can just move to a Python module (which we do anyway in Day 5)"
|
| 152 |
+
]
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"cell_type": "code",
|
| 156 |
+
"execution_count": 7,
|
| 157 |
+
"metadata": {},
|
| 158 |
+
"outputs": [],
|
| 159 |
+
"source": [
|
| 160 |
+
"# Introducing nest_asyncio\n",
|
| 161 |
+
"# Python async code only allows for one \"event loop\" processing aynchronous events.\n",
|
| 162 |
+
"# The `nest_asyncio` library patches this, and is used for special situations, if you need to run a nested event loop.\n",
|
| 163 |
+
"\n",
|
| 164 |
+
"import nest_asyncio\n",
|
| 165 |
+
"nest_asyncio.apply()"
|
| 166 |
+
]
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"cell_type": "markdown",
|
| 170 |
+
"metadata": {},
|
| 171 |
+
"source": [
|
| 172 |
+
"### The LangChain community\n",
|
| 173 |
+
"\n",
|
| 174 |
+
"One of the remarkable things about LangChain is the rich community around it.\n",
|
| 175 |
+
"\n",
|
| 176 |
+
"Check this out:\n"
|
| 177 |
+
]
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"cell_type": "code",
|
| 181 |
+
"execution_count": 8,
|
| 182 |
+
"metadata": {},
|
| 183 |
+
"outputs": [],
|
| 184 |
+
"source": [
|
| 185 |
+
"from langchain_community.agent_toolkits import PlayWrightBrowserToolkit\n",
|
| 186 |
+
"from langchain_community.tools.playwright.utils import create_async_playwright_browser\n",
|
| 187 |
+
"\n",
|
| 188 |
+
"# If you get a NotImplementedError here or later, see the Heads Up at the top of the notebook\n",
|
| 189 |
+
"\n",
|
| 190 |
+
"async_browser = create_async_playwright_browser(headless=False) # headful mode\n",
|
| 191 |
+
"toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=async_browser)\n",
|
| 192 |
+
"tools = toolkit.get_tools()"
|
| 193 |
+
]
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"cell_type": "code",
|
| 197 |
+
"execution_count": 9,
|
| 198 |
+
"metadata": {},
|
| 199 |
+
"outputs": [
|
| 200 |
+
{
|
| 201 |
+
"name": "stdout",
|
| 202 |
+
"output_type": "stream",
|
| 203 |
+
"text": [
|
| 204 |
+
"click_element=async_browser=<Browser type=<BrowserType name=chromium executable_path=/home/dynamodenis/.cache/ms-playwright/chromium-1169/chrome-linux/chrome> version=136.0.7103.25>\n",
|
| 205 |
+
"navigate_browser=async_browser=<Browser type=<BrowserType name=chromium executable_path=/home/dynamodenis/.cache/ms-playwright/chromium-1169/chrome-linux/chrome> version=136.0.7103.25>\n",
|
| 206 |
+
"previous_webpage=async_browser=<Browser type=<BrowserType name=chromium executable_path=/home/dynamodenis/.cache/ms-playwright/chromium-1169/chrome-linux/chrome> version=136.0.7103.25>\n",
|
| 207 |
+
"extract_text=async_browser=<Browser type=<BrowserType name=chromium executable_path=/home/dynamodenis/.cache/ms-playwright/chromium-1169/chrome-linux/chrome> version=136.0.7103.25>\n",
|
| 208 |
+
"extract_hyperlinks=async_browser=<Browser type=<BrowserType name=chromium executable_path=/home/dynamodenis/.cache/ms-playwright/chromium-1169/chrome-linux/chrome> version=136.0.7103.25>\n",
|
| 209 |
+
"get_elements=async_browser=<Browser type=<BrowserType name=chromium executable_path=/home/dynamodenis/.cache/ms-playwright/chromium-1169/chrome-linux/chrome> version=136.0.7103.25>\n",
|
| 210 |
+
"current_webpage=async_browser=<Browser type=<BrowserType name=chromium executable_path=/home/dynamodenis/.cache/ms-playwright/chromium-1169/chrome-linux/chrome> version=136.0.7103.25>\n"
|
| 211 |
+
]
|
| 212 |
+
}
|
| 213 |
+
],
|
| 214 |
+
"source": [
|
| 215 |
+
"for tool in tools:\n",
|
| 216 |
+
" print(f\"{tool.name}={tool}\")"
|
| 217 |
+
]
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"cell_type": "code",
|
| 221 |
+
"execution_count": 12,
|
| 222 |
+
"metadata": {},
|
| 223 |
+
"outputs": [],
|
| 224 |
+
"source": [
|
| 225 |
+
"tool_dict = {tool.name:tool for tool in tools}\n",
|
| 226 |
+
"\n",
|
| 227 |
+
"navigate_tool = tool_dict.get(\"navigate_browser\")\n",
|
| 228 |
+
"extract_text_tool = tool_dict.get(\"extract_text\")\n",
|
| 229 |
+
"\n",
|
| 230 |
+
" \n",
|
| 231 |
+
"await navigate_tool.arun({\"url\": \"https://www.cnn.com\"})\n",
|
| 232 |
+
"text = await extract_text_tool.arun({})"
|
| 233 |
+
]
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"cell_type": "code",
|
| 237 |
+
"execution_count": 13,
|
| 238 |
+
"metadata": {},
|
| 239 |
+
"outputs": [
|
| 240 |
+
{
|
| 241 |
+
"name": "stdout",
|
| 242 |
+
"output_type": "stream",
|
| 243 |
+
"text": [
|
| 244 |
+
"Breaking News, Latest News and Videos | CNN CNN values your feedback\n",
|
| 245 |
+
"1. How relevant is this ad to you? 2. Did you encounter any technical\n",
|
| 246 |
+
"issues? No Video player was slow to load content Video content never\n",
|
| 247 |
+
"loaded Ad froze or did not finish loading Video content did not start\n",
|
| 248 |
+
"after ad Audio on ad was too loud Other issues Ad never loaded Ad\n",
|
| 249 |
+
"prevented/slowed the page from loading Content moved around while ad\n",
|
| 250 |
+
"loaded Ad was repetitive to ads I've seen previously Other issues\n",
|
| 251 |
+
"Cancel Submit Thank You! Your effort and contribution in providing\n",
|
| 252 |
+
"this feedback is much\n",
|
| 253 |
+
"appreciated. Close Ad Feedback Close icon US World Politics Business\n",
|
| 254 |
+
"Health Entertainment Style Travel Sports Science Climate Weather\n",
|
| 255 |
+
"Ukraine-Russia War Israel-Hamas War Games More US World Politics\n",
|
| 256 |
+
"Business Health Entertainment Style Travel Sports Science Climate\n",
|
| 257 |
+
"Weather Ukraine-Russia War Israel-Hamas War Games Watch Listen Live TV\n",
|
| 258 |
+
"Subscribe Sign in My Account Settings Newsletters Topics you follow\n",
|
| 259 |
+
"Sign out Your CNN account Sign in to your CNN account Sign in My\n",
|
| 260 |
+
"Account Settings Newsletters Topics you follow Sign out Your CNN\n",
|
| 261 |
+
"account Sign in to your CNN account Live TV Listen Watch Edition US\n",
|
| 262 |
+
"International Arabic Español Edition US International Arabic Español\n",
|
| 263 |
+
"World Africa Americas Asia Australia China Europe India Middle East\n",
|
| 264 |
+
"United Kingdom US Politics Trump Facts First CNN Polls 2025 Elections\n",
|
| 265 |
+
"Business Tech Media Calculators Videos Markets Pre-markets After-Hours\n",
|
| 266 |
+
"Fear & Greed Investing Markets Now Nightcap Health Life, But Better\n",
|
| 267 |
+
"Fitness Food Sleep Mindfulness Relationships Entertainment Movies\n",
|
| 268 |
+
"Television Celebrity Tech Innovate Foreseeable Future Mission: Ahead\n",
|
| 269 |
+
"Work Transformed Innovative Cities Style Arts Design Fashion\n",
|
| 270 |
+
"Architecture Luxury Beauty Video Travel Destinations Food & Drink Stay\n",
|
| 271 |
+
"News Videos Sports Football Tennis Golf Motorsport US Sports Olympics\n",
|
| 272 |
+
"Climbing Esports Hockey Science Space Life Unearthed Climate Solutions\n",
|
| 273 |
+
"Weather Weather Video Climate Ukraine-Russia War Israel-Hamas War\n",
|
| 274 |
+
"Features As Equals Call to Earth Freedom Project Impact Your World\n",
|
| 275 |
+
"Inside Africa CNN Heroes Watch Live TV CNN Fast Shows A-Z CNN10 CNN\n",
|
| 276 |
+
"Max CNN TV Schedules Listen CNN 5 Things Chasing Life with Dr. Sanjay\n",
|
| 277 |
+
"Gupta The Assignment with Audie Cornish One Thing Tug of War CNN\n",
|
| 278 |
+
"Political Briefing The Axe Files All There Is with Anderson Cooper All\n",
|
| 279 |
+
"CNN Audio podcasts Games Daily Crossword Jumble Crossword Photo\n",
|
| 280 |
+
"Shuffle Sudoblock Sudoku 5 Things Quiz About CNN Photos Investigations\n",
|
| 281 |
+
"CNN Profiles CNN Leadership CNN Newsletters Work for CNN Follow CNN\n",
|
| 282 |
+
"Shein in France Beckham knighted Stabbings in Britain Orcas on the\n",
|
| 283 |
+
"hunt Identifying dinosaurs’ sex Tattoos of ancient symbols Peanut\n",
|
| 284 |
+
"allergies Angelina Katsanis/AFP/Getty Images The inside story of\n",
|
| 285 |
+
"Zohran Mamdani’s triumph and what happens next for New York Video\n",
|
| 286 |
+
"Mamdani addresses Trump in victory speech 2:55 New York’s South Asians\n",
|
| 287 |
+
"celebrate the ascension of one of their own Samuel Corum/Getty Images\n",
|
| 288 |
+
"Last shutdown, Trump hunkered down at the White House ‘all alone.’\n",
|
| 289 |
+
"This time, Mar-a-Lago beckons Government shutdown is now the longest -\n",
|
| 290 |
+
"and likely the most damaging in US history Democrats sweep major races\n",
|
| 291 |
+
"as voters send Trump a message Show all Reuters/AP/Getty Images\n",
|
| 292 |
+
"Candidates across the party’s ideological spectrum were elected in a\n",
|
| 293 |
+
"vivid show of discontent with the president. Here are 5 takeaways.\n",
|
| 294 |
+
"Live Updates California ‘stood tall’ and ‘stood firm’ in redistricting\n",
|
| 295 |
+
"fight, governor says Spanberger and Sherrill were roommates on Capitol\n",
|
| 296 |
+
"Hill. They’re now making history in their states The historic firsts\n",
|
| 297 |
+
"of Tuesday’s elections Video New Yorkers share who they voted for\n",
|
| 298 |
+
"mayor on Election Day 1:08 Play Subtitles Off Caption Styling Settings\n",
|
| 299 |
+
"Catch up on today's global news • Source: CNN Video Catch up on\n",
|
| 300 |
+
"today’s global news picture alliance/CHROMORANGE/SIPA The best\n",
|
| 301 |
+
"Christmas markets of 2025 • Video 4:39 CNN Video Can you control your\n",
|
| 302 |
+
"dreams? This CNN producer took classes to do it 4:39 Black&Steil Bored\n",
|
| 303 |
+
"of her desk job, she started designing lights. Now they sell for\n",
|
| 304 |
+
"$90,000 Allen J. Schaben/Los Angeles Times/Getty Images How to see a\n",
|
| 305 |
+
"supermoon and meteors in the night sky this week Ad Feedback More top\n",
|
| 306 |
+
"stories • Breaking News Breaking News Philippe Lopez/AFP/Getty Images\n",
|
| 307 |
+
"Breaking News Car ramming injures 10 people on French island, interior\n",
|
| 308 |
+
"minister says Live Updates At least 7 killed, 11 injured after a UPS\n",
|
| 309 |
+
"plane crashes near the Louisville airport Fearing US effort to remove\n",
|
| 310 |
+
"him, Maduro urges Venezuelans to snitch via app 11 killed in fire at\n",
|
| 311 |
+
"Bosnian retirement home Video ‘You look cute’: Obama jokes with a\n",
|
| 312 |
+
"supporter 0:31 Closing arguments are set for today in lawsuit filed by\n",
|
| 313 |
+
"Virginia teacher shot by 6-year-old student Dick Cheney • Analysis\n",
|
| 314 |
+
"Analysis Bill Haber/AP • Analysis Analysis Analysis Dick Cheney helped\n",
|
| 315 |
+
"design the ‘war on terror.’ It opened a Pandora’s box that still\n",
|
| 316 |
+
"hasn’t been closed CNN Video Why Dick Cheney accepted ‘Darth Vader’\n",
|
| 317 |
+
"reputation 1:32 Nov 4, 2025 Bill Clark/CQ-Roll Call, Inc./Getty Images\n",
|
| 318 |
+
"How Dick Cheney helped his daughter stand up to Trump Nov 4, 2025 Ad\n",
|
| 319 |
+
"Feedback CLIMATE & EXTREME WEATHER Jacqueline Hernandez/AP Typhoon\n",
|
| 320 |
+
"Kalmaegi kills at least 66 in the Philippines How the US could shape\n",
|
| 321 |
+
"the COP30 climate summit without even being there Ad Feedback ART &\n",
|
| 322 |
+
"CULTURE © Bryan Sansivero, excerpted from America the Abandoned A\n",
|
| 323 |
+
"portal into another life: Photographing America’s abandoned homes\n",
|
| 324 |
+
"Timothy Norris/Getty Images Amaarae is redefining what it means to be\n",
|
| 325 |
+
"a pop star Emmanuel Item There’s a hidden meaning behind these tattoos\n",
|
| 326 |
+
"of ancient symbols Ad Feedback MIDDLE EAST Mohammed\n",
|
| 327 |
+
"Eslayeh/Anadolu/Getty Images Trump administration advancing UN\n",
|
| 328 |
+
"resolution for Gaza security force, source says • Video 3:56 Obtained\n",
|
| 329 |
+
"by CNN Video Palestinian olive pickers attacked over and over 3:56 •\n",
|
| 330 |
+
"Video 2:25 Clipped From Video Video On GPS: Qatari PM on Israel’s\n",
|
| 331 |
+
"‘disproportionate’ attack on Gaza during the ceasefire 2:25 Business\n",
|
| 332 |
+
"Astrid Stawiarz/Getty Images ‘The Big Short’s’ Michael Burry is back\n",
|
| 333 |
+
"with cryptic messages — and two massive bets Aurelien Morissard/AP\n",
|
| 334 |
+
"Ultra-fast fashion and ‘childlike’ sex dolls: Why Shein isn’t winning\n",
|
| 335 |
+
"friends in France Andrej Ivanov/Bloomberg/Getty Images Stellantis\n",
|
| 336 |
+
"recalls 375,000 Jeep SUVs over fire risks, urges owners to park\n",
|
| 337 |
+
"outside Ad Feedback SPORTS Bryan Bedder/New York Road Runners/Getty\n",
|
| 338 |
+
"Images Meet the shark-attack victim and Paralympian who finished the\n",
|
| 339 |
+
"New York City marathon Aryna Sabalenka to play Nick Kyrgios in ‘Battle\n",
|
| 340 |
+
"of the Sexes’ exhibition match Travel & beyond Maggie Hiufu Wong/CNN\n",
|
| 341 |
+
"‘It’s not profitable’: The story behind the world’s most expensive\n",
|
| 342 |
+
"rice Video See the re-opened passageway beneath the Roman Colosseum\n",
|
| 343 |
+
"that was used by emperors 2:16 Science CCTV Chinese astronauts’ return\n",
|
| 344 |
+
"to Earth delayed over fears spaceship damaged by debris Mating\n",
|
| 345 |
+
"injuries may lead scientists to identify dinosaurs’ sex Ad Feedback\n",
|
| 346 |
+
"Health & Wellness MementoJpeg/Moment RF/Getty Images What to know\n",
|
| 347 |
+
"about melatonin use and heart failure Ditch the scale and focus on\n",
|
| 348 |
+
"fitness, experts say Celebrities & Entertainment Jonathan Brady/PA/AP\n",
|
| 349 |
+
"David Beckham knighted by King Charles at Windsor Castle Paramount\n",
|
| 350 |
+
"renews Jon Stewart’s ‘Daily Show’ hosting gig, resolving speculation\n",
|
| 351 |
+
"about his future Crime & Courts Kirsty Wigglesworth/AP Why are there\n",
|
| 352 |
+
"so many stabbings in Britain? Two men accused of hacking and extorting\n",
|
| 353 |
+
"US companies previously worked for cybersecurity firms CNN Shorts CNN\n",
|
| 354 |
+
"Shorts • Video 0:36 Flying Camera PH Video Typhoon Kalmaegi devastates\n",
|
| 355 |
+
"central Philippines 0:36 • Video 2:37 AP Video Protests erupt as Shein\n",
|
| 356 |
+
"opens in Paris 2:37 • Video 1:15 Getty Images Video Musk ally Isaacman\n",
|
| 357 |
+
"back in as Trump’s pick for NASA administrator 1:15 • Video 0:56 ABC /\n",
|
| 358 |
+
"\"The View\" Video MTG says she yelled at Speaker Johnson over shutdown\n",
|
| 359 |
+
"0:56 • Video 1:08 POOL Video Democrats sweep key races in nationwide\n",
|
| 360 |
+
"elections 1:08 • Video 0:36 Flying Camera PH Video Typhoon Kalmaegi\n",
|
| 361 |
+
"devastates central Philippines 0:36 • Video 2:37 AP Video Protests\n",
|
| 362 |
+
"erupt as Shein opens in Paris 2:37 • Video 1:05 CNN Video Election\n",
|
| 363 |
+
"night at Mamdani’s favorite restaurant 1:05 • Video 1:08 POOL Video\n",
|
| 364 |
+
"Democrats sweep key races in nationwide elections 1:08 • Video 1:27\n",
|
| 365 |
+
"Pool Video Newsom targets Trump after Prop 50 passes 1:27 • Video 0:41\n",
|
| 366 |
+
"CNN Video Analysis: What election results mean for Trump 0:41 • Video\n",
|
| 367 |
+
"1:18 CNN Video Is the NJ governor victory a blueprint for Democrats?\n",
|
| 368 |
+
"1:18 • Video 2:55 CNN Video ‘Turn the volume up!’: Mamdani addresses\n",
|
| 369 |
+
"Trump in victory speech 2:55 • Video 0:46 MediaSource Video Sen.\n",
|
| 370 |
+
"Booker: GOP will ‘wake up’ after election results 0:46 • Video 0:50\n",
|
| 371 |
+
"cnn Video Cuomo congratulates Mamdani through boos from supporters\n",
|
| 372 |
+
"0:50 • Video 2:49 Getty Images Video Who is Zohran Mamdani? 2:49 •\n",
|
| 373 |
+
"Video 1:26 cnn Video Trump reacts to election results 1:26 • Video\n",
|
| 374 |
+
"0:32 Reuters Video Sliwa warns mayor-elect during his concession\n",
|
| 375 |
+
"speech 0:32 • Video 1:25 CNN Video Spanberger’s win gives Dems ‘an air\n",
|
| 376 |
+
"of confidence’ 1:25 • Video 1:16 CNN Video How will Democrats respond\n",
|
| 377 |
+
"to Mamdani’s victory? 1:16 • Video 0:42 WLKY Video UPS plane catches\n",
|
| 378 |
+
"fire and crashes shortly after take-off 0:42 • Video 1:21 cnn Video\n",
|
| 379 |
+
"Spanberger jokes with daughters during victory speech 1:21 • Video\n",
|
| 380 |
+
"1:18 CNN Video An inside look at how votes are counted in Virginia\n",
|
| 381 |
+
"1:18 • Video 0:54 cnn Video Ben Shapiro on political violence 0:54 •\n",
|
| 382 |
+
"Video 1:36 cnn Video Swisher to Democrats: Let candidates be who they\n",
|
| 383 |
+
"are 1:36 Inside Africa Show all Dogs4Wildlife Meet the Welsh puppies\n",
|
| 384 |
+
"that are stopping wildlife poachers in Africa Dara Ojo Photos reveal\n",
|
| 385 |
+
"creepy crawlies in extreme detail Ralph Ziman An artist used 35\n",
|
| 386 |
+
"million beads to transform a supersonic jet • Gallery Gallery Regula\n",
|
| 387 |
+
"Tschumi Gallery In photos: The secretive, colorful world of Ghanaian\n",
|
| 388 |
+
"funerals Ad Feedback More from CNN Grand Egyptian Museum More than $1\n",
|
| 389 |
+
"billion and two decades later, the Grand Egyptian Museum is — finally\n",
|
| 390 |
+
"— ready to share its treasures White House shares images of Xi that\n",
|
| 391 |
+
"most Chinese don’t see at home NASA may be quietly gutting an iconic\n",
|
| 392 |
+
"campus with what it calls strategic closures, workers fear ‘Paddington\n",
|
| 393 |
+
"the Musical’ brings beloved bear to life on stage Maldives just made\n",
|
| 394 |
+
"it illegal for some generations to smoke What to watch for as the\n",
|
| 395 |
+
"Supreme Court reviews Trump’s sweeping global tariffs How phone calls,\n",
|
| 396 |
+
"sessions at gun ranges and secret meetings in parks led the FBI to\n",
|
| 397 |
+
"charge suspects in alleged terrorist plot Drones spotted above Belgian\n",
|
| 398 |
+
"airbase were ‘spying’ on military planes, defense minister says\n",
|
| 399 |
+
"Analysis Nuclear threats are worsening. Why? Subscribe Sign in My\n",
|
| 400 |
+
"Account Settings Newsletters Topics you follow Sign out Your CNN\n",
|
| 401 |
+
"account Sign in to your CNN account Live TV Listen Watch World Africa\n",
|
| 402 |
+
"Americas Asia Australia China Europe India Middle East United Kingdom\n",
|
| 403 |
+
"US Politics Trump Facts First CNN Polls 2025 Elections Business Tech\n",
|
| 404 |
+
"Media Calculators Videos Markets Pre-markets After-Hours Fear & Greed\n",
|
| 405 |
+
"Investing Markets Now Nightcap Health Life, But Better Fitness Food\n",
|
| 406 |
+
"Sleep Mindfulness Relationships Entertainment Movies Television\n",
|
| 407 |
+
"Celebrity Tech Innovate Foreseeable Future Mission: Ahead Work\n",
|
| 408 |
+
"Transformed Innovative Cities Style Arts Design Fashion Architecture\n",
|
| 409 |
+
"Luxury Beauty Video Travel Destinations Food & Drink Stay News Videos\n",
|
| 410 |
+
"Sports Football Tennis Golf Motorsport US Sports Olympics Climbing\n",
|
| 411 |
+
"Esports Hockey Science Space Life Unearthed Climate Solutions Weather\n",
|
| 412 |
+
"Weather Video Climate Ukraine-Russia War Israel-Hamas War Features As\n",
|
| 413 |
+
"Equals Call to Earth Freedom Project Impact Your World Inside Africa\n",
|
| 414 |
+
"CNN Heroes Watch Featured Shows & Films Network TV Clips CNN Headlines\n",
|
| 415 |
+
"CNN Shorts TV Shows A-Z CNN 10 CNN Max TV Schedule Listen CNN 5 Things\n",
|
| 416 |
+
"Chasing Life with Dr. Sanjay Gupta The Assignment with Audie Cornish\n",
|
| 417 |
+
"One Thing Tug of War CNN Political Briefing The Axe Files All There Is\n",
|
| 418 |
+
"with Anderson Cooper All CNN Audio podcasts Games Daily Crossword\n",
|
| 419 |
+
"Jumble Crossword Photo Shuffle Sudoblock Sudoku 5 Things Quiz About\n",
|
| 420 |
+
"CNN Photos Investigations CNN Profiles CNN Leadership CNN Newsletters\n",
|
| 421 |
+
"Work for CNN Watch Listen Live TV Follow CNN Subscribe Sign in My\n",
|
| 422 |
+
"Account Settings Newsletters Topics you follow Sign out Your CNN\n",
|
| 423 |
+
"account Sign in to your CNN account Terms of Use Privacy Policy Manage\n",
|
| 424 |
+
"Cookies Ad Choices Accessibility & CC About Newsletters Transcripts ©\n",
|
| 425 |
+
"2025 Cable News Network. A Warner Bros. Discovery Company. All Rights\n",
|
| 426 |
+
"Reserved. CNN Sans ™ & © 2016 Cable News Network. Privacy Policy For\n",
|
| 427 |
+
"privacy options, please see our privacy policy:\n",
|
| 428 |
+
"https://www.cnn.com/privacy . Back Button Cookie List Search Icon\n",
|
| 429 |
+
"Filter Icon Clear checkbox label label Apply Cancel Consent\n",
|
| 430 |
+
"Leg.Interest checkbox label label checkbox label label checkbox label\n",
|
| 431 |
+
"label Close\n"
|
| 432 |
+
]
|
| 433 |
+
}
|
| 434 |
+
],
|
| 435 |
+
"source": [
|
| 436 |
+
"import textwrap\n",
|
| 437 |
+
"print(textwrap.fill(text))"
|
| 438 |
+
]
|
| 439 |
+
},
|
| 440 |
+
{
|
| 441 |
+
"cell_type": "code",
|
| 442 |
+
"execution_count": 14,
|
| 443 |
+
"metadata": {},
|
| 444 |
+
"outputs": [],
|
| 445 |
+
"source": [
|
| 446 |
+
"all_tools = tools + [tool_push]"
|
| 447 |
+
]
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"cell_type": "code",
|
| 451 |
+
"execution_count": 15,
|
| 452 |
+
"metadata": {},
|
| 453 |
+
"outputs": [],
|
| 454 |
+
"source": [
|
| 455 |
+
"\n",
|
| 456 |
+
"llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
| 457 |
+
"llm_with_tools = llm.bind_tools(all_tools)\n",
|
| 458 |
+
"\n",
|
| 459 |
+
"\n",
|
| 460 |
+
"def chatbot(state: State):\n",
|
| 461 |
+
" return {\"messages\": [llm_with_tools.invoke(state[\"messages\"])]}\n"
|
| 462 |
+
]
|
| 463 |
+
},
|
| 464 |
+
{
|
| 465 |
+
"cell_type": "code",
|
| 466 |
+
"execution_count": 16,
|
| 467 |
+
"metadata": {},
|
| 468 |
+
"outputs": [
|
| 469 |
+
{
|
| 470 |
+
"data": {
|
| 471 |
+
"image/png": "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",
|
| 472 |
+
"text/plain": [
|
| 473 |
+
"<IPython.core.display.Image object>"
|
| 474 |
+
]
|
| 475 |
+
},
|
| 476 |
+
"metadata": {},
|
| 477 |
+
"output_type": "display_data"
|
| 478 |
+
}
|
| 479 |
+
],
|
| 480 |
+
"source": [
|
| 481 |
+
"\n",
|
| 482 |
+
"graph_builder = StateGraph(State)\n",
|
| 483 |
+
"graph_builder.add_node(\"chatbot\", chatbot)\n",
|
| 484 |
+
"graph_builder.add_node(\"tools\", ToolNode(tools=all_tools))\n",
|
| 485 |
+
"graph_builder.add_conditional_edges( \"chatbot\", tools_condition, \"tools\")\n",
|
| 486 |
+
"graph_builder.add_edge(\"tools\", \"chatbot\")\n",
|
| 487 |
+
"graph_builder.add_edge(START, \"chatbot\")\n",
|
| 488 |
+
"\n",
|
| 489 |
+
"memory = MemorySaver()\n",
|
| 490 |
+
"graph = graph_builder.compile(checkpointer=memory)\n",
|
| 491 |
+
"display(Image(graph.get_graph().draw_mermaid_png()))"
|
| 492 |
+
]
|
| 493 |
+
},
|
| 494 |
+
{
|
| 495 |
+
"cell_type": "code",
|
| 496 |
+
"execution_count": 17,
|
| 497 |
+
"metadata": {},
|
| 498 |
+
"outputs": [
|
| 499 |
+
{
|
| 500 |
+
"name": "stdout",
|
| 501 |
+
"output_type": "stream",
|
| 502 |
+
"text": [
|
| 503 |
+
"* Running on local URL: http://127.0.0.1:7860\n",
|
| 504 |
+
"* To create a public link, set `share=True` in `launch()`.\n"
|
| 505 |
+
]
|
| 506 |
+
},
|
| 507 |
+
{
|
| 508 |
+
"data": {
|
| 509 |
+
"text/html": [
|
| 510 |
+
"<div><iframe src=\"http://127.0.0.1:7860/\" width=\"100%\" height=\"500\" allow=\"autoplay; camera; microphone; clipboard-read; clipboard-write;\" frameborder=\"0\" allowfullscreen></iframe></div>"
|
| 511 |
+
],
|
| 512 |
+
"text/plain": [
|
| 513 |
+
"<IPython.core.display.HTML object>"
|
| 514 |
+
]
|
| 515 |
+
},
|
| 516 |
+
"metadata": {},
|
| 517 |
+
"output_type": "display_data"
|
| 518 |
+
},
|
| 519 |
+
{
|
| 520 |
+
"data": {
|
| 521 |
+
"text/plain": []
|
| 522 |
+
},
|
| 523 |
+
"execution_count": 17,
|
| 524 |
+
"metadata": {},
|
| 525 |
+
"output_type": "execute_result"
|
| 526 |
+
}
|
| 527 |
+
],
|
| 528 |
+
"source": [
|
| 529 |
+
"config = {\"configurable\": {\"thread_id\": \"10\"}}\n",
|
| 530 |
+
"\n",
|
| 531 |
+
"async def chat(user_input: str, history):\n",
|
| 532 |
+
" result = await graph.ainvoke({\"messages\": [{\"role\": \"user\", \"content\": user_input}]}, config=config)\n",
|
| 533 |
+
" return result[\"messages\"][-1].content\n",
|
| 534 |
+
"\n",
|
| 535 |
+
"\n",
|
| 536 |
+
"gr.ChatInterface(chat, type=\"messages\").launch()"
|
| 537 |
+
]
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"cell_type": "code",
|
| 541 |
+
"execution_count": null,
|
| 542 |
+
"metadata": {},
|
| 543 |
+
"outputs": [],
|
| 544 |
+
"source": []
|
| 545 |
+
}
|
| 546 |
+
],
|
| 547 |
+
"metadata": {
|
| 548 |
+
"kernelspec": {
|
| 549 |
+
"display_name": ".venv",
|
| 550 |
+
"language": "python",
|
| 551 |
+
"name": "python3"
|
| 552 |
+
},
|
| 553 |
+
"language_info": {
|
| 554 |
+
"codemirror_mode": {
|
| 555 |
+
"name": "ipython",
|
| 556 |
+
"version": 3
|
| 557 |
+
},
|
| 558 |
+
"file_extension": ".py",
|
| 559 |
+
"mimetype": "text/x-python",
|
| 560 |
+
"name": "python",
|
| 561 |
+
"nbconvert_exporter": "python",
|
| 562 |
+
"pygments_lexer": "ipython3",
|
| 563 |
+
"version": "3.12.3"
|
| 564 |
+
}
|
| 565 |
+
},
|
| 566 |
+
"nbformat": 4,
|
| 567 |
+
"nbformat_minor": 2
|
| 568 |
+
}
|
4_lab4.ipynb
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"## Week 4 Day 4 - preparing the big project!\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"# The Sidekick\n",
|
| 10 |
+
"\n",
|
| 11 |
+
"It's time to introduce:\n",
|
| 12 |
+
"\n",
|
| 13 |
+
"1. Structured Outputs\n",
|
| 14 |
+
"2. A multi-agent flow"
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"cell_type": "code",
|
| 19 |
+
"execution_count": 1,
|
| 20 |
+
"metadata": {},
|
| 21 |
+
"outputs": [],
|
| 22 |
+
"source": [
|
| 23 |
+
"from typing import Annotated, TypedDict, List, Dict, Any, Optional\n",
|
| 24 |
+
"from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n",
|
| 25 |
+
"from langchain_openai import ChatOpenAI\n",
|
| 26 |
+
"from langchain_community.agent_toolkits import PlayWrightBrowserToolkit\n",
|
| 27 |
+
"from langchain_community.tools.playwright.utils import create_async_playwright_browser\n",
|
| 28 |
+
"from langgraph.graph import StateGraph, START, END\n",
|
| 29 |
+
"from langgraph.checkpoint.memory import MemorySaver\n",
|
| 30 |
+
"from langgraph.prebuilt import ToolNode\n",
|
| 31 |
+
"from langgraph.graph.message import add_messages\n",
|
| 32 |
+
"from pydantic import BaseModel, Field\n",
|
| 33 |
+
"from IPython.display import Image, display\n",
|
| 34 |
+
"import gradio as gr\n",
|
| 35 |
+
"import uuid\n",
|
| 36 |
+
"from dotenv import load_dotenv"
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"cell_type": "code",
|
| 41 |
+
"execution_count": 2,
|
| 42 |
+
"metadata": {},
|
| 43 |
+
"outputs": [
|
| 44 |
+
{
|
| 45 |
+
"data": {
|
| 46 |
+
"text/plain": [
|
| 47 |
+
"True"
|
| 48 |
+
]
|
| 49 |
+
},
|
| 50 |
+
"execution_count": 2,
|
| 51 |
+
"metadata": {},
|
| 52 |
+
"output_type": "execute_result"
|
| 53 |
+
}
|
| 54 |
+
],
|
| 55 |
+
"source": [
|
| 56 |
+
"load_dotenv(override=True)"
|
| 57 |
+
]
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"cell_type": "markdown",
|
| 61 |
+
"metadata": {},
|
| 62 |
+
"source": [
|
| 63 |
+
"### For structured outputs, we define a Pydantic object for the Schema"
|
| 64 |
+
]
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"cell_type": "code",
|
| 68 |
+
"execution_count": 3,
|
| 69 |
+
"metadata": {},
|
| 70 |
+
"outputs": [],
|
| 71 |
+
"source": [
|
| 72 |
+
"# First define a structured output\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"class EvaluatorOutput(BaseModel):\n",
|
| 75 |
+
" feedback: str = Field(description=\"Feedback on the assistant's response\")\n",
|
| 76 |
+
" success_criteria_met: bool = Field(description=\"Whether the success criteria have been met\")\n",
|
| 77 |
+
" user_input_needed: bool = Field(description=\"True if more input is needed from the user, or clarifications, or the assistant is stuck\")\n"
|
| 78 |
+
]
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"cell_type": "markdown",
|
| 82 |
+
"metadata": {},
|
| 83 |
+
"source": [
|
| 84 |
+
"### And for the State, we'll use TypedDict again\n",
|
| 85 |
+
"\n",
|
| 86 |
+
"But now we have some real information to maintain!\n",
|
| 87 |
+
"\n",
|
| 88 |
+
"The messages uses the reducer. The others are simply values that we overwrite with any state change."
|
| 89 |
+
]
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"cell_type": "code",
|
| 93 |
+
"execution_count": 4,
|
| 94 |
+
"metadata": {},
|
| 95 |
+
"outputs": [],
|
| 96 |
+
"source": [
|
| 97 |
+
"# The state\n",
|
| 98 |
+
"\n",
|
| 99 |
+
"class State(TypedDict):\n",
|
| 100 |
+
" messages: Annotated[List[Any], add_messages]\n",
|
| 101 |
+
" success_criteria: str\n",
|
| 102 |
+
" feedback_on_work: Optional[str]\n",
|
| 103 |
+
" success_criteria_met: bool\n",
|
| 104 |
+
" user_input_needed: bool"
|
| 105 |
+
]
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"cell_type": "code",
|
| 109 |
+
"execution_count": null,
|
| 110 |
+
"metadata": {},
|
| 111 |
+
"outputs": [],
|
| 112 |
+
"source": [
|
| 113 |
+
"# Get our async Playwright tools\n",
|
| 114 |
+
"# Load async playwright browser\n",
|
| 115 |
+
"\n",
|
| 116 |
+
"import nest_asyncio\n",
|
| 117 |
+
"nest_asyncio.apply()\n",
|
| 118 |
+
"async_browser = create_async_playwright_browser(headless=False) # headful mode\n",
|
| 119 |
+
"toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=async_browser)\n",
|
| 120 |
+
"tools = toolkit.get_tools()"
|
| 121 |
+
]
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"cell_type": "code",
|
| 125 |
+
"execution_count": 6,
|
| 126 |
+
"metadata": {},
|
| 127 |
+
"outputs": [],
|
| 128 |
+
"source": [
|
| 129 |
+
"# Initialize the LLMs\n",
|
| 130 |
+
"\n",
|
| 131 |
+
"worker_llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
| 132 |
+
"worker_llm_with_tools = worker_llm.bind_tools(tools)\n",
|
| 133 |
+
"\n",
|
| 134 |
+
"evaluator_llm = ChatOpenAI(model=\"gpt-4o-mini\")\n",
|
| 135 |
+
"evaluator_llm_with_output = evaluator_llm.with_structured_output(EvaluatorOutput)"
|
| 136 |
+
]
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"cell_type": "code",
|
| 140 |
+
"execution_count": 7,
|
| 141 |
+
"metadata": {},
|
| 142 |
+
"outputs": [],
|
| 143 |
+
"source": [
|
| 144 |
+
"# The worker node\n",
|
| 145 |
+
"\n",
|
| 146 |
+
"def worker(state: State) -> Dict[str, Any]:\n",
|
| 147 |
+
" system_message = f\"\"\"You are a helpful assistant that can use tools to complete tasks.\n",
|
| 148 |
+
"You keep working on a task until either you have a question or clarification for the user, or the success criteria is met.\n",
|
| 149 |
+
"This is the success criteria:\n",
|
| 150 |
+
"{state['success_criteria']}\n",
|
| 151 |
+
"You should reply either with a question for the user about this assignment, or with your final response.\n",
|
| 152 |
+
"If you have a question for the user, you need to reply by clearly stating your question. An example might be:\n",
|
| 153 |
+
"\n",
|
| 154 |
+
"Question: please clarify whether you want a summary or a detailed answer\n",
|
| 155 |
+
"\n",
|
| 156 |
+
"If you've finished, reply with the final answer, and don't ask a question; simply reply with the answer.\n",
|
| 157 |
+
"\"\"\"\n",
|
| 158 |
+
" \n",
|
| 159 |
+
" if state.get(\"feedback_on_work\"):\n",
|
| 160 |
+
" system_message += f\"\"\"\n",
|
| 161 |
+
"Previously you thought you completed the assignment, but your reply was rejected because the success criteria was not met.\n",
|
| 162 |
+
"Here is the feedback on why this was rejected:\n",
|
| 163 |
+
"{state['feedback_on_work']}\n",
|
| 164 |
+
"With this feedback, please continue the assignment, ensuring that you meet the success criteria or have a question for the user.\"\"\"\n",
|
| 165 |
+
" \n",
|
| 166 |
+
" # Add in the system message\n",
|
| 167 |
+
"\n",
|
| 168 |
+
" found_system_message = False\n",
|
| 169 |
+
" messages = state[\"messages\"]\n",
|
| 170 |
+
" for message in messages:\n",
|
| 171 |
+
" if isinstance(message, SystemMessage):\n",
|
| 172 |
+
" message.content = system_message\n",
|
| 173 |
+
" found_system_message = True\n",
|
| 174 |
+
" \n",
|
| 175 |
+
" if not found_system_message:\n",
|
| 176 |
+
" messages = [SystemMessage(content=system_message)] + messages\n",
|
| 177 |
+
" \n",
|
| 178 |
+
" # Invoke the LLM with tools\n",
|
| 179 |
+
" response = worker_llm_with_tools.invoke(messages)\n",
|
| 180 |
+
" \n",
|
| 181 |
+
" # Return updated state\n",
|
| 182 |
+
" return {\n",
|
| 183 |
+
" \"messages\": [response],\n",
|
| 184 |
+
" }"
|
| 185 |
+
]
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"cell_type": "code",
|
| 189 |
+
"execution_count": 8,
|
| 190 |
+
"metadata": {},
|
| 191 |
+
"outputs": [],
|
| 192 |
+
"source": [
|
| 193 |
+
"def worker_router(state: State) -> str:\n",
|
| 194 |
+
" last_message = state[\"messages\"][-1]\n",
|
| 195 |
+
" \n",
|
| 196 |
+
" if hasattr(last_message, \"tool_calls\") and last_message.tool_calls:\n",
|
| 197 |
+
" return \"tools\"\n",
|
| 198 |
+
" else:\n",
|
| 199 |
+
" return \"evaluator\""
|
| 200 |
+
]
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"cell_type": "code",
|
| 204 |
+
"execution_count": 9,
|
| 205 |
+
"metadata": {},
|
| 206 |
+
"outputs": [],
|
| 207 |
+
"source": [
|
| 208 |
+
"def format_conversation(messages: List[Any]) -> str:\n",
|
| 209 |
+
" conversation = \"Conversation history:\\n\\n\"\n",
|
| 210 |
+
" for message in messages:\n",
|
| 211 |
+
" if isinstance(message, HumanMessage):\n",
|
| 212 |
+
" conversation += f\"User: {message.content}\\n\"\n",
|
| 213 |
+
" elif isinstance(message, AIMessage):\n",
|
| 214 |
+
" text = message.content or \"[Tools use]\"\n",
|
| 215 |
+
" conversation += f\"Assistant: {text}\\n\"\n",
|
| 216 |
+
" return conversation"
|
| 217 |
+
]
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"cell_type": "code",
|
| 221 |
+
"execution_count": 10,
|
| 222 |
+
"metadata": {},
|
| 223 |
+
"outputs": [],
|
| 224 |
+
"source": [
|
| 225 |
+
"# This is the evaluator node\n",
|
| 226 |
+
"def evaluator(state: State) -> State:\n",
|
| 227 |
+
" last_response = state[\"messages\"][-1].content\n",
|
| 228 |
+
"\n",
|
| 229 |
+
" system_message = \"\"\"You are an evaluator that determines if a task has been completed successfully by an Assistant.\n",
|
| 230 |
+
"Assess the Assistant's last response based on the given criteria. Respond with your feedback, and with your decision on whether the success criteria has been met,\n",
|
| 231 |
+
"and whether more input is needed from the user.\"\"\"\n",
|
| 232 |
+
" \n",
|
| 233 |
+
" user_message = f\"\"\"You are evaluating a conversation between the User and Assistant. You decide what action to take based on the last response from the Assistant.\n",
|
| 234 |
+
"\n",
|
| 235 |
+
"The entire conversation with the assistant, with the user's original request and all replies, is:\n",
|
| 236 |
+
"{format_conversation(state['messages'])}\n",
|
| 237 |
+
"\n",
|
| 238 |
+
"The success criteria for this assignment is:\n",
|
| 239 |
+
"{state['success_criteria']}\n",
|
| 240 |
+
"\n",
|
| 241 |
+
"And the final response from the Assistant that you are evaluating is:\n",
|
| 242 |
+
"{last_response}\n",
|
| 243 |
+
"\n",
|
| 244 |
+
"Respond with your feedback, and decide if the success criteria is met by this response.\n",
|
| 245 |
+
"Also, decide if more user input is required, either because the assistant has a question, needs clarification, or seems to be stuck and unable to answer without help.\n",
|
| 246 |
+
"\"\"\"\n",
|
| 247 |
+
" if state[\"feedback_on_work\"]:\n",
|
| 248 |
+
" user_message += f\"Also, note that in a prior attempt from the Assistant, you provided this feedback: {state['feedback_on_work']}\\n\"\n",
|
| 249 |
+
" user_message += \"If you're seeing the Assistant repeating the same mistakes, then consider responding that user input is required.\"\n",
|
| 250 |
+
" \n",
|
| 251 |
+
" evaluator_messages = [SystemMessage(content=system_message), HumanMessage(content=user_message)]\n",
|
| 252 |
+
"\n",
|
| 253 |
+
" eval_result = evaluator_llm_with_output.invoke(evaluator_messages)\n",
|
| 254 |
+
" new_state = {\n",
|
| 255 |
+
" \"messages\": [{\"role\": \"assistant\", \"content\": f\"Evaluator Feedback on this answer: {eval_result.feedback}\"}],\n",
|
| 256 |
+
" \"feedback_on_work\": eval_result.feedback,\n",
|
| 257 |
+
" \"success_criteria_met\": eval_result.success_criteria_met,\n",
|
| 258 |
+
" \"user_input_needed\": eval_result.user_input_needed\n",
|
| 259 |
+
" }\n",
|
| 260 |
+
" return new_state"
|
| 261 |
+
]
|
| 262 |
+
},
|
| 263 |
+
{
|
| 264 |
+
"cell_type": "code",
|
| 265 |
+
"execution_count": 11,
|
| 266 |
+
"metadata": {},
|
| 267 |
+
"outputs": [],
|
| 268 |
+
"source": [
|
| 269 |
+
"def route_based_on_evaluation(state: State) -> str:\n",
|
| 270 |
+
" if state[\"success_criteria_met\"] or state[\"user_input_needed\"]:\n",
|
| 271 |
+
" return \"END\"\n",
|
| 272 |
+
" else:\n",
|
| 273 |
+
" return \"worker\""
|
| 274 |
+
]
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
"cell_type": "code",
|
| 278 |
+
"execution_count": 12,
|
| 279 |
+
"metadata": {},
|
| 280 |
+
"outputs": [],
|
| 281 |
+
"source": [
|
| 282 |
+
"# Set up Graph Builder with State\n",
|
| 283 |
+
"graph_builder = StateGraph(State)\n",
|
| 284 |
+
"\n",
|
| 285 |
+
"# Add nodes\n",
|
| 286 |
+
"graph_builder.add_node(\"worker\", worker)\n",
|
| 287 |
+
"graph_builder.add_node(\"tools\", ToolNode(tools=tools))\n",
|
| 288 |
+
"graph_builder.add_node(\"evaluator\", evaluator)\n",
|
| 289 |
+
"\n",
|
| 290 |
+
"# Add edges\n",
|
| 291 |
+
"graph_builder.add_conditional_edges(\"worker\", worker_router, {\"tools\": \"tools\", \"evaluator\": \"evaluator\"})\n",
|
| 292 |
+
"graph_builder.add_edge(\"tools\", \"worker\")\n",
|
| 293 |
+
"graph_builder.add_conditional_edges(\"evaluator\", route_based_on_evaluation, {\"worker\": \"worker\", \"END\": END})\n",
|
| 294 |
+
"graph_builder.add_edge(START, \"worker\")\n",
|
| 295 |
+
"\n",
|
| 296 |
+
"# Compile the graph\n",
|
| 297 |
+
"memory = MemorySaver()\n",
|
| 298 |
+
"graph = graph_builder.compile(checkpointer=memory)"
|
| 299 |
+
]
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"cell_type": "code",
|
| 303 |
+
"execution_count": 13,
|
| 304 |
+
"metadata": {},
|
| 305 |
+
"outputs": [
|
| 306 |
+
{
|
| 307 |
+
"data": {
|
| 308 |
+
"image/png": "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",
|
| 309 |
+
"text/plain": [
|
| 310 |
+
"<IPython.core.display.Image object>"
|
| 311 |
+
]
|
| 312 |
+
},
|
| 313 |
+
"metadata": {},
|
| 314 |
+
"output_type": "display_data"
|
| 315 |
+
}
|
| 316 |
+
],
|
| 317 |
+
"source": [
|
| 318 |
+
"display(Image(graph.get_graph().draw_mermaid_png()))"
|
| 319 |
+
]
|
| 320 |
+
},
|
| 321 |
+
{
|
| 322 |
+
"cell_type": "markdown",
|
| 323 |
+
"metadata": {},
|
| 324 |
+
"source": [
|
| 325 |
+
"### Next comes the gradio Callback to kick off a super-step"
|
| 326 |
+
]
|
| 327 |
+
},
|
| 328 |
+
{
|
| 329 |
+
"cell_type": "code",
|
| 330 |
+
"execution_count": 14,
|
| 331 |
+
"metadata": {},
|
| 332 |
+
"outputs": [],
|
| 333 |
+
"source": [
|
| 334 |
+
"def make_thread_id() -> str:\n",
|
| 335 |
+
" return str(uuid.uuid4())\n",
|
| 336 |
+
"\n",
|
| 337 |
+
"\n",
|
| 338 |
+
"async def process_message(message, success_criteria, history, thread):\n",
|
| 339 |
+
"\n",
|
| 340 |
+
" config = {\"configurable\": {\"thread_id\": thread}}\n",
|
| 341 |
+
"\n",
|
| 342 |
+
" state = {\n",
|
| 343 |
+
" \"messages\": message,\n",
|
| 344 |
+
" \"success_criteria\": success_criteria,\n",
|
| 345 |
+
" \"feedback_on_work\": None,\n",
|
| 346 |
+
" \"success_criteria_met\": False,\n",
|
| 347 |
+
" \"user_input_needed\": False\n",
|
| 348 |
+
" }\n",
|
| 349 |
+
" result = await graph.ainvoke(state, config=config)\n",
|
| 350 |
+
" user = {\"role\": \"user\", \"content\": message}\n",
|
| 351 |
+
" reply = {\"role\": \"assistant\", \"content\": result[\"messages\"][-2].content}\n",
|
| 352 |
+
" feedback = {\"role\": \"assistant\", \"content\": result[\"messages\"][-1].content}\n",
|
| 353 |
+
" return history + [user, reply, feedback]\n",
|
| 354 |
+
"\n",
|
| 355 |
+
"async def reset():\n",
|
| 356 |
+
" return \"\", \"\", None, make_thread_id()\n",
|
| 357 |
+
"\n"
|
| 358 |
+
]
|
| 359 |
+
},
|
| 360 |
+
{
|
| 361 |
+
"cell_type": "markdown",
|
| 362 |
+
"metadata": {},
|
| 363 |
+
"source": [
|
| 364 |
+
"### And now launch our Sidekick UI"
|
| 365 |
+
]
|
| 366 |
+
},
|
| 367 |
+
{
|
| 368 |
+
"cell_type": "code",
|
| 369 |
+
"execution_count": null,
|
| 370 |
+
"metadata": {},
|
| 371 |
+
"outputs": [
|
| 372 |
+
{
|
| 373 |
+
"name": "stdout",
|
| 374 |
+
"output_type": "stream",
|
| 375 |
+
"text": [
|
| 376 |
+
"* Running on local URL: http://127.0.0.1:7861\n",
|
| 377 |
+
"* To create a public link, set `share=True` in `launch()`.\n"
|
| 378 |
+
]
|
| 379 |
+
},
|
| 380 |
+
{
|
| 381 |
+
"data": {
|
| 382 |
+
"text/html": [
|
| 383 |
+
"<div><iframe src=\"http://127.0.0.1:7861/\" width=\"100%\" height=\"500\" allow=\"autoplay; camera; microphone; clipboard-read; clipboard-write;\" frameborder=\"0\" allowfullscreen></iframe></div>"
|
| 384 |
+
],
|
| 385 |
+
"text/plain": [
|
| 386 |
+
"<IPython.core.display.HTML object>"
|
| 387 |
+
]
|
| 388 |
+
},
|
| 389 |
+
"metadata": {},
|
| 390 |
+
"output_type": "display_data"
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
"data": {
|
| 394 |
+
"text/plain": []
|
| 395 |
+
},
|
| 396 |
+
"execution_count": 15,
|
| 397 |
+
"metadata": {},
|
| 398 |
+
"output_type": "execute_result"
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
"name": "stderr",
|
| 402 |
+
"output_type": "stream",
|
| 403 |
+
"text": [
|
| 404 |
+
"Traceback (most recent call last):\n",
|
| 405 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/queueing.py\", line 625, in process_events\n",
|
| 406 |
+
" response = await route_utils.call_process_api(\n",
|
| 407 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 408 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/route_utils.py\", line 322, in call_process_api\n",
|
| 409 |
+
" output = await app.get_blocks().process_api(\n",
|
| 410 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 411 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/blocks.py\", line 2220, in process_api\n",
|
| 412 |
+
" result = await self.call_function(\n",
|
| 413 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 414 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/blocks.py\", line 1729, in call_function\n",
|
| 415 |
+
" prediction = await fn(*processed_input)\n",
|
| 416 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 417 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/utils.py\", line 871, in async_wrapper\n",
|
| 418 |
+
" response = await f(*args, **kwargs)\n",
|
| 419 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 420 |
+
" File \"/tmp/ipykernel_2775962/1717601295.py\", line 16, in process_message\n",
|
| 421 |
+
" result = await graph.ainvoke(state, config=config)\n",
|
| 422 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 423 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langgraph/pregel/__init__.py\", line 2788, in ainvoke\n",
|
| 424 |
+
" async for chunk in self.astream(\n",
|
| 425 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langgraph/pregel/__init__.py\", line 2677, in astream\n",
|
| 426 |
+
" raise GraphRecursionError(msg)\n",
|
| 427 |
+
"langgraph.errors.GraphRecursionError: Recursion limit of 25 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.\n",
|
| 428 |
+
"For troubleshooting, visit: https://python.langchain.com/docs/troubleshooting/errors/GRAPH_RECURSION_LIMIT\n",
|
| 429 |
+
"Traceback (most recent call last):\n",
|
| 430 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/queueing.py\", line 625, in process_events\n",
|
| 431 |
+
" response = await route_utils.call_process_api(\n",
|
| 432 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 433 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/route_utils.py\", line 322, in call_process_api\n",
|
| 434 |
+
" output = await app.get_blocks().process_api(\n",
|
| 435 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 436 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/blocks.py\", line 2220, in process_api\n",
|
| 437 |
+
" result = await self.call_function(\n",
|
| 438 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 439 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/blocks.py\", line 1729, in call_function\n",
|
| 440 |
+
" prediction = await fn(*processed_input)\n",
|
| 441 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 442 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/gradio/utils.py\", line 871, in async_wrapper\n",
|
| 443 |
+
" response = await f(*args, **kwargs)\n",
|
| 444 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 445 |
+
" File \"/tmp/ipykernel_2775962/1717601295.py\", line 16, in process_message\n",
|
| 446 |
+
" result = await graph.ainvoke(state, config=config)\n",
|
| 447 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 448 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langgraph/pregel/__init__.py\", line 2788, in ainvoke\n",
|
| 449 |
+
" async for chunk in self.astream(\n",
|
| 450 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langgraph/pregel/__init__.py\", line 2655, in astream\n",
|
| 451 |
+
" async for _ in runner.atick(\n",
|
| 452 |
+
" File \"/usr/lib/python3.12/asyncio/futures.py\", line 287, in __await__\n",
|
| 453 |
+
" yield self # This tells Task to wait for completion.\n",
|
| 454 |
+
" ^^^^^^^^^^\n",
|
| 455 |
+
" File \"/usr/lib/python3.12/asyncio/tasks.py\", line 385, in __wakeup\n",
|
| 456 |
+
" future.result()\n",
|
| 457 |
+
" File \"/usr/lib/python3.12/asyncio/futures.py\", line 203, in result\n",
|
| 458 |
+
" raise self._exception.with_traceback(self._exception_tb)\n",
|
| 459 |
+
" File \"/usr/lib/python3.12/asyncio/tasks.py\", line 316, in __step_run_and_handle_result\n",
|
| 460 |
+
" result = coro.throw(exc)\n",
|
| 461 |
+
" ^^^^^^^^^^^^^^^\n",
|
| 462 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langgraph/utils/runnable.py\", line 440, in ainvoke\n",
|
| 463 |
+
" ret = await self.afunc(*args, **kwargs)\n",
|
| 464 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 465 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langchain_core/runnables/config.py\", line 616, in run_in_executor\n",
|
| 466 |
+
" return await asyncio.get_running_loop().run_in_executor(\n",
|
| 467 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 468 |
+
" File \"/usr/lib/python3.12/asyncio/futures.py\", line 287, in __await__\n",
|
| 469 |
+
" yield self # This tells Task to wait for completion.\n",
|
| 470 |
+
" ^^^^^^^^^^\n",
|
| 471 |
+
" File \"/usr/lib/python3.12/asyncio/tasks.py\", line 385, in __wakeup\n",
|
| 472 |
+
" future.result()\n",
|
| 473 |
+
" File \"/usr/lib/python3.12/asyncio/futures.py\", line 203, in result\n",
|
| 474 |
+
" raise self._exception.with_traceback(self._exception_tb)\n",
|
| 475 |
+
" File \"/usr/lib/python3.12/concurrent/futures/thread.py\", line 58, in run\n",
|
| 476 |
+
" result = self.fn(*self.args, **self.kwargs)\n",
|
| 477 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 478 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langchain_core/runnables/config.py\", line 607, in wrapper\n",
|
| 479 |
+
" return func(*args, **kwargs)\n",
|
| 480 |
+
" ^^^^^^^^^^^^^^^^^^^^^\n",
|
| 481 |
+
" File \"/tmp/ipykernel_2775962/2770951305.py\", line 36, in worker\n",
|
| 482 |
+
" response = worker_llm_with_tools.invoke(messages)\n",
|
| 483 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 484 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langchain_core/runnables/base.py\", line 5431, in invoke\n",
|
| 485 |
+
" return self.bound.invoke(\n",
|
| 486 |
+
" ^^^^^^^^^^^^^^^^^^\n",
|
| 487 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langchain_core/language_models/chat_models.py\", line 372, in invoke\n",
|
| 488 |
+
" self.generate_prompt(\n",
|
| 489 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langchain_core/language_models/chat_models.py\", line 957, in generate_prompt\n",
|
| 490 |
+
" return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n",
|
| 491 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 492 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langchain_core/language_models/chat_models.py\", line 776, in generate\n",
|
| 493 |
+
" self._generate_with_cache(\n",
|
| 494 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langchain_core/language_models/chat_models.py\", line 1022, in _generate_with_cache\n",
|
| 495 |
+
" result = self._generate(\n",
|
| 496 |
+
" ^^^^^^^^^^^^^^^\n",
|
| 497 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/langchain_openai/chat_models/base.py\", line 1071, in _generate\n",
|
| 498 |
+
" response = self.client.create(**payload)\n",
|
| 499 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 500 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/openai/_utils/_utils.py\", line 287, in wrapper\n",
|
| 501 |
+
" return func(*args, **kwargs)\n",
|
| 502 |
+
" ^^^^^^^^^^^^^^^^^^^^^\n",
|
| 503 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/openai/resources/chat/completions/completions.py\", line 925, in create\n",
|
| 504 |
+
" return self._post(\n",
|
| 505 |
+
" ^^^^^^^^^^^\n",
|
| 506 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/openai/_base_client.py\", line 1249, in post\n",
|
| 507 |
+
" return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n",
|
| 508 |
+
" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n",
|
| 509 |
+
" File \"/home/dynamodenis/Desktop/projects/agentic-ai/agents/.venv/lib/python3.12/site-packages/openai/_base_client.py\", line 1037, in request\n",
|
| 510 |
+
" raise self._make_status_error_from_response(err.response) from None\n",
|
| 511 |
+
"openai.BadRequestError: Error code: 400 - {'error': {'message': \"An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_H3GdN48hsAjtNaQeYNI69dn5\", 'type': 'invalid_request_error', 'param': 'messages.[30].role', 'code': None}}\n",
|
| 512 |
+
"During task with name 'worker' and id 'd853cdff-51ff-8029-ab86-533f414fe498'\n"
|
| 513 |
+
]
|
| 514 |
+
}
|
| 515 |
+
],
|
| 516 |
+
"source": [
|
| 517 |
+
"\n",
|
| 518 |
+
"with gr.Blocks(theme=gr.themes.Default(primary_hue=\"emerald\")) as demo:\n",
|
| 519 |
+
" gr.Markdown(\"## Sidekick Personal Co-worker\")\n",
|
| 520 |
+
" thread = gr.State(make_thread_id())\n",
|
| 521 |
+
" \n",
|
| 522 |
+
" with gr.Row():\n",
|
| 523 |
+
" chatbot = gr.Chatbot(label=\"Sidekick\", height=300, type=\"messages\")\n",
|
| 524 |
+
" with gr.Group():\n",
|
| 525 |
+
" with gr.Row():\n",
|
| 526 |
+
" message = gr.Textbox(show_label=False, placeholder=\"Your request to your sidekick\")\n",
|
| 527 |
+
" with gr.Row():\n",
|
| 528 |
+
" success_criteria = gr.Textbox(show_label=False, placeholder=\"What are your success critiera?\")\n",
|
| 529 |
+
" with gr.Row():\n",
|
| 530 |
+
" reset_button = gr.Button(\"Reset\", variant=\"stop\")\n",
|
| 531 |
+
" go_button = gr.Button(\"Go!\", variant=\"primary\")\n",
|
| 532 |
+
" message.submit(process_message, [message, success_criteria, chatbot, thread], [chatbot])\n",
|
| 533 |
+
" success_criteria.submit(process_message, [message, success_criteria, chatbot, thread], [chatbot])\n",
|
| 534 |
+
" go_button.click(process_message, [message, success_criteria, chatbot, thread], [chatbot])\n",
|
| 535 |
+
" reset_button.click(reset, [], [message, success_criteria, chatbot, thread])\n",
|
| 536 |
+
"\n",
|
| 537 |
+
" \n",
|
| 538 |
+
"demo.launch()"
|
| 539 |
+
]
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"cell_type": "markdown",
|
| 543 |
+
"metadata": {},
|
| 544 |
+
"source": [
|
| 545 |
+
"<table style=\"margin: 0; text-align: left; width:100%\">\n",
|
| 546 |
+
" <tr>\n",
|
| 547 |
+
" <td style=\"width: 150px; height: 150px; vertical-align: middle;\">\n",
|
| 548 |
+
" <img src=\"../assets/thanks.png\" width=\"150\" height=\"150\" style=\"display: block;\" />\n",
|
| 549 |
+
" </td>\n",
|
| 550 |
+
" <td>\n",
|
| 551 |
+
" <h2 style=\"color:#00cc00;\">Congratulations on making the first version of Sidekick!</h2>\n",
|
| 552 |
+
" <span style=\"color:#00cc00;\">This is a pretty epic moment in the course. You've made the start of something very powerful. And you've upskilled on an impressive Agent framework in LangGraph. Maybe like me you're being converted from a LangGraph skeptic to a LangGraph fan..<br/><br/>My editor would kill me if I didn't mention again: if you're able to rate the course on Udemy, I'd be so very grateful: it's the main way that Udemy decides whether to show the course to others and it makes a massive difference.<br/><br/>And another reminder that I love <a href=\"https://www.linkedin.com/in/eddonner/\">connecting on LinkedIn</a> if you haven't yet! If you wanted to post about your progress on the course, please tag me and I'll weigh in to increase your exposure.\n",
|
| 553 |
+
" </span>\n",
|
| 554 |
+
" </td>\n",
|
| 555 |
+
" </tr>"
|
| 556 |
+
]
|
| 557 |
+
}
|
| 558 |
+
],
|
| 559 |
+
"metadata": {
|
| 560 |
+
"kernelspec": {
|
| 561 |
+
"display_name": ".venv",
|
| 562 |
+
"language": "python",
|
| 563 |
+
"name": "python3"
|
| 564 |
+
},
|
| 565 |
+
"language_info": {
|
| 566 |
+
"codemirror_mode": {
|
| 567 |
+
"name": "ipython",
|
| 568 |
+
"version": 3
|
| 569 |
+
},
|
| 570 |
+
"file_extension": ".py",
|
| 571 |
+
"mimetype": "text/x-python",
|
| 572 |
+
"name": "python",
|
| 573 |
+
"nbconvert_exporter": "python",
|
| 574 |
+
"pygments_lexer": "ipython3",
|
| 575 |
+
"version": "3.12.3"
|
| 576 |
+
}
|
| 577 |
+
},
|
| 578 |
+
"nbformat": 4,
|
| 579 |
+
"nbformat_minor": 2
|
| 580 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from sidekick import Sidekick
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
async def setup():
|
| 6 |
+
sidekick = Sidekick()
|
| 7 |
+
await sidekick.setup()
|
| 8 |
+
return sidekick
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
async def process_message(sidekick, message, success_criteria, history, files):
|
| 12 |
+
results = await sidekick.run_superstep(message, success_criteria, history, files)
|
| 13 |
+
return results, sidekick
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
async def reset():
|
| 17 |
+
new_sidekick = Sidekick()
|
| 18 |
+
await new_sidekick.setup()
|
| 19 |
+
return "", "", None, new_sidekick
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def free_resources(sidekick):
|
| 23 |
+
print("Cleaning up")
|
| 24 |
+
try:
|
| 25 |
+
if sidekick:
|
| 26 |
+
sidekick.cleanup()
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f"Exception during cleanup: {e}")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
with gr.Blocks(title="Sidekick", theme=gr.themes.Default(primary_hue="emerald")) as ui:
|
| 33 |
+
gr.Markdown("## Sidekick Personal Co-Worker")
|
| 34 |
+
sidekick = gr.State(delete_callback=free_resources)
|
| 35 |
+
|
| 36 |
+
with gr.Row():
|
| 37 |
+
chatbot = gr.Chatbot(label="Sidekick", height=300, type="messages")
|
| 38 |
+
|
| 39 |
+
with gr.Group():
|
| 40 |
+
with gr.Row():
|
| 41 |
+
file_uploader = gr.File(
|
| 42 |
+
label="Upload FIles",
|
| 43 |
+
file_count="multiple",
|
| 44 |
+
type="filepath",
|
| 45 |
+
height="auto",
|
| 46 |
+
elem_classes="inline-file",
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
with gr.Row():
|
| 50 |
+
message = gr.Textbox(
|
| 51 |
+
show_label=False,
|
| 52 |
+
placeholder="Your request to the Sidekick",
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
with gr.Row():
|
| 56 |
+
success_criteria = gr.Textbox(
|
| 57 |
+
show_label=False, placeholder="What are your success critiera?"
|
| 58 |
+
)
|
| 59 |
+
with gr.Row():
|
| 60 |
+
reset_button = gr.Button("Reset", variant="stop")
|
| 61 |
+
go_button = gr.Button("Go!", variant="primary")
|
| 62 |
+
|
| 63 |
+
ui.load(setup, [], [sidekick])
|
| 64 |
+
message.submit(
|
| 65 |
+
process_message, [sidekick, message, success_criteria, chatbot, file_uploader], [chatbot, sidekick]
|
| 66 |
+
)
|
| 67 |
+
success_criteria.submit(
|
| 68 |
+
process_message, [sidekick, message, success_criteria, chatbot, file_uploader], [chatbot, sidekick]
|
| 69 |
+
)
|
| 70 |
+
go_button.click(
|
| 71 |
+
process_message, [sidekick, message, success_criteria, chatbot, file_uploader], [chatbot, sidekick]
|
| 72 |
+
)
|
| 73 |
+
reset_button.click(reset, [], [message, success_criteria, chatbot, sidekick, file_uploader])
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
ui.launch(inbrowser=True)
|
sandbox/dinner.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dinner Report: Le Bernardin
|
| 2 |
+
|
| 3 |
+
## Restaurant Overview
|
| 4 |
+
- **Name**: Le Bernardin
|
| 5 |
+
- **Address**: 155 W 51st St, New York, NY 10019
|
| 6 |
+
- **Phone**: (212) 554-1515
|
| 7 |
+
- **Website**: [le-bernardin.com](http://le-bernardin.com)
|
| 8 |
+
|
| 9 |
+
## Cuisine
|
| 10 |
+
Le Bernardin specializes in refined seafood dishes, crafted with the utmost respect for the ingredients. The menu includes a variety of seafood preparations, with an emphasis on freshness and simplicity.
|
| 11 |
+
|
| 12 |
+
## Menu Highlights
|
| 13 |
+
- **Tuna Tartare**: Diced raw tuna, served with toasted sesame and avocado.
|
| 14 |
+
- **Wild Salmon**: Lightly cooked, served with a warm ginger-soy emulsion and bok choy.
|
| 15 |
+
- **Poached Lobster**: Accompanied by truffle butter and a delicate sauce.
|
| 16 |
+
- **Chocolate Soufflé**: A classic dessert, rich and airy.
|
| 17 |
+
|
| 18 |
+
## Ambiance
|
| 19 |
+
The atmosphere at Le Bernardin is elegant and serene, characterized by minimalist decor and a focus on the dining experience.
|
| 20 |
+
|
| 21 |
+
## Summary of Reviews
|
| 22 |
+
Le Bernardin has consistently received rave reviews for its exceptional cuisine and impeccable service. It is a three-Michelin star restaurant, recognized as one of the best seafood restaurants not only in New York but also in the world. Diners emphasize the harmonious flavors and the artistry of the dishes, making it a top choice for special occasions.
|
| 23 |
+
|
| 24 |
+
**In conclusion**, Le Bernardin represents the pinnacle of fine dining, offering diners an unforgettable experience centered around seafood excellence.
|
sandbox/kenya_presidents_and_economic_changes.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Presidents of Kenya from 1963
|
| 2 |
+
|
| 3 |
+
1. **Jomo Kenyatta** (1964–1978)
|
| 4 |
+
- First President of Kenya and a key figure in the country's transition to independence.
|
| 5 |
+
|
| 6 |
+
2. **Daniel arap Moi** (1978–2002)
|
| 7 |
+
- Served as Kenya's second president and led the country for more than two decades.
|
| 8 |
+
|
| 9 |
+
3. **Mwai Kibaki** (2002–2013)
|
| 10 |
+
- Third president, known for implementing significant economic reforms and policies.
|
| 11 |
+
|
| 12 |
+
4. **Uhuru Kenyatta** (2013–2022)
|
| 13 |
+
- Fourth president and son of Jomo Kenyatta, focused on development and infrastructure.
|
| 14 |
+
|
| 15 |
+
5. **William Ruto** (2022–present)
|
| 16 |
+
- The current president, previously served as Deputy President.
|
| 17 |
+
|
| 18 |
+
# Major Economic Changes During Their Administrations
|
| 19 |
+
|
| 20 |
+
### Jomo Kenyatta Administration (1964–1978)
|
| 21 |
+
- **Africanization of the Economy**: Promoted policies that favored Kenyan citizens in business ownership to reduce foreign control.
|
| 22 |
+
- **Infrastructure Development**: Focused on building roads, schools, and hospitals to improve access and services.
|
| 23 |
+
- **Land Redistribution**: Implemented land reforms to redistribute land to Kenyans, though it often favored loyalists.
|
| 24 |
+
- **Education Expansion**: Increased access to education, which improved literacy rates and skilled labor availability.
|
| 25 |
+
- **Pro-Western Economic Policies**: Established relationships with Western countries, facilitating trade and economic aid.
|
| 26 |
+
|
| 27 |
+
### Daniel arap Moi Administration (1978–2002)
|
| 28 |
+
- **Decentralization of Government**: Attempted to decentralize powers to local governments for better resource allocation.
|
| 29 |
+
- **Structural Adjustment Programs**: Engaged in economic reforms due to IMF and World Bank pressures, leading to privatization of some state-owned enterprises.
|
| 30 |
+
- **Tourism Promotion**: Marketed Kenya as a tourist destination, leading to growth in the tourism sector.
|
| 31 |
+
- **Agricultural Reforms**: Emphasized agricultural productivity and introduced new farming techniques to enhance food security.
|
| 32 |
+
- **Infrastructure Investment**: Continued investment in infrastructure, particularly in rural areas, to support economic activities.
|
| 33 |
+
|
| 34 |
+
### Mwai Kibaki Administration (2002–2013)
|
| 35 |
+
- **Economic Growth Initiatives**: Implemented policies that led to significant economic growth, reducing poverty levels.
|
| 36 |
+
- **Vision 2030**: Launched a development program aimed at transforming Kenya into a newly industrialized middle-income country.
|
| 37 |
+
- **Healthcare Reforms**: Improved healthcare access and services through investments in health systems.
|
| 38 |
+
- **Investment in ICT**: Promoted the ICT sector, leading to Kenya becoming a hub for technology in Africa.
|
| 39 |
+
- **Improved Business Environment**: Enacted reforms to enhance the ease of doing business, attracting foreign investments.
|
| 40 |
+
|
| 41 |
+
### Uhuru Kenyatta Administration (2013–2022)
|
| 42 |
+
- **Infrastructure Development**: Focused on major infrastructure projects, including roads, railways, and power projects to support growth.
|
| 43 |
+
- **Big Four Agenda**: Introduced initiatives aiming at enhancing manufacturing, affordable housing, universal healthcare, and enhancing agriculture.
|
| 44 |
+
- **Tourism Recovery**: Implemented measures to revitalize tourism, which was affected by security concerns.
|
| 45 |
+
- **Strengthened Financial Services Sector**: Promoted financial inclusion and access to banking services.
|
| 46 |
+
- **Digital Economy Initiatives**: Encouraged the use of technology to improve government services and business operations.
|
| 47 |
+
|
| 48 |
+
### William Ruto Administration (2022–present)
|
| 49 |
+
- **Focus on Agricultural Development**: Emphasis on improving agricultural production and food security.
|
| 50 |
+
- **Youth Empowerment Programs**: Initiatives aimed at creating jobs for the youth and promoting entrepreneurship.
|
| 51 |
+
- **Affordable Housing Initiatives**: Continued focus on housing as part of urban planning and housing policies.
|
| 52 |
+
- **Private Sector Engagement**: Promoting partnerships between the government and the private sector to drive economic growth.
|
| 53 |
+
- **Digital Transformation**: Continued encouragement of digital innovation and technology adoption in various sectors.
|
| 54 |
+
|
sidekick.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dis import opmap
|
| 2 |
+
from typing import Annotated
|
| 3 |
+
from typing_extensions import TypedDict
|
| 4 |
+
from langgraph.graph import StateGraph, START, END
|
| 5 |
+
from langgraph.graph.message import add_messages
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
from langgraph.prebuilt import ToolNode
|
| 8 |
+
from langchain_openai import ChatOpenAI
|
| 9 |
+
from langgraph.checkpoint.memory import MemorySaver
|
| 10 |
+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
|
| 11 |
+
from typing import List, Any, Optional, Dict
|
| 12 |
+
from pydantic import BaseModel, Field
|
| 13 |
+
from sidekick_tools import playwright_tools, other_tools
|
| 14 |
+
import uuid
|
| 15 |
+
import asyncio
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
|
| 18 |
+
load_dotenv(override=True)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class State(TypedDict):
|
| 22 |
+
messages: Annotated[List[Any], add_messages]
|
| 23 |
+
success_criteria: str
|
| 24 |
+
feedback_on_work: Optional[str]
|
| 25 |
+
success_criteria_met: bool
|
| 26 |
+
user_input_needed: bool
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class EvaluatorOutput(BaseModel):
|
| 30 |
+
feedback: str = Field(description="Feedback on the assistant's response")
|
| 31 |
+
success_criteria_met: bool = Field(description="Whether the success criteria have been met")
|
| 32 |
+
user_input_needed: bool = Field(
|
| 33 |
+
description="True if more input is needed from the user, or clarifications, or the assistant is stuck"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class Sidekick:
|
| 38 |
+
def __init__(self):
|
| 39 |
+
self.worker_llm_with_tools = None
|
| 40 |
+
self.evaluator_llm_with_output = None
|
| 41 |
+
self.tools = None
|
| 42 |
+
self.llm_with_tools = None
|
| 43 |
+
self.graph = None
|
| 44 |
+
self.sidekick_id = str(uuid.uuid4())
|
| 45 |
+
self.memory = MemorySaver()
|
| 46 |
+
self.browser = None
|
| 47 |
+
self.playwright = None
|
| 48 |
+
|
| 49 |
+
async def setup(self):
|
| 50 |
+
self.tools, self.browser, self.playwright = await playwright_tools()
|
| 51 |
+
self.tools += await other_tools()
|
| 52 |
+
worker_llm = ChatOpenAI(model="gpt-4.1-mini")
|
| 53 |
+
self.worker_llm_with_tools = worker_llm.bind_tools(self.tools)
|
| 54 |
+
evaluator_llm = ChatOpenAI(model="gpt-4o-mini")
|
| 55 |
+
self.evaluator_llm_with_output = evaluator_llm.with_structured_output(EvaluatorOutput)
|
| 56 |
+
await self.build_graph()
|
| 57 |
+
|
| 58 |
+
def worker(self, state: State) -> Dict[str, Any]:
|
| 59 |
+
system_message = f"""You are a helpful assistant that can use tools to complete tasks.
|
| 60 |
+
You keep working on a task until either you have a question or clarification for the user, or the success criteria is met.
|
| 61 |
+
You have many tools to help you, including tools to browse the internet, navigating and retrieving web pages.
|
| 62 |
+
You have a tool to run python code, but note that you would need to include a print() statement if you wanted to receive output.
|
| 63 |
+
The current date and time is {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
| 64 |
+
|
| 65 |
+
This is the success criteria:
|
| 66 |
+
{state["success_criteria"]}
|
| 67 |
+
You should reply either with a question for the user about this assignment, or with your final response.
|
| 68 |
+
If you have a question for the user, you need to reply by clearly stating your question. An example might be:
|
| 69 |
+
|
| 70 |
+
Question: please clarify whether you want a summary or a detailed answer
|
| 71 |
+
|
| 72 |
+
If you've finished, reply with the final answer, and don't ask a question; simply reply with the answer.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
if state.get("feedback_on_work"):
|
| 76 |
+
system_message += f"""
|
| 77 |
+
Previously you thought you completed the assignment, but your reply was rejected because the success criteria was not met.
|
| 78 |
+
Here is the feedback on why this was rejected:
|
| 79 |
+
{state["feedback_on_work"]}
|
| 80 |
+
With this feedback, please continue the assignment, ensuring that you meet the success criteria or have a question for the user."""
|
| 81 |
+
|
| 82 |
+
# Add in the system message
|
| 83 |
+
|
| 84 |
+
found_system_message = False
|
| 85 |
+
messages = state["messages"]
|
| 86 |
+
for message in messages:
|
| 87 |
+
if isinstance(message, SystemMessage):
|
| 88 |
+
message.content = system_message
|
| 89 |
+
found_system_message = True
|
| 90 |
+
|
| 91 |
+
if not found_system_message:
|
| 92 |
+
messages = [SystemMessage(content=system_message)] + messages
|
| 93 |
+
|
| 94 |
+
# Invoke the LLM with tools
|
| 95 |
+
response = self.worker_llm_with_tools.invoke(messages)
|
| 96 |
+
|
| 97 |
+
# Return updated state
|
| 98 |
+
return {
|
| 99 |
+
"messages": [response],
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
def worker_router(self, state: State) -> str:
|
| 103 |
+
last_message = state["messages"][-1]
|
| 104 |
+
|
| 105 |
+
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
| 106 |
+
return "tools"
|
| 107 |
+
else:
|
| 108 |
+
return "evaluator"
|
| 109 |
+
|
| 110 |
+
def format_conversation(self, messages: List[Any]) -> str:
|
| 111 |
+
conversation = "Conversation history:\n\n"
|
| 112 |
+
for message in messages:
|
| 113 |
+
if isinstance(message, HumanMessage):
|
| 114 |
+
conversation += f"User: {message.content}\n"
|
| 115 |
+
elif isinstance(message, AIMessage):
|
| 116 |
+
text = message.content or "[Tools use]"
|
| 117 |
+
conversation += f"Assistant: {text}\n"
|
| 118 |
+
return conversation
|
| 119 |
+
|
| 120 |
+
def evaluator(self, state: State) -> State:
|
| 121 |
+
last_response = state["messages"][-1].content
|
| 122 |
+
|
| 123 |
+
system_message = """You are an evaluator that determines if a task has been completed successfully by an Assistant.
|
| 124 |
+
Assess the Assistant's last response based on the given criteria. Respond with your feedback, and with your decision on whether the success criteria has been met,
|
| 125 |
+
and whether more input is needed from the user."""
|
| 126 |
+
|
| 127 |
+
user_message = f"""You are evaluating a conversation between the User and Assistant. You decide what action to take based on the last response from the Assistant.
|
| 128 |
+
|
| 129 |
+
The entire conversation with the assistant, with the user's original request and all replies, is:
|
| 130 |
+
{self.format_conversation(state["messages"])}
|
| 131 |
+
|
| 132 |
+
The success criteria for this assignment is:
|
| 133 |
+
{state["success_criteria"]}
|
| 134 |
+
|
| 135 |
+
And the final response from the Assistant that you are evaluating is:
|
| 136 |
+
{last_response}
|
| 137 |
+
|
| 138 |
+
Respond with your feedback, and decide if the success criteria is met by this response.
|
| 139 |
+
Also, decide if more user input is required, either because the assistant has a question, needs clarification, or seems to be stuck and unable to answer without help.
|
| 140 |
+
|
| 141 |
+
The Assistant has access to a tool to write files. If the Assistant says they have written a file, then you can assume they have done so.
|
| 142 |
+
Overall you should give the Assistant the benefit of the doubt if they say they've done something. But you should reject if you feel that more work should go into this.
|
| 143 |
+
|
| 144 |
+
"""
|
| 145 |
+
if state["feedback_on_work"]:
|
| 146 |
+
user_message += f"Also, note that in a prior attempt from the Assistant, you provided this feedback: {state['feedback_on_work']}\n"
|
| 147 |
+
user_message += "If you're seeing the Assistant repeating the same mistakes, then consider responding that user input is required."
|
| 148 |
+
|
| 149 |
+
evaluator_messages = [
|
| 150 |
+
SystemMessage(content=system_message),
|
| 151 |
+
HumanMessage(content=user_message),
|
| 152 |
+
]
|
| 153 |
+
|
| 154 |
+
eval_result = self.evaluator_llm_with_output.invoke(evaluator_messages)
|
| 155 |
+
new_state = {
|
| 156 |
+
"messages": [
|
| 157 |
+
{
|
| 158 |
+
"role": "assistant",
|
| 159 |
+
"content": f"Evaluator Feedback on this answer: {eval_result.feedback}",
|
| 160 |
+
}
|
| 161 |
+
],
|
| 162 |
+
"feedback_on_work": eval_result.feedback,
|
| 163 |
+
"success_criteria_met": eval_result.success_criteria_met,
|
| 164 |
+
"user_input_needed": eval_result.user_input_needed,
|
| 165 |
+
}
|
| 166 |
+
return new_state
|
| 167 |
+
|
| 168 |
+
def route_based_on_evaluation(self, state: State) -> str:
|
| 169 |
+
if state["success_criteria_met"] or state["user_input_needed"]:
|
| 170 |
+
return "END"
|
| 171 |
+
else:
|
| 172 |
+
return "worker"
|
| 173 |
+
|
| 174 |
+
async def build_graph(self):
|
| 175 |
+
# Set up Graph Builder with State
|
| 176 |
+
graph_builder = StateGraph(State)
|
| 177 |
+
|
| 178 |
+
# Add nodes
|
| 179 |
+
graph_builder.add_node("worker", self.worker)
|
| 180 |
+
graph_builder.add_node("tools", ToolNode(tools=self.tools))
|
| 181 |
+
graph_builder.add_node("evaluator", self.evaluator)
|
| 182 |
+
|
| 183 |
+
# Add edges
|
| 184 |
+
graph_builder.add_conditional_edges(
|
| 185 |
+
"worker", self.worker_router, {"tools": "tools", "evaluator": "evaluator"}
|
| 186 |
+
)
|
| 187 |
+
graph_builder.add_edge("tools", "worker")
|
| 188 |
+
graph_builder.add_conditional_edges(
|
| 189 |
+
"evaluator", self.route_based_on_evaluation, {"worker": "worker", "END": END}
|
| 190 |
+
)
|
| 191 |
+
graph_builder.add_edge(START, "worker")
|
| 192 |
+
|
| 193 |
+
# Compile the graph
|
| 194 |
+
self.graph = graph_builder.compile(checkpointer=self.memory)
|
| 195 |
+
|
| 196 |
+
async def run_superstep(self, message, success_criteria, history, files):
|
| 197 |
+
|
| 198 |
+
config = {"configurable": {"thread_id": self.sidekick_id}}
|
| 199 |
+
|
| 200 |
+
# files will be a list of filepaths, and contents or None
|
| 201 |
+
file_info = ""
|
| 202 |
+
file_contents = []
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
if files:
|
| 206 |
+
# Convert the list of paths to something usable
|
| 207 |
+
file_info = "\nUploaded Files:\n"
|
| 208 |
+
for f in files:
|
| 209 |
+
# OPen the contents
|
| 210 |
+
file_info += f"- {f}\n"
|
| 211 |
+
with open(f, "rb") as file:
|
| 212 |
+
data = file.read()
|
| 213 |
+
|
| 214 |
+
file_contents.append({"name": f.split("/")[-1], "data": data})
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
# print(f"file info {file_contents}")
|
| 219 |
+
# print(f"message {message}")
|
| 220 |
+
|
| 221 |
+
full_message = f"{message} {f'Use the provided files {file_contents}' if len(file_contents) > 0 else ""}"
|
| 222 |
+
|
| 223 |
+
print(f"full message {full_message}")
|
| 224 |
+
|
| 225 |
+
state = {
|
| 226 |
+
"messages": full_message,
|
| 227 |
+
"success_criteria": success_criteria or "The answer should be clear and accurate",
|
| 228 |
+
"feedback_on_work": None,
|
| 229 |
+
"success_criteria_met": False,
|
| 230 |
+
"user_input_needed": False,
|
| 231 |
+
}
|
| 232 |
+
result = await self.graph.ainvoke(state, config=config)
|
| 233 |
+
user = {"role": "user", "content": full_message}
|
| 234 |
+
reply = {"role": "assistant", "content": result["messages"][-2].content}
|
| 235 |
+
feedback = {"role": "assistant", "content": result["messages"][-1].content}
|
| 236 |
+
return history + [user, reply, feedback]
|
| 237 |
+
|
| 238 |
+
def cleanup(self):
|
| 239 |
+
if self.browser:
|
| 240 |
+
try:
|
| 241 |
+
loop = asyncio.get_running_loop()
|
| 242 |
+
loop.create_task(self.browser.close())
|
| 243 |
+
if self.playwright:
|
| 244 |
+
loop.create_task(self.playwright.stop())
|
| 245 |
+
except RuntimeError:
|
| 246 |
+
# If no loop is running, do a direct run
|
| 247 |
+
asyncio.run(self.browser.close())
|
| 248 |
+
if self.playwright:
|
| 249 |
+
asyncio.run(self.playwright.stop())
|
sidekick_tools.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from playwright.async_api import async_playwright
|
| 2 |
+
from langchain_community.agent_toolkits import PlayWrightBrowserToolkit
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
import os
|
| 5 |
+
import requests
|
| 6 |
+
from langchain.agents import Tool
|
| 7 |
+
from langchain_community.agent_toolkits import FileManagementToolkit
|
| 8 |
+
from langchain_community.tools.wikipedia.tool import WikipediaQueryRun
|
| 9 |
+
from langchain_experimental.tools import PythonREPLTool
|
| 10 |
+
from langchain_community.utilities import GoogleSerperAPIWrapper
|
| 11 |
+
from langchain_community.utilities.wikipedia import WikipediaAPIWrapper
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
load_dotenv(override=True)
|
| 16 |
+
pushover_token = os.getenv("PUSHOVER_TOKEN")
|
| 17 |
+
pushover_user = os.getenv("PUSHOVER_USER")
|
| 18 |
+
pushover_url = "https://api.pushover.net/1/messages.json"
|
| 19 |
+
serper = GoogleSerperAPIWrapper()
|
| 20 |
+
|
| 21 |
+
async def playwright_tools():
|
| 22 |
+
playwright = await async_playwright().start()
|
| 23 |
+
browser = await playwright.chromium.launch(headless=False)
|
| 24 |
+
toolkit = PlayWrightBrowserToolkit.from_browser(async_browser=browser)
|
| 25 |
+
return toolkit.get_tools(), browser, playwright
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def push(text: str):
|
| 29 |
+
"""Send a push notification to the user"""
|
| 30 |
+
requests.post(pushover_url, data = {"token": pushover_token, "user": pushover_user, "message": text})
|
| 31 |
+
return "success"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_file_tools():
|
| 35 |
+
toolkit = FileManagementToolkit(root_dir="sandbox")
|
| 36 |
+
return toolkit.get_tools()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
async def other_tools():
|
| 40 |
+
push_tool = Tool(name="send_push_notification", func=push, description="Use this tool when you want to send a push notification")
|
| 41 |
+
file_tools = get_file_tools()
|
| 42 |
+
|
| 43 |
+
tool_search =Tool(
|
| 44 |
+
name="search",
|
| 45 |
+
func=serper.run,
|
| 46 |
+
description="Use this tool when you want to get the results of an online web search"
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
wikipedia = WikipediaAPIWrapper()
|
| 50 |
+
wiki_tool = WikipediaQueryRun(api_wrapper=wikipedia)
|
| 51 |
+
|
| 52 |
+
python_repl = PythonREPLTool()
|
| 53 |
+
|
| 54 |
+
return file_tools + [push_tool, tool_search, python_repl, wiki_tool]
|
| 55 |
+
|