├── .gitignore ├── README.md ├── notebooks ├── img │ └── memory_course_email.png ├── lesson_2_baseline.ipynb ├── lesson_3_semantic.ipynb ├── lesson_4_episodic.ipynb └── lesson_5_procedural.ipynb ├── pyproject.toml └── src └── memory_course ├── __init__.py ├── examples.py ├── prompts.py ├── schemas.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Jupyter Notebook 24 | .ipynb_checkpoints 25 | *.ipynb 26 | 27 | # Virtual Environment 28 | venv/ 29 | env/ 30 | ENV/ 31 | .env 32 | 33 | # IDE 34 | .idea/ 35 | .vscode/ 36 | *.swp 37 | *.swo 38 | .DS_Store 39 | 40 | # Testing 41 | .coverage 42 | htmlcov/ 43 | .pytest_cache/ 44 | .tox/ 45 | 46 | # Distribution 47 | *.tar.gz 48 | *.whl 49 | 50 | # Misc 51 | .DS_Store 52 | .env.local 53 | .env.development.local 54 | .env.test.local 55 | .env.production.local -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Long-Term Memory Course 2 | 3 | ## Motivation 4 | 5 | TO ADD 6 | 7 | ## End result 8 | 9 | TO ADD 10 | 11 | ![Memory Course App](notebooks/img/memory_course_email.png) 12 | 13 | ## Organization of the course 14 | 15 | The first lesson is a conceptual introduction (slides [here](https://docs.google.com/presentation/d/1zdVyTUydRkgrSx_ZzlNuuKYapy6dTkHKkqYLdcxtTIQ/edit?usp=sharing)) to the different types of memories that we will use in this course. 16 | 17 | The lessons are shown in the `notebooks` folder, structured as follows: 18 | 19 | ``` 20 | - Lesson 2: Baseline Email Assistant 21 | - Lesson 3: Adding Semantic Memory 22 | - Lesson 4: Adding Episodic Memory 23 | - Lesson 5: Adding Procedural Memory 24 | - Lesson 6: Deploying the App 25 | ``` 26 | 27 | ## Running the Application 28 | 29 | TODO -------------------------------------------------------------------------------- /notebooks/img/memory_course_email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/long_term_memory_course/d5d43be824b78a042dc662dcf852d7b328e8f714/notebooks/img/memory_course_email.png -------------------------------------------------------------------------------- /notebooks/lesson_2_baseline.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "0c9a132e", 6 | "metadata": {}, 7 | "source": [ 8 | "# Lesson 2: Baseline Email Assistant\n", 9 | "\n", 10 | "This lesson builds an email assistant that:\n", 11 | "- Classifies incoming messages (respond, ignore, notify)\n", 12 | "- Drafts responses\n", 13 | "- Schedules meetings\n", 14 | "\n", 15 | "We'll start with a simple implementation - one that uses hard-coded rules to handle emails.\n", 16 | "\n", 17 | "![Memory Course App](./img/memory_course_email.png)\n", 18 | "\n", 19 | "First, install the prerequisite packages:" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 1, 25 | "id": "01b255f0", 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "%%capture stderr\n", 30 | "%pip install -e ..\n", 31 | "%pip install -U -q langchain-anthropic langgraph langchain" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 1, 37 | "id": "bc3d445b", 38 | "metadata": { 39 | "lines_to_next_cell": 2 40 | }, 41 | "outputs": [ 42 | { 43 | "name": "stdin", 44 | "output_type": "stream", 45 | "text": [ 46 | "ANTHROPIC_API_KEY: ········\n" 47 | ] 48 | } 49 | ], 50 | "source": [ 51 | "import os\n", 52 | "from getpass import getpass\n", 53 | "\n", 54 | "if not os.environ.get(\"ANTHROPIC_API_KEY\"):\n", 55 | " os.environ[\"ANTHROPIC_API_KEY\"] = getpass(\"ANTHROPIC_API_KEY: \")" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "id": "507fee0d-e3e7-4a71-9a06-902b573ecdd0", 61 | "metadata": {}, 62 | "source": [ 63 | "### Define profile information and example" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 3, 69 | "id": "913c8641-c277-442c-bd85-31f48383832e", 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "# Example user profile\n", 74 | "profile = {\n", 75 | " \"name\": \"John\",\n", 76 | " \"full_name\": \"John Doe\",\n", 77 | " \"user_profile_background\": \"Senior software engineer leading a team of 5 developers\",\n", 78 | "}\n", 79 | "\n", 80 | "prompt_instructions = {\n", 81 | " \"triage_rules\": {\n", 82 | " \"ignore\": \"Marketing newsletters, spam emails, mass company announcements\",\n", 83 | " \"notify\": \"Team member out sick, build system notifications, project status updates\",\n", 84 | " \"respond\": \"Direct questions from team members, meeting requests, critical bug reports\",\n", 85 | " },\n", 86 | " \"agent_instructions\": \"Use these tools when appropriate to help manage John's tasks efficiently.\"\n", 87 | "}\n", 88 | "\n", 89 | "# Example incoming email\n", 90 | "email = {\n", 91 | " \"from\": \"Alice Smith \",\n", 92 | " \"to\": \"John Doe \",\n", 93 | " \"subject\": \"Quick question about API documentation\",\n", 94 | " \"body\": \"\"\"\n", 95 | "Hi John,\n", 96 | "\n", 97 | "I was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\n", 98 | "\n", 99 | "Specifically, I'm looking at:\n", 100 | "- /auth/refresh\n", 101 | "- /auth/validate\n", 102 | "\n", 103 | "Thanks!\n", 104 | "Alice\"\"\",\n", 105 | "}" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "id": "0dc3e26f", 111 | "metadata": {}, 112 | "source": [ 113 | "### Define Triage\n", 114 | "\n", 115 | "The triage step is the \"first line of defense\" against incoming emails. It helps the assistant determine if the email should be responded to, ignored, or notified." 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 4, 121 | "id": "6f5f75bc", 122 | "metadata": {}, 123 | "outputs": [ 124 | { 125 | "name": "stdout", 126 | "output_type": "stream", 127 | "text": [ 128 | "Classification: respond\n", 129 | "Reason: 1. This is a direct question from a team member (Alice)\n", 130 | "2. It's regarding important technical documentation (API endpoints)\n", 131 | "3. The question requires specific information about whether certain API endpoints were intentionally omitted\n", 132 | "4. As a team lead, John should address documentation concerns to ensure proper team coordination\n", 133 | "5. This is related to authentication service which is typically a critical component\n", 134 | "6. A lack of response could lead to confusion or implementation issues\n" 135 | ] 136 | } 137 | ], 138 | "source": [ 139 | "from memory_course.schemas import Router\n", 140 | "from langchain.chat_models import init_chat_model\n", 141 | "from memory_course.prompts import triage_system_prompt, triage_user_prompt\n", 142 | "\n", 143 | "llm = init_chat_model(\"anthropic:claude-3-5-sonnet-latest\")\n", 144 | "\n", 145 | "# We'll use structured output to generate classification results\n", 146 | "llm_router = llm.with_structured_output(Router)\n", 147 | "\n", 148 | "system_prompt = triage_system_prompt.format(\n", 149 | " full_name=profile[\"full_name\"],\n", 150 | " name=profile[\"name\"],\n", 151 | " examples=None,\n", 152 | " user_profile_background=profile[\"user_profile_background\"],\n", 153 | " triage_no=prompt_instructions[\"triage_rules\"][\"ignore\"],\n", 154 | " triage_notify=prompt_instructions[\"triage_rules\"][\"notify\"],\n", 155 | " triage_email=prompt_instructions[\"triage_rules\"][\"respond\"],\n", 156 | ")\n", 157 | "\n", 158 | "user_prompt = triage_user_prompt.format(\n", 159 | " author=email[\"from\"],\n", 160 | " to=email[\"to\"],\n", 161 | " subject=email[\"subject\"],\n", 162 | " email_thread=email[\"body\"],\n", 163 | ")\n", 164 | "\n", 165 | "# Test classification\n", 166 | "result = llm_router.invoke(\n", 167 | " [\n", 168 | " {\"role\": \"system\", \"content\": system_prompt},\n", 169 | " {\"role\": \"user\", \"content\": user_prompt},\n", 170 | " ]\n", 171 | ")\n", 172 | "print(f\"Classification: {result.classification}\\nReason: {result.reasoning}\")" 173 | ] 174 | }, 175 | { 176 | "cell_type": "markdown", 177 | "id": "f97a1bf5", 178 | "metadata": {}, 179 | "source": [ 180 | "### Define Tools\n", 181 | "\n", 182 | "Define tools that the agent can use. These are place-holder tools for the purpose of testing the LLM." 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 5, 188 | "id": "11ec23f0", 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "from langchain_core.tools import tool\n", 193 | "\n", 194 | "\n", 195 | "@tool\n", 196 | "def write_email(to: str, subject: str, content: str) -> str:\n", 197 | " \"\"\"Write and send an email.\"\"\"\n", 198 | " # Placeholder response - in real app would send email\n", 199 | " return f\"Email sent to {to} with subject '{subject}'\"\n", 200 | "\n", 201 | "\n", 202 | "@tool\n", 203 | "def schedule_meeting(\n", 204 | " attendees: list[str], subject: str, duration_minutes: int, preferred_day: str\n", 205 | ") -> str:\n", 206 | " \"\"\"Schedule a calendar meeting.\"\"\"\n", 207 | " # Placeholder response - in real app would check calendar and schedule\n", 208 | " return f\"Meeting '{subject}' scheduled for {preferred_day} with {len(attendees)} attendees\"\n", 209 | "\n", 210 | "\n", 211 | "@tool\n", 212 | "def check_calendar_availability(day: str) -> str:\n", 213 | " \"\"\"Check calendar availability for a given day.\"\"\"\n", 214 | " # Placeholder response - in real app would check actual calendar\n", 215 | " return f\"Available times on {day}: 9:00 AM, 2:00 PM, 4:00 PM\"" 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "id": "6d7b02e3-6105-46e0-93b7-3d507566ff35", 221 | "metadata": {}, 222 | "source": [ 223 | "### Create agent" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 6, 229 | "id": "d9ddcdb8-0fd6-4320-b713-264df726f956", 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [ 233 | "def create_prompt(state):\n", 234 | " return [\n", 235 | " {\"role\": \"system\", \"content\": agent_system_prompt.format(instructions=prompt_instructions[\"agent_instructions\"], **profile)}\n", 236 | " ] + state['messages']" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": 7, 242 | "id": "167a1474", 243 | "metadata": {}, 244 | "outputs": [ 245 | { 246 | "name": "stdout", 247 | "output_type": "stream", 248 | "text": [ 249 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 250 | "\n", 251 | "Based on the calendar check, you have three available time slots on Tuesday:\n", 252 | "- 9:00 AM\n", 253 | "- 2:00 PM\n", 254 | "- 4:00 PM\n", 255 | "\n", 256 | "Would you like me to schedule any meetings during these available times?\n" 257 | ] 258 | } 259 | ], 260 | "source": [ 261 | "\n", 262 | "from memory_course.prompts import agent_system_prompt\n", 263 | "from langgraph.prebuilt import create_react_agent\n", 264 | "\n", 265 | "agent = create_react_agent(\n", 266 | " \"anthropic:claude-3-5-sonnet-latest\",\n", 267 | " tools=[write_email, schedule_meeting, check_calendar_availability],\n", 268 | " prompt=create_prompt,\n", 269 | ")\n", 270 | "\n", 271 | "# Test the agent\n", 272 | "response = agent.invoke(\n", 273 | " {\"messages\": [{\"role\": \"user\", \"content\": \"What's my availability on Tuesday?\"}]}\n", 274 | ")\n", 275 | "response[\"messages\"][-1].pretty_print()" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "id": "fb4d4c67", 281 | "metadata": {}, 282 | "source": [ 283 | "### Build agent\n", 284 | "\n", 285 | "Combine triage with tool calling agent\n" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 8, 291 | "id": "55cf741a", 292 | "metadata": { 293 | "lines_to_next_cell": 2 294 | }, 295 | "outputs": [ 296 | { 297 | "data": { 298 | "image/png": "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", 299 | "text/plain": [ 300 | "" 301 | ] 302 | }, 303 | "metadata": {}, 304 | "output_type": "display_data" 305 | } 306 | ], 307 | "source": [ 308 | "from langgraph.graph import StateGraph, START, END\n", 309 | "from langgraph.types import Command\n", 310 | "from typing import Literal\n", 311 | "from IPython.display import Image, display\n", 312 | "from memory_course.schemas import State\n", 313 | "from memory_course.utils import parse_email\n", 314 | "\n", 315 | "\n", 316 | "def triage_router(state: State) -> Command[Literal[\"response_agent\", \"__end__\"]]:\n", 317 | " \"\"\"Analyze email content to decide if we should respond, notify, or ignore.\n", 318 | "\n", 319 | " The triage step prevents the assistant from wasting time on:\n", 320 | " - Marketing emails and spam\n", 321 | " - Company-wide announcements\n", 322 | " - Messages meant for other teams\n", 323 | " \"\"\"\n", 324 | " author, to, subject, email_thread = parse_email(state[\"email_input\"])\n", 325 | " system_prompt = triage_system_prompt.format(\n", 326 | " full_name=profile[\"full_name\"],\n", 327 | " name=profile[\"name\"],\n", 328 | " user_profile_background=profile[\"user_profile_background\"],\n", 329 | " triage_no=prompt_instructions[\"triage_rules\"][\"ignore\"],\n", 330 | " triage_notify=prompt_instructions[\"triage_rules\"][\"notify\"],\n", 331 | " triage_email=prompt_instructions[\"triage_rules\"][\"respond\"],\n", 332 | " examples=None\n", 333 | " )\n", 334 | "\n", 335 | " user_prompt = triage_user_prompt.format(\n", 336 | " author=author, to=to, subject=subject, email_thread=email_thread\n", 337 | " )\n", 338 | "\n", 339 | " result = llm_router.invoke(\n", 340 | " [\n", 341 | " {\"role\": \"system\", \"content\": system_prompt},\n", 342 | " {\"role\": \"user\", \"content\": user_prompt},\n", 343 | " ]\n", 344 | " )\n", 345 | " if result.classification == \"respond\":\n", 346 | " print(\"📧 Classification: RESPOND - This email requires a response\")\n", 347 | " goto = \"response_agent\"\n", 348 | " update = {\n", 349 | " \"messages\": [\n", 350 | " {\n", 351 | " \"role\": \"user\",\n", 352 | " \"content\": f\"Respond to the email {state['email_input']}\",\n", 353 | " }\n", 354 | " ]\n", 355 | " }\n", 356 | " elif result.classification == \"ignore\":\n", 357 | " print(\"🚫 Classification: IGNORE - This email can be safely ignored\")\n", 358 | " update = None\n", 359 | " goto = END\n", 360 | " elif result.classification == \"notify\":\n", 361 | " # If real life, this would do something else\n", 362 | " print(\"🔔 Classification: NOTIFY - This email contains important information\")\n", 363 | " update = None\n", 364 | " goto = END\n", 365 | " else:\n", 366 | " raise ValueError(f\"Invalid classification: {result.classification}\")\n", 367 | " return Command(goto=goto, update=update)\n", 368 | "\n", 369 | "\n", 370 | "# Build workflow\n", 371 | "agent = (\n", 372 | " StateGraph(State)\n", 373 | " .add_node(triage_router)\n", 374 | " .add_node(\"response_agent\", agent)\n", 375 | " .add_edge(START, \"triage_router\")\n", 376 | " .compile()\n", 377 | ")\n", 378 | "\n", 379 | "# Show the agent\n", 380 | "display(Image(agent.get_graph(xray=True).draw_mermaid_png()))" 381 | ] 382 | }, 383 | { 384 | "cell_type": "code", 385 | "execution_count": 9, 386 | "id": "12ae5d5c", 387 | "metadata": {}, 388 | "outputs": [ 389 | { 390 | "name": "stdout", 391 | "output_type": "stream", 392 | "text": [ 393 | "🚫 Classification: IGNORE - This email can be safely ignored\n" 394 | ] 395 | } 396 | ], 397 | "source": [ 398 | "email_input = {\n", 399 | " \"author\": \"Marketing Team \",\n", 400 | " \"to\": \"John Doe \",\n", 401 | " \"subject\": \"🔥 EXCLUSIVE OFFER: Limited Time Discount on Developer Tools! 🔥\",\n", 402 | " \"email_thread\": \"\"\"Dear Valued Developer,\n", 403 | "\n", 404 | "Don't miss out on this INCREDIBLE opportunity! \n", 405 | "\n", 406 | "🚀 For a LIMITED TIME ONLY, get 80% OFF on our Premium Developer Suite! \n", 407 | "\n", 408 | "✨ FEATURES:\n", 409 | "- Revolutionary AI-powered code completion\n", 410 | "- Cloud-based development environment\n", 411 | "- 24/7 customer support\n", 412 | "- And much more!\n", 413 | "\n", 414 | "💰 Regular Price: $999/month\n", 415 | "🎉 YOUR SPECIAL PRICE: Just $199/month!\n", 416 | "\n", 417 | "🕒 Hurry! This offer expires in:\n", 418 | "24 HOURS ONLY!\n", 419 | "\n", 420 | "Click here to claim your discount: https://amazingdeals.com/special-offer\n", 421 | "\n", 422 | "Best regards,\n", 423 | "Marketing Team\n", 424 | "---\n", 425 | "To unsubscribe, click here\n", 426 | "\"\"\",\n", 427 | "}\n", 428 | "\n", 429 | "response = agent.invoke({\"email_input\": email_input})" 430 | ] 431 | }, 432 | { 433 | "cell_type": "code", 434 | "execution_count": 10, 435 | "id": "ebca8e98", 436 | "metadata": {}, 437 | "outputs": [ 438 | { 439 | "name": "stdout", 440 | "output_type": "stream", 441 | "text": [ 442 | "📧 Classification: RESPOND - This email requires a response\n", 443 | "================================\u001b[1m Human Message \u001b[0m=================================\n", 444 | "\n", 445 | "Respond to the email {'author': 'Alice Smith ', 'to': 'John Doe ', 'subject': 'Quick question about API documentation', 'email_thread': \"Hi John,\\n\\nI was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\\n\\nSpecifically, I'm looking at:\\n- /auth/refresh\\n- /auth/validate\\n\\nThanks!\\nAlice\"}\n", 446 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 447 | "\n", 448 | "[{'citations': None, 'text': \"I'll help draft a polite and professional response to Alice regarding her API documentation query. I'll use the write_email function to send the response.\", 'type': 'text'}, {'id': 'toolu_015dZQfhhfrR3pUopagsZFhk', 'input': {'to': 'alice.smith@company.com', 'subject': 'Re: Quick question about API documentation', 'content': \"Hi Alice,\\n\\nThank you for bringing this to John's attention. I'm John's assistant, and I've received your inquiry about the API documentation for the authentication service endpoints.\\n\\nI'll make sure John reviews this specific concern about the missing endpoints (/auth/refresh and /auth/validate) in the documentation. He'll get back to you with clarification about whether these omissions were intentional or if the documentation needs to be updated.\\n\\nIn the meantime, would it be helpful to schedule a brief meeting with John to discuss this in detail?\\n\\nBest regards,\\nJohn's Assistant\"}, 'name': 'write_email', 'type': 'tool_use'}]\n", 449 | "Tool Calls:\n", 450 | " write_email (toolu_015dZQfhhfrR3pUopagsZFhk)\n", 451 | " Call ID: toolu_015dZQfhhfrR3pUopagsZFhk\n", 452 | " Args:\n", 453 | " to: alice.smith@company.com\n", 454 | " subject: Re: Quick question about API documentation\n", 455 | " content: Hi Alice,\n", 456 | "\n", 457 | "Thank you for bringing this to John's attention. I'm John's assistant, and I've received your inquiry about the API documentation for the authentication service endpoints.\n", 458 | "\n", 459 | "I'll make sure John reviews this specific concern about the missing endpoints (/auth/refresh and /auth/validate) in the documentation. He'll get back to you with clarification about whether these omissions were intentional or if the documentation needs to be updated.\n", 460 | "\n", 461 | "In the meantime, would it be helpful to schedule a brief meeting with John to discuss this in detail?\n", 462 | "\n", 463 | "Best regards,\n", 464 | "John's Assistant\n", 465 | "=================================\u001b[1m Tool Message \u001b[0m=================================\n", 466 | "Name: write_email\n", 467 | "\n", 468 | "Email sent to alice.smith@company.com with subject 'Re: Quick question about API documentation'\n", 469 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 470 | "\n", 471 | "I've sent an initial response to Alice that:\n", 472 | "1. Acknowledges receipt of her email\n", 473 | "2. Confirms the specific endpoints she's asking about\n", 474 | "3. Indicates that John will review the concern\n", 475 | "4. Opens the door for a potential meeting if needed for detailed discussion\n", 476 | "\n", 477 | "Would you like me to also schedule a follow-up meeting between John and Alice to discuss this matter in more detail?\n" 478 | ] 479 | } 480 | ], 481 | "source": [ 482 | "email_input = {\n", 483 | " \"author\": \"Alice Smith \",\n", 484 | " \"to\": \"John Doe \",\n", 485 | " \"subject\": \"Quick question about API documentation\",\n", 486 | " \"email_thread\": \"\"\"Hi John,\n", 487 | "\n", 488 | "I was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\n", 489 | "\n", 490 | "Specifically, I'm looking at:\n", 491 | "- /auth/refresh\n", 492 | "- /auth/validate\n", 493 | "\n", 494 | "Thanks!\n", 495 | "Alice\"\"\",\n", 496 | "}\n", 497 | "\n", 498 | "response = agent.invoke({\"email_input\": email_input})\n", 499 | "for m in response[\"messages\"]:\n", 500 | " m.pretty_print()" 501 | ] 502 | }, 503 | { 504 | "cell_type": "code", 505 | "execution_count": null, 506 | "id": "87dd01b5-06cc-412b-a4ff-c430de81d7b0", 507 | "metadata": {}, 508 | "outputs": [], 509 | "source": [] 510 | } 511 | ], 512 | "metadata": { 513 | "kernelspec": { 514 | "display_name": "Python 3 (ipykernel)", 515 | "language": "python", 516 | "name": "python3" 517 | }, 518 | "language_info": { 519 | "codemirror_mode": { 520 | "name": "ipython", 521 | "version": 3 522 | }, 523 | "file_extension": ".py", 524 | "mimetype": "text/x-python", 525 | "name": "python", 526 | "nbconvert_exporter": "python", 527 | "pygments_lexer": "ipython3", 528 | "version": "3.11.7" 529 | } 530 | }, 531 | "nbformat": 4, 532 | "nbformat_minor": 5 533 | } 534 | -------------------------------------------------------------------------------- /notebooks/lesson_3_semantic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Lesson 3: Email Assistant with Semantic Memory\n", 8 | "\n", 9 | "We previously built an email assistant that:\n", 10 | "- Classifies incoming messages (respond, ignore, notify)\n", 11 | "- Drafts responses\n", 12 | "- Schedules meetings\n", 13 | "\n", 14 | "Now, we'll add memory. \n", 15 | "\n", 16 | "We'll give the assistant the ability to remember details from previous emails. \n", 17 | "\n", 18 | "First, install the prerequisite packages:" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 23, 24 | "metadata": { 25 | "vscode": { 26 | "languageId": "shellscript" 27 | } 28 | }, 29 | "outputs": [], 30 | "source": [ 31 | "%%capture stderr\n", 32 | "%pip install -e ..\n", 33 | "%pip install -U -q langchain-anthropic langgraph langchain langmem==0.0.5rc9" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "import os\n", 43 | "from getpass import getpass\n", 44 | "\n", 45 | "if not os.environ.get(\"ANTHROPIC_API_KEY\"):\n", 46 | " # For the LLM\n", 47 | " os.environ[\"ANTHROPIC_API_KEY\"] = getpass(\"ANTHROPIC_API_KEY: \")\n", 48 | "if not os.environ.get(\"OPENAI_API_KEY\"):\n", 49 | " # For the embedding model\n", 50 | " os.environ[\"OPENAI_API_KEY\"] = getpass(\"OPENAI_API_KEY: \")" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "### Define profile information and example" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 2, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "# Example user profile\n", 67 | "profile = {\n", 68 | " \"name\": \"John\",\n", 69 | " \"full_name\": \"John Doe\",\n", 70 | " \"user_profile_background\": \"Senior software engineer leading a team of 5 developers\",\n", 71 | "}\n", 72 | "\n", 73 | "prompt_instructions = {\n", 74 | " \"triage_rules\": {\n", 75 | " \"ignore\": \"Marketing newsletters, spam emails, mass company announcements\",\n", 76 | " \"notify\": \"Team member out sick, build system notifications, project status updates\",\n", 77 | " \"respond\": \"Direct questions from team members, meeting requests, critical bug reports\",\n", 78 | " },\n", 79 | " \"agent_instructions\": \"Use these tools when appropriate to help manage John's tasks efficiently.\"\n", 80 | "}\n", 81 | "\n", 82 | "# Example incoming email\n", 83 | "email = {\n", 84 | " \"from\": \"Alice Smith \",\n", 85 | " \"to\": \"John Doe \",\n", 86 | " \"subject\": \"Quick question about API documentation\",\n", 87 | " \"body\": \"\"\"\n", 88 | "Hi John,\n", 89 | "\n", 90 | "I was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\n", 91 | "\n", 92 | "Specifically, I'm looking at:\n", 93 | "- /auth/refresh\n", 94 | "- /auth/validate\n", 95 | "\n", 96 | "Thanks!\n", 97 | "Alice\"\"\",\n", 98 | "}" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "### Define Triage\n", 106 | "\n", 107 | "The triage step is the \"first line of defense\" against incoming emails. It helps the assistant determine if the email should be responded to, ignored, or notified." 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 3, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "from memory_course.schemas import Router\n", 117 | "from langchain.chat_models import init_chat_model\n", 118 | "from memory_course.prompts import triage_system_prompt, triage_user_prompt\n", 119 | "\n", 120 | "llm = init_chat_model(\"anthropic:claude-3-5-sonnet-latest\")\n", 121 | "\n", 122 | "# We'll use structured output to generate classification results\n", 123 | "llm_router = llm.with_structured_output(Router)\n" 124 | ] 125 | }, 126 | { 127 | "cell_type": "markdown", 128 | "metadata": {}, 129 | "source": [ 130 | "### Define Tools\n", 131 | "\n", 132 | "Define tools that the agent can use. These are place-holder tools for the purpose of testing the LLM." 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 4, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "from langchain_core.tools import tool\n", 142 | "\n", 143 | "@tool\n", 144 | "def write_email(to: str, subject: str, content: str) -> str:\n", 145 | " \"\"\"Write and send an email.\"\"\"\n", 146 | " # Placeholder response - in real app would send email\n", 147 | " return f\"Email sent to {to} with subject '{subject}'\"\n", 148 | "\n", 149 | "\n", 150 | "@tool\n", 151 | "def schedule_meeting(\n", 152 | " attendees: list[str], subject: str, duration_minutes: int, preferred_day: str\n", 153 | ") -> str:\n", 154 | " \"\"\"Schedule a calendar meeting.\"\"\"\n", 155 | " # Placeholder response - in real app would check calendar and schedule\n", 156 | " return f\"Meeting '{subject}' scheduled for {preferred_day} with {len(attendees)} attendees\"\n", 157 | "\n", 158 | "\n", 159 | "@tool\n", 160 | "def check_calendar_availability(day: str) -> str:\n", 161 | " \"\"\"Check calendar availability for a given day.\"\"\"\n", 162 | " # Placeholder response - in real app would check actual calendar\n", 163 | " return f\"Available times on {day}: 9:00 AM, 2:00 PM, 4:00 PM\"" 164 | ] 165 | }, 166 | { 167 | "cell_type": "markdown", 168 | "metadata": {}, 169 | "source": [ 170 | "### Define memory store and tools\n", 171 | "\n", 172 | "We will now create two tools for managing memory. These are for creating and searching memory collections" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 5, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "from langgraph.store.memory import InMemoryStore\n", 182 | "\n", 183 | "# Memory store\n", 184 | "store = InMemoryStore(index={\"embed\": \"openai:text-embedding-3-small\"})" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": 6, 190 | "metadata": {}, 191 | "outputs": [], 192 | "source": [ 193 | "from langmem import create_manage_memory_tool, create_search_memory_tool\n", 194 | "\n", 195 | "manage_memory_tool = create_manage_memory_tool(namespace=(\"email_assistant\", \"{langgraph_user_id}\", \"collection\"))\n", 196 | "search_memory_tool = create_search_memory_tool(namespace=(\"email_assistant\", \"{langgraph_user_id}\", \"collection\"))" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "### Define agent\n" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": 7, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "agent_system_prompt_memory = \"\"\"\n", 213 | "< Role >\n", 214 | "You are {full_name}'s executive assistant. You are a top-notch executive assistant who cares about {name} performing as well as possible.\n", 215 | "\n", 216 | "\n", 217 | "< Tools >\n", 218 | "You have access to the following tools to help manage {name}'s communications and schedule:\n", 219 | "\n", 220 | "1. write_email(to, subject, content) - Send emails to specified recipients\n", 221 | "2. schedule_meeting(attendees, subject, duration_minutes, preferred_day) - Schedule calendar meetings\n", 222 | "3. check_calendar_availability(day) - Check available time slots for a given day\n", 223 | "4. manage_memory - Store any relevant information about contacts, actions, discussion, etc. in memory for future reference\n", 224 | "5. search_memory - Search for any relevant information that may have been stored in memory\n", 225 | "\n", 226 | "\n", 227 | "< Instructions >\n", 228 | "{instructions}\n", 229 | "\n", 230 | "\"\"\"" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 8, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "# Create agent\n", 240 | "\n", 241 | "from langgraph.prebuilt import create_react_agent\n", 242 | "import json\n", 243 | "\n", 244 | "def create_prompt(state):\n", 245 | " return [\n", 246 | " {\"role\": \"system\", \"content\": agent_system_prompt_memory.format(instructions=prompt_instructions[\"agent_instructions\"], **profile)}\n", 247 | " ] + state['messages']\n", 248 | "\n", 249 | "\n", 250 | "# Create agent\n", 251 | "response_agent = create_react_agent(\n", 252 | " \"anthropic:claude-3-5-sonnet-latest\",\n", 253 | " tools=[write_email, schedule_meeting, \n", 254 | " check_calendar_availability, \n", 255 | " manage_memory_tool,\n", 256 | " search_memory_tool\n", 257 | " ],\n", 258 | " prompt=create_prompt,\n", 259 | " # Use this to ensure the store is passed to the agent \n", 260 | " store=store\n", 261 | ")" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 9, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "# Test the agent\n", 271 | "config = {\"configurable\": {\"langgraph_user_id\": \"lance\"}}\n", 272 | "response = response_agent.invoke(\n", 273 | " {\"messages\": [{\"role\": \"user\", \"content\": \"Jim is my friend\"}]},\n", 274 | " config=config\n", 275 | ")" 276 | ] 277 | }, 278 | { 279 | "cell_type": "markdown", 280 | "metadata": {}, 281 | "source": [ 282 | "We can see that it calls the memory tool to store information about Jim in the memory database." 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": 10, 288 | "metadata": {}, 289 | "outputs": [ 290 | { 291 | "data": { 292 | "text/plain": [ 293 | "[HumanMessage(content='Jim is my friend', additional_kwargs={}, response_metadata={}, id='10ba37bf-9734-4d55-8abb-eb1a2b9e1176'),\n", 294 | " AIMessage(content=[{'citations': None, 'text': \"I'll help store this information about Jim in the memory system for future reference.\", 'type': 'text'}, {'id': 'toolu_01VRZj2xsThQuURjbdbNVvuU', 'input': {'content': \"Jim is John Doe's friend.\"}, 'name': 'manage_memory', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_016CpRpEi23sKwyXzaj1qG14', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 1121, 'output_tokens': 76}}, id='run-0b659560-f0f2-4134-a73b-7a6ea37a648d-0', tool_calls=[{'name': 'manage_memory', 'args': {'content': \"Jim is John Doe's friend.\"}, 'id': 'toolu_01VRZj2xsThQuURjbdbNVvuU', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1121, 'output_tokens': 76, 'total_tokens': 1197, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}),\n", 295 | " ToolMessage(content='created memory d890dac2-0b33-44ec-9ae0-1c5010a62793', name='manage_memory', id='27fe7ba6-5f1c-401e-a996-c9d133c2dd63', tool_call_id='toolu_01VRZj2xsThQuURjbdbNVvuU'),\n", 296 | " AIMessage(content=\"I've made a note that Jim is your friend. This information will be helpful for future interactions or communications involving Jim. Is there anything specific about Jim that you'd like me to know or help you with, such as scheduling a meeting or sending an email?\", additional_kwargs={}, response_metadata={'id': 'msg_01YPgpoKSguUdUSx239jJk9T', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 1234, 'output_tokens': 56}}, id='run-deedcba3-893a-4c68-9b15-48c499e9e404-0', usage_metadata={'input_tokens': 1234, 'output_tokens': 56, 'total_tokens': 1290, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}})]" 297 | ] 298 | }, 299 | "execution_count": 10, 300 | "metadata": {}, 301 | "output_type": "execute_result" 302 | } 303 | ], 304 | "source": [ 305 | "response[\"messages\"]" 306 | ] 307 | }, 308 | { 309 | "cell_type": "markdown", 310 | "metadata": {}, 311 | "source": [ 312 | "If we call it again, we can see that it remembers who Jim is" 313 | ] 314 | }, 315 | { 316 | "cell_type": "code", 317 | "execution_count": 11, 318 | "metadata": { 319 | "scrolled": true 320 | }, 321 | "outputs": [ 322 | { 323 | "name": "stderr", 324 | "output_type": "stream", 325 | "text": [ 326 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 327 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 328 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 329 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 330 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 331 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 332 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 333 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 334 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 335 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 336 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n" 337 | ] 338 | } 339 | ], 340 | "source": [ 341 | "# Test the agent\n", 342 | "config = {\"configurable\": {\"langgraph_user_id\": \"lance\"}}\n", 343 | "response = response_agent.invoke(\n", 344 | " {\"messages\": [{\"role\": \"user\", \"content\": \"who is jim?\"}]},\n", 345 | " config=config\n", 346 | ")" 347 | ] 348 | }, 349 | { 350 | "cell_type": "code", 351 | "execution_count": 12, 352 | "metadata": {}, 353 | "outputs": [ 354 | { 355 | "data": { 356 | "text/plain": [ 357 | "[HumanMessage(content='who is jim?', additional_kwargs={}, response_metadata={}, id='535b96db-541e-48d6-ae85-45388a4687ab'),\n", 358 | " AIMessage(content=[{'citations': None, 'text': 'Let me search through my memories to see if I have any information about Jim.', 'type': 'text'}, {'id': 'toolu_01QX1caNMSK6PVpnjuVhPQ9o', 'input': {'query': 'Jim'}, 'name': 'search_memory', 'type': 'tool_use'}], additional_kwargs={}, response_metadata={'id': 'msg_015kLvTsp25yfACx3TWnpWmo', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 1121, 'output_tokens': 70}}, id='run-94d767d1-18b4-4567-8af6-11d709fee901-0', tool_calls=[{'name': 'search_memory', 'args': {'query': 'Jim'}, 'id': 'toolu_01QX1caNMSK6PVpnjuVhPQ9o', 'type': 'tool_call'}], usage_metadata={'input_tokens': 1121, 'output_tokens': 70, 'total_tokens': 1191, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}}),\n", 359 | " ToolMessage(content='[{\"namespace\": [\"email_assistant\", \"lance\", \"collection\"], \"key\": \"d890dac2-0b33-44ec-9ae0-1c5010a62793\", \"value\": {\"content\": \"Jim is John Doe\\'s friend.\"}, \"created_at\": \"2025-02-09T21:01:46.167740+00:00\", \"updated_at\": \"2025-02-09T21:01:46.167744+00:00\", \"score\": null}]', name='search_memory', id='994ab38c-bb11-4e16-9b9b-801b2d161ac3', tool_call_id='toolu_01QX1caNMSK6PVpnjuVhPQ9o', artifact=[Item(namespace=['email_assistant', 'lance', 'collection'], key='d890dac2-0b33-44ec-9ae0-1c5010a62793', value={'content': \"Jim is John Doe's friend.\"}, created_at='2025-02-09T21:01:46.167740+00:00', updated_at='2025-02-09T21:01:46.167744+00:00', score=None)]),\n", 360 | " AIMessage(content=\"Based on the stored memory, Jim is John Doe's friend. However, I don't seem to have much detailed information about him. If you'd like to know more specific details about Jim or if you'd like me to remember additional information about him, please let me know and I can store that in my memory for future reference.\", additional_kwargs={}, response_metadata={'id': 'msg_017gcjTE1KDxvNjzjCEXquqo', 'model': 'claude-3-5-sonnet-20241022', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'input_tokens': 1319, 'output_tokens': 72}}, id='run-76c93bf2-c65a-4195-8aac-c140aee65004-0', usage_metadata={'input_tokens': 1319, 'output_tokens': 72, 'total_tokens': 1391, 'input_token_details': {'cache_read': 0, 'cache_creation': 0}})]" 361 | ] 362 | }, 363 | "execution_count": 12, 364 | "metadata": {}, 365 | "output_type": "execute_result" 366 | } 367 | ], 368 | "source": [ 369 | "response[\"messages\"]" 370 | ] 371 | }, 372 | { 373 | "cell_type": "markdown", 374 | "metadata": {}, 375 | "source": [ 376 | "### Build agent + triage workflow\n", 377 | "\n", 378 | "Combine triage with tool calling agent\n", 379 | "\n" 380 | ] 381 | }, 382 | { 383 | "cell_type": "code", 384 | "execution_count": 13, 385 | "metadata": {}, 386 | "outputs": [ 387 | { 388 | "data": { 389 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAaEAAAHxCAIAAABLRJRiAAAAAXNSR0IArs4c6QAAIABJREFUeJzt3WdcU+ffBvBfBhAgbGSDuMWBgmDRoqJCVURcuHDg1pZqrVprq/86Wvdsq1WqrXtULSrinq1YFyrWPVFEZI8QICHreREfaitDEHJyDtf344uY5JxcYVzcZ+Q+PI1GQwAAHMVnOgAAQA1CxwEAl6HjAIDL0HEAwGXoOADgMnQcAHCZkOkAANWvQKLKSSsulCgLJCqVUqNSsuAEKb6AhAZ8U3OBibnQys7Q1ELAdCKO4OH8OOCMvEzFoxvSxNtSHo8nMOCZmgtNzAWm5kKlQs10tIoJhbxCqaogT1UgUapVGpVKU7+FuGErsaWdAdPR2A0dB1wgK1D/dShTLlNb1jGo38LUvq6I6UTvKy1JnnhbmpuuMDDit+9lYyzGsK6K0HHAejfO5l47ld2+l20zP3Oms1S/e5clFw5lteli5dXFkuksrISOA3Y7sumVcwPjVh05/vv/9/m8Fw8Le45xZDoI++C4KrDYbyteNGljzvmCIyLPDhYebc13LkliOgj7YBwHbLV94fNOYXaujY2ZDqI7KU9lp3amjZhdl+kgbIKOA1Y6tjm1YWtxw9ZipoPo2tO/C+7HS4JHY6P1XaHjgH1u/pFHpGnVifubqKX6+3yeWqVpHVBL335lYX8csIyiWHPxSGatLTjtvrkrx7OLi1hw0p8+QMcBy/x1KPPDXrZMp2BY+142Fw5lMp2CHdBxwCbSXKU0V9nS30I3L3f79m25XM7U4uVo0d5CVqCWZCtrYuUcg44DNnl6q8DMSkcfsj506NDIkSOLiooYWbxCZtbCp39La2jlXIKOAzZ5eltar4WOjqVWeQimPY5XQyO4EvVbmD69hY6rGDoOWENZrCmWqWvihLjnz59PnDjR398/ODh44cKFarX60KFDixcvJqLAwEAfH59Dhw4RUVpa2pw5cwIDA/38/AYNGnTs2DHt4rm5uT4+Ptu2bZs9e7a/v/+4ceNKXbx6OTUw1mhIVoAjDxXA3ErAGrmZCpWiRk51+vbbb589ezZt2rSCgoL4+Hg+n//hhx8OGzZs+/btq1evFovFbm5uRKRUKu/cuRMWFmZpaXnmzJnZs2e7uro2b95cu5JffvllwIAB69evFwgE9vb2by9e7VQqTV6WQmRqVBMr5wx0HLBGQZ7S1KJGfmJTUlKaNm3at29fIho2bBgRWVtbu7i4EFGLFi0sLV+fp+Ls7Lx3714ej0dEvXv3DgwMPHfuXEnHtWzZMjIysmSdby9e7cTmwoI8JRE6rjzYVgXWKJQoTc1rpOOCg4MvXbq0dOnS7Ozs8p/58OHDqVOndu/evW/fviqVKisrq+Shtm3b1kS2cphYCAskOLRaAXQcsIaGeAZGNfITGxkZOXXq1BMnToSGhu7Zs6esp129ejUiIqK4uHjOnDlLly61sLBQq//ZHWZsrOtPzhoY8gkfU6oIOg5Yw0TMl2QV18SaeTxeeHj4wYMHO3XqtHTp0oSEhJKH3vyw48aNG11cXFavXt2uXTtPT893KbUa/aykJFthbIa5MyuAjgPWMDEXFkhUNbFm7XkepqamEydOJKL79++XjMsyMjJKnpabm9u4cWOhUEhExcXFhYWFb47j/uPtxatdzW28cwm+QMAaZlYGpmY18hP75ZdfisViPz+/uLg4IvLw8CCiVq1aCQSC5cuXh4aGyuXy/v37a88COXjwoIWFxY4dOyQSyZMnT8oaqb29eLXHFpkKzCzxK1wBwdy5c5nOAPBODAx5N//MtXUyElf3L3ZycnJcXNyxY8eKioomTZoUEBBARObm5vb29idPnjx//rxEIgkJCWnVqtXTp093794dHx8fFBQ0aNCg48ePN23a1MbGZuvWrf7+/s2aNStZ59uLV2/mzJfy+1ck3l2tqne13IO5lYBN4k/mKBVqv2AbpoMw78qxbCJq292a6SD6DgNdYJN6zcXxp8o7vaOwsDA4OLjUh1xcXJKTk9++v1OnTvPmzau+jKVbs2bNvn373r7fyMio1E99ubu7b968uZwV5mYovLpgEFcxjOOAZY5uetXY26xBq9I/tapWq1NTU0t9iMcr/afd2NjYyqrGyyIvL6+goODt+4uLiw0NDd++XygU2tnZlbW2p7cK7l2R4BI27wLjOGCZ9r1sD65PKavj+Hy+k5OTzkNVzMLCwsKi2qaE+utQZs+x+vg29RDOHQGWsbA1aOxl9vB67Z1y49ENaX1PsZWdAdNB2AEdB+zj19P6+pmcjOSanbxIP2W9Ko4/md0+BEdd3hU6Dlhp8HTXPStfaGrfxEK7liYNmVEjs5hwFY45AFuplJpNc56FTXGxrFMrttryMhV7V78YNaeewIDHdBY2QccBi6lVmp1Lkvz71HFvZsJ0lpqVdL/w3L6M8BluQkMUXOWg44D1/vw9I/NV8Ye9bOzripjOUv3Sk+QXDmVa2xt2CqvDdBZWQscBF7x8XPTXoUwHd2P7ukb1WogN2D/YURZrEm8XpCXJUp4Wte9l69JI1xM3cQY6Drjj2Z3CB9clibcL6rcQG5nwTc2FJmYCY1OBSs2CH3K+gC+TKgvzVQUSZXGR+snf0notTBt7m9VrYcp0NHZDxwEHJT8syk4rLsxXFkpURCSXVfPx18uXL7dt21Y76Xl1MTTi8Xg8E3OBiZnQyt6wJi7NUzuh4wAq7YMPPrhw4YJ2IjnQczg/DgC4DB0HAFyGjgOotJYtW1bvzjioOeg4gEq7desWdmSzBToOoNKsrKwwjmMLdBxApeXk5GAcxxboOIBKc3FxwTiOLdBxAJWWnJyMcRxboOMAKs3LywvjOLZAxwFU2o0bNzCOYwt0HABwGToOoNJsbGywrcoW6DiASsvKysK2Klug4wAqzcHBAeM4tkDHAVRaamoqxnFsgY4DAC5DxwFUWpMmTbCtyhboOIBKe/DgAbZV2QIdBwBcho4DqLRWrVphW5Ut0HEAlXbz5k1sq7IFOg4AuAwdB1BpmHeERdBxAJWGeUdYBB0HAFyGjgOoNFx7kEXQcQCVhmsPsgg6DgC4DB0HUGm4viqLoOMAKg3XV2URdBxApXl4eGAcxxboOIBKu3fvHsZxbIGOAwAuQ8cBVJqzszO2VdkCHQdQaS9fvsS2Klug4wAqDfPHsQg6DqDSMH8ci6DjACqtdevWGMexBToOoNISEhIwjmMLdBxApbm7u2McxxY8/DkCeEfdu3c3NDQkooyMDBsbGz6fr1ar3dzcfvrpJ6ajQZmETAcAYI309HQ+//WmT2pqKhGZm5uPGDGC6VxQHmyrAryr9u3b/+eexo0b+/n5MRQH3gk6DuBdRUREmJubl/zX3Nx85MiRjCaCiqHjAN6Vr69vkyZNtLc1Go2HhwcGcfoPHQdQCaNHj7axsSEiCwuL4cOHMx0HKoaOA6gEX19fDw8PjUbTpEkTDOJYAcdVgXlFUlXGS3mxTM10kHfSM2BMTrJRry6DHt+UMp3lnRgZ8W1djIzFAqaDMAPnxwGTlArNye1pL58UuTYxLZazo+NYx0jET7pf4NzAOHCovYFhrTt1GR0HjJEXqX//IfmDHnZ2dUVMZ+G+jBfyS4fT+01yFpnUrj1Utevdgl7ZvTwpYJAjCk436rgadQ133LU0iekguoaOA2bcviBp0MrczMqA6SC1iIm5sImvxc0/85gOolPoOGBGapLM1ByHvHTN1FyYliRjOoVOoeOAGcUytZkNBnG6ZmZtwJbj19UFHQfMkBWoNLXrd00vaNQkL1AxnUKn0HEAwGXoOADgMnQcAHAZOg4AuAwdBwBcho4DAC5DxwEAl6HjAIDL0HEAwGXoOADgMnQcAHAZOg5YQ6VS3bqVUP5zlErlsBF9161fratQ1enuvdtyuZzpFFyDjgPWWLbi25WrF5b/HB6PZ2ZmLhKxb97NY8cPRX46UiYrYjoI12ACL2CN4nLHOBqNhsfjCQSCdWu3VHbNL1OSnRydebxquNaBNkYVFqzyCK7Kr1hLoOOAHRYvnXv23Eki6tzVh4h27ohxdHAaNWZgPfcG7u4Novfvlstla37YNHb8ECIaNnT0mNGfENHRYzEHDux5mvjY2NikrW+7TyOnW1paEZFCofh107pTp48WFRV6eno/fHhv+LCxvUPDiOhGQvyGjWuePHloZWXt1dp37JhIGxvbcoKd++PUvPkzv523/Le92+7fvzNkcMToUR9nZWWuW7/q8pULSqWyZYvWEydMqV+/IRFN+myMsch46ZI12mV/27NtfdT3x45cOHvuxOrvFxNRn36BRPTljDndu/UqJ8x/3nj0vpPGxsa6+lawDDoO2GFY+OiM9LRXr15+NXM+EdlYv+6dq1cvyuSyhd+tKiwqdHZ2/Xb+8nnzZ5YsdffuLTc396Cg4Jyc7Oj9uwsKCxYtWE1E63/+PiZm39gxkba2duvWr5LLZT26hxLRtetXZn41OSgwuG+fQfmSvN+jd02dPjFq3fYKN36//3HJ2NGRo0d97OLsJpPJpk6fKJHkjR83WWQk2vXblqnTJ27but9MbFbW4h+0/XDggGF79m5ftGC1qanYxcWtwjBvvnEUXDnQccAOLi5uFhaW2TlZLVu2fvN+gVD4v1kLS37J/T8MeHPDbernX5f8VygUbt/xq1wuFwqFsbHRPYP7DBo4XLutt2Dh7Fu3E9p4t/1xzbJeIf0mT5qhXcTHxy9iVNjV+Isd/DuXH69vn0HduoVobx+KjU5KerZi+TpvL18iatnSK3xYaHT07ogR48pa3MrK2snJhYg8PFpYWFhq7yw/zH/eOJQFHQfs5uHRopzfc4VCEb1/98lTR9LTU42MRGq1Ojc3x8DAoLi42NnZVfsc7Y38fElq6qvnzxNfvnwRe3j/mytJT0+rMIa3d9uS2zdvXhObirUFR0QODo5ubu4PHt6t1PuqMEz5bxxKoOOA3YxFZf6eazSar2dNefDwbsSI8c2aeZ4/f2b3b1vVGrWFhaXYVHzrVsKAsKFEdO/ebSJqUL9RTk4WEUWMGN+xQ5c312NtXd7+OC0TY5OS29ICqYWl1ZuPmptbZGVmVOp9VRimnDcOb0LHAZtU6pLnN29ev3b9yqyvvwvs2p2IXia/vrSoQCAYMmTkho1rvlswy9bW7mDM3v79hri61n3x4jkRyeUyNzf39wlZx9bu7t1bb96TnZ1lb+egPbWl/GVL3qBYbFYtYQDnxwFriETG2dlZavW7XuomT5JLRI0bNX3zv9rF+/Qe6Ovjl5OTLZXmz/r6u08jp2l3+dnbOxw9FlNU9PokNaVSqVAoKpuzeXPP/HyJdnhIRE+ePHr58oV2N6KlhVVWdmbJM1NTU0pua8dlmf8/3KuuMICOA9Zo5emdny9ZuWrh8eOxf/31Z4XPb+bR0tDQcMPGNZcuX9i5a/PmLVFElPj0MRF9u+Brc3OL4OA+Xl6+POKlpaVqB1mRn0zLysqMnDTywMG90dG7Iz8deTBmb2VzBnbt4eLiNnf+l7GH9x85enD2/6ZaWlr1Dh1ARL6+7Z4+fbxn7/aHj+5v3hJ1+MiBkqWat2glEAjW/LT8+PHYmEO/V1cYQMcBawQFBfftM/DcHyd/3vjjnbt/V/j8OnXsZs9a8Ojx/bnzZly7dnnliig/P//o/buJyNvL9+Kl898tmPXdglmzv5k2dHjvEycOE1EH/86LFqw2EBqs/WnF1u0b7e0dPT29K5tTKBQuW7K2SeNm69av+nHNMjc39+9XbbCysiaiHt1DBw4Ytvu3rdOmT8zISB84YFjJUs5OLtOmznrx4vmatcvPnTtZXWGAV6kdHADVJXrNy5YdrB3cmdlxrlKpBAKB9rYkXzLzq8lCofCH1RsZCaNL6UmyhDOZ/T9zYTqI7uCYA9RGK1YuePLkYbt2HS0trZJePHv69FHPnn3LerJUKh0yNKTUhyaM/yyk7AVBH6DjoDZq27Z9enrq79E7FQqFo6PziOHjtOeRlMrExOTnqJ2lPmRuZlGTMaEaoOOgNgroFBjQKfAdn8zn8x0dnGo4EdQUHHMAAC5DxwEAl6HjAIDL0HEAwGXoOADgMnQcAHAZOg4AuAwdBwBcho4DAC5DxwEAl6HjgBkWNkJMecMIC1tDpiPoFDoOmGFiJsxMljGdotbJSJaJxLXrt752vVvQH+7NTPMyi5lOUetIMovrepgynUKn0HHAgD/++OPgic22ToYXYyt3tSp4H5cOZ1jYCl0b164LemFuJdCdO3fuNG/ePDU19eDBg8OGDfP2trp+Jjduf5pDPRNbZyOBoIJrVkHVqFSazJfytOdFNg4Gvh9ZvcMSnIK5zqHGKRQKAwODjz/+uKCgYOvWrRqN5s1L8CXdK3p4Q1JUoM5JxaZr5eTkZJuYmBoZGZX/NGtHQ5EJv2FrM/dmJuU/k5PQcVCDrl+/HhUVNXny5ObNmycnJ7u41KLLCOiAVCr95ZdfPvvss8zMTFvbii90XTuh46D6xcfHS6XSgICA/fv3u7q6+vj4MJ2I4y5durR9+/bFixeLxWKms+gddBxUmxcvXri6ul68ePHMmTODBw9u0KAB04lqkYsXL8rl8oCAAKlUiqZ7EzoOqoFUKp04caKVldWPP/4ol8sr3EMENWfWrFl+fn69evViOoi+QMdB1cXHx+/ateu7775TKBQpKSlNmzZlOhEQEZ08eTIoKCg+Ph57CXB+HFTF48ePk5KSiOj8+fO9evUyNjY2NzdHwemPoKAgIkpJSenXr19RURHTcRiGcRy8K+215Xfs2BETE/P99987ODgwnQgq8Pz5c5FIZG9vX5sPaqPjoGJFRUWLFy/m8Xhz585NTU1Fu7HO8OHDu3fvPnRomdfJ5jBsq0KZ5HL5nj17iCgzM9PX13fu3LlEhIJjo23bttnb2xPR7du3mc6ia+g4KIVMJiOi3r17p6enE5Grq2tISAjToeC9BAYGElFqaurw4cPlcjnTcXQH26rwL3FxcWvXrl2+fLmzszPTWaBG3L1719zc3MrKytS0VkxAgnEckPbP+927d7V7qefNm4eC47BmzZq5uLgIBIKAgIA7d+4wHafGoeOATpw4MXbsWJFIRERDhw5t3Lgx04mgxolEotjY2Pj4eCLi9sYctlVrr+PHjz969OjTTz999uyZu7s703GAMXPmzPnwww8/+ugjpoPUCIzjaqmnT5/+8ccf/fr1IyIUXC03b968s2fPKpVKpoPUCIzjapfo6OiVK1fGxcUplUqhEDOkwj/UavWFCxfMzMxat27NdJbqhHFcraBSqW7duqX9OT579iwRoeDgP/h8/ocffrhmzZrnz58znaU6oeO47+nTp+3atdOWWlhYmIGBAdOJQE/x+fyNGzfyeLzMzEyms1QbdByX7d+/n4h4PN6VK1c8PDyYjgPs4ObmZm5u7ufnx42mQ8dx1oABA/Lz84moXr16TGcBljE0NDx//vyZM2eYDlINcMyBa+7cuZOamtq1a1eJRGJubs50HGC93bt3Dx48mOkUVYdxHKfcvHlzyZIlbdq0ISIUHFQLhUJx8uRJplNUHcZxHBEXF+fv75+UlOTm5sZ0FuAaVk8pjHEcF+zevfvUqVPavcVMZwEO8vHxuX79+oEDB5gOUhUYx7FbWlqavb19QkICx87bBD109uzZ/Pz80NBQpoNUDjqOxc6ePXvz5s0pU6YwHQRAf2FblcWuXbuGggMd27JlS1paGtMpKgHjOFZKTk4WiUS2trZMB4FaJzs7e9CgQSw60oqOY58VK1Y4OjqGh4czHQRqKYVCoVKptBMO6j90HMukpqYKhUKM4IBZCQkJDRs2FIvFTAepGPbHsUlqamp+fj4KDhgnk8lmzpzJdIp3ggl2WCMjI2PkyJHHjh1jOggA+fn5SSQS7alLTGepALZVWSMuLq5Vq1ZmZmZMBwFgE2yrsoa/vz8KDvTKvHnz9P9Sreg4dujSpYv2us4A+oPH4+n/zhNsq7JAbGxsWlramDFjmA4C8C9SqTQzM1PPr3mEjgMALsO2qr6TSqX3799nOgVA6SIjI589e8Z0ivKg4/Tdrl27zp07x3QKgNLZ2trevn2b6RTlwflx+q6wsLBnz55MpwAo3ZdffqlWq5lOUR7sjwMALsO2ql6TyWR//vkn0ykAynT79m09n+ALHafXHjx4sHnzZqZTAJTJ1NQ0OTmZ6RTlwbaqXrt79+6dO3cGDBjAdBCA0mk0Grlcrs/zLKHjAIDLsK2q1549e3bv3j2mUwCUSSaThYSEMJ2iPDh3RK/FxcVlZmZ6eHgwHQSgdAKBIDMzk+kU5UHH6TV3d3cHBwemUwCUycDAQM8/ll/6/rjiYolCIWEiDwAXmJq6MB0BXit9HPfo0a5Hj3YaGrJgsnZuy8xUqtUaOzsDpoNAJRQUpIWFXeLxasVGklqt7t69+4kTJ5gOUqYyvw2NGgU3bz5Qt2HgvzZv3p+fXzBy5DCmg0Al7Ns3mOkIuqPRaHJzc5lOUZ5a8aeGvWxtrYyN9ffMIwCBQLBlyxamU5QHHafXQkICmI4AUAE9P+6P8+P0Wn5+QW4uDv6A/lKr1VOnTmU6RXnQcXrt999PbNsWw3SKSlCpVAkJOGm5FtFoNHFxcUynKA86Tq+Zm4stLNh0La5vv12/cOEGplOA7ggEgmXLljGdojzYH1cBjUbD4/GYevV+/YJ0/IrJyanOzvZVfstyeXF1JwJ916lTJ6YjlAcd91+nTl2cOXPl8uVfbNt26M6dxxERvT/+eLBMJl+7duexY3FyuaJuXcfhw0M/+uhDInr+PGXRog23bz8yNxf7+3vPnDmWz+cHBEQ0b96wqEj+4EGipaV5SEincePChEIhEWVm5qxatfXChetKpap166ZTpgxv2LAuEe3cGXvixF9Dh4asXbsrMzOnadN6s2dPdHd3Tk/PunTp5o4dh5OTU52c7MLCPho0qAcRlZWnLMXFxRs27Dt+/EJaWpatrVXPnh0nTBgoEAiISKFQrFv329Gj5wsLZd7eHvfuPR07tn9YWDciio+/vWbNzocPn1lbW/j6toiMDLe1tSKigICIr74ad/bslbi462KxSf/+QePGDSCiuXPXnjz5FxH5+AwgopiYtU5Odrr7zgET1Gr1xIkTf/75Z6aDlAkdV7olS36JjBzy8ceD3Nwc1Wr1558vTknJGDWqr7W1RXz8na+/Xl1UJO/du8u336579ixl2rSRBQVF8fG3+fzX2/7PnqV8/vmIOnWszp+/tmnT/vz8ghkzxshk8okT5+Xl5U+ePEwkMtqy5eDEifP37//BzMyUiG7ffrRtW8zs2ROUStWCBVFz5qzZsmXRwYOnN2zY16RJvdmzJz5+nJSRka39qSorT1lvRyAQXL78d8eOPi4u9g8eJP76a7S5uXjYsF5E9P332/ftOxEZOcTOznrVqq0ymTw0tDMRXbny9+TJi4KDOwwa1CMvL3/XriMTJ87bvn2JSGRERHPmrJ0wYWBERO+TJy9GRe3x8Kjv799m9Oi+aWmZL1+mz5//KRHZ2lrq8DsGzNBoNAkJCUynKA86rnSDBnUvOW/j1KmLN27cP3RobZ061kTUvXuHwkLZrl2He/fukpKS0bRpvb59A4lIWxlaQUHtAgPbEVGrVk3z8qTR0acmTBh4+vSlZ89erlv3ja9vSyLy8vIIDY3cvfuIdhBERKtWzbSxsSSiwYODV63akpeXb2trrVKpu3Tx69GjQ8nKz5y5XFaest6OQCDYsmVRyRZocnLamTOXhw3rpVKpoqNP9unTZfjwUO3P6+zZPyQk3G/b1nPZsk39+gXOmPH6oq5+fq3CwqZcvJjQufMHRNS7d5dRo/oSUePG7gcOnL548aa/fxs3NydLS/OsrLzWrfX6ZAKoRgKBQJ8/5ICOK1Pbti1LbsfFXVcqlaGhkSX3qFRqsdiEiIKDO2zefGDp0l/Gju1vbV36sKV9+9b795+6fz/x2rW7YrGJtuCIyNGxjru78927T0qeaWxs9P8P2RJRRkZ2nz5dDx0698svvxsbG/XrF2hoaFh+nnJkZ+dt2LD30qW/JRIpEWkHj7m5+cXFCldXR+1ztDckkoJXrzISE5NfvEjdv//UmytJS8v6T1SBQGBnZ60dYELtZGmp1wN2dFzpTEyMS25nZeXa2lqtXz/nzScIhQIiiowMt7a2+PXX/TExZydPHjZwYPe3V6Vtk8LCIqm00MrK/M2HLCzEGRk5by9iYGCgba6UlPTp00fGxJxdvXrb9u2H5s+f5O3drJw8ZcnKyh06dIaJiejjjwe5uDj89NOu589TiMjS0kwsNklIuDd0aIh2e5mIGjWqm5WVS0Tjxw/o0uWDN9dT6uanUChUqfT6ykxQc9Rq9eDBg/fs2cN0kDKh4ypmbi7OyZE4OtYxMjL8z0M8Hi88PKR37y4LF25YuvSXxo3rvr2Zlp6eTUT29jZ2dta3bj1886GsrFwHB9tyXvrkyb/y8wtmzhw3fHjotGlLp05dcuTI+nLylOX3309kZ+dt3rzAwaEOETk42Go7TiAQjBzZZ82anbNmfW9nZ7137/EhQ4Lr1nXSPiqTyd3dnd/xJUpgZulaRaPR4BrSrNe2bUuVSrVv3z87HYqKZNob2lMlTE1NJk4cSET37yf+Z1mNRhMTc9bMzLRePRdPz8YSiVQ7ViKiR4+ev3iRWv6uq8aN3Rs3diciZ2f7wYODpdLClJT0cvKUJTc338rKXFtw2v+WFNHAgd39/FplZ+fm5xd8993kadNGEZGbm6ODg21MzNmSNSuVSoVCUeHXytjYKCsrV88vuAnVSCAQxMbGMp2iPBjHVSw4uEN09Mnvv9+WkpLetGm9hw+fnT17Zd++1SKR0ZdfrhCLTfz8WsXFXSciD4/62kVOnPjL1tZKJDI8depSfPztyZOHGRuLevTosGnT/i+/XDl2bH8+n79x4z4rK/MBAz4q56V9fVv07Tv5/v0wt9DxAAAgAElEQVTEBg1c9+49LhabuLg41K3rVFaestbj49N8z55j69btbtWqyZkzly9cuKFWq3NzJZaW5l9/vdrCwqxjxzbaYWlqaoaDQx0ejzdt2sgvvlg+cuSssLCPVCpVbOwfwcEdwsMrmNXa27tZTMzZhQt/bt26qbm5uGNHn6p+1YE17Oz0+gwhdFzFDAwM1q6d/eOPO48fvxAdfdLNzSks7CPt/q8WLRrFxv5x5sxlOzubWbMmtGrVVLuInZ11bOy5589T7O1tP/tsuPaopVAoXLv2fytXbl61aqtarfby8pg2bWRZRyq0nj1Ladas/tGj56XSwoYN3VavnqktsrLylKVLF7+xY8P27Dm2Z8+xjh19Nm9e8M03a3777diECQN9fVtGRe05fvz1x3EEAsE333zcs2enzp0/WL165vr1e1as2CwWm3h5NfX2blbh1yo4uOPdu08OH/7z/PlrvXoFoOM4T61WR0REbNu2jekgZSp9HuA7d6KIcjF/XNUEBET06dN1ypQRVV5Dr16fKBRKjUYtkxVrNBoTE2ONRq1QqM6c2VStSUn7CVPtycBEJJFIJ09eKBQKNm78ttpfqPbYt29w//4XuD1H5qRJkx48eCAUCjUaTWZmprW1tUAgUKvVejjvOZe/Dezl6Gh77drdktPZCgtlRNSggWv5S40d+7/Hj5Pevr9TJ9958z4ta6kFC6IePnzesWMbKyuLZ89ePnr0XHu6H0A5QkJC7t+/n56erv1vVlaW9u8l07lKgY7TR0OHhjx+nCyR5JfcY2hooN3gLceiRZ8rFMq37y85l61U7dt7paZm7tx5WKFQOjvbjxsXpj2PBKAc3bp127FjR07OP2c+qdXqDz74oNyFmIGOq37nzr3vtKidOrVt3PjI1au3S4Zyrq4OFc6Xqf3YQ2UFBr7+SAZApQwdOnThwoUFBQXa/1pZWQ0aNIjpUKXAuSN6avDgYEvL1ycMGxoKhw/vVdESADrVrVs3Nzc37W2NRlOvXr3OnTszHaoU6Dg9FRDQtmQHnJubU0iIPv70QC0XHh5uampKRGKxODw8nOk4pUPH6a/w8J4WFmaGhgbh4T2ZzgJQih49eri7uxNR3bp1u3Qpc0oIZmF/XHkkmcTg55K8WrRtUj9OKi3s1L5LXiZzOTRkUYe5V4d3JslS6v6DdP1Dh2e+ihoSNiYvs+KPwVQvPo9nZlNxg+H8uFLkZdLFw8Inf8tdmxjnpOr6O6dvLO2ELx7K6rcwbNtNZVvpT6/WRjo+P06l0Jzbl/koQeLSyDQrRa6bF9UH1o6GKU+KGnmZdR5Qh1f2FinGcf+VnSqIiVJ1GWzfPtSwnC9craLRUF6m4tjWlKBwsq+Lz6LqEXmR+pf/JX40wsk70FZoyNik/ExRyNVZKfKfpj8et7CBoaj0t49f4n/JzaCYKFX/KfWsHFBw/+DxyLKOQe9P6p7aRWmlnGUMjNk46+mwWQ3s6xrXwoIjIgMjvkM94/CvGvz6zdOynoPf43+5dJTfZTC2x8rUdYhL/An8zOiLuIOZAQMd8cdYaMhr39v+UmxWqY/W+i/Pvz1OUFjav+ukbLWQqaUg6aFCiWtv6Yek+4Vm1gZMp9ALZlYGzx8UlvoQOu4fuenk3syYuQsNsoO7h0lOGmbB1AuGIoGlHf4kExFZ2RkaGJbeZui4f2iIctIxRKlAXpYCE/3qibSkInwvtNQaSk8ufaZYdBwAcBk6DgC4DB0HAFyGjgMALkPHAQCXoeMAgMvQcQDAZeg4AOAydBwAcBk6DgC4DB0HAFyGjtMLUqn04aP7TC0OUKMGDOqxctVCpl4dHacXxo4ffPToQaYWB+AwdJxeKC6u4nwn2stxVHlxAM7D9Rx07dKluJ83/piSkuzg4BTaK6xf30GDw0NycrIPHNx74OBee3uH3TtjiejosZgDB/Y8TXxsbGzS1rfdp5HTLS2tiOj7H5b88efp6VNn/7R+1cuXL5Yv+2nZ8vlvLw5QXW4kxG/YuObJk4dWVtZerX3Hjom0sbElol69A6Z89lVc3NlLl+NMTcW9QvpHjBinXUSlUm3dtiH28H6ZrKh1ax+5rPRZj3QDHadThYWFc+d/6V63/rSpsxMTH2dlZRDR3DlLZ3z5aetWbQaEDTUwfD3l4d27t9zc3IOCgnNysqP37y4oLFi0YLX2oYIC6S+bfpry2UyZrMjby7fUxQGqxbXrV2Z+NTkoMLhvn0H5krzfo3dNnT4xat12kUhERIuXzBkZMWHw4Ihz505u3hLVpLGHn5+/9i/xodjoHt1DW3l6X7n6V740n8G3gI7TqZzcbLlc3qFDl6DAHiV3Nm3STCgU2tjYtmzZuuTOqZ9/zfv/KYmFQuH2Hb/K5XIjIyPtlun0qbM9PFqUszhAtfhxzbJeIf0mT5qh/a+Pj1/EqLCr8Rc7+HcmouAevYeGjyKihg0aHz5y4Er8RT8//4eP7h+KjR42dPSY0Z8QUbduIQk3rzH4FtBxOuXk6Ny8uef2Hb+IRMa9QvoZlj3sUigU0ft3nzx1JD091chIpFarc3Nz7O0diEgkEpUUHEDNSU199fx54suXL2IP73/z/vT0NO0NkchYe0MgENSpY5eVmUFE58+fIaKwsKElz+fzmdzvj47TKR6Pt3jhDxt/WbM+avXefdu/+nJ+q1bebz9No9F8PWvKg4d3I0aMb9bM8/z5M7t/26rWvL6wqbGxic6DQ22Uk5NFRBEjxnfs0OXN+62tbd9+slAgVKlVRJSWnioWiy3MLXSYtDw4rqprYrF4ymczt2z+3dRUPPt/UwsLX19MSPPGzPw3b16/dv3KZ5NnhvUPb+bRon69hhWuVoOJ/aG6icVmRCSXy9zc3N/8JxaLy1nK0sJKKpXqz7F+dJyuyeVy7UZrv76DpQXS1NQUIjIWGWdlZZY8J0+SS0SNGzV9879qdZkXqP/P4gDVwsXFzd7e4eixmKKiIu09SqVSoVCUv1Tjxh5EdPrMMZ1krBi2VXVKoVBEjOof0CmonnuDgwf3ik3FTk4uRNSypdfpM8d27tpsZmbevJlnM4+WhoaGGzau6dmz79Onj3bu2kREiU8fOzu5lLra/yxev37F4z6ACvF4vMhPpn0z54vISSNDe4WpVarjJ2KDgoLD+oeXs1TngKBt2zeuXLUwMfFJo4ZN7tz9OzMzQ4ep/wvjOJ0qkhV5tfY9dfro6h8WCw0MFi5YrT0GP2H8ZK/WPtu2b9y5c9PLlBd16tjNnrXg0eP7c+fNuHbt8soVUX5+/tH7d5e12v8srtv3BFzWwb/zogWrDYQGa39asXX7Rnt7R0/PUvYgv0kgECxZ9KOPj1/MoX3rf/6ez+dbWFjqKm8peKXux7lzJ4oot3nzgUxEYkxOOsVupD6R9ZgOotcOb3jeZaDKzg2X2i7Tvn2D+/e/wOPV+EbS2mmPh81uyOhBS32hKNbsWf504pIGbz+EbdWq27pt495929++v1Ejj0eP7pW6yJofNtWtW7MdKpVKhwwNKfUhCwurvLyct+9fvPCH5s09azQVMO76jatz5n7x9v1iUzNpQenn6E4Y/1lIz77VFeDSpbgFi2a/fb9Go9FoNKWeX7J82bomjT3e83XRcVXXp8/AoKDgt+/n83jqMo5y1rG1q+lUJiYmP0ftLPUhRbHCwNDg7fttSjsVADimmUfL0n8wNERlDMrNzarz/I/WrX1KDaBWqzVqtUBYShdVy08mOq7qzM3Mzc3MmU7xX3w+39HBiekUoHdEIhGzPxhMBcCmPABwGToOALgMHQcAXIaOAwAuQ8cBAJeh4wCAy9BxAMBl6DgA4DJ0HABwGToOALgMHfcGjcbKvpSPc8KbLGwNePip0Q+O7iY8zP9CREQ8HtnXNS71Ify0/sPKnpd0T6ZSYtLw8iTeLrRxxC+WXiiWqXJS5Uyn0As5qXJlcekTZaPj/qWRtzAnVV/moddDeRmK+i2EfAHTOYCIiNybm+ZlVjDzeC0hyVLU9Sj9Wk7ouH/p0Ftzckcy0yn016ntye1CMM7VF37B1pePpOfnKJkOwrCctOLrZzLbdrMu9VF03L+ITGnYTP6OhY9fPS0slNT2H50Shfmq1GeFvy170ncSzwJzzemT0d/Wj12f9PyeND+7Ng7oJFmKZ3ekx7ckj5xT5tSzmD/uv0zMacx8wYVDry4cJAsbg4xkJjdd1WoNEfH5TO7/snE2kGQp6zXjhX/JNzHDIE6/CAQ0fnH9v2Iyr5/KMrc2SHtexHQi3bFzM5bmKhq0Eo9bUL+cp6HjSmFgRAFh/IAwUshVzH6JduyIyc8vnDhxMIMZSKM2EGG8r9fah9q2D7VVFteuy+zy+DzhO5wHgY4rj4GRdipo5vCVxFcYGNWiH1yoMqEhlTlteS2Gv88AwGUYx+k1Y2Mjtbr0s34A4F1gHKfXiorkBQW1aC8yQLXDOE6vmZoa8/BpHYD3gI7TawUFRfn5BUynAGAxdJxewzgO4D2h4/QaxnEA7wnHHPSaUCgUCvF3CKDq0HF6TalUKpX42CxA1aHjAIDLsB2k18Ti0qfEAoB3hHGcXpNKC3HMAeB9oOMAgMuwrarXRCJDlQqfVwWoOozj9JpMVlxYiM+rAlQdOg4AuAwdp9cEAgHOAQZ4H+g4vaZSqXAOMMD7QMfpNR6Ph8/kA7wPdJxe02g0teoqJADVDh0HAFyGjtNrQqHAwADHHACqDh2n15RKlUKBYw4AVYeOAwAuw3aQXsO1BwHeE8Zxeg3XHgR4T+g4AOAybKvqNVyXC+A9oeP0Gq7LBfCesK0KAFyGjtNrmHcE4D2h4/Qa5h0BeE8YI+g1HHMAeE/oOL2GYw4A7wkdp9dEIiOFQsV0CgAWw/44vSaTyWUyGdMpAFgMHafXDA0NRCIjplMAsBg6Tq8VFytkMjnTKQBYDPvj9JqJCY6rArwXdJxeKyzEcVWA94KO00eDBk19/DiJx+NpL1izefMBHo/n6uq4f/8PTEcDYBnsj9NHgwf3MDIyLLn2II/HEwj4/foFMp0LgH3Qcfqob98gFxeHN+9xc3McMKA7c4kA2Aodp6fCw4MNDQ20t/l8Xq9enUUiQ6ZDAbAPOk5P9ekT6Oxsp71dt67zgAHdmE4EwEroOP01ZEhPIyNDgYAfEtLJ2FjEdBwAVkLH6a9+/YKcnOxcXR0GDsSeOIAqwrkj7yX5ESWc4+XnaCRZNfLJ+S51l2qIts7lE1X/+i3thSZiatGe6rXQVPvKAfQEOq7q7l/l370saOJrZetoZGgsYDpOpSnk6qxXsruX8yTZilYdcRVX4CZ0XBVdP0OvEg2DhjsyHaTqhIYCFzNTl8amfx1MK5IW+gUzHQigBmB/XFXkpFHKU4OOYSwuuDe1722fk26YnsR0DoAagI6ripSnGkORAdMpqpPIxPDlE+yVAw5Cx1WFNJdn52rCdIrqVMfNRJqHCU6Ag7A/riqKpMQ34NROerVKXZCnIULNAddgHAcAXIaOAwAuQ8cBAJeh4wCAy9BxAMBl6DgA4DJ0HABwGToOALgMHQcAXIaOAwAuQ8cBAJeh4wCAy9BxnKJSqW7dSmA6BYAeQcdxyrIV365cvZDpFAB6BB2nX16mJGs0VZ+rslgur9Y4AKyH+eN05OixmAMH9jxNfGxsbNLWt92nkdMtLa2ISKFQ/Lpp3anTR4uKCj09vR8+vDd82NjeoWFEdCMhfsPGNU+ePLSysvZq7Tt2TKSNjS0R9eodMOWzr+Lizl66HGdqKu4V0j9ixDgiWrx07tlzJ4moc1cfItq9M9be3oHp9w3AMHScjty9e8vNzT0oKDgnJzt6/+6CwoJFC1YT0fqfv4+J2Td2TKStrd269avkclmP7qFEdO36lZlfTQ4KDO7bZ1C+JO/36F1Tp0+MWrddJBIR0eIlc0ZGTBg8OOLcuZObt0Q1aezh5+c/LHx0Rnraq1cvv5o5n4isrW2YftMAzEPH6cjUz7/m8V7PsisUCrfv+FUulwuFwtjY6J7BfQYNHE5EGo1mwcLZt24ntPFu++OaZb1C+k2eNEO7iI+PX8SosKvxFzv4dyai4B69h4aPIqKGDRofPnLgSvxFPz9/Fxc3CwvL7Jysli1bM/peAfQIOk5HFApF9P7dJ08dSU9PNTISqdXq3NwcAwOD4uJiZ2dX7XO0N/LzJampr54/T3z58kXs4f1vriQ9PU17QyQy1t4QCAR16thlZWbo/A0BsAM6Thc0Gs3Xs6Y8eHg3YsT4Zs08z58/s/u3rWqN2sLCUmwqvnUrYUDYUCK6d+82ETWo3ygnJ4uIIkaM79ihy5vrsba2fXvlQoFQpVbp8N0AsAk6Thdu3rx+7fqVWV9/F9i1OxG9TH59KVOBQDBkyMgNG9d8t2CWra3dwZi9/fsNcXWt++LFcyKSy2Vubu6Vfa33OSwLwD04d0QX8iS5RNS4UdM3/6tWq4moT++Bvj5+OTnZUmn+rK+/+zRyGhG5uLjZ2zscPRZTVFSkXUSpVCoUigpfSCQyzs7O0q4ZANBxOtLMo6WhoeGGjWsuXb6wc9fmzVuiiCjx6WMi+nbB1+bmFsHBfby8fHnES0tLJSIejxf5ybSsrMzISSMPHNwbHb078tORB2P2VvhCrTy98/MlK1ctPH489mr8JZ28OQC9hm1VXahTx272rAVrf1oxd96M5s08V66I2rR5ffT+3f7+Ad5evpu3RJ0+c1z7TIFAMGP6Nx991LODf+dFC1Zv2rx+7U8rTE3Fni29PD29K3yhoKDgBw/vnjh5+OKl8337DPL18av5Nweg13il7r65cyeKKLd584FMRGKBc3vJ1Mq6qa/F+69KpVIJBALtbUm+ZOZXk4VC4Q+rN77/misl8XZ+yuOM7hG4hnQ12LdvcP/+F3g8DCD0Ar4NDFuxcsGTJw/btetoaWmV9OLZ06ePevbsy3QoAO5AxzGsbdv26empv0fvVCgUjo7OI4aP055HAgDVAh3HsIBOgQGdAplOAcBZOK4KAFyGjgMALkPHAQCXoeMAgMvQcQDAZeg4AOAydBwAcBk6DgC4DB0HAFyGjquKInm+0JBTXzqBgC8yxQfygYM49YuqG+vW7f7r8sXcVBnTQapTTprc2BQTCAMHoeMq4c6dx0Tk4dFgwqSPlAol03GqU7FMYeeKcRxwEDrunaSnZwcGjtbOtRcQ4OvSmFRK2YP4PKZzVY/E21JpbmG9FkznAKgB6LgK3Lv3hIgyM3P27VvdokWjkvu7R1Dmi7zb53OUChZv4qmUmofXJIl/Z4eOZzoKQM3A3Erl+emnXTdv3o+KmtesWYO3H+0+UnXxcM5vS7OsHAxKrg/NIgIhL+253Mwldcin9oRJa4Gj8JNdumPHznfv3sHbu9knnwwp52ntevLa9RTkpGuKpNVzhdPCwqL589c2alRvzJj+1bLCcohMeNYOgvR0Y3//YX/+uc3Q0KCmXxFA99Bxpejefdz06aOJyM+v1bs838pOY2VXPeO46dPX3H16NU/+7Cu3vkKhoFrWWT47O+tLl3bn5kpevcqoW9dJB68IoEvYH/ePO3cea4+cRkf/GBjYTvcBdu48fPXqbSKSSgvj42/r8qUtLc35fP748XN0+aIAOoCOey0m5uySJRu1AxkTE5HuA9y792Tv3mMFBUVElJubf+HCDR0HcHV1mDBhYFzcdW0GAG5Ax9GVK7eIyN3daevWxWKxCVMxFiyISkp6pb3N4/EuXtR1xxFRmzbN/f29X71K//XXaN2/OkBNqNUdp1Kpw8KmSCRSIvL0bMJgkoULf376NPnNg7NSaZF2w1n3GjasW1Qku3v3CSOvDlC9amnHqdXqpKRXcnnxsmXTGdn19h+nTv1VXKx4857MzJwLF64zlScyMtzJya6oSJaYmMxUBoBqURs77tGj5x98MNjMzNTERFSvngvTcYiIzpzZHB+/Nz5+r5GRoZWVuUhkqNFoTp++xGAkS0szY2PRF18sv337EYMxAN5T7Tp3JCsr18bGMj09++rVPUxnKZ2HR/116+YYGOjL92XfvtWxsefe/IAHALvUonHcb78d/fbbdUT04YdeTGcpXWJicm5uvv4UnFZISAARTZu2hOkgAFVRKzqusFBGRBKJdPXqr5jOUp7ExOQOHdownaJ048YNmD37e6ZTAFQa9ztu+/ZDFy8maH9Lmc5SgStXbjk72zGdonRNm9afOzeSiLB7DtiF4x2XkHAvIyO7a1c/poO8k/z8Ai+vZkynKJNQKCSiM2cunzp1keksAO+Ksx135szloiJZvXqun38ewXSWd5KWlnXjxr0GDVyZDlKByZOHvXyZxnQKgHfFzY47deri0aN/GhuLLCzETGd5V1ev3goO7sR0incSEdFHuxOA6SAAFeNax8lkxURUp471smVfMJ2lcmJj//jgg5ZMp6iEpk3rr169lekUABXgVMc9e/YyPHw6EbVqxeQHs6ogOzvvyZMkX182dZyPT3N9+IgIQPk41XEnTlyIjv6B6RRVcerUXwMGdGM6RaW1aNEoJSV9+/YYpoMAlIkjHbd37zEiGj9+INNBqmjLloOhoV2YTlEVTk52Pj4tZsxYznQQgNJxoeMOHDgtEhkxnaLqLly4/sEHrRwcbJkOUkVNm9ZfunQ60ykASseFjnN1dejVqzPTKapu/frfwsI+YjrF+7p9+9GBA6eZTgHwX+zuuLNnL9+/n9imTXOmg1Td+fPXbGwsS73uF7u0aNFIKi3ctg375kC/6NfHvytl48Z9xsaizp0/YDrIezl+/MLHHw9mOkX1GDasF9MRAP6LxR03dmwY0xHe1759J0xNRU2a1GM6SHVav353eHiIuTlrzr4GbmPltmp+fgEHdv0olaply3756iuuXaH+o48+nDRpAdMpAF5jZcdNmrRA/z/XWaG1a3cuXPg50ymqX/36ruvXz9HOZwXAOPZtq+bmSpYunW5nZ810kPdy5MifmZk5bJkQpbKMjUXJyWmGhkLtVCUADGLfOM7S0pztBZebK1m5cvO3305mOkgNunPn8Tff/Mh0CgC2ddyFCzemTmX9pNsLFkStW8fxK9J36/Zho0Z1MzKymQ4CtR3LNiX++utGr14BTKd4L3Pnru3Y0adRo7pMB6lxo0b1YzoCANvGcV98MZrVJ8Tt3n1ELDZm9acyKmXx4g3a2a4AmMKmjtNoNDKZnOkUVXf9+t2bNx9Mnz6a6SC6w+PxYmLOMJ0CajU2ddyDB4ljx/6P6RRVlJycOn/+T4sWcfBkkXJMnDi4fn29uEo31Fps2h9XXKxwcKjDdIqqUCpV/ft/dvnyb0wH0TULC7GPTwumU0CtxqZxnKdnk+XLWTaDuVbv3pHHj29kOgUzFi36OSHhPtMpoPZiU8cplcrs7DymU1Ranz6fRkXNtbQ0YzoIMywtzePjbzOdAmovNnWcUCgMDY0sKmLTh4SmTl2yePFUFxcHpoMwZtiwXt27+zOdAmovNnUcEXXt6nf/fiLTKd7VqFGzRo7s07RpfaaDMMnMzLQ2VzwwjmUdN2/ep15eHkyneCejR8+aMmWEpyfLrhBW7XJyJOw9Gg4cwLKOUyiUN27cYzpFxcaOnb18+QzWXQKxJpiYiO7efcJ0Cqi9WNZxBgbC/ftPHT78B9NByhMe/sUXX4yxtrZgOoheMDIy/PXXBRqNhukgUEuxrOOIKDIy/MWLV9rbISETw8I+YzrRv3z55Yo5cz7h2NS+76lp03o8Ho/pFFBLsekcYC17e5uJEweHhn7y8mU6ETVq5MZ0on907z4+KmpO3brOTAfRC23ahJVUm0aj0d7u2zdw1qwJTEeDWoR9HRcaGpmSkq79LCQR6cl8s0qlctSo2du2La5Th91z21WjunWdkpJej7i13yxHR7sxY/oznQtqFzZtqw4dOqNNmzBtwWlpNHqxnycrK9fff9jPP89Fwb2pR49/nRan0WgCAnzYe6lsYCk2ddyOHUt9fVsaGPwz9uTxeEKhgNFQ9Phx0pw5P166tNvYWMRsEn0zeHBPV1fHkv86O9uFh4cwmghqIzZ1HBGtXz9n1Ki+Vlb6csjy2rU7s2atXrMG53+VwszMtGQop9FoOnb0cXRk5ZQKwGos6zgiGj9+4OLFnzs722s3UxUKpUqlYiTJ6dOXoqL2/PbbSkZenRWGDOnp4mJPRC4u9sOGhTIdB2oj9nUcEbVp0/zgwTWBge1MTUU8Hk+tZmCfXHT0yePH437+eZ7uX5pFzMxMQ0ICNBpNhw7e2BMHjOCVutP+zp0ootzmzQfqOE1+Nt25RJIskmS/U23l5ubnS6Subo7v8NzqpFKpcqSvmjR1dapPTXx0/OJVkXiHXjygYhk/N0PXY161Wp2cnObsbCcQ6HrPqWUdvoGhxqk+NfLW6evu2ze4f/8LPB77TlrgJD36NiTepr9iee7NzN2ai4QG73jKqK7brYSA75qVKk9PVtyPl/SeqNcnuJ7eTTy+idjSyKWJiJExrzcxc8KggM/PTJG9elb88EZ+zzF6/T2CmqMvHfcogX/vskHox05MB6kEWxcRET26bhC7MTdkrJrpOKU7f4AnNBR7d62l24m2LkZEdOei8MS2/I+GM7PfFpilF/vjCiV0/bSm82A2FVyJRt4WdVzM4k/r4zDhQbxGqTCptQVXonk7KxNz01txTOcAJuhFxz35W2PjaMJ0iqpzaiB+cFUfx3EPr/Mc3MVMp9ALTg1M78frwfnioHN60XF5WTw7NxZ3nKWdoUDIZ+gMlvIoink2zjgzmYjIxkmkVvMJLVf76EXHSXNJw/KfPkm2Sql/10rOTFHxGf4YiL7gCyjzpZKJIy7AML3oOACAGoKOAwAuQ8cBAJeh4wCAy9BxAMBl6DgA4DJ0HABwGToOALgMHQcAXIaOAwAuQ8cBAJeh4wCAy9jacSqV6tathPdcyfc/LOkX9lE1JYKqGzCox28XO5EAAAhlSURBVMpVC5lOAdzE1o5btuLblavxWwEAFWBrxxXL5UxHAAAW0JfrOVTK4qVzz547SUSdu/oQ0c4dMY4OTkR04sThHbs2paQk29jY9gzuOzR8FJ/PJ6KsrMx161ddvnJBqVS2bNF64oQp9es3fHu1O3dtPnBwT36+pGHDJiMjJrTxbsvEm2OfGwnxGzauefLkoZWVtVdr37FjIm1sbImoV++AKZ99FRd39tLlOFNTca+Q/hEjxmkXUalUW7dtiD28XyYrat3aRy6TMf0mgLNYOY4bFj7a28vX0cHph9Ubf1i90cbaloiOH49dtGROo0ZN/zd7YUCnoF83rduxcxMRyWSyqdMnXrt+Zfy4yVOnfJ2ZlTF1+sR8af5/1nnt+pUNG9d4enpPnfK1g71jUWEhQ2+OZa5dvzLjy0/d69afPu1/A8OG/f339anTJ8r+v7MWL5nTsGGT1as2BAUGb94SdenS60sqfP/Dkq3bNn7Q9sPJn84QGYne/nYAVBdWjuNcXNwsLCyzc7JatmytvUej0Wz8dW3Llq1nf/0dEXXs0CU/X7L7ty39+w05feZYUtKzFcvXeXv5ElHLll7hw0Kjo3eXjCm0UlNTiKhv74HNm3sGBQUz9M7Y58c1y3qF9Js8aYb2vz4+fhGjwq7GX+zg35mIgnv0Hho+iogaNmh8+MiBK/EX/fz8Hz66fyg2etjQ0WNGf0JE3bqFJNy8xvT7AM5iZce9LTk5KTMzY9DA4SX3+Pq2O3L0YPLLpJs3r4lNxdqCIyIHB0c3N/cHD+/+Zw1+H/ibmZkvXPS/SZ9+4efnr9v4bJWa+ur588SXL1/EHt7/5v3p6WnaGyKRsfaGQCCoU8cuKzODiM6fP0NEYWFDS56v3aUAUBM40nHSAikRWVpal9xjZmZORJkZ6dICqYWl1ZtPNje30P6yvcnGxnbND7+uXbfyq1lTWrRo9c3sRXXq2OkqPlvl5GQRUcSI8R07dHnzfmvrUq52KBQIVWoVEaWlp4rFYgtzCx0mhdqLxX8/NZp/LkBiV8eeiPLyckvuycnJ1jZdHVs7iSTvzQWzs7PEYrO3V+jm5r5k0Q8rlq9LTHy8ZOncGo7PBdovo1wuc3Nzf/OfWFzeBQ8tLaykUmlxsf5d4we4iK0dJxIZZ2dnqdWvr2pqY2PrYO945cqFkif88ccpkUjUsGGT5s098/Ml9+7d1t7/5Mmjly9faHfkGRgYFhUVKpVK7UPa3zpvL18/vw4PH91n4m2xjIuLm729w9FjMUVFRdp7lEqlQqEof6nGjT2I6PSZYzrJCLUdW7dVW3l6Hz0Ws3LVwpYtWpuZmbdv33FkxITFS+cuW/6tr2+769evxF04FzFivLGxcWDXHjt2bpo7/8vhw8by+fxt2zZaWlr1Dh1ARI0aNpHJZHPnf/nxxM8lkrx587/s03ugsbHJlSt/NW3SjOm3yAI8Hi/yk2nfzPkictLI0F5hapXq+InYoKDgsP7h5SzVOSBo2/aNK1ctTEx80qhhkzt3/858a9cBQHVha8cFBQU/eHj3xMnDFy+d796tV/v2Hbt1C5HJZXv37Thx8rCtTZ3x4yYNHjSCiIRC4bIla39at3Ld+lVqtdqzpVfkJ9OsrKyJqGvX7o+fPDx95tizxCcODk513ert3LlJo9G0at1m8qczmH6L7NDBv/OiBas3bV6/9qcVpqZiz5Zenp7e5S8iEAiWLPrx+x+XxBzaZ2oq7tSxq4WFpa7yQq3De3OvVok7d6KIcps3H6ibEMe2kGMD2/otS9lHxha7ljyJ+B/fyJjpHP/289fqfp/VMxKxdY9E9do6//HHywQ6OIS7b9/g/v0v8HhsHUBwDL4N8NqtWwlfz57y9v1iUzNpQenn6E4Y/1lIz77VFeDSpbgFi2a/fb9Go9FoNKWeX7J44Q/Nm3tWVwDgJHQcvNa4scfPUTtLeUBDxCt9EXOz6jz/o3Vrn1IDqNVqjVotEJbys2pT2kkqAG9Cx8FrRkZG2o/9MkUkEjEbADgJe2oAgMvQcQDAZeg4AOAydBwAcBk6DgC4DB0HAFyGjgMALkPHAQCXoeMAgMv0ouOEBjw+v4yPC7GEkYhPVMrsBswyEvHY/WWtViITPk/vvkVQ4/Si44zFamluBRMr6jNlsaZIqjIy1rs+ERpSoUTJdAq9UJSv4vE0PAHTOUDn9KLj7FypII/FHZeXWezSWB8/+evUgJeXxeIvbDXKzSx2boiGq430ouMaefHSnklz09k6wf+VY+leAWqmU5TCJ5CuHktnOoVeuHo8o01XbKnWRnrRcUQU9hnv4qHUjGT2XS/91PYUn0C1c0O921AlIjMr6jmGf2RjkkpZq3+9j29J7tSXbJ2ZzgFM0JctLCMT6hupOrLplSSLHOuZEk9fyrcsIhP+q0Sp0FDTzE9Vv6U+FpyWnav6w1DNmZ3Pi4t5Lg3NZIX6ON6sISITfspTqdBA3bqTxqUx02mAIfrScURkYES9J1JOGmWmSIuk+j7uMDDi1W9Jdq48oYH+FpyWSyOeS0NN2gtNTmpusVzfv7DVyMCI18CTHOry9P4vJtQgPeo4LSt7srLnlTnzLFQNj+zdyN4NX1iodfAHDgC4DB0HAFyGjgMALkPHAQCXoeMAgMvQcQDAZeg4AOAydBwAcBk6DgC4DB0HAFyGjgMALkPHAQCXoeMAgMvQcQDAZeg4AOAydBwAcBk6DgC4DB0HAFyGjgMALkPHAQCXoeMAgMvQcQDAZWVeezA1NaG4WKrbMABcoNHUogt167/SO87R8UMjI0udhwHgAi+vFjyegOkU8BpPo6lFF04HgNoG++MAgMvQcQDAZeg4AOAydBwAcBk6DgC4DB0HAFz2f2CBihr+eor3AAAAAElFTkSuQmCC", 390 | "text/plain": [ 391 | "" 392 | ] 393 | }, 394 | "metadata": {}, 395 | "output_type": "display_data" 396 | } 397 | ], 398 | "source": [ 399 | "from typing import Literal\n", 400 | "from IPython.display import Image, display\n", 401 | "\n", 402 | "from langgraph.graph import StateGraph, START, END\n", 403 | "from langgraph.types import Command\n", 404 | "from langgraph.store.base import BaseStore\n", 405 | "\n", 406 | "from memory_course.schemas import State\n", 407 | "from memory_course.utils import parse_email\n", 408 | "\n", 409 | "def triage_router(state: State) -> Command[Literal[\"response_agent\", \"__end__\"]]:\n", 410 | " \"\"\"Analyze email content to decide if we should respond, notify, or ignore.\n", 411 | "\n", 412 | " The triage step prevents the assistant from wasting time on:\n", 413 | " - Marketing emails and spam\n", 414 | " - Company-wide announcements\n", 415 | " - Messages meant for other teams\n", 416 | " \"\"\"\n", 417 | "\n", 418 | " # Parse email\n", 419 | " author, to, subject, email_thread = parse_email(state[\"email_input\"])\n", 420 | " system_prompt = triage_system_prompt.format(\n", 421 | " full_name=profile[\"full_name\"],\n", 422 | " name=profile[\"name\"],\n", 423 | " user_profile_background=profile[\"user_profile_background\"],\n", 424 | " triage_no=prompt_instructions[\"triage_rules\"][\"ignore\"],\n", 425 | " triage_notify=prompt_instructions[\"triage_rules\"][\"notify\"],\n", 426 | " triage_email=prompt_instructions[\"triage_rules\"][\"respond\"],\n", 427 | " examples=None\n", 428 | " )\n", 429 | "\n", 430 | " user_prompt = triage_user_prompt.format(\n", 431 | " author=author, to=to, subject=subject, email_thread=email_thread\n", 432 | " )\n", 433 | "\n", 434 | " result = llm_router.invoke(\n", 435 | " [\n", 436 | " {\"role\": \"system\", \"content\": system_prompt},\n", 437 | " {\"role\": \"user\", \"content\": user_prompt},\n", 438 | " ]\n", 439 | " )\n", 440 | " update = None\n", 441 | " goto = END\n", 442 | " if result.classification == \"respond\":\n", 443 | " print(\"📧 Classification: RESPOND - This email requires a response\")\n", 444 | " goto = \"response_agent\"\n", 445 | " update = {\n", 446 | " \"messages\": [\n", 447 | " {\n", 448 | " \"role\": \"user\",\n", 449 | " \"content\": f\"Respond to the email {state['email_input']}\",\n", 450 | " }\n", 451 | " ]\n", 452 | " }\n", 453 | " elif result.classification == \"ignore\":\n", 454 | " print(\"🚫 Classification: IGNORE - This email can be safely ignored\")\n", 455 | " elif result.classification == \"notify\":\n", 456 | " print(\"🔔 Classification: NOTIFY - This email contains important information\")\n", 457 | " else:\n", 458 | " raise ValueError(f\"Invalid classification: {result.classification}\")\n", 459 | " return Command(goto=goto, update=update)\n", 460 | "\n", 461 | "# Build workflow with store, as defined above\n", 462 | "agent = (\n", 463 | " StateGraph(State)\n", 464 | " .add_node(triage_router)\n", 465 | " .add_node(\"response_agent\", response_agent)\n", 466 | " .add_edge(START, \"triage_router\")\n", 467 | " .compile(store=store)\n", 468 | ")\n", 469 | "\n", 470 | "# Show the agent\n", 471 | "display(Image(agent.get_graph(xray=True).draw_mermaid_png()))" 472 | ] 473 | }, 474 | { 475 | "cell_type": "code", 476 | "execution_count": 14, 477 | "metadata": {}, 478 | "outputs": [ 479 | { 480 | "name": "stdout", 481 | "output_type": "stream", 482 | "text": [ 483 | "📧 Classification: RESPOND - This email requires a response\n", 484 | "================================\u001b[1m Human Message \u001b[0m=================================\n", 485 | "\n", 486 | "Respond to the email {'author': 'Alice Smith ', 'to': 'John Doe ', 'subject': 'Quick question about API documentation', 'email_thread': \"Hi John,\\n\\nI was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\\n\\nSpecifically, I'm looking at:\\n- /auth/refresh\\n- /auth/validate\\n\\nThanks!\\nAlice\"}\n", 487 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 488 | "\n", 489 | "[{'citations': None, 'text': \"I'll help draft a response to Alice regarding the API documentation question. I'll use the write_email function to send the reply.\", 'type': 'text'}, {'id': 'toolu_01X1GrdvGREJn89CM814LmJ3', 'input': {'to': 'alice.smith@company.com', 'subject': 'Re: Quick question about API documentation', 'content': \"Hi Alice,\\n\\nThanks for bringing this to my attention. I'll need to review the current documentation and check with the development team about these specific endpoints.\\n\\nLet me look into this and get back to you with a complete answer by tomorrow. I'll make sure to clarify whether these endpoints should be included in the documentation or if there's a reason for their omission.\\n\\nBest regards,\\nJohn\"}, 'name': 'write_email', 'type': 'tool_use'}]\n", 490 | "Tool Calls:\n", 491 | " write_email (toolu_01X1GrdvGREJn89CM814LmJ3)\n", 492 | " Call ID: toolu_01X1GrdvGREJn89CM814LmJ3\n", 493 | " Args:\n", 494 | " to: alice.smith@company.com\n", 495 | " subject: Re: Quick question about API documentation\n", 496 | " content: Hi Alice,\n", 497 | "\n", 498 | "Thanks for bringing this to my attention. I'll need to review the current documentation and check with the development team about these specific endpoints.\n", 499 | "\n", 500 | "Let me look into this and get back to you with a complete answer by tomorrow. I'll make sure to clarify whether these endpoints should be included in the documentation or if there's a reason for their omission.\n", 501 | "\n", 502 | "Best regards,\n", 503 | "John\n", 504 | "=================================\u001b[1m Tool Message \u001b[0m=================================\n", 505 | "Name: write_email\n", 506 | "\n", 507 | "Email sent to alice.smith@company.com with subject 'Re: Quick question about API documentation'\n", 508 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 509 | "\n", 510 | "[{'citations': None, 'text': \"I'll also create a memory to track this follow-up task regarding the API documentation review.\", 'type': 'text'}, {'id': 'toolu_01NhAJAtt6B86Ki9HQdNsnNb', 'input': {'action': 'create', 'content': 'Follow up needed: Review API documentation for authentication service, specifically regarding endpoints /auth/refresh and /auth/validate. Need to respond to Alice Smith with findings. Topic: API Documentation Review'}, 'name': 'manage_memory', 'type': 'tool_use'}]\n", 511 | "Tool Calls:\n", 512 | " manage_memory (toolu_01NhAJAtt6B86Ki9HQdNsnNb)\n", 513 | " Call ID: toolu_01NhAJAtt6B86Ki9HQdNsnNb\n", 514 | " Args:\n", 515 | " action: create\n", 516 | " content: Follow up needed: Review API documentation for authentication service, specifically regarding endpoints /auth/refresh and /auth/validate. Need to respond to Alice Smith with findings. Topic: API Documentation Review\n", 517 | "=================================\u001b[1m Tool Message \u001b[0m=================================\n", 518 | "Name: manage_memory\n", 519 | "\n", 520 | "created memory 9186395a-03e1-4a9f-a090-b7fd3807c43d\n", 521 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 522 | "\n", 523 | "I've sent an acknowledgment email to Alice and created a memory to track the follow-up needed. The response acknowledges her question and sets the expectation that John will review and provide a complete answer tomorrow. This gives John time to properly investigate the documentation and consult with the development team if needed.\n", 524 | "\n", 525 | "Is there anything else you need assistance with regarding this matter?\n" 526 | ] 527 | } 528 | ], 529 | "source": [ 530 | "email_input = {\n", 531 | " \"author\": \"Alice Smith \",\n", 532 | " \"to\": \"John Doe \",\n", 533 | " \"subject\": \"Quick question about API documentation\",\n", 534 | " \"email_thread\": \"\"\"Hi John,\n", 535 | "\n", 536 | "I was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\n", 537 | "\n", 538 | "Specifically, I'm looking at:\n", 539 | "- /auth/refresh\n", 540 | "- /auth/validate\n", 541 | "\n", 542 | "Thanks!\n", 543 | "Alice\"\"\",\n", 544 | "}\n", 545 | "\n", 546 | "response = agent.invoke({\"email_input\": email_input}, config=config)\n", 547 | "for m in response[\"messages\"]:\n", 548 | " m.pretty_print()" 549 | ] 550 | }, 551 | { 552 | "cell_type": "code", 553 | "execution_count": null, 554 | "metadata": {}, 555 | "outputs": [], 556 | "source": [ 557 | "If we call it again, we can see that it remembers information about Alice" 558 | ] 559 | }, 560 | { 561 | "cell_type": "code", 562 | "execution_count": 36, 563 | "metadata": {}, 564 | "outputs": [ 565 | { 566 | "name": "stdout", 567 | "output_type": "stream", 568 | "text": [ 569 | "📧 Classification: RESPOND - This email requires a response\n" 570 | ] 571 | }, 572 | { 573 | "name": "stderr", 574 | "output_type": "stream", 575 | "text": [ 576 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 577 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 578 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 579 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 580 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 581 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 582 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 583 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 584 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 585 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 586 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 587 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 588 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 589 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 590 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 591 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 592 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 593 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 594 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 595 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 596 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 597 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 598 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 599 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 600 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 601 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 602 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 603 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n", 604 | "Failed to use model_dump to serialize to JSON: PydanticSerializationError(Unable to serialize unknown type: )\n" 605 | ] 606 | }, 607 | { 608 | "name": "stdout", 609 | "output_type": "stream", 610 | "text": [ 611 | "================================\u001b[1m Human Message \u001b[0m=================================\n", 612 | "\n", 613 | "Respond to the email {'author': 'Alice Smith ', 'to': 'John Doe ', 'subject': 'Follow up', 'email_thread': 'Hi John,\\n\\nAny update on my previous ask?'}\n", 614 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 615 | "\n", 616 | "[{'citations': None, 'text': \"I'll help respond to Alice's follow-up email. However, I notice that I don't have context about her previous request. Let me check our memory first to see if we have any relevant information about prior interactions with Alice.\", 'type': 'text'}, {'id': 'toolu_019pJup1jbeDoQfKTCqNhadd', 'input': {'query': 'Alice Smith previous ask request'}, 'name': 'search_memory', 'type': 'tool_use'}]\n", 617 | "Tool Calls:\n", 618 | " search_memory (toolu_019pJup1jbeDoQfKTCqNhadd)\n", 619 | " Call ID: toolu_019pJup1jbeDoQfKTCqNhadd\n", 620 | " Args:\n", 621 | " query: Alice Smith previous ask request\n", 622 | "=================================\u001b[1m Tool Message \u001b[0m=================================\n", 623 | "Name: search_memory\n", 624 | "\n", 625 | "[{\"namespace\": [\"email_assistant\", \"lance\", \"collection\"], \"key\": \"90bcdd2a-7961-4280-90f7-3da2c89b683b\", \"value\": {\"content\": \"Jim is John Doe's friend\"}, \"created_at\": \"2025-02-08T21:05:46.119213+00:00\", \"updated_at\": \"2025-02-08T21:05:46.119216+00:00\", \"score\": null}, {\"namespace\": [\"email_assistant\", \"lance\", \"collection\"], \"key\": \"f966041d-de05-4673-ab42-fe1ba41c753f\", \"value\": {\"content\": \"Alice Smith (alice.smith@company.com) inquired about missing API endpoints (/auth/refresh and /auth/validate) in the authentication service documentation on [current_date]. Awaiting John's review and response.\"}, \"created_at\": \"2025-02-08T21:09:46.379532+00:00\", \"updated_at\": \"2025-02-08T21:09:46.379534+00:00\", \"score\": null}]\n", 626 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 627 | "\n", 628 | "[{'citations': None, 'text': \"Based on the memory search, I can see that Alice previously inquired about missing API endpoints in the authentication service documentation. I'll craft a response acknowledging her follow-up and asking for a bit more time if John hasn't had the chance to review it yet.\", 'type': 'text'}, {'id': 'toolu_0168xqZEWGpWr3mLKdHyy7kS', 'input': {'to': 'alice.smith@company.com', 'subject': 'Re: Follow up', 'content': 'Hi Alice,\\n\\nThank you for following up regarding the API endpoints documentation (/auth/refresh and /auth/validate). John is currently reviewing this matter and will get back to you with a complete response soon.\\n\\nWould you need this information by a specific deadline? That would help us prioritize accordingly.\\n\\nBest regards,\\nOn behalf of John'}, 'name': 'write_email', 'type': 'tool_use'}]\n", 629 | "Tool Calls:\n", 630 | " write_email (toolu_0168xqZEWGpWr3mLKdHyy7kS)\n", 631 | " Call ID: toolu_0168xqZEWGpWr3mLKdHyy7kS\n", 632 | " Args:\n", 633 | " to: alice.smith@company.com\n", 634 | " subject: Re: Follow up\n", 635 | " content: Hi Alice,\n", 636 | "\n", 637 | "Thank you for following up regarding the API endpoints documentation (/auth/refresh and /auth/validate). John is currently reviewing this matter and will get back to you with a complete response soon.\n", 638 | "\n", 639 | "Would you need this information by a specific deadline? That would help us prioritize accordingly.\n", 640 | "\n", 641 | "Best regards,\n", 642 | "On behalf of John\n", 643 | "=================================\u001b[1m Tool Message \u001b[0m=================================\n", 644 | "Name: write_email\n", 645 | "\n", 646 | "Email sent to alice.smith@company.com with subject 'Re: Follow up'\n", 647 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 648 | "\n", 649 | "[{'citations': None, 'text': \"I've sent a professional response that:\\n1. Acknowledges her follow-up\\n2. Shows we remember the specific topic (API documentation)\\n3. Provides a status update\\n4. Asks about any deadline to help with prioritization\\n\\nLet me also create a memory to track this follow-up interaction so we can maintain continuity in future communications.\", 'type': 'text'}, {'id': 'toolu_01AqMuWQkiD4Tvp8CipQSaBq', 'input': {'action': 'create', 'content': 'Alice Smith followed up on [current_date] regarding the pending API documentation review (auth/refresh and auth/validate endpoints). Response sent asking about deadline requirements.'}, 'name': 'manage_memory', 'type': 'tool_use'}]\n", 650 | "Tool Calls:\n", 651 | " manage_memory (toolu_01AqMuWQkiD4Tvp8CipQSaBq)\n", 652 | " Call ID: toolu_01AqMuWQkiD4Tvp8CipQSaBq\n", 653 | " Args:\n", 654 | " action: create\n", 655 | " content: Alice Smith followed up on [current_date] regarding the pending API documentation review (auth/refresh and auth/validate endpoints). Response sent asking about deadline requirements.\n", 656 | "=================================\u001b[1m Tool Message \u001b[0m=================================\n", 657 | "Name: manage_memory\n", 658 | "\n", 659 | "created memory 216648be-a74d-4388-9917-415c06beb4ef\n", 660 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 661 | "\n", 662 | "Would you like me to take any additional actions regarding Alice's follow-up?\n" 663 | ] 664 | } 665 | ], 666 | "source": [ 667 | "email_input = {\n", 668 | " \"author\": \"Alice Smith \",\n", 669 | " \"to\": \"John Doe \",\n", 670 | " \"subject\": \"Follow up\",\n", 671 | " \"email_thread\": \"\"\"Hi John,\n", 672 | "\n", 673 | "Any update on my previous ask?\"\"\",\n", 674 | "}\n", 675 | "\n", 676 | "response = agent.invoke({\"email_input\": email_input}, config=config)\n", 677 | "for m in response[\"messages\"]:\n", 678 | " m.pretty_print()" 679 | ] 680 | }, 681 | { 682 | "cell_type": "code", 683 | "execution_count": null, 684 | "metadata": {}, 685 | "outputs": [], 686 | "source": [] 687 | } 688 | ], 689 | "metadata": { 690 | "kernelspec": { 691 | "display_name": "Python 3 (ipykernel)", 692 | "language": "python", 693 | "name": "python3" 694 | }, 695 | "language_info": { 696 | "codemirror_mode": { 697 | "name": "ipython", 698 | "version": 3 699 | }, 700 | "file_extension": ".py", 701 | "mimetype": "text/x-python", 702 | "name": "python", 703 | "nbconvert_exporter": "python", 704 | "pygments_lexer": "ipython3", 705 | "version": "3.11.7" 706 | } 707 | }, 708 | "nbformat": 4, 709 | "nbformat_minor": 4 710 | } 711 | -------------------------------------------------------------------------------- /notebooks/lesson_4_episodic.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Lesson 4: Email Assistant with Semantic + Episodic Memory\n", 8 | "\n", 9 | "We previously built an email assistant that:\n", 10 | "- Classifies incoming messages (respond, ignore, notify)\n", 11 | "- Drafts responses\n", 12 | "- Schedules meetings\n", 13 | "- Uses memory to remember details from previous emails \n", 14 | "\n", 15 | "Now, we'll add human-in-the-loop following the triage step to better refine the assistant's ability to classify emails." 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 23, 21 | "metadata": { 22 | "vscode": { 23 | "languageId": "shellscript" 24 | } 25 | }, 26 | "outputs": [], 27 | "source": [ 28 | "%%capture stderr\n", 29 | "%pip install -e ..\n", 30 | "%pip install -U -q langchain-anthropic langgraph langchain langmem==0.0.5rc9" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "import os\n", 40 | "from getpass import getpass\n", 41 | "\n", 42 | "if not os.environ.get(\"ANTHROPIC_API_KEY\"):\n", 43 | " os.environ[\"ANTHROPIC_API_KEY\"] = getpass(\"ANTHROPIC_API_KEY: \")" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "### Define profile" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 1, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "# Example user profile\n", 60 | "profile = {\n", 61 | " \"name\": \"John\",\n", 62 | " \"full_name\": \"John Doe\",\n", 63 | " \"user_profile_background\": \"Senior software engineer leading a team of 5 developers\",\n", 64 | "}\n", 65 | "\n", 66 | "prompt_instructions = {\n", 67 | " \"triage_rules\": {\n", 68 | " \"ignore\": \"Marketing newsletters, spam emails, mass company announcements\",\n", 69 | " \"notify\": \"Team member out sick, build system notifications, project status updates\",\n", 70 | " \"respond\": \"Direct questions from team members, meeting requests, critical bug reports\",\n", 71 | " },\n", 72 | " \"agent_instructions\": \"Use these tools when appropriate to help manage John's tasks efficiently.\"\n", 73 | "}" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "### Define Triage\n", 81 | "\n", 82 | "The triage step is the \"first line of defense\" against incoming emails. It helps the assistant determine if the email should be responded to, ignored, or notified." 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 2, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "from memory_course.schemas import Router\n", 92 | "from langchain.chat_models import init_chat_model\n", 93 | "from memory_course.prompts import triage_system_prompt, triage_user_prompt\n", 94 | "\n", 95 | "llm = init_chat_model(\"openai:gpt-4o-mini\")\n", 96 | "\n", 97 | "# We'll use structured output to generate classification results\n", 98 | "llm_router = llm.with_structured_output(Router)\n" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "### Define Tools\n", 106 | "\n", 107 | "Define tools that the agent can use. These are place-holder tools for the purpose of testing the LLM." 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 3, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "from langchain_core.tools import tool\n", 117 | "\n", 118 | "@tool\n", 119 | "def write_email(to: str, subject: str, content: str) -> str:\n", 120 | " \"\"\"Write and send an email.\"\"\"\n", 121 | " # Placeholder response - in real app would send email\n", 122 | " return f\"Email sent to {to} with subject '{subject}'\"\n", 123 | "\n", 124 | "\n", 125 | "@tool\n", 126 | "def schedule_meeting(\n", 127 | " attendees: list[str], subject: str, duration_minutes: int, preferred_day: str\n", 128 | ") -> str:\n", 129 | " \"\"\"Schedule a calendar meeting.\"\"\"\n", 130 | " # Placeholder response - in real app would check calendar and schedule\n", 131 | " return f\"Meeting '{subject}' scheduled for {preferred_day} with {len(attendees)} attendees\"\n", 132 | "\n", 133 | "\n", 134 | "@tool\n", 135 | "def check_calendar_availability(day: str) -> str:\n", 136 | " \"\"\"Check calendar availability for a given day.\"\"\"\n", 137 | " # Placeholder response - in real app would check actual calendar\n", 138 | " return f\"Available times on {day}: 9:00 AM, 2:00 PM, 4:00 PM\"" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": 72, 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "### Define memory store and tools" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 4, 158 | "metadata": {}, 159 | "outputs": [ 160 | { 161 | "name": "stderr", 162 | "output_type": "stream", 163 | "text": [ 164 | "/var/folders/bm/ylzhm36n075cslb9fvvbgq640000gn/T/ipykernel_51568/393540479.py:5: LangChainBetaWarning: The function `init_embeddings` is in beta. It is actively being worked on, so the API may change.\n", 165 | " \"embed\": init_embeddings(\"openai:text-embedding-3-small\"),\n" 166 | ] 167 | } 168 | ], 169 | "source": [ 170 | "\n", 171 | "from langgraph.store.memory import InMemoryStore\n", 172 | "from langchain.embeddings import init_embeddings\n", 173 | "store = InMemoryStore(index={\n", 174 | " \"dims\": 1536,\n", 175 | " \"embed\": init_embeddings(\"openai:text-embedding-3-small\"),\n", 176 | "})" 177 | ] 178 | }, 179 | { 180 | "cell_type": "code", 181 | "execution_count": 5, 182 | "metadata": {}, 183 | "outputs": [], 184 | "source": [ 185 | "from langmem import create_manage_memory_tool, create_search_memory_tool\n", 186 | "\n", 187 | "manage_memory_tool = create_manage_memory_tool(namespace=(\"email_assistant\", \"{langgraph_user_id}\", \"collection\"))\n", 188 | "search_memory_tool = create_search_memory_tool(namespace=(\"email_assistant\", \"{langgraph_user_id}\", \"collection\"))" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "### Define agent\n" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": 6, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "agent_system_prompt_memory = \"\"\"\n", 205 | "< Role >\n", 206 | "You are {full_name}'s executive assistant. You are a top-notch executive assistant who cares about {name} performing as well as possible.\n", 207 | "\n", 208 | "\n", 209 | "< Tools >\n", 210 | "You have access to the following tools to help manage {name}'s communications and schedule:\n", 211 | "\n", 212 | "1. write_email(to, subject, content) - Send emails to specified recipients\n", 213 | "2. schedule_meeting(attendees, subject, duration_minutes, preferred_day) - Schedule calendar meetings\n", 214 | "3. check_calendar_availability(day) - Check available time slots for a given day\n", 215 | "4. manage_memory - Store any relevant information about contacts, actions, discussion, etc. in memory for future reference\n", 216 | "5. search_memory - Search for any relevant information that may have been stored in memory\n", 217 | "\n", 218 | "\n", 219 | "< Instructions >\n", 220 | "{instructions}\n", 221 | "\n", 222 | "\"\"\"" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": 7, 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [ 231 | "# Create agent\n", 232 | "from langgraph.prebuilt import create_react_agent\n", 233 | "import json\n", 234 | "\n", 235 | "def create_prompt(state):\n", 236 | " return [\n", 237 | " {\"role\": \"system\", \"content\": agent_system_prompt_memory.format(instructions=prompt_instructions[\"agent_instructions\"], **profile)}\n", 238 | " ] + state['messages']\n", 239 | "\n", 240 | "\n", 241 | "# Create agent\n", 242 | "response_agent = create_react_agent(\n", 243 | " \"anthropic:claude-3-5-sonnet-latest\",\n", 244 | " tools=[write_email, schedule_meeting, \n", 245 | " check_calendar_availability, \n", 246 | " manage_memory_tool,\n", 247 | " search_memory_tool\n", 248 | " ],\n", 249 | " prompt=create_prompt,\n", 250 | " # Use this to ensure the store is passed to the agent \n", 251 | " store=store\n", 252 | ")" 253 | ] 254 | }, 255 | { 256 | "cell_type": "markdown", 257 | "metadata": {}, 258 | "source": [ 259 | "### Few shot prompting\n", 260 | "\n", 261 | "Here we will use episodic memory - aka do few-shot prompting.\n" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 8, 267 | "metadata": {}, 268 | "outputs": [], 269 | "source": [ 270 | "\n", 271 | "import uuid\n", 272 | "\n", 273 | "# Template for formating an example to put in prompt\n", 274 | "template = \"\"\"Email Subject: {subject}\n", 275 | "Email From: {from_email}\n", 276 | "Email To: {to_email}\n", 277 | "Email Content: \n", 278 | "```\n", 279 | "{content}\n", 280 | "```\n", 281 | "> Triage Result: {result}\"\"\"\n", 282 | "\n", 283 | "# Format list of few shots\n", 284 | "def format_few_shot_examples(examples):\n", 285 | " strs = [\"Here are some previous examples:\"]\n", 286 | " for eg in examples:\n", 287 | " strs.append(\n", 288 | " template.format(\n", 289 | " subject=eg.value[\"email\"][\"subject\"],\n", 290 | " to_email=eg.value[\"email\"][\"to\"],\n", 291 | " from_email=eg.value[\"email\"][\"author\"],\n", 292 | " content=eg.value[\"email\"][\"email_thread\"][:400],\n", 293 | " result=eg.value[\"label\"],\n", 294 | " )\n", 295 | " )\n", 296 | " return \"\\n\\n------------\\n\\n\".join(strs)" 297 | ] 298 | }, 299 | { 300 | "cell_type": "markdown", 301 | "metadata": {}, 302 | "source": [ 303 | "We'll add some examples to the store" 304 | ] 305 | }, 306 | { 307 | "cell_type": "code", 308 | "execution_count": 9, 309 | "metadata": {}, 310 | "outputs": [], 311 | "source": [ 312 | "\n", 313 | "data = {\n", 314 | " \"email\": {\n", 315 | " \"author\": \"Alice Smith \",\n", 316 | " \"to\": \"John Doe \",\n", 317 | " \"subject\": \"Quick question about API documentation\",\n", 318 | " \"email_thread\": \"\"\"Hi John,\n", 319 | " \n", 320 | " I was reviewing the API documentation for the new authentication service and noticed a few endpoints seem to be missing from the specs. Could you help clarify if this was intentional or if we should update the docs?\n", 321 | " \n", 322 | " Specifically, I'm looking at:\n", 323 | " - /auth/refresh\n", 324 | " - /auth/validate\n", 325 | " \n", 326 | " Thanks!\n", 327 | " Alice\"\"\",\n", 328 | " },\n", 329 | " # This is to start changing the behavior of the agent\n", 330 | " \"label\": \"ignore\"\n", 331 | "}\n", 332 | "store.put((\"email_assistant\", \"lance\", \"examples\"), str(uuid.uuid4()), data)" 333 | ] 334 | }, 335 | { 336 | "cell_type": "code", 337 | "execution_count": 10, 338 | "metadata": {}, 339 | "outputs": [], 340 | "source": [ 341 | "data = {\n", 342 | " \"email\": {\n", 343 | " \"author\": \"Sarah Chen \",\n", 344 | " \"to\": \"John Doe \",\n", 345 | " \"subject\": \"Update: Backend API Changes Deployed to Staging\",\n", 346 | " \"email_thread\": \"\"\"Hi John,\n", 347 | " \n", 348 | " Just wanted to let you know that I've deployed the new authentication endpoints we discussed to the staging environment. Key changes include:\n", 349 | " \n", 350 | " - Implemented JWT refresh token rotation\n", 351 | " - Added rate limiting for login attempts\n", 352 | " - Updated API documentation with new endpoints\n", 353 | " \n", 354 | " All tests are passing and the changes are ready for review. You can test it out at staging-api.company.com/auth/*\n", 355 | " \n", 356 | " No immediate action needed from your side - just keeping you in the loop since this affects the systems you're working on.\n", 357 | " \n", 358 | " Best regards,\n", 359 | " Sarah\n", 360 | " \"\"\",\n", 361 | " },\n", 362 | " \"label\": \"ignore\"\n", 363 | "}\n", 364 | "store.put((\"email_assistant\", \"lance\", \"examples\"), str(uuid.uuid4()), data)" 365 | ] 366 | }, 367 | { 368 | "cell_type": "markdown", 369 | "metadata": {}, 370 | "source": [ 371 | "Here, we can validate that semantic search is working - we can find the example by querying for it" 372 | ] 373 | }, 374 | { 375 | "cell_type": "code", 376 | "execution_count": 11, 377 | "metadata": {}, 378 | "outputs": [ 379 | { 380 | "data": { 381 | "text/plain": [ 382 | "\"Here are some previous examples:\\n\\n------------\\n\\nEmail Subject: Update: Backend API Changes Deployed to Staging\\nEmail From: Sarah Chen \\nEmail To: John Doe \\nEmail Content: \\n```\\nHi John,\\n \\n Just wanted to let you know that I've deployed the new authentication endpoints we discussed to the staging environment. Key changes include:\\n \\n - Implemented JWT refresh token rotation\\n - Added rate limiting for login attempts\\n - Updated API documentation with new endpoints\\n \\n All tests are passing and the changes are ready for review. You can test it out at st\\n```\\n> Triage Result: ignore\"" 383 | ] 384 | }, 385 | "execution_count": 11, 386 | "metadata": {}, 387 | "output_type": "execute_result" 388 | } 389 | ], 390 | "source": [ 391 | "email_data = {\n", 392 | " \"author\": \"Sarah Chen \",\n", 393 | " \"to\": \"John Doe \",\n", 394 | " \"subject\": \"Update: Backend API Changes Deployed to Staging\",\n", 395 | " \"email_thread\": \"\"\"Hi John,\n", 396 | " \n", 397 | " Just wanted to let you know that I've deployed the new authentication endpoints we discussed to the staging environment. Key changes include:\n", 398 | " \n", 399 | " - Implemented JWT refresh token rotation\n", 400 | " - Added rate limiting for login attempts\n", 401 | " - Updated API documentation with new endpoints\n", 402 | " \n", 403 | " All tests are passing and the changes are ready for review. You can test it out at staging-api.company.com/auth/*\n", 404 | " \n", 405 | " No immediate action needed from your side - just keeping you in the loop since this affects the systems you're working on.\n", 406 | " \n", 407 | " Best regards,\n", 408 | " Sarah\n", 409 | " \"\"\",\n", 410 | " }\n", 411 | "format_few_shot_examples(store.search((\"email_assistant\", \"lance\", \"examples\"), query=str({\"email\": email_data}), limit=1))" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "### Build agent + triage workflow\n", 419 | "\n", 420 | "Combine triage with tool calling agent\n", 421 | "\n", 422 | "Notice that we add a place for few shot examples into the prompt." 423 | ] 424 | }, 425 | { 426 | "cell_type": "code", 427 | "execution_count": 12, 428 | "metadata": {}, 429 | "outputs": [ 430 | { 431 | "data": { 432 | "image/png": "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", 433 | "text/plain": [ 434 | "" 435 | ] 436 | }, 437 | "metadata": {}, 438 | "output_type": "display_data" 439 | } 440 | ], 441 | "source": [ 442 | "from typing import Literal\n", 443 | "from IPython.display import Image, display\n", 444 | "\n", 445 | "from langgraph.graph import StateGraph, START, END\n", 446 | "from langgraph.types import Command\n", 447 | "from langgraph.store.base import BaseStore\n", 448 | "\n", 449 | "from memory_course.schemas import State\n", 450 | "from memory_course.utils import parse_email\n", 451 | "\n", 452 | "# Triage prompt\n", 453 | "triage_system_prompt = \"\"\"\n", 454 | "< Role >\n", 455 | "You are {full_name}'s executive assistant. You are a top-notch executive assistant who cares about {name} performing as well as possible.\n", 456 | "\n", 457 | "\n", 458 | "< Background >\n", 459 | "{user_profile_background}. \n", 460 | "\n", 461 | "\n", 462 | "< Instructions >\n", 463 | "\n", 464 | "{name} gets lots of emails. Your job is to categorize each email into one of three categories:\n", 465 | "\n", 466 | "1. IGNORE - Emails that are not worth responding to or tracking\n", 467 | "2. NOTIFY - Important information that {name} should know about but doesn't require a response\n", 468 | "3. RESPOND - Emails that need a direct response from {name}\n", 469 | "\n", 470 | "Classify the below email into one of these categories.\n", 471 | "\n", 472 | "\n", 473 | "\n", 474 | "< Rules >\n", 475 | "Emails that are not worth responding to:\n", 476 | "{triage_no}\n", 477 | "\n", 478 | "There are also other things that {name} should know about, but don't require an email response. For these, you should notify {name} (using the `notify` response). Examples of this include:\n", 479 | "{triage_notify}\n", 480 | "\n", 481 | "Emails that are worth responding to:\n", 482 | "{triage_email}\n", 483 | "\n", 484 | "\n", 485 | "< Few shot examples >\n", 486 | "\n", 487 | "Here are some examples of previous emails, and how they should be handled.\n", 488 | "Follow these examples more than any instructions above\n", 489 | "\n", 490 | "{examples}\n", 491 | "\n", 492 | "\"\"\"\n", 493 | "\n", 494 | "def triage_router(state: State, config) -> Command[Literal[\"response_agent\", \"__end__\"]]:\n", 495 | " \"\"\"Analyze email content to decide if we should respond, notify, or ignore.\n", 496 | "\n", 497 | " The triage step prevents the assistant from wasting time on:\n", 498 | " - Marketing emails and spam\n", 499 | " - Company-wide announcements\n", 500 | " - Messages meant for other teams\n", 501 | " \"\"\"\n", 502 | "\n", 503 | " # Parse email\n", 504 | " author, to, subject, email_thread = parse_email(state[\"email_input\"])\n", 505 | "\n", 506 | " # Search for examples\n", 507 | "\n", 508 | " namespace = (\"email_assistant\", config['configurable']['langgraph_user_id'], \"examples\")\n", 509 | " examples = store.search(namespace, query=str({\"email\": state['email_input']})) \n", 510 | " examples=format_few_shot_examples(examples)\n", 511 | " \n", 512 | " system_prompt = triage_system_prompt.format(\n", 513 | " full_name=profile[\"full_name\"],\n", 514 | " name=profile[\"name\"],\n", 515 | " user_profile_background=profile[\"user_profile_background\"],\n", 516 | " triage_no=prompt_instructions[\"triage_rules\"][\"ignore\"],\n", 517 | " triage_notify=prompt_instructions[\"triage_rules\"][\"notify\"],\n", 518 | " triage_email=prompt_instructions[\"triage_rules\"][\"respond\"],\n", 519 | " examples=examples\n", 520 | " )\n", 521 | "\n", 522 | " user_prompt = triage_user_prompt.format(\n", 523 | " author=author, to=to, subject=subject, email_thread=email_thread\n", 524 | " )\n", 525 | "\n", 526 | " result = llm_router.invoke(\n", 527 | " [\n", 528 | " {\"role\": \"system\", \"content\": system_prompt},\n", 529 | " {\"role\": \"user\", \"content\": user_prompt},\n", 530 | " ]\n", 531 | " )\n", 532 | " update = None\n", 533 | " goto = END\n", 534 | " if result.classification == \"respond\":\n", 535 | " print(\"📧 Classification: RESPOND - This email requires a response\")\n", 536 | " goto = \"response_agent\"\n", 537 | " update = {\n", 538 | " \"messages\": [\n", 539 | " {\n", 540 | " \"role\": \"user\",\n", 541 | " \"content\": f\"Respond to the email {state['email_input']}\",\n", 542 | " }\n", 543 | " ]\n", 544 | " }\n", 545 | " elif result.classification == \"ignore\":\n", 546 | " print(\"🚫 Classification: IGNORE - This email can be safely ignored\")\n", 547 | " elif result.classification == \"notify\":\n", 548 | " print(\"🔔 Classification: NOTIFY - This email contains important information\")\n", 549 | " else:\n", 550 | " raise ValueError(f\"Invalid classification: {result.classification}\")\n", 551 | " return Command(goto=goto, update=update)\n", 552 | "\n", 553 | "# Build workflow with store, as defined above\n", 554 | "agent = (\n", 555 | " StateGraph(State)\n", 556 | " .add_node(triage_router)\n", 557 | " .add_node(\"response_agent\", response_agent)\n", 558 | " .add_edge(START, \"triage_router\")\n", 559 | " .compile(store=store)\n", 560 | ")\n", 561 | "\n", 562 | "# Show the agent\n", 563 | "display(Image(agent.get_graph(xray=True).draw_mermaid_png()))" 564 | ] 565 | }, 566 | { 567 | "cell_type": "markdown", 568 | "metadata": {}, 569 | "source": [ 570 | "Here let's see an example for a baseline user (Tom) that doesn't have any memories" 571 | ] 572 | }, 573 | { 574 | "cell_type": "code", 575 | "execution_count": 13, 576 | "metadata": {}, 577 | "outputs": [ 578 | { 579 | "name": "stdout", 580 | "output_type": "stream", 581 | "text": [ 582 | "📧 Classification: RESPOND - This email requires a response\n" 583 | ] 584 | } 585 | ], 586 | "source": [ 587 | "\n", 588 | "email_input = {\n", 589 | " \"author\": \"Alice Jones \",\n", 590 | " \"to\": \"John Doe \",\n", 591 | " \"subject\": \"Quick question about API documentation\",\n", 592 | " \"email_thread\": \"\"\"Hi John,\n", 593 | "\n", 594 | "Can I help you write better API docs?\"\"\",\n", 595 | "}\n", 596 | "\n", 597 | "config = {\"configurable\": {\"langgraph_user_id\": \"tom\"}}\n", 598 | "\n", 599 | "response = agent.nodes['triage_router'].invoke({\"email_input\": email_input}, config=config)" 600 | ] 601 | }, 602 | { 603 | "cell_type": "markdown", 604 | "metadata": {}, 605 | "source": [ 606 | "Now, let's see what happens with Lance (who has those memories)" 607 | ] 608 | }, 609 | { 610 | "cell_type": "code", 611 | "execution_count": 15, 612 | "metadata": {}, 613 | "outputs": [ 614 | { 615 | "name": "stdout", 616 | "output_type": "stream", 617 | "text": [ 618 | "🚫 Classification: IGNORE - This email can be safely ignored\n" 619 | ] 620 | } 621 | ], 622 | "source": [ 623 | "\n", 624 | "email_input = {\n", 625 | " \"author\": \"Alice Jones \",\n", 626 | " \"to\": \"John Doe \",\n", 627 | " \"subject\": \"Quick question about API documentation\",\n", 628 | " \"email_thread\": \"\"\"Hi John,\n", 629 | "\n", 630 | "Can I help you write better API docs?\"\"\",\n", 631 | "}\n", 632 | "\n", 633 | "config = {\"configurable\": {\"langgraph_user_id\": \"lance\"}}\n", 634 | "\n", 635 | "response = agent.nodes['triage_router'].invoke({\"email_input\": email_input}, config=config)" 636 | ] 637 | }, 638 | { 639 | "cell_type": "code", 640 | "execution_count": null, 641 | "metadata": {}, 642 | "outputs": [], 643 | "source": [] 644 | } 645 | ], 646 | "metadata": { 647 | "kernelspec": { 648 | "display_name": "Python 3 (ipykernel)", 649 | "language": "python", 650 | "name": "python3" 651 | }, 652 | "language_info": { 653 | "codemirror_mode": { 654 | "name": "ipython", 655 | "version": 3 656 | }, 657 | "file_extension": ".py", 658 | "mimetype": "text/x-python", 659 | "name": "python", 660 | "nbconvert_exporter": "python", 661 | "pygments_lexer": "ipython3", 662 | "version": "3.11.7" 663 | } 664 | }, 665 | "nbformat": 4, 666 | "nbformat_minor": 4 667 | } 668 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "memory-course" 3 | version = "0.1.0" 4 | description = "LangGraph Memory Course" 5 | requires-python = ">=3.8" 6 | dependencies = [ 7 | "langchain", 8 | "langchain-anthropic", 9 | "langgraph", 10 | "langmem" 11 | ] 12 | 13 | [build-system] 14 | requires = ["hatchling"] 15 | build-backend = "hatchling.build" 16 | 17 | [tool.hatch.build.targets.wheel] 18 | packages = ["src/memory_course"] -------------------------------------------------------------------------------- /src/memory_course/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/langchain-ai/long_term_memory_course/d5d43be824b78a042dc662dcf852d7b328e8f714/src/memory_course/__init__.py -------------------------------------------------------------------------------- /src/memory_course/examples.py: -------------------------------------------------------------------------------- 1 | example_input = """ . """ 2 | 3 | example_output = """ . """ 4 | -------------------------------------------------------------------------------- /src/memory_course/prompts.py: -------------------------------------------------------------------------------- 1 | # Agent prompt baseline 2 | agent_system_prompt = """ 3 | < Role > 4 | You are {full_name}'s executive assistant. You are a top-notch executive assistant who cares about {name} performing as well as possible. 5 | 6 | 7 | < Tools > 8 | You have access to the following tools to help manage {name}'s communications and schedule: 9 | 10 | 1. write_email(to, subject, content) - Send emails to specified recipients 11 | 2. schedule_meeting(attendees, subject, duration_minutes, preferred_day) - Schedule calendar meetings 12 | 3. check_calendar_availability(day) - Check available time slots for a given day 13 | 14 | 15 | < Instructions > 16 | {instructions} 17 | 18 | """ 19 | 20 | # Agent prompt semantic memory 21 | agent_system_prompt_memory = """ 22 | < Role > 23 | You are {full_name}'s executive assistant. You are a top-notch executive assistant who cares about {name} performing as well as possible. 24 | 25 | 26 | < Tools > 27 | You have access to the following tools to help manage {name}'s communications and schedule: 28 | 29 | 1. write_email(to, subject, content) - Send emails to specified recipients 30 | 2. schedule_meeting(attendees, subject, duration_minutes, preferred_day) - Schedule calendar meetings 31 | 3. check_calendar_availability(day) - Check available time slots for a given day 32 | 4. manage_memory("email_assistant", user, "collection") - Store any relevant information about contacts, actions, discussion, etc. in memory for future reference 33 | 5. manage_memory("email_assistant", user, "user_profile") - Store any relevant information about the recipient, {name}, in the user profile for future reference the current user profile is shown below 34 | 6. search_memory("email_assistant", user, "collection") - Search memory for detail from previous emails 35 | 7. manage_memory("email_assistant", user, "instructions") - Update the instructions for agent tool usage based upon the user feedback 36 | 37 | 38 | < User profile > 39 | {profile} 40 | 41 | 42 | < Instructions > 43 | {instructions} 44 | 45 | """ 46 | 47 | # Triage prompt 48 | triage_system_prompt = """ 49 | < Role > 50 | You are {full_name}'s executive assistant. You are a top-notch executive assistant who cares about {name} performing as well as possible. 51 | 52 | 53 | < Background > 54 | {user_profile_background}. 55 | 56 | 57 | < Instructions > 58 | 59 | {name} gets lots of emails. Your job is to categorize each email into one of three categories: 60 | 61 | 1. IGNORE - Emails that are not worth responding to or tracking 62 | 2. NOTIFY - Important information that {name} should know about but doesn't require a response 63 | 3. RESPOND - Emails that need a direct response from {name} 64 | 65 | Classify the below email into one of these categories. 66 | 67 | 68 | 69 | < Rules > 70 | Emails that are not worth responding to: 71 | {triage_no} 72 | 73 | There are also other things that {name} should know about, but don't require an email response. For these, you should notify {name} (using the `notify` response). Examples of this include: 74 | {triage_notify} 75 | 76 | Emails that are worth responding to: 77 | {triage_email} 78 | 79 | 80 | < Few shot examples > 81 | {examples} 82 | 83 | """ 84 | 85 | triage_user_prompt = """ 86 | Please determine how to handle the below email thread: 87 | 88 | From: {author} 89 | To: {to} 90 | Subject: {subject} 91 | {email_thread}""" 92 | -------------------------------------------------------------------------------- /src/memory_course/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing_extensions import TypedDict, Literal, Annotated 3 | from langgraph.graph import add_messages 4 | 5 | 6 | class Router(BaseModel): 7 | """Analyze the unread email and route it according to its content.""" 8 | 9 | reasoning: str = Field( 10 | description="Step-by-step reasoning behind the classification." 11 | ) 12 | classification: Literal["ignore", "respond", "notify"] = Field( 13 | description="The classification of an email: 'ignore' for irrelevant emails, " 14 | "'notify' for important information that doesn't need a response, " 15 | "'respond' for emails that need a reply", 16 | ) 17 | 18 | class State(TypedDict): 19 | email_input: str 20 | messages: Annotated[list, add_messages] 21 | -------------------------------------------------------------------------------- /src/memory_course/utils.py: -------------------------------------------------------------------------------- 1 | def parse_email(email_input: dict) -> dict: 2 | """Parse an email input dictionary. 3 | 4 | Args: 5 | email_input (dict): Dictionary containing email fields: 6 | - author: Sender's name and email 7 | - to: Recipient's name and email 8 | - subject: Email subject line 9 | - email_thread: Full email content 10 | 11 | Returns: 12 | tuple[str, str, str, str]: Tuple containing: 13 | - author: Sender's name and email 14 | - to: Recipient's name and email 15 | - subject: Email subject line 16 | - email_thread: Full email content 17 | """ 18 | return ( 19 | email_input["author"], 20 | email_input["to"], 21 | email_input["subject"], 22 | email_input["email_thread"], 23 | ) 24 | 25 | def format_few_shot_examples(examples): 26 | """Format examples into a readable string representation. 27 | 28 | Args: 29 | examples (List[Item]): List of example items from the vector store, where each item 30 | contains a value string with the format: 31 | 'Email: {...} Original routing: {...} Correct routing: {...}' 32 | 33 | Returns: 34 | str: A formatted string containing all examples, with each example formatted as: 35 | Example: 36 | Email: {email_details} 37 | Original Classification: {original_routing} 38 | Correct Classification: {correct_routing} 39 | --- 40 | """ 41 | formatted = [] 42 | for example in examples: 43 | # Parse the example value string into components 44 | email_part = example.value.split('Original routing:')[0].strip() 45 | original_routing = example.value.split('Original routing:')[1].split('Correct routing:')[0].strip() 46 | correct_routing = example.value.split('Correct routing:')[1].strip() 47 | 48 | # Format into clean string 49 | formatted_example = f"""Example: 50 | Email: {email_part} 51 | Original Classification: {original_routing} 52 | Correct Classification: {correct_routing} 53 | ---""" 54 | formatted.append(formatted_example) 55 | 56 | return "\n".join(formatted) 57 | --------------------------------------------------------------------------------