diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 3f10c4499..280ee167b 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -58,13 +58,13 @@ These guides show how to use different streaming modes. - [How to call tools using ToolNode](tool-calling.ipynb) - [How to handle tool calling errors](tool-calling-errors.ipynb) +- [How to pass graph state to tools](pass-run-time-values-to-tools.ipynb) ## Other - [How to run graph asynchronously](async.ipynb) - [How to visualize your graph](visualization.ipynb) - [How to add runtime configuration to your graph](configuration.ipynb) -- [How to pass runtime values to tools](pass-run-time-values-to-tools.ipynb) - [How to use a Pydantic model as your state](state-model.ipynb) - [How to use a context object in state](state-context-key.ipynb) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index ba725707c..998d78fbc 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -155,11 +155,11 @@ nav: - Tool calling: - Call tools using ToolNode: how-tos/tool-calling.ipynb - Handle tool calling errors: how-tos/tool-calling-errors.ipynb + - Pass graph state to tools: how-tos/pass-run-time-values-to-tools.ipynb - Other: - Run graph asynchronously: how-tos/async.ipynb - Visualize your graph: how-tos/visualization.ipynb - Add runtime configuration: how-tos/configuration.ipynb - - Pass runtime values to tools: how-tos/pass-run-time-values-to-tools.ipynb - Use Pydantic model as state: how-tos/state-model.ipynb - Use a context object in state: how-tos/state-context-key.ipynb - Prebuilt ReAct Agent: diff --git a/examples/pass-run-time-values-to-tools.ipynb b/examples/pass-run-time-values-to-tools.ipynb index f8af76496..48d575aa1 100644 --- a/examples/pass-run-time-values-to-tools.ipynb +++ b/examples/pass-run-time-values-to-tools.ipynb @@ -5,18 +5,13 @@ "id": "51466c8d-8ce4-4b3d-be4e-18fdbeda5f53", "metadata": {}, "source": [ - "# How to pass runtime values to tools\n", + "# How to pass graph state to tools\n", "\n", - "You may need to bind values to a tool that are only known at runtime. For example, the tool logic may require using the ID of the user who made the request.\n", + "Sometimes we need to pass in agent state to our tools. This type of stateful tools is useful when a tool's output is affected by past agent steps (e.g. if you're using a sub-agent as a tool, and want to pass the message history in to the sub-agent), or when a tool's input needs to be validated given context from past agent steps. \n", "\n", - "Most of the time, such values should not be controlled by the LLM. In fact, allowing the LLM to control the user ID may lead to a security risk.\n", + "In this guide we'll demonstrate how to create tools that take agent state as input.\n", "\n", - "Instead, the LLM should only control the parameters of the tool that are meant to be controlled by the LLM, while other parameters (such as user ID) should be fixed by the application logic.\n", - "\n", - "To pass run time information, we will leverage the [Runnable interface](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html#langchain_core.runnables.base.Runnable).\n", - "The standard runnables methods (`invoke`, `batch`, `stream` etc.) accept a 2nd argument which is a [RunnableConfig](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.config.RunnableConfig.html). `RunnableConfig` has a few standard fields, but allows users to use other fields for run time information.\n", - "\n", - "Here, we will show how to set up a simple agent that has access to three tools for saving, reading, and deleting a list of the user's favorite pets." + "This is a special case of [passing runtime arguments to tools](https://python.langchain.com/v0.2/docs/how_to/tool_runtime/), which you can learn about in the LangChain docs." ] }, { @@ -35,14 +30,17 @@ "id": "af4ce0ba-7596-4e5f-8bf8-0b0bd6e62833", "metadata": {}, "outputs": [], - "source": ["%%capture --no-stderr\n%pip install --quiet -U langgraph langchain langchain_openai"] + "source": [ + "%%capture --no-stderr\n", + "%pip install --quiet -U langgraph langchain langchain-openai" + ] }, { "cell_type": "markdown", "id": "0abe11f4-62ed-4dc4-8875-3db21e260d1d", "metadata": {}, "source": [ - "Next, we need to set API keys for OpenAI (the LLM we will use)." + "Next, we need to set API keys for OpenAI (the chat model we will use)." ] }, { @@ -51,7 +49,14 @@ "id": "c903a1cf-2977-4e2d-ad7d-8b3946821d89", "metadata": {}, "outputs": [], - "source": ["import getpass\nimport os\n\nif \"OPENAI_API_KEY\" not in os.environ:\n os.environ[\"OPENAI_API_KEY\"] = getpass.getpass(\"OpenAI API Key:\")"] + "source": [ + "import getpass\n", + "import os\n", + "\n", + "os.environ[\"OPENAI_API_KEY\"] = os.environ.get(\"OPENAI_API_KEY\") or getpass.getpass(\n", + " \"OpenAI API Key:\"\n", + ")" + ] }, { "cell_type": "markdown", @@ -67,52 +72,171 @@ "id": "95e25aec-7c9f-4a63-b143-225d0e9a79c3", "metadata": {}, "outputs": [], - "source": ["os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n\nif \"LANGCHAIN_API_KEY\" not in os.environ:\n os.environ[\"LANGCHAIN_API_KEY\"] = getpass.getpass(\"LangSmith API Key:\")"] + "source": [ + "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = os.environ.get(\n", + " \"LANGCHAIN_API_KEY\"\n", + ") or getpass.getpass(\"LangSmith API Key:\")" + ] }, { "cell_type": "markdown", "id": "21ac643b-cb06-4724-a80c-2862ba4773f1", "metadata": {}, "source": [ - "## Set up the tools\n", + "## Defining the tools\n", "\n", - "Here, we will make a function that dynamically creates 3 [custom-tools](https://python.langchain.com/v0.2/docs/how_to/custom_tools).\n", + "We'll want our tool to take graph state as an input, but we don't want the model to try to generate this input when calling the tool. We can use the `InjectedToolArg` annotation to mark `state` as being injected at runtime. Any argument annotated with `InjectedToolArg` will not be generated by the model.\n", "\n", - "This function will bind to the tools the correct `user_id`, allowing the LLM to only fill in the other relevant values. Importantly,\n", - "the LLM will be **unaware** that a user ID even exists!" + "In this example we'll create a tool that returns Documents and then another tool that actually cites the Documents that justify a claim." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 63, "id": "1d36e782-80f4-4334-b7d7-ee4c79864480", "metadata": {}, "outputs": [], - "source": ["from typing import List\n\nfrom langchain_core.tools import BaseTool, tool\n\n# A global dict that the tools will be updating in this example.\nuser_to_pets = {}\n\n\ndef generate_tools_for_user(user_id: str) -> List[BaseTool]:\n \"\"\"Generate a set of tools that have a user id associated with them.\"\"\"\n\n @tool\n def update_favorite_pets(pets: List[str]) -> None:\n \"\"\"Add the list of favorite pets.\"\"\"\n user_to_pets[user_id] = pets\n\n @tool\n def delete_favorite_pets() -> None:\n \"\"\"Delete the list of favorite pets.\"\"\"\n if user_id in user_to_pets:\n del user_to_pets[user_id]\n\n @tool\n def list_favorite_pets() -> None:\n \"\"\"List favorite pets if any.\"\"\"\n return user_to_pets.get(user_id, [])\n\n return [update_favorite_pets, delete_favorite_pets, list_favorite_pets]"] + "source": [ + "from typing import List, Tuple\n", + "\n", + "from langchain_core.documents import Document\n", + "from langchain_core.pydantic_v1 import BaseModel\n", + "from langchain_core.tools import InjectedToolArg, tool\n", + "from typing_extensions import Annotated\n", + "\n", + "\n", + "@tool(parse_docstring=True, response_format=\"content_and_artifact\")\n", + "def get_context(\n", + " question: List[str], state: Annotated[dict, InjectedToolArg]\n", + ") -> Tuple[str, List[Document]]:\n", + " \"\"\"Get context on the question.\n", + "\n", + " Args:\n", + " question: The user question\n", + " \"\"\"\n", + " # return constant dummy output\n", + " docs = [\n", + " Document(\n", + " \"FooBar company just raised 1 Billion dollars!\",\n", + " metadata={\"source\": \"twitter\"},\n", + " ),\n", + " Document(\n", + " \"FooBar company is now only hiring AI's\", metadata={\"source\": \"twitter\"}\n", + " ),\n", + " Document(\n", + " \"FooBar company was founded in 2019\", metadata={\"source\": \"wikipedia\"}\n", + " ),\n", + " Document(\n", + " \"FooBar company makes friendly robots\", metadata={\"source\": \"wikipedia\"}\n", + " ),\n", + " ]\n", + " return \"\\n\\n\".join(doc.page_content for doc in docs), docs\n", + "\n", + "\n", + "@tool(parse_docstring=True, response_format=\"content_and_artifact\")\n", + "def cite_context_sources(\n", + " claim: str, state: Annotated[dict, InjectedToolArg]\n", + ") -> Tuple[str, List[Document]]:\n", + " \"\"\"Cite which source a claim was based on.\n", + "\n", + " Args:\n", + " claim: The claim that was made.\n", + " \"\"\"\n", + " docs = []\n", + " # We get the potentially cited docs from past ToolMessages in our state.\n", + " for msg in state[\"messages\"]:\n", + " if isinstance(msg, ToolMessage) and msg.name == \"get_context\":\n", + " docs.extend(msg.artifact)\n", + "\n", + " class Cite(BaseModel):\n", + " \"\"\"Return the index(es) of the documents that justify the claim\"\"\"\n", + "\n", + " indexes: List[int]\n", + "\n", + " structured_model = model.with_structured_output(Cite)\n", + " system = f\"Which of the following documents best justifies the claim:\\n\\n{claim}\"\n", + " context = \"\\n\\n\".join(\n", + " f\"Document {i}:\\n\" + doc.page_content for i, doc in enumerate(docs)\n", + " )\n", + " citation = structured_model.invoke([(\"system\", system), (\"human\", context)])\n", + " cited_docs = [docs[i] for i in citation.indexes]\n", + " sources = \", \".join(doc.metadata[\"source\"] for doc in cited_docs)\n", + " return sources, cited_docs" + ] }, { "cell_type": "markdown", - "id": "5497ed70-fce3-47f1-9cad-46f912bad6a5", + "id": "1c2d0de0-0f3e-4bbe-b0b6-cc0f70b11993", "metadata": {}, "source": [ - "## Set up the model\n", - "\n", - "Now we need to load the chat model we want to use.\n", - "Importantly, this should satisfy two criteria:\n", - "\n", - "1. It should work with messages. We will represent all agent state in the form of messages, so it needs to be able to work well with them.\n", - "2. It should work with OpenAI function calling. This means it should either be an OpenAI model or a model that exposes a similar interface.\n", - "\n", - "Note: these model requirements are not requirements for using LangGraph - they are just requirements for this one example.\n" + "If we look at the input schemas for these tools, we'll see that `state` is still listed:" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "892b54b9-75f0-4804-9ed0-88b5e5532989", + "execution_count": 64, + "id": "1092929b-c939-4b2a-9f9c-e725b0e34af2", "metadata": {}, - "outputs": [], - "source": ["from langchain_openai import ChatOpenAI\n\n# We will set streaming=True so that we can stream tokens\n# See the streaming section for more information on this.\nmodel = ChatOpenAI(temperature=0, streaming=True)"] + "outputs": [ + { + "data": { + "text/plain": [ + "{'title': 'get_contextSchema',\n", + " 'description': 'Get context on the question.',\n", + " 'type': 'object',\n", + " 'properties': {'question': {'title': 'Question',\n", + " 'description': 'The user question',\n", + " 'type': 'array',\n", + " 'items': {'type': 'string'}},\n", + " 'state': {'title': 'State', 'type': 'object'}},\n", + " 'required': ['question', 'state']}" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_context.get_input_schema().schema()" + ] + }, + { + "cell_type": "markdown", + "id": "e346a26e-e00b-48e5-82c5-c930ea6084a4", + "metadata": {}, + "source": [ + "But if we look at the tool call schema, which is what is passed to the model for tool-calling, `state` has been removed:" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "3912bb51-3107-4335-a659-021c5d89fb37", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'title': 'get_context',\n", + " 'description': 'Get context on the question.',\n", + " 'type': 'object',\n", + " 'properties': {'question': {'title': 'Question',\n", + " 'description': 'The user question',\n", + " 'type': 'array',\n", + " 'items': {'type': 'string'}}},\n", + " 'required': ['question']}" + ] + }, + "execution_count": 65, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_context.tool_call_schema.schema()" + ] }, { "cell_type": "markdown", @@ -121,7 +245,7 @@ "source": [ "## Define the agent state\n", "\n", - "The main type of graph in `langgraph` is the `StatefulGraph`.\n", + "The main type of graph in `langgraph` is the `StateGraph`.\n", "This graph is parameterized by a state object that it passes around to each node.\n", "Each node then returns operations to update that state.\n", "These operations can either SET specific attributes on the state (e.g. overwrite the existing values) or ADD to the existing attribute.\n", @@ -134,11 +258,20 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 66, "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], - "source": ["import operator\nfrom typing import Annotated, Sequence, TypedDict\n\nfrom langchain_core.messages import BaseMessage\n\n\nclass AgentState(TypedDict):\n messages: Annotated[Sequence[BaseMessage], operator.add]"] + "source": [ + "import operator\n", + "from typing import Annotated, Sequence, TypedDict\n", + "\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]" + ] }, { "cell_type": "markdown", @@ -169,11 +302,66 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 67, "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], - "source": ["from langchain_core.messages import ToolMessage\n\nfrom langgraph.prebuilt import ToolExecutor, ToolInvocation\n\n\n# Define the function that determines whether to continue or not\ndef should_continue(state, config):\n messages = state[\"messages\"]\n last_message = messages[-1]\n # If there is no function call, then we finish\n if not last_message.tool_calls:\n return \"end\"\n # Otherwise if there is, we continue\n else:\n return \"continue\"\n\n\n# Define the function that calls the model\ndef call_model(state, config):\n messages = state[\"messages\"]\n tools = generate_tools_for_user(config[\"user_id\"])\n model_with_tools = model.bind_tools(tools)\n response = model_with_tools.invoke(messages)\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ndef call_tool(state, config):\n messages = state[\"messages\"]\n # Based on the continue condition\n # we know the last message involves a function call\n last_message = messages[-1]\n # We construct an ToolInvocation for each tool call\n tool_invocations = []\n for tool_call in last_message.tool_calls:\n action = ToolInvocation(\n tool=tool_call[\"name\"],\n tool_input=tool_call[\"args\"],\n )\n tool_invocations.append(action)\n\n # We call the tool_executor and get back a response\n # We can now wrap these tools in a simple ToolExecutor.\n # This is a real simple class that takes in a ToolInvocation and calls that tool, returning the output.\n # A ToolInvocation is any class with `tool` and `tool_input` attribute.\n tools = generate_tools_for_user(config[\"user_id\"])\n tool_executor = ToolExecutor(tools)\n responses = tool_executor.batch(tool_invocations, return_exceptions=True)\n # We use the response to create tool messages\n tool_messages = [\n ToolMessage(\n content=str(response),\n name=tc[\"name\"],\n tool_call_id=tc[\"id\"],\n )\n for tc, response in zip(last_message.tool_calls, responses)\n ]\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": tool_messages}"] + "source": [ + "from copy import deepcopy\n", + "\n", + "from langchain_core.messages import ToolMessage\n", + "from langchain_openai import ChatOpenAI\n", + "\n", + "from langgraph.prebuilt import ToolExecutor, ToolInvocation\n", + "\n", + "model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n", + "\n", + "\n", + "# Define the function that determines whether to continue or not\n", + "def should_continue(state, config):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " # If there is no function call, then we finish\n", + " if not last_message.tool_calls:\n", + " return \"end\"\n", + " # Otherwise if there is, we continue\n", + " else:\n", + " return \"continue\"\n", + "\n", + "\n", + "tools = [get_context, cite_context_sources]\n", + "tool_map = {tool_.name: tool_ for tool_ in tools}\n", + "\n", + "\n", + "# Define the function that calls the model\n", + "def call_model(state, config):\n", + " messages = state[\"messages\"]\n", + " model_with_tools = model.bind_tools(tools)\n", + " response = model_with_tools.invoke(messages)\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": [response]}\n", + "\n", + "\n", + "# Helper function for adding state to each tool call's arguments\n", + "def inject_state(message, state):\n", + " tool_calls = []\n", + " for tool_call in message.tool_calls:\n", + " tool_call_copy = deepcopy(tool_call)\n", + " tool_call_copy[\"args\"][\"state\"] = state\n", + " tool_calls.append(tool_call_copy)\n", + " return tool_calls\n", + "\n", + "\n", + "# Define the function to execute tools\n", + "def call_tool(state, config):\n", + " messages = state[\"messages\"]\n", + " last_message = messages[-1]\n", + " tool_messages = []\n", + " for tool_call in inject_state(last_message, state):\n", + " tool_messages.append(tool_map[tool_call[\"name\"]].invoke(tool_call, config))\n", + " # We return a list, because this will get added to the existing list\n", + " return {\"messages\": tool_messages}" + ] }, { "cell_type": "markdown", @@ -187,21 +375,64 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 68, "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], - "source": ["from langgraph.graph import END, StateGraph, START\n\n# Define a new graph\nworkflow = StateGraph(AgentState)\n\n# Define the two nodes we will cycle between\nworkflow.add_node(\"agent\", call_model)\nworkflow.add_node(\"action\", call_tool)\n\n# Set the entrypoint as `agent`\n# This means that this node is the first one called\nworkflow.add_edge(START, \"agent\")\n\n# We now add a conditional edge\nworkflow.add_conditional_edges(\n # First, we define the start node. We use `agent`.\n # This means these are the edges taken after the `agent` node is called.\n \"agent\",\n # Next, we pass in the function that will determine which node is called next.\n should_continue,\n # Finally we pass in a mapping.\n # The keys are strings, and the values are other nodes.\n # END is a special node marking that the graph should finish.\n # What will happen is we will call `should_continue`, and then the output of that\n # will be matched against the keys in this mapping.\n # Based on which one it matches, that node will then be called.\n {\n # If `tools`, then we call the tool node.\n \"continue\": \"action\",\n # Otherwise we finish.\n \"end\": END,\n },\n)\n\n# We now add a normal edge from `tools` to `agent`.\n# This means that after `tools` is called, `agent` node is called next.\nworkflow.add_edge(\"action\", \"agent\")\n\n# Finally, we compile it!\n# This compiles it into a LangChain Runnable,\n# meaning you can use it as you would any other runnable\napp = workflow.compile()"] + "source": [ + "from langgraph.graph import END, START, StateGraph\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "\n", + "# Define the two nodes we will cycle between\n", + "workflow.add_node(\"agent\", call_model)\n", + "workflow.add_node(\"action\", call_tool)\n", + "\n", + "# Set the entrypoint as `agent`\n", + "# This means that this node is the first one called\n", + "workflow.add_edge(START, \"agent\")\n", + "\n", + "# We now add a conditional edge\n", + "workflow.add_conditional_edges(\n", + " # First, we define the start node. We use `agent`.\n", + " # This means these are the edges taken after the `agent` node is called.\n", + " \"agent\",\n", + " # Next, we pass in the function that will determine which node is called next.\n", + " should_continue,\n", + " # Finally we pass in a mapping.\n", + " # The keys are strings, and the values are other nodes.\n", + " # END is a special node marking that the graph should finish.\n", + " # What will happen is we will call `should_continue`, and then the output of that\n", + " # will be matched against the keys in this mapping.\n", + " # Based on which one it matches, that node will then be called.\n", + " {\n", + " # If `tools`, then we call the tool node.\n", + " \"continue\": \"action\",\n", + " # Otherwise we finish.\n", + " \"end\": END,\n", + " },\n", + ")\n", + "\n", + "# We now add a normal edge from `tools` to `agent`.\n", + "# This means that after `tools` is called, `agent` node is called next.\n", + "workflow.add_edge(\"action\", \"agent\")\n", + "\n", + "# Finally, we compile it!\n", + "# This compiles it into a LangChain Runnable,\n", + "# meaning you can use it as you would any other runnable\n", + "app = workflow.compile()" + ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 69, "id": "a8afd6ef", "metadata": {}, "outputs": [ { "data": { - "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCADtAOgDASIAAhEBAxEB/8QAHQABAAIDAAMBAAAAAAAAAAAAAAYHBAUIAgMJAf/EAFYQAAEDBAADAgcICg8FCQAAAAECAwQABQYRBxIhEzEIFBUiQVXRFjJRVnOTlJUXI1NUYXGBorThJDQ1Njc4QkNSdZGhsbKzCXSCktQYJTNiZYOGlsH/xAAbAQEAAgMBAQAAAAAAAAAAAAAAAgMBBAUGB//EADsRAAIBAgIFCAcIAgMAAAAAAAABAgMRBCEVMUFRkQUSExRSYaHRIlNicZKx8DIzNEKBwdLhI6JjcsL/2gAMAwEAAhEDEQA/APqnSlKAUpSgFKUoBSlKAw5t5t9tcSiXOjRVqHMEvPJQSPh0TWP7qrL64gfSUe2oDmUCLP4luCTGZkBNoY5Q6gK19ue7t16fc9a/VsP5hHsrTxONo4Wp0cotuyezarnTo4LpYKfO1lie6qy+uIH0lHtp7qrL64gfSUe2q79z1r9Ww/mEeynuetfq2H8wj2Vq6Vw/YlxRdo72vAsT3VWX1xA+ko9tPdVZfXED6Sj21Xfuetfq2H8wj2U9z1r9Ww/mEeymlcP2JcUNHe14Fie6qy+uIH0lHtp7qrL64gfSUe2q79z1r9Ww/mEeynuetfq2H8wj2U0rh+xLiho72vAsT3VWX1xA+ko9tPdVZfXED6Sj21Xfuetfq2H8wj2U9z1r9Ww/mEeymlcP2JcUNHe14FjsZFaZTyGmbnDddWdJQiQhSlH4AAa2NUxcLRBh3PH3GIUdhwXaKAttpKT7/wCECrnrpUqsK9JVYJpNtZ9xoYij0ElG9xSlKsNUUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoCtMo/hMe/qhj/Werzrwyj+Ex7+qGP9Z6vOvMcrfin7o/JHp8J9zEVHc24g2Dh3b48y/wA/xJqS8I7CG2XH3XnCCeVDbaVLUdAnoDoCpFVYcebdbpllsz8uBkzs2HO7aBcsTiqkS7c92ax2pQkHaCCUFJSoHm6j0jlU0pSSZszbUW0YeTeEbj+P5VhlubZmzrdkUSRNE6Nb5bpbbQAEBLaGVKUVKJ2OhQEgqACgakV8424VjWVDHbnevE7p2rTCkuRXuxQ44AW0rfCOzSVBSdBSh3iqq8pZrHc4N5vluOXSdOgR7nGu7NpgF6S0X0oDDi47eynmS0CsJ6JUrXSonxvgZdm0DiFBm2nNrhclOtrx632pp1u1+JJQ05zuFJCHHuYO7Q4VL5gkIT3VuKjCTSf6599vruNV1ZpN/t3HQ9z4wYnactdxd+5Orv7RYC4EeDIfcSHujajyNkcndte+VOxzEbG9Nwi4423izNv0ONDmwpFtnyYyEvQpKEOMtOBAcLjjSUpWonZa3zp9I6E1j8P7VKHHHiTenbdKjwp8GzCLKkxltB0Jbf50pKgOqSpPMnvBI3qsLgs/OxjJ83xm52O7RX5ORXC7xrgqEswXo7yw4gpfHmc2laKd7BB6VS4QUXbXZbeJYpSclfVn/RcNKUrVNk1d4/b2P/1tF/z1blVHeP29j/8AW0X/AD1blex5O/Bx97/Y8/yh94vcKUpW+cwUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoCtMo/hMe/qhj/WeqO5Tw1xPOJLMnIcatV7kMo7Np24Q23lITvegVA6G6sfIcAgZHdk3F2VOiSgwI5VDf7MKQFKUARo+lRrX/Yqg+uL39N/VXPxWB6zV6WNTm5JansVjr0cXThTUJK5V54A8NCgIOA44UAkhPkxnQJ1s+9/AP7K3uK8PMXwVclWO49bLGqSEh82+KhkuhO+Xm5QN62db+E1M/sVQfXF7+m/qp9iqD64vf039Vaj5Lm1Z1vmXLG0FmomtpWy+xVB9cXv6b+qqi4jRZuM+ELwfw+Fe7omzZOi8KuCFyOZajGjJca5Va83zid/DUND/APKuDJ6QpbmWXWsyHGrTltrctt7tsW7W9wpUuLNZS62og7BKVAjoetSX7FUH1xe/pv6qfYqg+uL39N/VWVyQ1mqq4Mx1+k8mmVengBw0QdpwHHAdEbFrZ7j3/wAms2x8HMExm6sXO0YdY7ZcY5JalRLe0262SCDyqCdjYJH5asP7FUH1xe/pv6qfYqg+uL39N/VU3yXN5Ot8yPXaC/L4IjN4/b2P/wBbRf8APVuVDY3C22sTokpdwuspUV5L7bciVzI50nYJGutTKurQorD0VS517NvjY52KrRrTUoilKVaaYpSlAKUpQClKUApSlAKUpQClKUApSlAKUpQClKUApSlAK534zfxwfB0+SyP9CRXRFc78Zv44Pg6fJZH+hIoDoilKUApSlAKUpQClKUApSlAKUpQClKUApSlAKUpQClKUApSlAKUpQClKUApSlAK534zfxwfB0+SyP9CRXRFc78Zv44Pg6fJZH+hIoDoilKUApSlAKUpQClKUApSlAKUpQClKUApSlAKUpQClKUApSlAKUpQClKUApWhyPMoOOONx1pemXB1PM3Cip5nCneuZXcEJ2D5yiB0IGz0qLO53k8ghTFptcNB7kyJbjqx+PlQB/YTVqpSau7Jd7L4UalRXiibZHYIWV49dLJc2vGLbcorsKU1vXO04goWnf4UqIr4UcX+E114R8Vr5g8xtcmbAl9gwtCCTJbVpTK0gf00KSdDu3rvFfZ73Z5d97WT/AJnqqPP+DJ4jcZMP4j3OJafK+ODzGEFzspRSSpgudN7bWSoa7zoHoNVLol2lxLOqVtxY3gocFUcBuCVjxx1ATd3k+P3VQIO5bgTzjY6HkAQ2D6Q2D6auCq192eXfe1k/5nq/UZtliCCqDZnx6Uh91v8Av5Vf4U6L2lxHVK24smlRGx8Q48+UzCucN2zTnSEthxXaMOq7uVDo6b+BKglR9ANS6q5QlDWa8oSg7SVhSlKgQFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFaXMMgONWF+W02l6WopZisrOg48s8qAT6Bs7J9ABrdVAuJ61G4Yq0f8AwjOdWd+lQjucv+JP5KupJOeey74K5bSipzUXtNJBg+KB1xx1UmZIX2siU5795fwn4ABoADokAAaAFZVK5p4zcYMmxO/ZFdsYvl0uttx1+OifbWbNGNtjk9n2jL0pag6pwhfN9q3yc6QR3mtaUnN3es9JKUaUdWR0tSuZOM/FfKoF34ls2zMmMTexiLF8l2kxGHXbst5oL59uAqO1qLSA33KT133Vl3/iTxCybN8hs2NtX+IxjqI0ZSrXAtslT8lyOl1RkGS82Qkc4SEtJTvSjzDYAjYg60U7W+vpHSFfinEoUlKlBJWdJBPedb0PyA/2VQtuyHiRmmewccl3o4LKOIxbtOixIceUtqcqQ80sJU4FjkPKnY87okaKTsmHIu2RcWbrwCvjmRybBdLgxdEOu26NHWlDzbC0uOoS62sefykaOwAegB60DqrYvq6X7nU8qKzOjuMPtpdZcHKpCh0IqR8P76/IEyzTnlPyoAQpl9xXMt+OoEJUo+lQUlaSfTypUeqq0CQQkAnmIHefTTHVqa4kWwI/nbbLS4Nd6Q5HIP5Cdf8AEa2aPpXpvc3+qV/krFOLgpUnJ60WfSlKrPPilKUApSlAKUpQClKUApSlAKUpQClKUApSlAKUpQCovxEsz90sKX4ba351ueTNZZQdKd5QQtsfhUhS0j8JFSilThLmSUiUZOLUlsKsiympsZuQw4l1lxIUhae4iquyvwcsfy13I237xf4Vqv7pkz7RBmpaiuSORKe31yFQV5iDrm5CUjaTVgZzerJj3E+z4vaphbyjIEOzRZvFnXGFoRzFb6nEJIjlRBBUdpUrvAJKqynXLzEITKxi6JX6THDT6D+IpXv+0Cs9C5Z03dfpf69x3o4ilWj6Rz1xa4aZpM4gP3jGbdfpNzTDZZg3pu62zsG3UI0FONSGC42nmJKgydK2SACasGdwPbvlxbvz2RXrHcmmwWI16k41LEdq4LbRrmUlSFaI2oJUnlUEnW6sHyhP+Ll6+ifrrVz86iWu926zTYkuJd7kFmDb3w2iRK5BtfZNlfMvlHU8oOh306vV3Ek6N23LX3njBwGBAzY5SiRMcuJtLVmKHXApsstuLcCjscxWVLOyVaPTpvrUVPg+2JrEsXscO63q2u4286/bbtDkNomNFznDgKi2UFKg4oEFHdqrA8oT/i5evon66/Uy7m6QlrGby4o+gsob/vWsD++nV6u75E3OjtaMmKwYsVlkuuPltAQXXSCtehraiNdT3mtpw8gquN4n30g+LIb8Rhq3sODYU64PwFQQn/2j6CKjWUh7F8SueTZeh6245bme3lQbY2uXMdRsAglv3qevnBO+mzzpANWdid6tWR4vabpYnEO2SZFbfhLbaLSFMKSCgpQQCkcutAgdKkkqSavdvw/X6yOfisTGcejgbalKVScoUpSgFKUoBSlKAUpSgFKUoBSlKAUpSgFKUoBSlYkq6xIUqNFeksomSgvxeMp1KXH+UcyghJI5tDqdd3poDLqrLnmFy4n3PiBgViZyDDZ1sjNsNZe5BT4v4w4nmIY5z5/KkoOxroo6KSEqOEzjFy8IXDLe5nljvWACHefHWbPCvPK7KZaO2fGVM60CrS+RKtgoSQqrgoDS4jjfuVxu02py4TL0/b4qIpudyWHJUgJABU4sAbUdAk+k1uqUoDxddQy2txxaW20AqUtR0Egd5Jr4weEb4TV14g+Es5nlhmKYj2CU2zYVpPRLLCyUrI6b7RXMsg+hfKegr7E5jjEbNsRvmOzHpEaJd4L9veeiLCXm0OtqQpSCQQFAKJBII3roa+e3ED/Z/wDD3FePXCnCYl5yZy1ZYi7KnPPyo5fbMWOl1vsiGAkbUo83MlWx3a76A7x4RcS7bxh4bWDMLV5sS6xg6WubZZcBKXGifSULCkk/gqYVXXAvgXYPB8xKbjmNS7nJtci4O3BKLm+l5TClpQkttlKU6QOQEA7OyokndWLQCoXd+HD1w4i49lUTJrxambVGciO2KK6nyfMbUDrnbKTpSVcpCh10kDpU0pQED4Z8SLjmNouEjJMUn4HOiXNdtES7OtlMg7HZrZcSdOJUFJAI6c3MAVa3U8qK8SuF+M8XsXcx/K7Yi6WxTqX0oK1IW06nfK4haSFJUNnqD3EjuJFap685bi+cXuTeG7HH4Ww7T40zPQ64mbFcaSC6HUEFKkFPMQU9wR6SdUBP6VosJziw8R8Zg5DjVzYu9mmp52ZUc7SrR0QQeqVAggpIBBBBANb2gFKUoBSlKAUpSgFKUoBWP5Qi/fLPzgrIrntjjhbrnlMiz2fH8hvseJO8my7zboSVwY8gEJWhS1LCjyE6UUJUE9dnoaAvzyhF++WfnBTyhF++WfnBXOs/wjsbt91mNKt16dscG4C1zMmaiJNtjyecIKFuc/PoLUEFYQUAnRVXrvfhIWGxSshD1jyF63Y9O8Qu11YhoVFhq0g86lFwKUjTiSeRKlJHVSQCCQOjvKEX75Z+cFPKEX75Z+cFc4M8X7yvj9c8IGMz5Vmj26JIRPjIZ0hTq3Ap5xSngex0gJHKgq5kr2NcpOsx3j7Ft2N3a83prIJfaZY5YY9tctjCZUR0pTyRwhl1QcSDsBe+YlfUaG6AvzPslvEPFL4cOjQbplMeMHIUS4uqYjOuKJABd1o6AUeUEfyQSkKCq0lj4d2a53/Gc5y+HaJXEe22pMBy4RHVKYZWQS6WUqPTalLAURzcqiN6NV0rwg7HFsd/n3C0Xq1y7FMhQp9plsNCU2qU422wscrhbUhRcB2FnolXTY1W/wAh4sWDFMmnWa6uPQjCsi7/ACJriR4u3GQ52ahsHmK99dBPUenfSgLpbmR3VhKH21qPclKwSa91Udw4402/I84tdkl2DIMZm3Fp563eXISWUzUoRzL7MpWrSgk83IvlVrZ10NXjQClKUArnbjN/HC8HT5LI/wBCRXRNc7cZv44Xg6fJZH+hIoDomlKUApSlAK/O+v2lAV/l/Da6zJWJLw7JV4RDs1wMmXbIMJpUW4sLVt1paNDlUdrIUO5SySCdEZeJcUI+UZVlVhesl4scmwSENKkXWL2UeahfNyOx3NkLSeRXwEa6gVNaiHFXLbbg2HSL1dnVtQoziAQ22XHHFqPIhtCB1UtSlJSEjvJFASfyhF++WfnBTyhF++WfnBXOq/CIssCBfnrxYchx6baLU7el225xG0PyYjfRa2eVxSFaJSCkrBBUNgbrZY3xstGQZCm0SLZd8feegLukN+8x0MNTIqCkLcQQslPLzoJS4EKAUCU0BfKZ0ZaglMhpSidABY2a99cl/wDaIfynOuGLGNW2+26xXrIRHVdp9vbREucUMPK00pRKwCpKFAlKCoAkbG660oBSlKAUpSgFcp8N7bnfCd6XibWHJv8AY3LzIlxsgZujLKURpEhTqu2bX9sLiO0UNJSQrQ6jvrqytf5Bg/cPz1e2gONrtwtzz7HuRcJomPMu2S7XWQ61lZntBpiG/KMhfOyT2peSFKQAE8pPKeYCt5fOFuTS+GfHe0s2ztJ+TXOXItLRkNfsltcOO2hXMVaRtbax55B6b7iK6t8gwfuH56vbTyDB+4fnq9tAc4rx/KsW41x8ig48q+We62OFaJbrMxlpdvcZfcUpxSXFDnRyvE+Zs7QRrqDUYVwoyopcHkvqeKKcjH7Ia/c8FP27334D5nvv/LXWvkGD9w/PV7ahHEjK7NwufsUq42+6z4l7ukazIEJsONQ3HCrTzmiFhJ7ifOHROgNkkChuJHCLKMou3Fp+3wmiLs1j79qU9IQlMp2E8p5xs6JKN6SnagB5w9AOtVn3C7NON2R5Q7Px04hAuOHqtERyZOYkLEoS0PpDqWlK0k8veObzQd9Ty12L5Bg/cPz1e2nkGD9w/PV7aA5z4AYExb83t82bwVseCT4cZz/viG9EcUp8p5FJYDQKwhSVOecspOtDR2ddOVhsWmJGdS601yrT3HmJ/wD2sygFKVr7/f7di1knXi7zGrfbILKpEmU+rlQ02kbKifxUBpeJ/Eyw8IcIueVZJLES2QUcx11ceWfettp/lLUegH9ugCaqDgLw6yLOcxXxp4lRlQ8imMqZx3HlklNhgLHcQf59xJ8862ASOmylOg4cWG5eFjn8HijlcN2Fw3srxXh2Oyk6M1wHXlGQn0719rSe7vHQEudUUApSlAKUpQClKUAqoPCo4bzOKPCZ6025ER+4MT4twjxZ/wC1pSmXA52LvQ+asAp/KN9Kt+vTJiNTGwh5HOkHetkdfyUBxvK4UOXrhrn8W08GLXgV9nWJ+BCVHkwlPy3HEKCm+ZrzUo2G9FShv0gaqT5/wqvGZZJhzaWSxbmsYvFnnzA4jcZySzHbb83m5ldUL6p2By9SNiul/IMH7h+er21DOH2L5RGumXKy52FKgu3ZxdiTEJCmoPKORDmgPP3zd+/x0Bz7i2N8RrnP4NWW74Qi2sYddo5m3Vi6R3WH2mojrCXGmwoL5VFSTogKGwNEbI7FrBbssNpxK0s6Ukgg8yuh/trOoBSlKAUpSgFKUoBSlKAVj3BqQ/AktxXxFlLbUlp9SOcNrIPKop9Ojo69NZFQPjhh+V5zw2u1owrK38OyJ5siPcGUIIVtJSptaihSmwoKOnGuVxCglQPQpUBA7l4RVs8Hzhhavs1ZRaFZ4iMVSLfYz20iYr7YW1IZCUlPOG+XnUENBexzAEVddkvMPIrNAu1ue8Yt8+O3KjvcpTztrSFIVpQBGwQdEA18IeMXDvNOGmd3K3Z5DmMZA84qS9KmOF0zCtRJeDuz2vMdkq2eu99Qa+2/BQa4N4GP/QIH6O3QE0pSlAeLjiWkKWtQQhIJUpR0APhNcnTXZPhu5+uBGcdZ4FY1MAlvtkp9081s77NJHfGQdEke+7x1KSjN4uZbdvCY4gTODWDT3YOLW8gZtk0Q+8Rv9z2FdxcXohfwaIOwlaT0fiWJ2jBcat2P2GC1bbRb2UsRorI81CR/eSTsknqSSTsmgNlGjMwozUeO0hhhpAbbaaSEpQkDQSAOgAHTVe2lKAUpSgFKUoBSlKAUpSgI9l3EXFOH6YqsoyezY2mWVCObvcGooe5dc3J2ihza5k713bHw1TnC3irwUxC9Z1Kt/F2xzHb1fHLhKRc7ww0hl1SUgoYKikLa6DSklQ7+tbPwyOBo48cD7vaorPaX+3DylaiBtSn20nbQ+USVI13bKSe6vmX4FvAhXHLjjbIE6L2uO2g+UbsFp8xTaCOVk+g86+VJHfy85HdQH2lpSlAKUpQClKUApSlAKUrDu90j2O0zbjLUURYjK33VAbISlJUdfkFZScnZAwMlyuLjTbSVtuy5r+/F4ccbcc1rZ2dBKRsbUogdQO8gGGv5Plk9RWJFutLZ0Qy0wqSsfDtxSkg/kQPy1g21MmQXblcAPKk7TkjRJDfTzWk7/koB0O7Z5lEbUd5tWSqKm+bBJ9+vhst4/I7tHBwjG882QLi1wqa43455EzGXGukVCudlzxBCHo6/6TbiSFJPdvR0e4gipVZWcgx2zQLVb763HgQY7cWOz4ilXI2hIShO1KJOgANkk1s6w5d5gQLhAgyZjDE2epaYsdxwBx8oSVr5E950kEnXdWOsT3L4Y+RsdXo9k93lPLPjG39Xt+2sO8HKrzaZsBWVuRUymVsl+JDQ282FAgqQsHaVDfQjuPWvFOTW1eTuY8JBN4bhpnqj9mvowpZQFc2uX3ySNb307tVtKdYnuXwx8jHV6PZIfwpwiTwQxNjHsTet6ba0tTy25kNRckOq98446lwEqOu8g6AAA0ABa2N5w3d5Yt8+Kq13MglDalhbUgDqS0sa3odSlQSrvOtDdRmsefBbuEfsnOZJCgttxB0ttYO0rSfQoHqDRVlPKol70rW4WTKqmEpyXoqzLWpUfwa/u5DjzT0ooNwYWqLL7MaSXkHSlAegK6KA+BQqQVGUXCTi9hwWnF2YpSlRMClKUBCOIN8u9vu9jg2uaiCJaZC3XFMB0nkCNAA93vjWl8fyv4yN/V7ftrYcRP33Yv8AIzP8Gq9Fa2LxNShzI07Zrcnte9Hl+UsZXoV+ZTlZW7jG8fyv4yN/V7ftp4/lfxkb+r2/bWTStHSGI3r4Y+RytJYvt+C8jG8fyv4yN/V7ftqE8O+FCeFd0ye441cGoEvI5xuFwc8RQrncOzpOz5qAVLISOgK1aqf0ppDEb18MfIaSxfb8F5GN4/lfxkb+r2/bTx/K/jI39Xt+2sK95Va8cmWeLcZXi793l+IwkdmtXavci3OXaQQnzW1natDp37IrbU6/iN6+GPkZ0ji1nz/BeRjeP5X8ZG/q9v21hXq/Zba7NPmoyFpao0dx4JNvb0SlJOu/8FbatRl/7071/uT/APpqq6hjq8qsYtqza/LHf7idPlHFOcU57dy8i14DypEGM6vXO42lR18JANK8LT+5UL5FH+UUrpy1s9yZdKUqIFRLitz+4G6cm/5rn1/Q7VHP+bupbWJd7XHvlqm26Wkriy2VsOpB0ShSSk/3GraUlCpGT2NEouzTK8qluNbszJsztOJ2FzIV31FvduLjdqvxtEZpgrDaXXnUoWpaucEJQEke+KhrVW5bVSYxdttwI8qQeVuRoEBwa811O/5KwNjv0eZO9pOtBmPCrFs+nxJt8tnjUuK2plt5uQ6wotKIKm1ltSedBIG0K2n8FUTi4ScWeml/kh6O0onCc0yPi1B4SY9dsin2hm7WOZcrjOtb/i0m4vR3ENJaS6nRR0UXFcmidegVueI3DKGnilwdtD1+ySQ2V3VnxtV7kIk6EdTg+2oUlXN15eb3xSkAk1ZsvgVgs3GYGPuWBCbVb5DkmE0zIeaXEcWoqWWXErC2wSo+ahQHo1qvObwRwu4Yxb8ffsxNst8hUuIES30PMvKKipaXkrDnMorVs83XZ3uoFXRStZ56vCxUXGDMb7w0zniVNst0uDhawqNcmIsuU4/GiyFSnGC820olKeVCEqIA0Skk72a881TdeEV8gWy15ffr4xfsbvDsrypcFyVtPR4yXG5bKidtEqUU6QQnzk6AI3V4fY7x03F6c5bEPyX7UiyOl9xbqXISSpQaUlRKVDa17JGzvqTWlx7gTg2LIuKbdY+zM+Gq3PLelvvLEZQ0WW1OLUW0dfeoKR0HwCsmXTk3rKzwh2747kXBaarJb3dVZfbnhdmbnOU+y4sQRIStDZ81ohSSPMA2D12etdE1HmuH9gZcxhaIHKrGWy1aT2zn7GSWexI995/2s8vn7+Hv61uZ85u3sdo5zKKlBDbaBtbiydJQkelRPQCiTk7LWWwjzE7/AFkbvhfz+NZV39l5SRy7/peKsb1+Du/Lup3WgwewO49jzTMkIE99apUvsztPbLO1JB9IT0SD8CRW/raqtOeWyy4Kx5urJTm5IUpSqSoUpSgK+4ifvuxf5GZ/g1Xor38RP33Yv8jM/wAGqjeVRslksMDG7jarc8FHtlXWA7LSpOugSEPNcp36STXN5Q+1T/6/+pHjOV1fEpX2L9ze1V/hIZld8I4XyJlkdTFnyp0S3iYtwNpioefQ2pwrKVBGgogKKVcpIOjrR2QtnFHkIOS4jz7Gj7npWtdd9PHvxV7o2KZLkEebbM4mY1kGPzI6mXoESzPMKWSRrmU5JdBGt9OXe9EEarmxtFpt3OVBRhJSk00tmfkUrkVh4m8PsE4g3KRcpFvsreMTFtpcyh+6y2pqRtt5p1xhpbQ5ecEBRG+UgDVba6Xm88Jcqs0uJe7xkKLtid1ucqFdpipCHJUVth1C20no1zdopJS2Ep0R5o1Vm2ngRg9ksd7tEWzueIXmL4lOQ/PkvLdY0oBsLW4VJSAtWgkjWzqpG9hVlkXmz3VyEFz7RGeiQnS4vTTToQHE8u9K2G0dVAka6a2d2dJE2HiIPJq6z2a8str2nOEbGZQd4EZZPyy9ZHcr5eGZcoS5pXDC3YEhzbLPvWgnqkcuuhO9nu6rqtLb4O2B47cI1ystiRAuUF9cu3lUqSuPFfUlSeZDPahCU+edoSAD+MAjL8l8U/jLiB/+Oyv+uqM2p6mQrTjWas9W9W291ywK1GX/AL071/uT/wDpqqLeS+Kfxmw//wCuyv8ArqlOW79yV633+Ivd3yaqnQVq0M9q+ZVCKVSNnfNFqWn9yoXyKP8AKKUtP7lQvkUf5RSvQy+0z6QZdKUqIFKUoDSZLikTJW2lOLdizWObxeZHOnGt62OuwpJ0NpUCDoHWwCIa/jGWQFFKY9uu7YIAeafVGWR6dtqSoD8iz+SrNpVqqZc2STXf/WZfTr1KWUWVX5Myv4uN/WDfsp5Myv4uN/WDfsq1KVLnw9Wv9vMv67VKr8mZX8XG/rBv2U8mZX8XG/rBv2ValKc+Hq1/t5jrtUq9uw5dKVyptUCED/OypxVy/wDChB3+LY/HUoxvB27PKE+dKVdLmAQh1SAhpgHoQ0gb1sdCpRUrqRvR1UopWHUytGKXu/u7KqmIqVFaTyFKUqk1hSlKAUpSgIRxBsd2uF3sc61wkThETIQ62p8NEc4Rognv96a0viGV/FxH1g37KtGlJxp1EukgnbLbvvsa3mjWwVDES59SN372Vd4hlfxcR9YN+yniGV/FxH1g37KtGlQ6HD+qXGX8ijReE7Hi/Mq7xDK/i4j6wb9lPEMr+LiPrBv2VaNKdDh/VLjL+Q0XhOx4vzKu8Qyv4uI+sG/ZTxDK/i4j6wb9lWjSnQ4f1S4y/kNF4TseL8yrvEMr+LiPrBv2Vh3qw5ZdLNPhIx5tCpMdxkKM9vQKkkb7vw1btKlGnQhJSVNXXfL+RlcmYWLTUfF+ZjwGVR4MZpeudttKTr4QAKVkUqTd3c6h/9k=", + "image/jpeg": "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", "text/plain": [ "" ] @@ -210,7 +441,15 @@ "output_type": "display_data" } ], - "source": ["from IPython.display import Image, display\n\ntry:\n display(Image(app.get_graph(xray=True).draw_mermaid_png()))\nexcept Exception:\n # This requires some extra dependencies and is optional\n pass"] + "source": [ + "from IPython.display import Image, display\n", + "\n", + "try:\n", + " display(Image(app.get_graph(xray=True).draw_mermaid_png()))\n", + "except Exception:\n", + " # This requires some extra dependencies and is optional\n", + " pass" + ] }, { "cell_type": "markdown", @@ -225,7 +464,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 70, "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", "metadata": {}, "outputs": [ @@ -233,34 +472,44 @@ "name": "stdout", "output_type": "stream", "text": [ - "User information prior to run: {}\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_uasiYgme2ptUYOBX0DtsYkuI', 'function': {'arguments': '{\"pets\":[\"cats\",\"dogs\"]}', 'name': 'update_favorite_pets'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-9f2c8a6c-6427-4c08-865c-aa2750f88808-0', tool_calls=[{'name': 'update_favorite_pets', 'args': {'pets': ['cats', 'dogs']}, 'id': 'call_uasiYgme2ptUYOBX0DtsYkuI'}])]}\n", + "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_aFUFt3TdazRnmD3FTZfxFAgL', 'function': {'arguments': '{\"question\":[\"what\\'s the latest news about FooBar\"]}', 'name': 'get_context'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 87, 'total_tokens': 109}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-adf99f00-a903-49f2-b0c3-37b84b9b801f-0', tool_calls=[{'name': 'get_context', 'args': {'question': [\"what's the latest news about FooBar\"]}, 'id': 'call_aFUFt3TdazRnmD3FTZfxFAgL', 'type': 'tool_call'}], usage_metadata={'input_tokens': 87, 'output_tokens': 22, 'total_tokens': 109})]}\n", "\n", "---\n", "\n", "Output from node 'action':\n", "---\n", - "{'messages': [ToolMessage(content='None', name='update_favorite_pets', tool_call_id='call_uasiYgme2ptUYOBX0DtsYkuI')]}\n", + "{'messages': [ToolMessage(content=\"FooBar company just raised 1 Billion dollars!\\n\\nFooBar company is now only hiring AI's\\n\\nFooBar company was founded in 2019\\n\\nFooBar company makes friendly robots\", name='get_context', tool_call_id='call_aFUFt3TdazRnmD3FTZfxFAgL', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!'), Document(metadata={'source': 'twitter'}, page_content=\"FooBar company is now only hiring AI's\"), Document(metadata={'source': 'wikipedia'}, page_content='FooBar company was founded in 2019'), Document(metadata={'source': 'wikipedia'}, page_content='FooBar company makes friendly robots')])]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='I have updated your favorite pets to be cats and dogs.', response_metadata={'finish_reason': 'stop'}, id='run-448aa9a6-3fc2-4760-88d7-54d666cce827-0')]}\n", + "{'messages': [AIMessage(content='The latest news about FooBar is that the company just raised 1 billion dollars!', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 153, 'total_tokens': 171}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'stop', 'logprobs': None}, id='run-c229a397-fda3-415b-a188-1416fd5f21b7-0', usage_metadata={'input_tokens': 153, 'output_tokens': 18, 'total_tokens': 171})]}\n", "\n", "---\n", - "\n", - "User information prior to run: {'eugene': ['cats', 'dogs']}\n" + "\n" ] } ], - "source": ["from langchain_core.messages import HumanMessage\n\nuser_to_pets.clear() # Clear the state\n\nprint(f\"User information prior to run: {user_to_pets}\")\n\ninputs = {\"messages\": [HumanMessage(content=\"my favorite pets are cats and dogs\")]}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\nprint(f\"User information prior to run: {user_to_pets}\")"] + "source": [ + "from langchain_core.messages import HumanMessage\n", + "\n", + "messages = [HumanMessage(\"what's the latest news about FooBar\")]\n", + "for output in app.stream({\"messages\": messages}):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " messages.extend(value[\"messages\"])\n", + " print(\"\\n---\\n\")" + ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 71, "id": "4a2128ed-e23f-4f25-a026-0c6590f01a1c", "metadata": {}, "outputs": [ @@ -268,72 +517,44 @@ "name": "stdout", "output_type": "stream", "text": [ - "User information prior to run: {'eugene': ['cats', 'dogs']}\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_YVdogjeOnneDW64pShGbRhCC', 'function': {'arguments': '{}', 'name': 'list_favorite_pets'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-a3c17451-d5ae-43d4-a9cf-ac468ccbd8da-0', tool_calls=[{'name': 'list_favorite_pets', 'args': {}, 'id': 'call_YVdogjeOnneDW64pShGbRhCC'}])]}\n", + "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_qqB4kucZnVhrZ5mJSH1dF8Lb', 'function': {'arguments': '{\"claim\":\"The latest news about FooBar is that the company just raised 1 billion dollars!\"}', 'name': 'cite_context_sources'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 185, 'total_tokens': 217}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-686d4706-81c9-4ca0-8f09-d9af02f4ad7f-0', tool_calls=[{'name': 'cite_context_sources', 'args': {'claim': 'The latest news about FooBar is that the company just raised 1 billion dollars!'}, 'id': 'call_qqB4kucZnVhrZ5mJSH1dF8Lb', 'type': 'tool_call'}], usage_metadata={'input_tokens': 185, 'output_tokens': 32, 'total_tokens': 217})]}\n", "\n", "---\n", "\n", "Output from node 'action':\n", "---\n", - "{'messages': [ToolMessage(content=\"['cats', 'dogs']\", name='list_favorite_pets', tool_call_id='call_YVdogjeOnneDW64pShGbRhCC')]}\n", + "{'messages': [ToolMessage(content='twitter', name='cite_context_sources', tool_call_id='call_qqB4kucZnVhrZ5mJSH1dF8Lb', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!')])]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='Your favorite pets are cats and dogs.', response_metadata={'finish_reason': 'stop'}, id='run-eef9456f-18b6-4361-8a5d-3924f6febd3c-0')]}\n", + "{'messages': [AIMessage(content='The information about FooBar raising 1 billion dollars came from Twitter.', response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 227, 'total_tokens': 242}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_18cc0f1fa0', 'finish_reason': 'stop', 'logprobs': None}, id='run-343ad465-9a62-4d72-91bf-ab29c4fe8781-0', usage_metadata={'input_tokens': 227, 'output_tokens': 15, 'total_tokens': 242})]}\n", "\n", "---\n", - "\n", - "User information prior to run: {'eugene': ['cats', 'dogs']}\n" - ] - } - ], - "source": ["print(f\"User information prior to run: {user_to_pets}\")\n\n\ninputs = {\"messages\": [HumanMessage(content=\"what are my favorite pets?\")]}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\n\nprint(f\"User information prior to run: {user_to_pets}\")"] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "edbe393a-f325-454e-94c6-11ba70027000", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "User information prior to run: {'eugene': ['cats', 'dogs']}\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_tQxCShJCYKNzLMxe0Y2vPcI1', 'function': {'arguments': '{}', 'name': 'delete_favorite_pets'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls'}, id='run-183c9064-bf67-4bca-9967-4ae44b75ecb1-0', tool_calls=[{'name': 'delete_favorite_pets', 'args': {}, 'id': 'call_tQxCShJCYKNzLMxe0Y2vPcI1'}])]}\n", - "\n", - "---\n", - "\n", - "Output from node 'action':\n", - "---\n", - "{'messages': [ToolMessage(content='None', name='delete_favorite_pets', tool_call_id='call_tQxCShJCYKNzLMxe0Y2vPcI1')]}\n", - "\n", - "---\n", - "\n", - "Output from node 'agent':\n", - "---\n", - "{'messages': [AIMessage(content=\"I have forgotten the information about your favorite animals. If you have any new favorites you'd like to share, feel free to let me know!\", response_metadata={'finish_reason': 'stop'}, id='run-259ff9fb-165f-466b-ac6b-4f06cbcf09de-0')]}\n", - "\n", - "---\n", - "\n", - "User information prior to run: {}\n" + "\n" ] } ], - "source": ["print(f\"User information prior to run: {user_to_pets}\")\n\n\ninputs = {\n \"messages\": [\n HumanMessage(content=\"please forget what i told you about my favorite animals\")\n ]\n}\nfor output in app.stream(inputs, {\"user_id\": \"eugene\"}):\n # stream() yields dictionaries with output keyed by node name\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")\n\n\nprint(f\"User information prior to run: {user_to_pets}\")"] + "source": [ + "messages.append(HumanMessage(\"where did you get this information?\"))\n", + "for output in app.stream({\"messages\": messages}):\n", + " # stream() yields dictionaries with output keyed by node name\n", + " for key, value in output.items():\n", + " print(f\"Output from node '{key}':\")\n", + " print(\"---\")\n", + " print(value)\n", + " print(\"\\n---\\n\")" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "poetry-venv-311", "language": "python", - "name": "python3" + "name": "poetry-venv-311" }, "language_info": { "codemirror_mode": { @@ -345,7 +566,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.11.9" } }, "nbformat": 4,