├── .env.example ├── requirements.txt ├── langgraph.json ├── configuration.py ├── README.md ├── task_maistro.py └── ntbk └── audio_ux.ipynb /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=sk-xxx -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | langgraph 2 | langchain-core 3 | langchain-community 4 | langchain-openai 5 | trustcall 6 | ipython -------------------------------------------------------------------------------- /langgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "dockerfile_lines": [], 3 | "graphs": { 4 | "task_maistro": "./task_maistro.py:graph" 5 | }, 6 | "env": "./.env", 7 | "python_version": "3.11", 8 | "dependencies": [ 9 | "." 10 | ] 11 | } -------------------------------------------------------------------------------- /configuration.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass, field, fields 3 | from typing import Any, Optional 4 | 5 | from langchain_core.runnables import RunnableConfig 6 | from typing_extensions import Annotated 7 | from dataclasses import dataclass 8 | 9 | @dataclass(kw_only=True) 10 | class Configuration: 11 | """The configurable fields for the chatbot.""" 12 | user_id: str = "default-user" 13 | todo_category: str = "general" 14 | task_maistro_role: str = "You are a helpful task management assistant. You help you create, organize, and manage the user's ToDo list." 15 | 16 | @classmethod 17 | def from_runnable_config( 18 | cls, config: Optional[RunnableConfig] = None 19 | ) -> "Configuration": 20 | """Create a Configuration instance from a RunnableConfig.""" 21 | configurable = ( 22 | config["configurable"] if config and "configurable" in config else {} 23 | ) 24 | values: dict[str, Any] = { 25 | f.name: os.environ.get(f.name.upper(), configurable.get(f.name)) 26 | for f in fields(cls) 27 | if f.init 28 | } 29 | return cls(**{k: v for k, v in values.items() if v}) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Task mAIstro 2 | 3 | Managing tasks effectively is a universal challenge. Task mAIstro is an AI-powered task management agent that combines natural language processing with long-term memory to create a more intuitive and adaptive experience. This repo can be used to deploy Task mAIstro and interact with it through text or voice! 4 | 5 | Key features: 6 | * Natural conversation through text and / or voice to update or add tasks 7 | * Adaptive learning of your management style and preferences 8 | * Persistent memory of tasks, context, and preferences 9 | * Flexible deployment options - local or hosted 10 | 11 | ![audio_task_maistro](https://github.com/user-attachments/assets/170e1088-499a-4373-b724-da51e9778296) 12 | 13 | ## Quickstart 14 | 15 | 1. Populate the `.env` file: 16 | ``` 17 | $ cp .env.example .env 18 | ``` 19 | 20 | 2. Download the LangGraph Studio desktop app for Mac [here](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download). 21 | 22 | 3. Load this repository as a project in LangGraph Studio. 23 | 24 | 4. Start chatting with the task mAIstro through the text interface in LangGraph Studio. 25 | 26 | 5. Experiment with voice UX in `ntbk/audio_ux.ipynb`. 27 | 28 | ## Task mAIstro Application 29 | 30 | ### Architecture 31 | 32 | Task mAIstro leverages [LangGraph](https://langchain-ai.github.io/langgraph/) to maintain three memory types: 33 | 34 | 1. **ToDo List Memory** 35 | - Task descriptions and deadlines 36 | - Time estimates and status tracking 37 | - Actionable next steps 38 | 39 | 2. **User Profile Memory** 40 | - Personal preferences and context 41 | - Work/life patterns 42 | - Historical interactions 43 | 44 | 3. **Interaction Memory** 45 | - Task management style 46 | - Communication preferences 47 | - Organizational patterns 48 | 49 | The schema for each memory type as well as the graph flow is defined in `task_maistro.py`. 50 | The graph flow is orchestrated by a central `task_maistro` node that: 51 | - Chooses to update one of the three memories based on the user's input 52 | - Uses tool calling with the [Trustcall library](https://github.com/hinthornw/trustcall) to update the chosen memory type 53 | 54 | ### 📚 Learning Resources 55 | 56 | > **Want to learn more?** Check out [Module 5 of our LangGraph Course](https://academy.langchain.com/courses/intro-to-langgraph): 57 | > - All notebooks are available [here](https://github.com/langchain-ai/langchain-academy/tree/main/module-5) 58 | > - All notebooks have accompanying videos 59 | 60 | ### Deployment Options 61 | 62 | Task mAIstro offers three flexible deployment paths: 63 | 64 | #### 1. LangGraph Studio (Recommended for quickstart) 65 | The fastest way to get started is with [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio): 66 | - Download the [desktop app](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download) 67 | - Ensure [Docker Desktop](https://docs.docker.com/engine/install/) is running 68 | - Load this repository as a project 69 | 70 | ![Screenshot 2024-11-20 at 7 42 35 PM](https://github.com/user-attachments/assets/05c0b98f-2b8c-4098-a7fc-67044fd0db21) 71 | 72 | #### 2. LangGraph CLI 73 | For developers who prefer the command line: 74 | - Use the [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli/#up) 75 | - Deploy locally with a single command 76 | - Full control over deployment configuration 77 | 78 | #### 3. LangGraph Cloud 79 | For production deployments: 80 | - Deploy to [LangGraph Cloud](https://langchain-ai.github.io/langgraph/concepts/langgraph_cloud/) 81 | - Manage through your LangSmith account 82 | - Access via LangGraph Studio web UI 83 | 84 | ### 🚀 Learning Resources 85 | > **Want to learn more about deployments?** Check out [Module 6 of our LangGraph Course](https://academy.langchain.com/courses/intro-to-langgraph) 86 | > - All notebooks are available [here](https://github.com/langchain-ai/langchain-academy/tree/main/module-6) 87 | > - All notebooks have accompanying videos 88 | 89 | ## Voice Interface 90 | 91 | Task mAIstro supports voice interactions using: 92 | - [OpenAI's Whisper](https://platform.openai.com/docs/guides/speech-to-text) for speech-to-text 93 | - [ElevenLabs](https://github.com/elevenlabs/elevenlabs-python) for text-to-speech 94 | 95 | ### Setup 96 | 97 | 1. Install FFmpeg (required for ElevenLabs): 98 | ```bash 99 | # macOS 100 | brew install ffmpeg 101 | ``` 102 | 103 | 2. In `audio_ux.ipynb`, connect to your deployment using the URL endpoint: 104 | - **Studio**: Found in Studio UI 105 | - **CLI**: Printed to console (e.g., typically `http://localhost:8123`) 106 | - **Cloud**: Available in the LangSmith Deployment page 107 | -------------------------------------------------------------------------------- /task_maistro.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import datetime 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | from trustcall import create_extractor 7 | 8 | from typing import Literal, Optional, TypedDict 9 | 10 | from langchain_core.runnables import RunnableConfig 11 | from langchain_core.messages import merge_message_runs 12 | from langchain_core.messages import SystemMessage, HumanMessage 13 | 14 | from langchain_openai import ChatOpenAI 15 | 16 | from langgraph.checkpoint.memory import MemorySaver 17 | from langgraph.graph import StateGraph, MessagesState, START, END 18 | from langgraph.store.base import BaseStore 19 | from langgraph.store.memory import InMemoryStore 20 | 21 | import configuration 22 | 23 | ## Utilities 24 | 25 | # Inspect the tool calls for Trustcall 26 | class Spy: 27 | def __init__(self): 28 | self.called_tools = [] 29 | 30 | def __call__(self, run): 31 | q = [run] 32 | while q: 33 | r = q.pop() 34 | if r.child_runs: 35 | q.extend(r.child_runs) 36 | if r.run_type == "chat_model": 37 | self.called_tools.append( 38 | r.outputs["generations"][0][0]["message"]["kwargs"]["tool_calls"] 39 | ) 40 | 41 | # Extract information from tool calls for both patches and new memories in Trustcall 42 | def extract_tool_info(tool_calls, schema_name="Memory"): 43 | """Extract information from tool calls for both patches and new memories. 44 | 45 | Args: 46 | tool_calls: List of tool calls from the model 47 | schema_name: Name of the schema tool (e.g., "Memory", "ToDo", "Profile") 48 | """ 49 | # Initialize list of changes 50 | changes = [] 51 | 52 | for call_group in tool_calls: 53 | for call in call_group: 54 | if call['name'] == 'PatchDoc': 55 | # Check if there are any patches 56 | if call['args']['patches']: 57 | changes.append({ 58 | 'type': 'update', 59 | 'doc_id': call['args']['json_doc_id'], 60 | 'planned_edits': call['args']['planned_edits'], 61 | 'value': call['args']['patches'][0]['value'] 62 | }) 63 | else: 64 | # Handle case where no changes were needed 65 | changes.append({ 66 | 'type': 'no_update', 67 | 'doc_id': call['args']['json_doc_id'], 68 | 'planned_edits': call['args']['planned_edits'] 69 | }) 70 | elif call['name'] == schema_name: 71 | changes.append({ 72 | 'type': 'new', 73 | 'value': call['args'] 74 | }) 75 | 76 | # Format results as a single string 77 | result_parts = [] 78 | for change in changes: 79 | if change['type'] == 'update': 80 | result_parts.append( 81 | f"Document {change['doc_id']} updated:\n" 82 | f"Plan: {change['planned_edits']}\n" 83 | f"Added content: {change['value']}" 84 | ) 85 | elif change['type'] == 'no_update': 86 | result_parts.append( 87 | f"Document {change['doc_id']} unchanged:\n" 88 | f"{change['planned_edits']}" 89 | ) 90 | else: 91 | result_parts.append( 92 | f"New {schema_name} created:\n" 93 | f"Content: {change['value']}" 94 | ) 95 | 96 | return "\n\n".join(result_parts) 97 | 98 | ## Schema definitions 99 | 100 | # User profile schema 101 | class Profile(BaseModel): 102 | """This is the profile of the user you are chatting with""" 103 | name: Optional[str] = Field(description="The user's name", default=None) 104 | location: Optional[str] = Field(description="The user's location", default=None) 105 | job: Optional[str] = Field(description="The user's job", default=None) 106 | connections: list[str] = Field( 107 | description="Personal connection of the user, such as family members, friends, or coworkers", 108 | default_factory=list 109 | ) 110 | interests: list[str] = Field( 111 | description="Interests that the user has", 112 | default_factory=list 113 | ) 114 | 115 | # ToDo schema 116 | class ToDo(BaseModel): 117 | task: str = Field(description="The task to be completed.") 118 | time_to_complete: Optional[int] = Field(description="Estimated time to complete the task (minutes).") 119 | deadline: Optional[datetime] = Field( 120 | description="When the task needs to be completed by (if applicable)", 121 | default=None 122 | ) 123 | solutions: list[str] = Field( 124 | description="List of specific, actionable solutions (e.g., specific ideas, service providers, or concrete options relevant to completing the task)", 125 | min_items=1, 126 | default_factory=list 127 | ) 128 | status: Literal["not started", "in progress", "done", "archived"] = Field( 129 | description="Current status of the task", 130 | default="not started" 131 | ) 132 | 133 | ## Initialize the model and tools 134 | 135 | # Update memory tool 136 | class UpdateMemory(TypedDict): 137 | """ Decision on what memory type to update """ 138 | update_type: Literal['user', 'todo', 'instructions'] 139 | 140 | # Initialize the model 141 | model = ChatOpenAI(model="gpt-4o", temperature=0) 142 | 143 | ## Create the Trustcall extractors for updating the user profile and ToDo list 144 | profile_extractor = create_extractor( 145 | model, 146 | tools=[Profile], 147 | tool_choice="Profile", 148 | ) 149 | 150 | ## Prompts 151 | 152 | # Chatbot instruction for choosing what to update and what tools to call 153 | MODEL_SYSTEM_MESSAGE = """{task_maistro_role} 154 | 155 | You have a long term memory which keeps track of three things: 156 | 1. The user's profile (general information about them) 157 | 2. The user's ToDo list 158 | 3. General instructions for updating the ToDo list 159 | 160 | Here is the current User Profile (may be empty if no information has been collected yet): 161 | 162 | {user_profile} 163 | 164 | 165 | Here is the current ToDo List (may be empty if no tasks have been added yet): 166 | 167 | {todo} 168 | 169 | 170 | Here are the current user-specified preferences for updating the ToDo list (may be empty if no preferences have been specified yet): 171 | 172 | {instructions} 173 | 174 | 175 | Here are your instructions for reasoning about the user's messages: 176 | 177 | 1. Reason carefully about the user's messages as presented below. 178 | 179 | 2. Decide whether any of the your long-term memory should be updated: 180 | - If personal information was provided about the user, update the user's profile by calling UpdateMemory tool with type `user` 181 | - If tasks are mentioned, update the ToDo list by calling UpdateMemory tool with type `todo` 182 | - If the user has specified preferences for how to update the ToDo list, update the instructions by calling UpdateMemory tool with type `instructions` 183 | 184 | 3. Tell the user that you have updated your memory, if appropriate: 185 | - Do not tell the user you have updated the user's profile 186 | - Tell the user them when you update the todo list 187 | - Do not tell the user that you have updated instructions 188 | 189 | 4. Err on the side of updating the todo list. No need to ask for explicit permission. 190 | 191 | 5. Respond naturally to user user after a tool call was made to save memories, or if no tool call was made.""" 192 | 193 | # Trustcall instruction 194 | TRUSTCALL_INSTRUCTION = """Reflect on following interaction. 195 | 196 | Use the provided tools to retain any necessary memories about the user. 197 | 198 | Use parallel tool calling to handle updates and insertions simultaneously. 199 | 200 | System Time: {time}""" 201 | 202 | # Instructions for updating the ToDo list 203 | CREATE_INSTRUCTIONS = """Reflect on the following interaction. 204 | 205 | Based on this interaction, update your instructions for how to update ToDo list items. Use any feedback from the user to update how they like to have items added, etc. 206 | 207 | Your current instructions are: 208 | 209 | 210 | {current_instructions} 211 | """ 212 | 213 | ## Node definitions 214 | 215 | def task_mAIstro(state: MessagesState, config: RunnableConfig, store: BaseStore): 216 | 217 | """Load memories from the store and use them to personalize the chatbot's response.""" 218 | 219 | # Get the user ID from the config 220 | configurable = configuration.Configuration.from_runnable_config(config) 221 | user_id = configurable.user_id 222 | todo_category = configurable.todo_category 223 | task_maistro_role = configurable.task_maistro_role 224 | 225 | # Retrieve profile memory from the store 226 | namespace = ("profile", todo_category, user_id) 227 | memories = store.search(namespace) 228 | if memories: 229 | user_profile = memories[0].value 230 | else: 231 | user_profile = None 232 | 233 | # Retrieve people memory from the store 234 | namespace = ("todo", todo_category, user_id) 235 | memories = store.search(namespace) 236 | todo = "\n".join(f"{mem.value}" for mem in memories) 237 | 238 | # Retrieve custom instructions 239 | namespace = ("instructions", todo_category, user_id) 240 | memories = store.search(namespace) 241 | if memories: 242 | instructions = memories[0].value 243 | else: 244 | instructions = "" 245 | 246 | system_msg = MODEL_SYSTEM_MESSAGE.format(task_maistro_role=task_maistro_role, user_profile=user_profile, todo=todo, instructions=instructions) 247 | 248 | # Respond using memory as well as the chat history 249 | response = model.bind_tools([UpdateMemory], parallel_tool_calls=False).invoke([SystemMessage(content=system_msg)]+state["messages"]) 250 | 251 | return {"messages": [response]} 252 | 253 | def update_profile(state: MessagesState, config: RunnableConfig, store: BaseStore): 254 | 255 | """Reflect on the chat history and update the memory collection.""" 256 | 257 | # Get the user ID from the config 258 | configurable = configuration.Configuration.from_runnable_config(config) 259 | user_id = configurable.user_id 260 | todo_category = configurable.todo_category 261 | 262 | # Define the namespace for the memories 263 | namespace = ("profile", todo_category, user_id) 264 | 265 | # Retrieve the most recent memories for context 266 | existing_items = store.search(namespace) 267 | 268 | # Format the existing memories for the Trustcall extractor 269 | tool_name = "Profile" 270 | existing_memories = ([(existing_item.key, tool_name, existing_item.value) 271 | for existing_item in existing_items] 272 | if existing_items 273 | else None 274 | ) 275 | 276 | # Merge the chat history and the instruction 277 | TRUSTCALL_INSTRUCTION_FORMATTED=TRUSTCALL_INSTRUCTION.format(time=datetime.now().isoformat()) 278 | updated_messages=list(merge_message_runs(messages=[SystemMessage(content=TRUSTCALL_INSTRUCTION_FORMATTED)] + state["messages"][:-1])) 279 | 280 | # Invoke the extractor 281 | result = profile_extractor.invoke({"messages": updated_messages, 282 | "existing": existing_memories}) 283 | 284 | # Save save the memories from Trustcall to the store 285 | for r, rmeta in zip(result["responses"], result["response_metadata"]): 286 | store.put(namespace, 287 | rmeta.get("json_doc_id", str(uuid.uuid4())), 288 | r.model_dump(mode="json"), 289 | ) 290 | tool_calls = state['messages'][-1].tool_calls 291 | # Return tool message with update verification 292 | return {"messages": [{"role": "tool", "content": "updated profile", "tool_call_id":tool_calls[0]['id']}]} 293 | 294 | def update_todos(state: MessagesState, config: RunnableConfig, store: BaseStore): 295 | 296 | """Reflect on the chat history and update the memory collection.""" 297 | 298 | # Get the user ID from the config 299 | configurable = configuration.Configuration.from_runnable_config(config) 300 | user_id = configurable.user_id 301 | todo_category = configurable.todo_category 302 | 303 | # Define the namespace for the memories 304 | namespace = ("todo", todo_category, user_id) 305 | 306 | # Retrieve the most recent memories for context 307 | existing_items = store.search(namespace) 308 | 309 | # Format the existing memories for the Trustcall extractor 310 | tool_name = "ToDo" 311 | existing_memories = ([(existing_item.key, tool_name, existing_item.value) 312 | for existing_item in existing_items] 313 | if existing_items 314 | else None 315 | ) 316 | 317 | # Merge the chat history and the instruction 318 | TRUSTCALL_INSTRUCTION_FORMATTED=TRUSTCALL_INSTRUCTION.format(time=datetime.now().isoformat()) 319 | updated_messages=list(merge_message_runs(messages=[SystemMessage(content=TRUSTCALL_INSTRUCTION_FORMATTED)] + state["messages"][:-1])) 320 | 321 | # Initialize the spy for visibility into the tool calls made by Trustcall 322 | spy = Spy() 323 | 324 | # Create the Trustcall extractor for updating the ToDo list 325 | todo_extractor = create_extractor( 326 | model, 327 | tools=[ToDo], 328 | tool_choice=tool_name, 329 | enable_inserts=True 330 | ).with_listeners(on_end=spy) 331 | 332 | # Invoke the extractor 333 | result = todo_extractor.invoke({"messages": updated_messages, 334 | "existing": existing_memories}) 335 | 336 | # Save save the memories from Trustcall to the store 337 | for r, rmeta in zip(result["responses"], result["response_metadata"]): 338 | store.put(namespace, 339 | rmeta.get("json_doc_id", str(uuid.uuid4())), 340 | r.model_dump(mode="json"), 341 | ) 342 | 343 | # Respond to the tool call made in task_mAIstro, confirming the update 344 | tool_calls = state['messages'][-1].tool_calls 345 | 346 | # Extract the changes made by Trustcall and add the the ToolMessage returned to task_mAIstro 347 | todo_update_msg = extract_tool_info(spy.called_tools, tool_name) 348 | return {"messages": [{"role": "tool", "content": todo_update_msg, "tool_call_id":tool_calls[0]['id']}]} 349 | 350 | def update_instructions(state: MessagesState, config: RunnableConfig, store: BaseStore): 351 | 352 | """Reflect on the chat history and update the memory collection.""" 353 | 354 | # Get the user ID from the config 355 | configurable = configuration.Configuration.from_runnable_config(config) 356 | user_id = configurable.user_id 357 | todo_category = configurable.todo_category 358 | 359 | namespace = ("instructions", todo_category, user_id) 360 | 361 | existing_memory = store.get(namespace, "user_instructions") 362 | 363 | # Format the memory in the system prompt 364 | system_msg = CREATE_INSTRUCTIONS.format(current_instructions=existing_memory.value if existing_memory else None) 365 | new_memory = model.invoke([SystemMessage(content=system_msg)]+state['messages'][:-1] + [HumanMessage(content="Please update the instructions based on the conversation")]) 366 | 367 | # Overwrite the existing memory in the store 368 | key = "user_instructions" 369 | store.put(namespace, key, {"memory": new_memory.content}) 370 | tool_calls = state['messages'][-1].tool_calls 371 | # Return tool message with update verification 372 | return {"messages": [{"role": "tool", "content": "updated instructions", "tool_call_id":tool_calls[0]['id']}]} 373 | 374 | # Conditional edge 375 | def route_message(state: MessagesState, config: RunnableConfig, store: BaseStore) -> Literal[END, "update_todos", "update_instructions", "update_profile"]: 376 | 377 | """Reflect on the memories and chat history to decide whether to update the memory collection.""" 378 | message = state['messages'][-1] 379 | if len(message.tool_calls) ==0: 380 | return END 381 | else: 382 | tool_call = message.tool_calls[0] 383 | if tool_call['args']['update_type'] == "user": 384 | return "update_profile" 385 | elif tool_call['args']['update_type'] == "todo": 386 | return "update_todos" 387 | elif tool_call['args']['update_type'] == "instructions": 388 | return "update_instructions" 389 | else: 390 | raise ValueError 391 | 392 | # Create the graph + all nodes 393 | builder = StateGraph(MessagesState, config_schema=configuration.Configuration) 394 | 395 | # Define the flow of the memory extraction process 396 | builder.add_node(task_mAIstro) 397 | builder.add_node(update_todos) 398 | builder.add_node(update_profile) 399 | builder.add_node(update_instructions) 400 | 401 | # Define the flow 402 | builder.add_edge(START, "task_mAIstro") 403 | builder.add_conditional_edges("task_mAIstro", route_message) 404 | builder.add_edge("update_todos", "task_mAIstro") 405 | builder.add_edge("update_profile", "task_mAIstro") 406 | builder.add_edge("update_instructions", "task_mAIstro") 407 | 408 | # Compile the graph 409 | graph = builder.compile() -------------------------------------------------------------------------------- /ntbk/audio_ux.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "b4cd66e8", 6 | "metadata": {}, 7 | "source": [ 8 | "# Audio UX\n", 9 | "\n", 10 | "Task mAIstro supports voice interactions using:\n", 11 | "- [OpenAI's Whisper](https://platform.openai.com/docs/guides/speech-to-text) for speech-to-text\n", 12 | "- [ElevenLabs](https://github.com/elevenlabs/elevenlabs-python) for text-to-speech\n", 13 | "\n", 14 | "### Install dependencies\n", 15 | "\n", 16 | "Ensure you have `ffmpeg` installed for using ElevenLabs. \n", 17 | "\n", 18 | "On MacOS, you can install it with `brew install ffmpeg`. " 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "id": "d1fd88e4", 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [ 28 | "%%capture --no-stderr\n", 29 | "%pip install -U langchain_openai langgraph langchain_core sounddevice scipy elevenlabs " 30 | ] 31 | }, 32 | { 33 | "cell_type": "markdown", 34 | "id": "44b724c3", 35 | "metadata": {}, 36 | "source": [ 37 | "### Set environment variables\n", 38 | "\n", 39 | "* Set your `OPENAI_API_KEY`\n", 40 | "* Set your `ELEVENLABS_API_KEY` (available [here](https://elevenlabs.io/api))\n", 41 | "* Optional: Set your `LANGCHAIN_API_KEY` (available [here](https://smith.langchain.com/)) if you want tracing or want to connect with a hosted deployment." 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 1, 47 | "id": "c7311ebb", 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "import os, getpass\n", 52 | "\n", 53 | "def _set_env(var: str):\n", 54 | " # Check if the variable is set in the OS environment\n", 55 | " env_value = os.environ.get(var)\n", 56 | " if not env_value:\n", 57 | " # If not set, prompt the user for input\n", 58 | " env_value = getpass.getpass(f\"{var}: \")\n", 59 | " \n", 60 | " # Set the environment variable for the current process\n", 61 | " os.environ[var] = env_value\n", 62 | "\n", 63 | "_set_env(\"LANGCHAIN_API_KEY\")\n", 64 | "_set_env(\"ELEVENLABS_API_KEY\")\n", 65 | "_set_env(\"OPENAI_API_KEY\")\n", 66 | "\n", 67 | "import os\n", 68 | "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n", 69 | "os.environ[\"LANGCHAIN_PROJECT\"] = \"task-maistro-deployment\"" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "id": "45f5e99c-9c95-4ec9-a879-45cb80c76179", 75 | "metadata": {}, 76 | "source": [ 77 | "### Connect to your deployment\n", 78 | "\n", 79 | "Connect to your deployment using the URL endpoint:\n", 80 | "- **Studio**: Found in Studio UI \n", 81 | "- **CLI**: Printed to console (typically `http://localhost:8123`)\n", 82 | "- **Cloud**: Available in LangGraph Deployment page\n", 83 | "\n", 84 | "We'll connect to the deployment as a [RemoteGraph](https://langchain-ai.github.io/langgraph/how-tos/use-remote-graph/#how-to-interact-with-the-deployment-using-remotegraph). \n" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 2, 90 | "id": "b2bdfbed-694a-4dbc-8ddf-e2649ec28181", 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "from langgraph.pregel.remote import RemoteGraph\n", 95 | "from langchain_core.messages import convert_to_messages\n", 96 | "from langchain_core.messages import HumanMessage, SystemMessage\n", 97 | "\n", 98 | "# Local deployment (via LangGraph Studio)\n", 99 | "local_deployment_url = \"http://localhost:63557\"\n", 100 | "\n", 101 | "# Deployment URL\n", 102 | "cloud_deployment_url = \"https://task-maistro-1b681add7a2b549499bb0cd21a7e5be4.default.us.langgraph.app\"\n", 103 | "\n", 104 | "# Graph name\n", 105 | "graph_name = \"task_maistro\" \n", 106 | "\n", 107 | "# Connect to the deployment\n", 108 | "remote_graph = RemoteGraph(graph_name, url=local_deployment_url)" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "id": "1770e138", 114 | "metadata": {}, 115 | "source": [ 116 | "You can test your deployment by running the following. " 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 5, 122 | "id": "62c16a2c", 123 | "metadata": {}, 124 | "outputs": [ 125 | { 126 | "name": "stdout", 127 | "output_type": "stream", 128 | "text": [ 129 | "================================\u001b[1m Human Message \u001b[0m=================================\n", 130 | "\n", 131 | "Hi I'm Lance. I live in San Francisco with my wife and have a 1 year old.\n", 132 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 133 | "Tool Calls:\n", 134 | " UpdateMemory (call_ubXYV29olc4XYbbrD1khqtNU)\n", 135 | " Call ID: call_ubXYV29olc4XYbbrD1khqtNU\n", 136 | " Args:\n", 137 | " update_type: user\n", 138 | "=================================\u001b[1m Tool Message \u001b[0m=================================\n", 139 | "\n", 140 | "updated profile\n", 141 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 142 | "\n", 143 | "Hi Lance! It's great to meet you. If there's anything specific you'd like help with, feel free to let me know!\n" 144 | ] 145 | } 146 | ], 147 | "source": [ 148 | "# Int\n", 149 | "user_input = \"Hi I'm Lance. I live in San Francisco with my wife and have a 1 year old.\"\n", 150 | "config = {\"configurable\": {\"user_id\": \"Test-Deployment-User\"}}\n", 151 | "for chunk in remote_graph.stream({\"messages\": [HumanMessage(content=user_input)]}, stream_mode=\"values\", config=config):\n", 152 | " convert_to_messages(chunk[\"messages\"])[-1].pretty_print()" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "id": "1169896f", 158 | "metadata": {}, 159 | "source": [ 160 | "### Add audio\n", 161 | "\n", 162 | "Our deployed graph has some benefits: \n", 163 | "* It has built-in support for long-term memory \n", 164 | "* It implements all the logic for task mAIstro \n", 165 | "\n", 166 | "But, we have a challenge:\n", 167 | "* It takes test as input and returns text as output\n", 168 | "\n", 169 | "We need to add audio input and output to the graph. So, we'll simply add two nodes to our graph:\n", 170 | "\n", 171 | "1. **Audio Input Node**\n", 172 | " * Records microphone input (stop with Enter)\n", 173 | " * Transcribes speech using Whisper\n", 174 | " * Passes text to Task mAIstro\n", 175 | "\n", 176 | "2. **Audio Output Node**\n", 177 | " * Takes Task mAIstro's text response\n", 178 | " * Converts to speech via ElevenLabs\n", 179 | " * Plays audio response\n", 180 | "\n", 181 | "We can achieve this by embedding our deployed graph [as a node](https://langchain-ai.github.io/langgraph/how-tos/use-remote-graph/#using-as-a-subgraph) in a new graph. " 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": 3, 187 | "id": "ea79d376-af11-4a85-af8e-9da2f8f1da2d", 188 | "metadata": {}, 189 | "outputs": [ 190 | { 191 | "data": { 192 | "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAKoAvoDASIAAhEBAxEB/8QAHQABAQEAAgMBAQAAAAAAAAAAAAYFBAcBAwgCCf/EAF4QAAEDAwEDBgYOBQkFBAkFAQABAgMEBQYRBxIhExcxVZTRFBYiQVaSCBUyNTZRU2F0dZWytNNCcYGz0iMzN1JUcpGT1CQ0YnOhGCZDxCVERWSWscHD8CdlgoOEpP/EABsBAQACAwEBAAAAAAAAAAAAAAABAgMEBgUH/8QAPREBAAEBBAYFCwQCAgIDAAAAAAECAwQREhQhMVFSkRNTktHSBTIzNEFhcXKhscEVImKBI7JC8EPhgsLx/9oADAMBAAIRAxEAPwD+qYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6aqsp6KPfqJ46dn9aV6NT/ABUw62vrb9cJ7baplo6anXcq7o1qOc1/yUKORWq9OlznIrW6omjnK7c80+z/AB+F6yy2uCvqV03qqvb4TM5U86vfqv8AhwM8UU0+kn+o/wC6k4b3N8arL1vQdpZ3jxqsvXFB2lnePFay9UUHZmdw8VbL1PQdmZ3E/wCH3/ROo8arL1xQdpZ3jxqsvXFB2lnePFWy9T0HZmdw8VbL1PQdmZ3D/D7/AKGo8arL1xQdpZ3jxqsvXFB2lnePFWy9T0HZmdw8VbL1PQdmZ3D/AA+/6Go8arL1xQdpZ3nMpLjS17VdS1MNS1OlYZEfp/gcPxVsvU9B2ZnccSqwHHKt6SOs1HFO1dW1FNEkMzV+aRmjk/Yo/wAM+2fp/wCkam+CYjqKzEZoYa6qmuVnlckbK6fdWalcq6NbKqIm8xeCI/TVF03tdVclOY66MuvbEmAADGgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyctvDsfxi63GNEdLTUz5I2r0K9Grup+1dDWJ7aDRS1+E3qKBqvnSmdJGxqaq5zfKRET51boZbGIm0pirZjCY2tDHrOywWWkoGLvrEz+Uk88kirvPevzucrnL86qaJ6aOrir6OCqgdvwzxtkY742qmqL/gp7ilUzNUzVtQEltA2rYtsuit78kua0T7hI6KkghppqmadzW7z9yKFj3qjU4qumiapqqFadKeyVoKR8GO3OO35g3JLc+pktF8w63LWzUEro2o5k0SI5HRy8EVrmq1d3ireClRybp7JjH7btVxvE201dVUN7svtvDc6W3Vc6LvyQthajY4XeS5sjnOkVURmjUduq5Cgqtv2BUOctxCpv3g99fVNoWxS0c7YVqHJq2FJ1j5LlF1TRu/quqJodUx3fM8dzvZdn2Y4ndq2rqMRqbTeIceoH1j6Ouklppk34o9Va13JPTVNUavBV85AbW7fmeTzZMl4s2f3bILflcFXb6S2wTJZYbTBWRSRyRtjVI6iRYmqqpo+XfXg1ETgH0xV7dsJo8xrsUW6VFRkNDNHT1VBR22qqHwOkjbIxXrHE5GsVr2+Wq7uqqmuqKiZewXb3bdudiqayloa23VlPUVMclPPRVLI0jZUSRRubNJExj3OaxHOY1VViqrXIiocbZLj9bbNsW2m5VVtqKSC5Xa3upauaBzG1UbLdA1VY5U0e1r99vDVEXeTp1Mv2MdRcMXs90wS8Y9erbcrXdLpVeHVFC9tBUwy10ksboajTcermzNXdRdU3XaomgHeAAA49woKe6UFTRVcTZ6WpjdDNE/oexyKjmr8yoqoZGDV89djkLaqVZ6uklmoZpV11kfDI6JXrr53bm9+03yY2eN5SwTVia7lfXVVZHvJprG+Z6xrp87N1f2menXY1Y74/KfYpwAYEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUpJ2YHI6iqtIsfe9XUlX+hSarqsMq/os1Vdx/udPIXdVGb/ryvZHg20C4xXLI8Ssl/rWwpCyquFDFO9I0VXI1HORV3dXOXT51K17GyMcx7UcxyaK1yaoqfETT9n1uhVVt1TcLKi8eTt1W+OJP1RLqxv7Gp/0Q2JqotNdc4Tzx/wC/2nVO1PL7G3ZQrUbzb4tuoqqie1MGiL5/0fmQpsP2d4ts9hqYsYx62Y/FUua6dltpGQJKqaoiuRqJrpqvT8Z6fEmo9Kr9/nQ/lDxJqPSq/f50P5Q6Oz4/pJhG9UAl/Emo9Kr9/nQ/lEpcLddabapYcfZlN49rq2y3GvmV0sPK8rDPRMj3f5P3O7USa8Ond4p53R2fH9JMI3u0zFyzC7BndsbbsjstBfbe2RJm0txp2zxo9EVEduuRU1RHKmvzqcHxJqPSq/f50P5Q8Saj0qv3+dD+UOjs+P6SYRvYDfY3bKWI5G7OMXaj00ciWmDimqLovk/Gif4GljWxTZ/hl3iuthwqw2a5xI5sdZQ26KGViORUciOa1FTVFVF/Wc3xJqPSq/f50P5R58QKOod/6QuN1urNdeSqq16RL+tjN1rk+ZyKgyWcba+Uf/hhD83W4eNvL2a1S79K7WK4XGJ3kQs4o6KNydMy9HD3CauVUXda6lggjpYI4YY2xQxtRjGMTRrWomiIifFoeKWlhoaeOnpoY6eCNqNZFE1GtanmRETgiHtKV1xMZadkEyAAxIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOv7uref7E0VV3/Fm86Jpw08Ktmvn/V5l/Wnn7AOv7vrz+4nxbp4s3ngqJvf71bOjz6fq4dGvmA7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADr28In/aAxJd5qL4sXnyVTiv8Atdr4ounR+3zp+zsI69vGn/aBxLiu94r3rRN3/wB7tfn/APz/AKAdhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJ2/5PUUVclttVHHX3LcSWRJ5VihgYqqjVe5GuXVVRdGonHRdVRNFXK9vcw/sFj7XN+WbNN3rqjHVHxmE4LcER7e5h/YLH2ub8se3uYf2Cx9rm/LL6LXvjnBgtwRHt7mH9gsfa5vyx7e5h/YLH2ub8saLXvjnBgtz4DzP2e11x32RFNaavZXOuQWiOsx7wCK8I5aiSonpXNex3g+u6vg6aaJ5SSIvmQ+xfb3MP7BY+1zflnUF/9j9NkPsgrNtaqLfZkvFupeRWkSeRYp5morYp3Lyeu+xq6J/dZ0bvFote+OcGD6WBEe3uYf2Cx9rm/LHt7mH9gsfa5vyxote+OcGC3BEe3uYf2Cx9rm/LHt7mH9gsfa5vyxote+OcGC3BEe3uYf2Cx9rm/LHt7mH9gsfa5vyxote+OcGC3BMWfKq1bjDQXqigo56jVKaekmdLDK5EVVYu81qtduoqonFFRF0XhoU5r12dVnOFRhgAAxoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQdMuueZRr5kpE/Zya96myY1L8PMo/VS/ulNk9ev8A4/Cn7QtO0ABjVACRqNrGK0tPd55borYrTdIrNWuSmlXkquTktyPRGeVry0XlN1am9xVNF0gVwAJAGSuVWtMrbja1K+3TqJbilNyT+MCSJGr9/Td90qJprr59NDWIAAEgDg0l8t9wuVfb6atgqK63rGlXTxyI59Or27zEeie5VW8URfMqL5znAYeSLpcsYVOn22i4/wD9ciF+df5L744z9bRfckOwDFefNo/v7pnYAA0UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIOl+HmUfqpf3SmyY1L8PMo/VS/ulNk9ev/j8KftC1W1867f626ZLl9dZMVqclberLZPbGrkt2QraaGja90nJPejY3rPKqxv8hU3N1vFU1PZsjz++5htF2a1NyuM747vs19tKqlbIraeWrWek3peTTyd7R70RdNURyp0KdpZfsaw7Pb5Dd77ZW11fHB4KsiVEsTZod5XJFMxj0bMzVVXckRzeK8OKnDqdguDVVmsNrdZpIqWxMkitrqevqYZqaN6+XG2VkiPWNdETcVyt0RE00RENfCccVXQ+M5XkGc3DEsLqMqu9ut98yPJ5aq5Ula5lZNDR1TuRpIZ1VXRt0ei+ToqMi0aqJqZCWqrw3H89W3ZFf2VNt2pW6FtWtzlbNURypb43snc1U5Zu49W6P114Kuq8T6Km9jzs8nxClxhcbijstJWyXClghqJo30073Oc58UrXo+PVXO4NciIi6ImnA99s2D4NZ7BV2Wksix22rucN5nidWVD1krIljVkqvdIrtUWGNVTXRVbxRdV1jLI6Pya53+/Yjtl2gvzS92S74fdbhS2i30dasVDCyjY10TJaf3MyzrxVZEVdJGo3TRD6ex64y3iwWyvnhWmmqqWKd8K/+G5zEcrf2KuhH5FsDwHLMmkv91x6KruUr4pJ1WeZkNS+PTk3TQtekcqt0TRXtd0J8R7rpbdpclyqXW3I8Up7esjlp4qqwVMsrGa+Sj3trWo5dOlUa1F+JC0RMDrPaLcbnYtsudNo73doqebZ1VXNtJ4fKsFPUsk5NssMe9uxO3WpxaiLqqr0qpn4HLecayXYbXOym/Xd2Z2yZLxBda99RDI9LelSySONfJiVrmqnkImqLx1Xidyx7OKG8SSXLJoKS5ZFV2iSyVtZQpNTQy0j3q50bYllfuIqr07yu+JyJwObFs5x2CTFJGW/dfi0borOvLyf7K1YeQVPdeX/ACa7vl73x9PEjLO0fLuF19+xvYnsp2hR5lklxyG6Xagoauiud2lqqe4RVFSsL4uReqtRyMVXI9qI5NzVVU367NL9F7FzJLp7e3Fl3hzCSkZWeGSJOyNL82LkkfrvI3kvI3ddN3hppwLbYZ7FzHdmmPYtUXm2U1fmFohci1sdZUT00cqq7WSGKRUYxyovukjavSVF59jns8v9xrq2ux7lZa2rbcJo21tQyFalHtfy6RNkRjZFVqava1HO4oqqiqixFM4CI2UYXSR+yV2w3NLheFnpK6gkbTuutQtO/lqBqu5SHf3Ho1XKjEcioxERG6I1NPoAkqvZTi9bncOZPtrmZHGxka1kFVNEkrWoqM5SNj0ZJuo5URXtXTXgVpeIwGFkvvjjP1tF9yQ7AOv8l98cZ+tovuSHYBS8+bR/f3TOwABooAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQdL8PMo/VS/ulNk4t/s1wobzNeLVStuHhMccVTRLKkb1Vm9uyRq7yVXR2itcqaojVRU00dne21+9DLr2qi/PPX1WkRVExsiNcxGyIj2ytOttgxPba/ehl17VRfnj22v3oZde1UX54yfyjtR3mDbBie21+9DLr2qi/PHttfvQy69qovzxk/lHajvMG2DE9tr96GXXtVF+eZ02b19PkVHYpMUurbpV0s9bDBy9Iu/DC+Jkjt7ltE0dPEmirqu9wRdF0ZP5R2o7zBWAxPba/ehl17VRfnj22v3oZde1UX54yfyjtR3mDbBie21+9DLr2qi/PHttfvQy69qovzxk/lHajvMG2DE9tr96GXXtVF+ePba/ehl17VRfnjJ/KO1HeYPzkvvjjP1tF9yQ7AIy2Wi53y60NZc6D2po6CVZ4qaSZsk00u65qK7cVWta1HKqJvOVV09zu+VZmreao/bTE7O9E7gAGkgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIG7J/wDrziq6J8Grxx0/96tnn0/+qfqXThfHX14VvP8A4kmjd7xYvOirrvaeF2vXTzadHTx6NPOB2CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHX9315/cT4rp4s3nhvon/AK1bP0elf1+b9qHYB17eEb/2gsRVVXf8V71omnBU8LtevHX9Xm86/FxDsIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4llZBE+SRyMjYiuc5y6IiJ0qpgJtFxZU+ENs7UzvMNdtZ2XpKoj4zgmImdiiBO84mL+kNs7UzvHOJi/pDbO1M7zHpd36ynnCcs7lECd5xMX9IbZ2pneOcTF/SG2dqZ3jS7v1lPODLO5RAnecTF/SG2dqZ3jnExf0htnamd40u79ZTzgyzuUQJ3nExf0htnamd45xMX9IbZ2pneNLu/WU84Ms7lECd5xMX9IbZ2pneOcTF/SG2dqZ3jS7v1lPODLO5RAnecTF/SG2dqZ3jnExf0htnamd40u79ZTzgyzuUQJ3nExf0htnamd45xMX9IbZ2pneNLu/WU84Ms7lECd5xMX9IbZ2pneOcTF/SG2dqZ3jS7v1lPODLO5RAnecTF/SG2dqZ3jnExf0htnamd40u79ZTzgyzuUQJ3nExf0htnamd45xMX9IbZ2pneNLu/WU84Ms7lECd5xMX9IbZ2pneOcTF/SG2dqZ3jS7v1lPODLO5RAnecTF/SG2dqZ3jnExf0htnamd40u79ZTzgyzuUQJ3nExf0htnamd45xMX9IbZ2pneNLu/WU84Ms7lECd5xMX9IbZ2pneOcTF/SG2dqZ3jS7v1lPODLO5RAnecTF/SG2dqZ3jnExf0htnamd40u79ZTzgyzuUQJ3nExf0htnamd45xMX9IbZ2pneNLu/WU84Ms7lECd5xMX9IbZ2pneOcTF/SG2dqZ3jS7v1lPODLO5RAnecTF/SG2dqZ3jnExf0htnamd40u79ZTzgyzuUQJ3nExf0htnamd45xMX9IbZ2pneNLu/WU84Ms7lECd5xMX9IbZ2pneOcTF/SG2dqZ3jS7v1lPODLO5RAnecTF/SG2dqZ3jnExf0htnamd40u79ZTzgyzuUQJ3nExf0htnamd45xMX9IbZ2pneNLu/WU84Ms7lEdeXhyJ7ILEW7qKq4vel3uOqf7Xa+Hxcdf+hQ84mL+kNs7UzvIK7Z7jrtvGKzNyG3+Dsxq8MfpUt3d5aq2K3VddNdGu6ePTp5xF6u87LSOcGWdzt8E7ziYv6Q2ztTO8c4mL+kNs7UzvGl3frKecGWdyiBO84mL+kNs7UzvHOJi/pDbO1M7xpd36ynnBlncogTvOJi/pDbO1M7xziYv6Q2ztTO8aXd+sp5wZZ3KIE7ziYv6Q2ztTO8c4mL+kNs7UzvGl3frKecGWdyiBO84mL+kNs7UzvHOJi/pDbO1M7xpd36ynnBlncogTvOJi/pDbO1M7xziYv6Q2ztTO8aXd+sp5wZZ3KIE7ziYv6Q2ztTO8c4mL+kNs7UzvGl3frKecGWdyiBO84mL+kNs7UzvHOJi/pDbO1M7xpd36ynnBlncogTvOJi/pDbO1M7xziYv6Q2ztTO8aXd+sp5wZZ3KIE7ziYv6Q2ztTO8c4mL+kNs7UzvGl3frKecGWdyiBO84mL+kNs7UzvHOJi/pDbO1M7xpd36ynnBlncogTvOJi/pDbO1M7xziYv6Q2ztTO8aXd+sp5wZZ3KIE7ziYv6Q2ztTO8c4mL+kNs7UzvGl3frKecGWdyiBO84mL+kNs7UzvHOJi/pDbO1M7xpd36ynnBlncogTvOJi/pDbO1M7xziYv6Q2ztTO8aXd+sp5wZZ3KIE7ziYv6Q2ztTO8c4mL+kNs7UzvGl3frKecGWdyiBO84mL+kNs7UzvHOJi/pDbO1M7xpd36ynnBlncogTvOJi/pDbO1M7xziYv6Q2ztTO8aXd+sp5wZZ3KIE7ziYv6Q2ztTO8c4mL+kNs7UzvGl3frKecGWdyiBO84mL+kNs7UzvHOJi/pDbO1M7xpd36ynnBlncogTvOJi/pDbO1M7xziYv6Q2ztTO8aXd+sp5wZZ3KIE7ziYv6Q2ztTO8c4mL+kNs7UzvGl3frKecGWdyiBO84mL+kNs7UzvHOJi/pDbO1M7xpd36ynnBlncogca3XKku9GyroamKrpZNUbNC9Hsdoqouip8SoqfsOSbMTFURVTOMSqzsj+D10+iy/cU4uPe8Fs+ixfcQ5WR/B66fRZfuKcXHveC2fRYvuIeDf/AE9Pw/LLRsaAANFcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgtp22zFdkVfjNLktyprc6/VrqSCWpqYoWQo2Nz3zSK9zdI26Nark18qRifpF6dGeyfqaCx1uy3I7wxjLFaMqZJcaySLfjpYX0lTGj5OC7rN90aKq8EVU1L2cRVVhKJdo3naPiWOR26S7ZRZbXHcmo6idWXCGFKpFRFRYlc5N9F1T3OvShyskzKwYbQx1uQXy22OjlekcdRcquOnje5ehqOeqIq/MfMt2zDZ7ZNqu0a9582lrrJktktrsYnqaRZ466gSncktNS+SvlrKrnLGmjl5Ri6edJvAX0+zXJNl9TtpiSnoY8CgoLZU3mndLT0lfy7lmhfqjkZOsHg6au0VUY5qLrqi5uijDH/v8AXw9qMX0dge3Ow5Rs1jzS9VduxW1Pr6uibNX3GNIF5Gplga5JXIxvlpFvInm101XTU8SXWjvW23DK231tNXUNRi14khqKZ7ZWSt8KtmjmPbqit/UvHh06cPlvZPW45jcOy7Jspihg2c00mUU9JPU0qpR0Fa+5vWF0jVbpHvQtkY1XImioqcFU7D9jRCvOdNW0NNNQ4rcpcmrrBTuidCxaF9VadJGRqibjHSpO9qcOD9UTReF6rOKcZj3/AJMX0xkOTWfErY+43y60Nlt8aoj6u4VLIImqvQiveqIn+JwnbQMXZj9PfnZJaG2Ope2KC5rXRJTSvc7da1su9uuVXcERF4rwOtvZQZM7HsexiKZbdbrTX3qOnr8hulAythtEfJSOSZI3orEe5zUja96K1qycek+aqW4WS07Ocppai4uudhpNq1muDpay3tgSaildSuWbwdkbG8nIrJVTcYjX6KqIuvHHRZZqcSZfbVDtKxG54/WX6jyqyVdjotfCrnBcYX00GnTvyo7dbpr51Q9+O53jWYT1cNhyG1Xuajdu1MdurYqh0C/E9GOXdXgvSfGu2S72TOKXbhlGCoybEEweK33C40cCxUtbcUqXPZuroiSPjhVUc5NdN9qKp2dYchxTaF7I7Aq3Zu2CoprHaLhDkFdbqRYYI4JGRNpqWR261FekjVckfS1GuXRCZsoiMf8AuwxfTAANZYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY+B+8dT9Z3D8ZMUZOYH7x1P1ncPxkxRnQXH1Wy+WPsw1edLOyP4PXT6LL9xTi497wWz6LF9xDlZH8Hrp9Fl+4pxce94LZ9Fi+4h5t/wDT0/D8r0bGgADRXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACNyRjL/lDbJV/ylthom1ctKvuJ3Pe5jUen6TURjl3ehVVFVF0TTbut3m9WvRxOHtmfcK7wmFP/ABWesh48Kh+Wj9ZCNXZ/i7lVVxu0Kq9KrQRfwjm+xb0as/YIv4T3P0iy62ezHiUzQsvCoflo/WQeFQ/LR+shG832LejVn7BF/COb7FvRqz9gi/hH6RZdbPZjxGaFl4VD8tH6yH8v9pfsR7nJ7MqjxC2VFTT4hfZHXWKqhlcjaSiV29URIuujVa5FY1FXjrH8Z/Q7m+xb0as/YIv4RzfYt6NWfsEX8JmsvJ1nZTMxaTr/AIx4kTVEubh+FWrCX1TqK63itWpRqPS8X2quCN3ddNxJ5X7nTx3dNeGvQgzzCrTtDobVS3Gsmhjt11o7vEtLKxqumppWyxtdvNXVquaiKiaLp0KnScLm+xb0as/YIv4RzfYt6NWfsEX8Ji/SrPHHpZ7MeJOaHO2j4ZadqGC3rFLrVzU9uu1O6mnlo5WNma1elWq5HIi8POilGyogY1GpMzRE090hHc32LejVn7BF/COb7FvRqz9gi/hI/SbLDDpZ7MeIzQsvCoflo/WQ9qLqmqdBEc32LejVn7BF/CeLZSwYplFtorbG2kt9ybM2SjiTdibIxu+j2NRNGqqI5F00ReHDVNTDbeSqaLOquzrmZiMcJjDZrn2ymKonUuAAc8sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHwP3jqfrO4fjJijJzA/eOp+s7h+MmKM6C4+q2Xyx9mGrzpZ2R/B66fRZfuKcXHveC2fRYvuIcrI/g9dPosv3FOLj3vBbPosX3EPNv/AKen4flejY0AAaK4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARtR/SXWfVFP++nLIjaj+kus+qKf99Oe15J9PV8s/hE7JbQAOmYAAiNo21i37Oa6x26S1Xa/Xi9PmbRWyzQMlnkbExHyv8t7Go1qKmurteKaIpGOAtwdL3bble6TbNjOMUuHXmrtF0sD7pIjIIWVMUizQNRXpJO3dZGkjkkbort5zd1HaLpv37bpb8XyyK0XbG8loKCSuitzchmoGpbVnlVGxt5Tf391znNbv7m7qumpGMDskHWzduNFWZ/fcQtmNZDeLlY6iCC4T0dPClPAksLJWPWR8rdW6P00RFdq13k6aKuf7Hfa9etrmL1FdecarrPNHV1cbauRkLaWVrKqWJsbN2Z799jWIj95ETeRd1VTQYxjgO2QAWAwrn8NcV/5lT+4cbphXP4a4r/zKn9w4paeitPlq/wBZXo2rUAHBMoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMfA/eOp+s7h+MmKMnMD946n6zuH4yYozoLj6rZfLH2YavOlnZH8Hrp9Fl+4pxce94LZ9Fi+4hysj+D10+iy/cU4uPe8Fs+ixfcQ82/8Ap6fh+V6NjQABorgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABG1H9JdZ9UU/76csiNqP6S6z6op/3057Xkn09Xyz+ETsltEzku0/DcMr2UOQZbY7FWvjSZtNcrlDTyOYqqiORr3IqoqtcmvRwX4imPVLSQTu3pIY5HaaauaiqdKwInn62ZI1Hc4uJ7qroi+3lLoq+v86EPtaudm2wWKkjxGz0e019FK96V+NZJTU1ZZqhWfyUscyPTdV3la6O6G8WuTgd1+11L/Zof8tD2RU8UCKkUbI0Xp3GompGEztHQMGKbSsUyHZvllXaW5xe6PGprFfI6Sthp5GzSPglSZHSq1r01hVrtNF46oi9BCbQNiea36uyaaXBWZLkS5JHdrflFTdoE3bfHUxyxUlPG929E9I2cmrVRjFXecr114/XoIyxI632b4jdrDtL2p3avpPB6C93OjqKCblGO5aNlDDE9dEVVbo9jk0ciLw1ThxJXZNcK7YjZblj+c01DjmPU1zr56HKa67U0VLW+EVUk8caMc9Hsk3ZH6o5NP5NdFXU7yPxLDHO3dkY2Ruuujk1QnARDdvOzN6OVu0TE1RqauVL3TcE101Xy/jVP8Tn2Dazg+V3OO22TM8fvFxkRyspKC6QTzPRE1VUYx6quiIqrw8xR+11L/Zof8tD9R0dPC9HRwRscn6TWIik6x7jCufw1xX/mVP7hxumFc/hriv8AzKn9w4raeitPlq/1lejatQAcEygAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAx8D946n6zuH4yYoycwP3jqfrO4fjJijOguPqtl8sfZhq86Wdkfweun0WX7inFx73gtn0WL7iHKyP4PXT6LL9xTi497wWz6LF9xDzb/6en4flejY0AAaK4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAR1e1KbaMski7iVVrZHDrwR7o5Xq9E+NUSRq6FicK72ajvtJ4NXQpNFvI9ujla5jk6HNc1UVq8V4oqLxU3rleIu1rnqjVOqTbqcQGc7ZvaXOVfCb0mq68L5Wp/908c2to/tV7+3a3806H9Sum+rsx4mPJ72kDN5tbR/ar39u1v5o5tbR/ar39u1v5o/Urpvq7MeIye9pAzebW0f2q9/btb+aSNxxCGHa3j9ojuN7baamx3Krng9uaxd6aKooWxO3uU1TRs0yaaprvdC6cJjyjdJ9tXZjxGT3uwAZvNraP7Ve/t2t/NHNraP7Ve/t2t/NI/Urpvq7MeIye9pAzebW0f2q9/btb+aObW0f2q9/btb+aP1K6b6uzHiMnvaRiVbUqc5x2ONd6SnbUVEjU6WsVm4ir8WrnaJ+pfiOTza2j+1Xv7drfzTZsuPUGPxSsooXMWV29JLLK+WSRfNvPequdp5tV4GC28pXebOqmyxmZiY1xEbYw3ytFOE4tEAHLrAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAx8D946n6zuH4yYoycwP3jqfrO4fjJijOguPqtl8sfZhq86Wdkfweun0WX7inFx73gtn0WL7iHKyP4PXT6LL9xTi497wWz6LF9xDzb/AOnp+H5Xo2NAAGiuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEDd1Xn6xRNG6eLV44rpr/vVs6PPp+rh0a+Yvjr+7uTn9xNNHby4zeVRd7hp4VbPN5/Nx83H4y1O3mh2AACqQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY+B+8dT9Z3D8ZMUZOYH7x1P1ncPxkxRnQXH1Wy+WPsw1edLOyP4PXT6LL9xTi497wWz6LF9xDlZH8Hrp9Fl+4pxce94LZ9Fi+4h5t/9PT8PyvRsaAANFcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOv7u9U2+4mzfciLjN5XcT3K6VVs4r86a/9VOwCCuyrz8Yqmq6LjV4VU5RERf9qtn6PSv6/Nx+MtTt5i9ABUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj4H7x1P1ncPxkxRk5gfvHU/Wdw/GTFGdBcfVbL5Y+zDV50s7I/g9dPosv3FOLj3vBbPosX3EOVkfweun0WX7inFx73gtn0WL7iHm3/09Pw/K9GxoAA0VwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAONc7jT2e3VVdVyJFS00TppXr+i1qaqv+CExE1TERtHJBHLX5ZXpy8ElqtMb+LKaqpZKqViebfc2Vjd7o1REVEXVEV3Sv53sz63sX2RN/qj2I8k22GuqI/ufxCMY3rMEZvZn1vYvsib/VDezPrexfZE3+qJ/Sbbip+vcYxvWYIzezPrexfZE3+qG9mfW9i+yJv9UP0m24qfr3GMb1mfCGf+zYyPGfZQUmPO2WPqb9bWVePU9FHfFRK1aqekfFM1y03BFSnbomi6pL0pu8frzezPrexfZE3+qOurvsFlve2a0bT6qtszsmtlI6ki3bTJyL9dUbI9vhGqvajnI1deCL0cE0z2XkuumZzzTP9z3ImY9ku+ARm9mfW9i+yJv9UN7M+t7F9kTf6owfpNtxU/XuTjG9ZgjN7M+t7F9kTf6ob2Z9b2L7Im/1Q/Sbbip+vcYxvWYIzezPrexfZE3+qG9mfW9i+yJv9UP0m24qfr3GMb1mCObUZhAivdWWSu048glFNTb/AM3KctJu68OO6unxKUVjvMN/tcNdA18bXq5jopNN+N7XKx7HaKqatc1zV0VU1RdFU1LxcrW7UxVVhMb4TjE7HPABoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADHwP3jqfrO4fjJijJzA/eOp+s7h+MmKM6C4+q2Xyx9mGrzpZ2R/B66fRZfuKcXHveC2fRYvuIcrI/g9dPosv3FOLj3vBbPosX3EPNv/AKen4flejY0AAaK4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAExtNcrcAvqoqoqUzuKFOS+0/wDo/vv0ZxuXP1my+aPumNrQAPDnIxqucqNaiaqqroiIdo1nkHCs16oMitdNc7XWQ3C31TEkgqqZ6PjlYvQ5rk4Ki/GhzQAMe75dabDerHaa6r5C4XuaSnoIeTe7lnxxOlemqIqN0Yxy6uVE4aJx4GwQAAJAGbfMktmNMon3Stjom1tXFQ06yL/OzyO3Y40+dVPRcMwtFryi0Y7VVfJXi7RVE9FTck9eVZCjFlXeRN1u7yjPdKmuvDXRSBsg9Fwr6a1UFTW1tRFSUdNE6aeonejI4o2oquc5y8ERERVVV6EQ80VZBcaOCrpZWz008bZYpWLq17HJqjkX4lRUUke4AADO2cuV1lrlVVX/ANK16cfpMhombs495K/62r/xUh53lL1Sfmj7VMlHtVQAOQZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGPgfvHU/Wdw/GTFGTmB+8dT9Z3D8ZMUZ0Fx9Vsvlj7MNXnSzsj+D10+iy/cU4uPe8Fs+ixfcQ5WR/B66fRZfuKcXHveC2fRYvuIebf/T0/D8r0bGgADRXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACqiIqquiJ51AEvtP/o/vv0Zxza/Nsftm/wCE3qhicyqZQuby7Vc2of7iJURdUevSjekltpeYUlRguRRU1Jcql0Uy26VW0ErGsfpqr957Wo6NPPI1XN14aqvA3blE6TZfNH3gjarDMye3su2N3WikknhZUUssTpKWZ0MrUVipqx7VRzXfEqKip5jTB2bXfJWzeOhwv2IuErBX5VU3LKGW+ipKa2XqRkzqp6+TDDJI5W0sao12+rEREai6cdD2Y3T7Ubxi+1PB6G7VsN7sdzt0lJHPfnVNWlNLFHNNSsuDo2uRzmo5Gvc3Vqv010TeO6IPY5bPKWz3K1Q4+sVur546mSmZXVKMilY9z2PgTlP9nVHPcqcludKnmL2Omz2C3XShisDooLpyC1qsr6lslQ+F6vike9JN5ZGucq8prvrw1VdE0xZZHz7k+c1cbNllVhttyHIsmtWR3e3LZcmrElrYK72vmRYZZnOVHRs5Rr95HrqzoXU+g/Y9V8d32SWK5pe7hkFXXsdU1tZc5HLMlUrl5eLcVVSJI5EcxIm+S3c0TXpXRsOxfDMZ9o1tllbTPstXPX0UnhEzntqJo3Ryyvc56rK5zHuRVkV3m+JNOLVYFe8cuFwnwKssVhgulS+vuMV0t9TW8tVO0R0jEbVRtj3kamqNbxXVy8VUmImNYwfZFVNxa3ZxQW+83GyNumXU1BVTWyoWGWSB9NVb8e8nmXdT9SoipoqIqdNZLb75jtj26VVFnWXcpgk0dRY0nvMsqRa0cVS5su8qrUNVz1buyq9Eb0aLqq/RVFg92yGSikzyps18ktVfFcrU60UNRQeD1DGSM33o6pk5TyZFREXROnVF4ac24bK8XutJltNVWvlYMr0S8t8IlTwrSFsKcUdqz+TY1vkbvRr08RNMzrHzt7I7LaLPcnZjczrm2WwWB15ppLXaqquSK9zNTwLf5CN+5ybWyO8rTVJk06FKOPNU2n7T9hGSW57aWa643fJk1bvJTzrHSI9qovTuSI5qp/wqd6Yzgtjw+su9XaaJaapu87amtldNJK6Z7Y2xt4vcuiIxjURqaImnBOKmNaNi2G2GtoKugs/ITUDq91L/ALVM5sPhjkfVI1qvVER7moummjeO6jdVGWR86JTXGg2VbUcLza55Z4+MxCruVR4Ven1NDXxxo/8A2mjc1UWNjn7rXwqjPJduq1UVVWkyGx1uM7HNjlsx/KMgtvt5kNpjqa1LrNPPyMtK/lImPkc7SPRqaM9wi6KjTuLDdheDYE6vdZbCyF1dS+AzrVVE1Uq0/H+Qbyz37sfFfIbo35j8WPYPhGOW23UFBaJYqO3XGK60cMlwqZWwVEbXNjczfkXRrWuVEZ7jj7kjLI64ZidVk21+7YDJl+VWjH8dsdNW0jaS9ztrKyapmnV88tQ5yySNj5NrGscqtTXii8ELv2N2YXPO9i2O3i8VSV9xf4RTyVqNRvhSQ1EsLZtE4eW2Nr+HDyuBr55scxDaZV01VkNoWrq6eJ0EdTBVTUsvJOXV0TnwvY5zFXirHKrfmKizWahx200dstlJDQW6jibBT01OxGRxMamiNaidCIhaIwkcwzdnHvJX/W1f+KkNIkMKzix2WzXz2yuDLZHR3uqhmnrmughR8tTJyaJI9EY7XXTVqqiLwXReBpeUYxuk4cUfapko9rscHHpbjSV0lRHTVUNRJTv5OZkUiOWJ/TuuROhfmU5Bx7IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAx8D946n6zuH4yYoycwP3jqfrO4fjJijOguPqtl8sfZhq86Wdkfweun0WX7inFx73gtn0WL7iHKyP4PXT6LL9xTi497wWz6LF9xDzb/wCnp+H5Xo2NAAGiuAAAAAAAAAAAAAAAAAAAAAAAAAAAAcS43ahtFNPUV1bT0VPBEs0stRK2NkcadL3KqoiNTzqvADlgnajP7HClVyVXJcH09GyvdHbaaWre6F/uHMbE1yv3vMjdVVOOmh5qcorlWtZQ45cqySGlZUQvkWKCOoe7ohRXvRzXp0rvNRE+PXgThIoQTtTUZVUpWMpaK00KLSsdSz1NTJO7whfdtkiaxqbjfMrZFVy+ZvnVtjv9xZXxuyZ9tZUQRRwSWyiiSalkTjJI103KtdvdCI5io1PjXiMBRHorq+ltdJLVVlTFSU0Sb0k070YxifGrl4Ihi12FUl0dckrq66VMNfDHBJTpXyxRsa3zxpGrVY5y+6VFRV6Ojge5MJx/wmsqXWWhlqKyKKCpmlp2vfPHH/Nse5UVXI3pRF6F49I1D03HaDj1sZd1fco6mW0cj4fTUDH1dRT8tpySOiiRz9XIqKibuqouvRxFyy2el9to6LHrxdqm3uhYkMEUcKVKyaLrDJM+ON6MRdXLvcNFRNV4FCiIiIiJoiHrnnipYXzTSMhiYm8+SRyNa1PjVV6BqGFX1+TPfcorfaLe3kZYW0k9dXOayoYvGVytZG5WK3oROO8vSrUFXbclrFr2MvdFQRPqI3Ubqa3q6WKFP5xsivkVr3O8zka3dTzOXiYVTtzw1J309sub8nq2OVjqfG6WW5ua7+q9YGvbGv8AfVqJ5zGu+1nJlbU+A4lT2RsFOtVI7JrmxlQ2FF05RlJSJUSPTXREaqsVVXTgvAvFNW5C2qsQfcFrUq79eJYqiqjqWRRVDabwdGdETHQtY/cVeKo5zlXo104Hou2IYnTU1dWXqjopaSSrZcZ5Lu/loWTs9xInKqrWbvS3TREXimikLV2vaLkqVmt/rY3R00M8FPbKOKz0VU9+msT5pm1NSzdTi5WsYqcETVdWp72+x3tFXNWyV8qVEsng76SuqWuuVwpXt4zKlRXOnTy9Vam5HGrGqu7oqorZwiNtQ4ed7bbBBhGUt2eViXnJZaOpWhqbFQuq6Vtc6NUifNUNYsDf5Tc1WR/zLr0HyDsS9lptx2xUVda62y0mRY6xGw3O8toHRLSsd596PRm+ui8FTQ/oQmEWN1RVzT0DK11TPFUuZWudURskjTSNY2SKrYt3zIxGoiqq9Kqp6LLs5xvGsMXFLPaKa1WFYHU/glIzcRWq3dVyr0ueqdL3auVeKqqm3drezsK6appxwmJ5GvFzAYrocroESBtBQXZrOData1ad8ifG5nJKjV6NdFVFXXo6Dxy+Wej1B9qr+SdVFtYzGMWlPOPyx5JbYMTl8s9HqD7VX8kcvlno9Qfaq/kk9LZcdPajvMktsGJy+Wej1B9qr+SOXyz0eoPtVfyR0tlx09qO8yS2wYnL5Z6PUH2qv5JCXTbn7T7WrVs4qbbQsyi50b62CBLmqs3W66Nc7kuD3I1yonnRvzpraLSznZXT2o70ZZdrAmLreMttdE6pTFIKzdcxqxUtyV8mjnI1XInJJqjUXeXTjoi6Iq6IvM5fLPR6g+1V/JI6Wy46e1HenJLbBicvlno9Qfaq/kjl8s9HqD7VX8kjpbLjp7Ud5kltgxOXyz0eoPtVfyRy+Wej1B9qr+SOlsuOntR3mSW2fE3sk/ZD7Z9g9PVy47ZbbBh1Xc6xtNf/AAd1RLHMtRJvRyoq7rF11VurdHJ0Kqo5E+wGLllRrGlottGruHLSXB8rWfPuJEiu83DVuvxoaK4NaKvEZ8budLHd7ZVNkSriq2Nc2odI9XyOcmmmqvcruGmi9GmiGhfb1Y02UUYxVMzE6px1RE7vitFMw6C2H7UbLk2xrG6za/SRUmRXRG3Opul1sa0tFU6yOfSTNqWxpBvJC6LRd9HJ59NVO77fZLJkdFNcLDfqt1PW1jK11Za7q+eJ726atZq57GscnumNREXp014lPb7fS2mgpqGhpoaOipo2wwU1PGjI4o2oiNY1qcGtREREROCIhI3fYvhV4rZK51gp7dc5F1fcrO99vq3fFrPTuZIumq/pedTmJqiqZnYtg1JrLf4VndR5Gj1lrm1CNuNDHK2Kn/Tp2cksS8f0XuVzk8+90CSpymlWRUoLXcGuuDWRoyqkp3R0S9L3asejpW/1EVrXJ+k3oJ9Nn2UWR7XWDP690LdNKHIqSO4woieZHt5KfVfjdK79R4TJ9oVhRfbbDaO/wN/9Yxm5NSZ3zrT1SRNb+pJnkYY7MBQrlVXTqiVeOXWBH3P2vidE2KdHxr7mqXk3qrIV6F3kRzV6WonEJn9ga5rai4Jb3PuS2iJLjE+lWarTojj5VreU3v0Vbqj/ANFVJ+PbriNM9sd8qqrEJ1VG7mTUctvYrl6EbNK1Inr/AHHuLuirqa5UsVTSVEVVTSpvRzQvR7Hp8aKnBSJjDbA/NHcKW4se+kqYapjJHRPdDIj0a9q6OaunQqLwVPMcgx58OsVTUU1RJZqF09NVLXQy+DsR8dQqaOlRdNUeqcFd0qnScamwikt/gSUNddaSKlqn1XJJXyytlV/umP5RXas14o1NEb+joV1JUIJ6lst/ofBGtyNK+NtW+WodcaBjpJIF6ImLEsTWK1eh6tdqnSirxFHVZRC63x1tvtlUkk8rauopKt8fIxf+E9sbmLvqvQ5N5NOlFd5mAoQT1FllRJ7WMrsdu1sqK2aWHk3xxztg3NdHSPhe9rWvTi1VXz6KjV4Hmgz/AB64OtsbLpDT1FzkmhoqWtR1NPUPi/nGsilRr3K1EVVRE6OPRxGEigB6qWrgrqdk9NNHUQP4tliejmu/UqcFPaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMfA/eOp+s7h+MmKMnMD946n6zuH4yYozoLj6rZfLH2YavOlnZH8Hrp9Fl+4pxce94LZ9Fi+4hysj+D10+iy/cU4uPe8Fs+ixfcQ82/wDp6fh+V6NjQABorgAAAAAAAAAAAAADi1N2oaKspKSorKeCrq1elPBLK1skytbvO3GqurtG8V06E4mRb88tV59qnWzwu5U9zjllp6ulo5XU+7Hqiq+Xd3GaqmjUcqK79HVNVJwkUIJylvt+uUdJJFjbrcyelllkbdKyNstPKi6RxubDyrV3ulVa9d1PjXgfqChyeq8GdV3W30TVonR1EFFRue5KpeiSOV79NxqdDXR6qvFV04DAUJ6qqrgoYJJ6maOnhiY6R8kr0a1jWpq5yqvQiJxVTDhw9XtpfD73drlJFRvo5HvqUgSo3/dSvZCkbeU+JzUTd82i8T2UODY/b5aeaKz0jqqCh9rY6qaNJZ0pddVhWV+r1Yq8VRVXVeK6qNQ/Ds9sKycnBcG18i25bsyO3xvqnS0qdEkaRI5X6rwajdVcvuUU/KZZU1bUWhx661DZLatwhlnYymY5/wChTOSRySMlX4nM0anulReBQRxtijaxjUYxqI1rWpoiJ5kRD9DUJ1anKaxHclQ2u2tktiPZJUVElRJDXL/4b42ta10Tf6ySIrl4IjelT7HfaxJEqcjdTNltyUzkttHHGsdSvuqmNZeU0/4WO3kTz7xRAYidkweiq0l8OrLncEmt7bbMyevlbHJH+k9Y2K1iSO88jWo7zIqJwOXSYhY6Co8Igs9DHUrSsolqEp2rK6BnuInPVN5WJ5kVdDXAxkETRNE4IDBynPMcwmJkl+vlBaeU/m2VU7WPlXXTRjFXeeuvDRqKpN86twvjVTFcKvl4avBtZcovaml/Wq1G7MqfOyFyL0oTFMzrHYR4c5GtVVVEROKqvmOvvaXaRkLV9scitWJQuVP5DH6Tw2oYnn0qalEYvm6aczrjsuwilkpW5XPccyqaqrjpGR3+pmuEbpnIrmp4K3+Qj4IrlckbURE1VdE4Tlj2yht3LbZhdvqpaOG9x3m4RLuvoLFDJcqli+ZHRU7Xub0fpIiHF8fMxvrV9oMAqKZiro2qyivjoI3J/WSOJJ5eH9V7GKvzdJs2RLhHTUVPacepsbtcNXJFLTVaMa/wdqKjXwxwK5ib68U3nIqN6W6rupyKHEXb1rqLtda673CgdO9kzpVp4nrLqio+CLdjejWrut32uVqcdVcquWf2x7BDT+NF4q4KO7bQoqCaqjlmhocMtLVkVkWqSNdPPy6Lx8neRsS68E0U9FBsht15dR1VZi63PlqV061WdXGW6VNLU8UiRKVzpIkT9J3JyR6cERNVVW9r2u1UVjt8FBbqOnt9DTt3IaalibHFG34mtaiIifMhyhnmNhgl6TCnuo4YLldJ6iFbY63T0NC1KOjervdysYzy2OVPJTSRd1Ojjq5dm2WK3WZsaUVFBTKyCOmR7GJv8kxNGMV3SqInRqpzwUxmUgAIAAAAAAAAAAAD+dGf+xE2zZP7JmHLFyjG6XJa5819oZGVFS6KkjpZadjId7kPMk0aImnFGuP6LkDVtSt29Wp0aOc6241WJMqaaMSpqqbk9fPx8Dl0/ur8RnsbSbOZmETGK5qqaKtppqediSQysWN7F/SaqaKn+Bk4is0Fq9r56N9E63yOpImSVnhT5IGLuwyuevlavjRrlR/lIquRVdpvO2yZv8cGN3ZMm3bbRUvJcle7hWSOicykibK+N6OTyV3JHrrv6IjZJHbybu67DGvUlTAAgAAAAAAAAAAB+ZI2yxuY9qPY5Fa5rk1RUXpRUIat2HYVPUS1VDZkx6uldvyVmOzyWuaR39Z7qdzFf/8Az1RehUVOBdgmJmNg688SM2sSa2LPn3CJOilym3R1bUT+q2WBYJE/vPWRf19A8dM5sXC94H7ZxJ01WLXKOp4f1nRVCQOT+6xZF/WdhgnNvhCDo9uWFzVUVJX3dccr5XbjKPI6eW2Svd/VYlQ1nKL/AHFci+bUuo5GTRtkjc17HIjmuauqKi9Cop66ujp7hTSU9VBHU08ibr4pmI9jk+JUXgpDO2HYpRy8tYaeqw+feV+9jdXJQxK5elXQMXkXqv8AxscP2yL88KiLpqiLpx4nX7bPtFxtE8Bv1tzGmbr/ACF9p0oap3DhrUU7Vj//AOdDwm2OksaK3M7NccK3U1dW3BrZbd/e8LiV0cbf+asa/MMszs1inhwiwUtTa6imtFJSS2x0z6PwaJIkgWX+dVqN0Ty9dV4cV49J6bbhrLN7TR0V5vMdJbeXRaaprXVfhaSa6JPLPvyv3FXVqo9FTTRVVvA26Osp7jSxVNLPHU00rUfHNC9Hse1ehUVOCp857iMZSnbdbsmoEtEU95orpFE2ZLhNUUKxT1CrqsKxqx6Mj3eCORWO3ulN3oVQXfI422uO54/Ak00czqyW2V6TQ0z267iIsjInv3006Gpuqui8PKKIDETtFm9LOtujqqC62uprYJJ2w1dBJpCjPdNllYjomO0TVGq/Vye514nMs+WWXIaejntl2oq+KsjdNTOp52v5ZiLo5zUReKIvBfiXgprHDrbNb7jNHNV0NNUzRsfGySaJr3Na9NHtRVTVEcnBU841DmAnKfAbTb46Vlt8MtMdLSSUVPFQ1kscMUb/ADpDvcmrmrxa5zVVvQioiqi/qCxXqgWlbT5FJVQwUToHNuVLHI+eb9CZ7o+T4p52oiIqfEvEYRvFCCdircooo4UqrZb7juUL5J5aGqdE99U3ojjikbojHJ+k6XyV4Kip5R48dI6ZF9srTdLXyds9s53S03LRwonu4VfEr2ulb52NV2qcW7yDCRRgy7blNnu9Qymo7nSz1T6aOtSmbKnLJA/3Eix+6Rq+ZVQ1CNgAAAAAAAAAAAAAAAAAADHwP3jqfrO4fjJijJzA/eOp+s7h+MmKM6C4+q2Xyx9mGrzpZ2R/B66fRZfuKcXHveC2fRYvuIcrI/g9dPosv3FOLj3vBbPosX3EPNv/AKen4flejY0AAaK4AAAAAAGNc3XC5VslupHT2yONsMz7kjGOSRFkXehjR2vHdYqOcqcEkbu6rruhzLlerfZlpUuFfTUK1U7aan8JmbHy0zvcxs1VN5y+ZqcVM2HK33B8PtdZ7lVxLXPop5poPBGwIzXem0m3HPj1TRro2uR2uqat8o5tvxy3Wueomgpk5eepfWPlle6V/KvajXORXKqt8lEaiJoiIiIiInA0idQnqSPKayShlq5rVa2MqJXVVJTskq1mh6Imtldye47zuXk3J5k/rL5osP5J9rmr7zd7rV2+SaVk81UsDZVk14SxQJHFI1qLo1HsVG8F915RQAYjIsuI2THaSiprZaqSigokkSmbFC1OR5R29Jur0pvOVVd8a8V1NcAjaAAAAAAAYGVZ3ZMMZClzq1bVT6+D0NNE+oqqlU01SKGNFe/pTXdRdNeOgiJnVA3zLyLKLPiNv8Ovd0pLTSbyMSasmbG1z16Gt1Xi5ehGpxVehCR8IzzN2p4MyPZ/anr/ADtSyOsur2/G1mroIF86K/lviVjV6NXHdluPY7c23ZKV90vyN3FvV2lWqrNNOKNkfrybV/qR7rf+EvhEbZGWzaRecncrMRxGuqoFRd27X/etdJ0cN1j2rUP/AFpEjVTof5zwmz7JcjRVyrNazkX9NtxeNbXBp8SzI59Sqp0bzJY0XVV3U4adhAjNhsgTeK7NsXwmSWayWOjoaub+erWx71TN88kztXvX53OU2rpcG2m21NY+GeobBG6TkaaNZJZNE9yxqcXOXoRPjU5RNZP4NbLvbr/cKelS3WynqnT3GomVq0COa1Vl3V8nd3WORz14tReHkueRtnWPbJbbrfVqmV9Utst7n08lNDb3rHVIjUR0jZpUVU0c7ydI9NGtXy139G6Vrsdusi1a2+hp6JauofVVK08TWLNM73Uj9E8py6Jqq8eCfEc1rke1HNVHNVNUVF1RUPIxAAEAAAAAAAAAAAAAAAAAAQdXtBrMmqprbg1NFdJmLuTX6qRVtdI7zpvNcjqh6cf5OJdNUVr5I101mIxFBlWW0uLQUzXRSVtyrJORobdTJrNVSfE1PM1E4ueujWN1VyohxsLxiosjK643SSKpyC6yJPXzQ/zbNE3Y4I1VEVY42+SiqiK5d56oivU84rg9LjdRUXGeomu9/q2NZV3esXWWRqdDGInkxRIuqpHGiN1VXKiuc5y0hMzEaoA8PY2RjmPajmuTRWqmqKnxHkFRg0k82O1XgdZLNVUMzppoK50UUcVGxFarad6tVP6ztx27pux7r3b+iybx6a2ip7lRVFHWU8VVSVEbopoJ2I+ORjk0c1zV4KioqoqL06mLJTXaxTvkolfeaGaenjbQyvZE6hhRqMkdE/d1kTg1+7Iu9xk0evkRpO0UAOBaL5Q32KokoZ0mSnnfSzNVqtdHKxdHMc1URUXoXj0oqKmqKirzyAAAAAAAAAAAAAAAAAAAELVbKaS21klyxCqdiFze5XyMpI9+gqXKuq8vSatY5VXXV7FZIuvu0NbEcrlvctZbLnTMt2RW5GLW0Ub1fHuPVyRzRPVE34n7j9F01RWvaujmqhSEFj3/AHj2qXy+0zn+1tuomWNsm7oyoqWyukn3V/SbGu5HqnDf5VvSxS+OaJxQvQAUSAAAAAAAA4N1sVtvtLU01yt9LcKepgdTTxVULZGyxO91G5HIqK1fOi8FMubBqJjalbdWXGzTTUbKFj6GrduU7Ge4dHC/eha9Oje3NVTguqcCiBOMwJyro8nom10tvuNDc18HjbSUlxgWFeWboj3STR68HprwSLyV6NU4Hm4ZXU2Vl0nuFjuCUNEyF7Km3x+Guqd/g9I4YtZlWNelNzinFuvFEogMd4z6HIbXc7ncbbSXGlqbjbnMbWUkUzXS0yvbvM5RiLq3ebxTVOKdBoGferBbshopKS5UcVXTvcxytkT9JjkexUXpRWuRHIqcUXihwZLbeLZVPmt1b7YRVNcyWelub9GwQK3dkbA9jdUXXR6NfvIqo5urEcisapG8DhWa8Ul/tsNfRPdJTTIu6r43RuRUVUVHNciOaqKioqKiKioqHNIAAAAAAAAAAAY+B+8dT9Z3D8ZMUZOYH7x1P1ncPxkxRnQXH1Wy+WPsw1edLOyP4PXT6LL9xTi497wWz6LF9xDlZH8Hrp9Fl+4pxce94LZ9Fi+4h5t/9PT8PyvRsaAANFcAM+83+349TsnuNXHSse7cjR3F0jtFXda1OLl0RV0RFXRFXzFqaKq6opojGZGgCW5zLB8tW/ZlV+WOcywfLVv2ZVflm5oF76qrsz3JwVJi3u0SeFNvFspKaa+QRLTxrUTPiZJCr2uexyt1/q6tVWu3VVdETedrwOcywfLVv2ZVflnqq9pVnfSzNpaqphqVY5IpJrRVSMa/TgrmoxquRF01RHJr8adIi43uP/FV2Z7kYP3HtWxCW9WezMyK3vvF2dKyjtzZkWokWPleU1jTymox0ErHK5ERHsVirvaIVZ/MduxPadhHspMf2h1FzqtoMLrzHVV13gpJaebkVduvR8MjGo1EjVURse81qIiJoiIh/Q/nMsHy1b9mVX5ZmtfJt5owy2dU4/xnuRGMqkEtzmWD5at+zKr8sc5lg+Wrfsyq/LMOgXvqquzPctgqQS3OZYPlq37MqvyxzmWD5at+zKr8saBe+qq7M9xgqQS3OZYPlq37MqvyxzmWD5at+zKr8saBe+qq7M9xgqTNyHJbVidrfcbxXwW2iY5GctUPRqOcq6NY3zuc5eCNTVVVUREVVIXLNsTqaSChxy11NbV1CLvXCvo6iKipE/rPRI1kkd50Yxui6aOfHqjji4/U4xQXOO9Xm5XPI8ja3RtfV2upSOn8nRUpoUj3IGrqqKrdXuRdHvfohaPJ962zZVdme5DU9s8v2gppaYpcIsL/AP2lX0yOuk7fjhp3oraf5nTtc7pRYU4KUOLYDZMPkqai30iuuNXp4Vc6uR09XU6dHKTPVXKieZuu63oaiJwPQ3aXjyr5VVUQt8756GeNifrc5iIn7VKaKVk8TJI3tkjeiOa9q6o5F6FRfOhgtbG2sdVpRNOO+Jj7mD9AA1gAAAA49wuNNaqOSqrJ2U1PHpvSSO0RNV0RP1qqoiJ51VEJiJqnCNoyKqxVdtkq6uxTNbVVMsDpKWvlkfTbjERjmxtRf5FXMRE1am7vNRytVVdryaDJaWrrHUc7ZLdXLPNDFS1u7HJUJHoqyRJqvKMVrmO3m66b2jt1yK1M1dpdgRf5+scnTq23VKov7UjONWZ5i9w5LwhKqZ0Llkic611O9E5Wq3eavJ6tduucmqaLo5U85vaDe522NXZnuMFUyvppK6aiZUROrIY2TSU6PRZGRvVyMe5vSjXLHIiKvBVY7ToU95/MTDMj2/497I+LaNccTyS4W5+lumoKqVk70taO8iDeTdRzmJ5W9om9Jq9U1c7X+iXOZYPlq37MqvyzLaeTb1Z4YWdU/wBSiMZVIJbnMsHy1b9mVX5Y5zLB8tW/ZlV+WYdAvfVVdme5bBUglucywfLVv2ZVfljnMsHy1b9mVX5Y0C99VV2Z7jBUglucywfLVv2ZVfljnMsHy1b9mVX5Y0C99VV2Z7jBUglucywfLVv2ZVfljnMsHy1b9mVX5Y0C99VV2Z7jBUglucywfLVv2ZVflnortqtho6OadqXKqdG1XNggtdSr5F8zW6sRNV6OKonxqicRoF76mrsz3IWBJZLtKttjuTrPQw1GQ5IjWu9pbUjZJ42u9y+ZVVGQMXRdHyuai6Kjd5eBApm1xz1FW8VtxwqxvTha7ZSVLrlK34pqpjFbD8StgVzk6Um8xXYxkOE4hbW22yUktspN9ZFjgtVS3fkd7qR7uT1c9y8XPcqqq8VVS2gXqNtlV2Z7jW9fiJec53pM5rYVtj+jF7W5yUat+KpmVEfUr8bdI4lRdHRv03i9pqaGipoqeniZBTwsSOOKJqNYxqJojUROCIicNEOPabxRXykSqoKllTAq7quYvQvnRU6UXo4LxOYaVcVUzlqjCY9gAAoAAAAEzLtIx6Nyo2slqERdOUpaSadi/qcxiov7FM1lYWttjFlTNWG6Jka1wsNDc6+grqinR1bQK91LUNVUfFvt3Xoip0oqdLV1RVRq6atRUyoay8YvRwsunK36jpqOWWpu1PD/ALS5zF1angsTVV7nM+STVXJojE3kRPXzmWD5at+zKr8sc5lg+Wrfsyq/LNnQb31NXZnuMFFQ10Fypo6imlSWKRrXtcnxOajk1TpTgqLovxnvOm9q2dUNsw3J71h9Dc5s2lo2MpH2+1PbPUyxu1gZI6WNGuiRzl3t5dUY6Td8pUPlz2HOYbYNmu0i+xZ9jF5mxvKayW411Z4OipS10jt506Rs6GvXg5rETTyVRNG6GWnyZeqqZq6OrV7pQ/oOCW5zLB8tW/ZlV+WOcywfLVv2ZVflmHQL31VXZnuWwVIJbnMsHy1b9mVX5Y5zLB8tW/ZlV+WNAvfVVdme4wVIJbnMsHy1b9mVX5Y5zLB8tW/ZlV+WNAvfVVdme4wVIJbnMsHy1b9mVX5Y5zLB8tW/ZlV+WNAvfVVdme4wVIJbnMsHy1b9mVX5Y5zLB8tW/ZlV+WNAvfVVdme4wVJ66mphoqaWoqJWQU8TFkkllcjWMaiaq5VXgiInHVSEybbFbrPbVltlsut9rnORkdJBQyxJqv6T3yMRGsTpVU3naa7rXLwWao6m1ZHPDX51Xy3yeKVs9PZ4LRVJbKN7V1YqMdHrPI1dFSSTociOYyNSY8n3vbNlV2Z7kKZLxctpqJFZZKi0Yo7dWW8qx8VTcGedlIi6OjY5OmoVNVaq8kmrmzMtbbbaWz0FPQ0NPHS0lOxI4oYm7rWNToREJ/nMsHy1b9mVX5Y5zLB8tW/ZlV+WJuN7nZY1dme4wVIJbnMsHy1b9mVX5Z5btKx9y8ampjTzuloKhjU/Wro0RCug3vqquzPcnBUA/EE8VVBHNDIyaGRqPZJG5HNc1U1RUVOlFTzn7NKYw1SgAAAAAAeiur6a2UklVVzsp6eNNXySO0anm/8AnwJ5dpdgReE9W5OlFZbqlyL+pUj0U2LK721tGNlRNXwiZFQCW5zLB8tW/ZlV+WOcywfLVv2ZVflmbQL31VXZnuTgqTOyDI7TiVoqLtfLpRWa10+7y1dcKhkEEe85Gt3nvVGpq5zUTVeKqiecx+cywfLVv2ZVflkvtQrMP2q7Pr9iV1krVortSup3O9q6pVjd0skT+T6WvRrk+dqExcL1jrsquzPcjW9uyjbFhOZJPbLRtCtOUXV9wuD46dtdAtVyaVUqo1sTXK50TG6NY9E0cxrXdDjsw+HfYG7HqXYRDkt9y6GpgyWtmdQUzW0E8iMpGO130VrFT+UciLp0ojE16T655zLB8tW/ZlV+WZLXyfeaa5iiyqmPlnuIxVIJbnMsHy1b9mVX5Y5zLB8tW/ZlV+WYtAvfVVdme5OCpBLc5lg+Wrfsyq/LNOzZXar/ADPhoqrfnY3fdDJG+KRG8OO69EXTinHTzoY67peLOnNXZ1RHviUNYAGqAAAx8D946n6zuH4yYoycwP3jqfrO4fjJijOguPqtl8sfZhq86Wdkfweun0WX7inFx73gtn0WL7iHKyP4PXT6LL9xTi497wWz6LF9xDzb/wCnp+H5Xo2NAAGiuEbA9arPL4+TynUsFPBDr+g1yOe7T4t5dNfj3G/EhZEXQfDjJ/1Uv7tT3PJEf5a5/j+YROyW4ADpGAAAAAAAAAAAAAAAAAMzZ+7km36iZ5NNRXN8UEadDGOiil3UTzIjpHaJ5jTMvA/9+y363/8AK05oX+MbrXj7vuyUe1WAA45kAAAJDIneFZ3Y6WVN+CKiqqtrF9zyrXwsa/T40bI9EX/iUryPvX9I9o+qaz99THreSoxvUfCr/WUTsbAAOqYAAAAAAAAAAAAAAAAAAAY1G5KTaNBHEm42utk8s6JwR7opYGscvxqiSuTX4tCxIxn9Jlp+qK799SFmcz5Wj/NTO+mPvMM8bIAAeKkAAExtGlczGOSRXIyqraOkl3V03o5amON7f1K1zkX5lObHG2JjWMajGNREa1qaIifEhn7Sfg9SfW1t/GQmkdd5M9U/+U/alSvZAAD0mIAAAAAAAAAAAAAAAAAAAAAAABmYIqQ1WS0MabtNSXJGwxp7liPp4ZXIieZN+R66fOpVklhHv3mP1pH+CpitOR8pRheqv6+0NgAB5oAACRyhfCczx2jlTfp0p6us3F9yskboGscqfGiSu0+JTWMjIf6Qse+rbh+8pDXO1ucYXSy+E/7VMVe0ABtqAAAAAAAABP5i5KWntdcxN2pp7nRMjkbwcjZaiOJ6a/ErHuRUKAns695qP62tn46AtTGNURPtWp86FyAD56zAAAx8D946n6zuH4yYoycwP3jqfrO4fjJijOguPqtl8sfZhq86Wdkfweun0WX7inFx73gtn0WL7iHKyP4PXT6LL9xTi497wWz6LF9xDzb/AOnp+H5Xo2NAAGiuEXQfDjJ/1Uv7tS0Iug+HGT/qpf3anueSPS1/L/8AalE7JbhIU+0Hl9rldg/gG74LY4Lz4fy3uuVnmh5Pk93hpyOu9vcd7TRNNVrzq3LMEy6m2tNzXEpLLO+rsjLLWUt5kmjSJI53yxzRrG12+qLK9FYu7ronlIdHLAmbN7JG95ZHhUOP4RBW3HJaO5VjYKm9chHTNpKhsKo5/IOV29va8G6ouiaKmrk5rPZKNqMap/BsWq583nvk2ONxbwpiObXRM5SXWfTd5FsWknK6e5VOGq6HWFlwTONlma7Icbtj7Bc8oobDf1mdWTTx0crX1cEmrXNjV7V8tv6KpwVPiUsofY85babfa8loL1aJtpVNkdXklQ6ojkbbJnVMPg8tMmmsjWJCjGtfoq6s1VOPDHE1D84TtLzSmyPbLX3SxotwtVVbEisFTf2JR0bHUrFe9tS9qMZGqLyrlRiL0+SrjPyv2TN2yfYHtLvWM0tFbMpxhqQzSUd1hr6aNr2Ne2eCdjFZL5Ll8lWtXeaqLpoe28+x/wA7yqbLLteJsYdc7pfbTeYrUyWofb6llHCkbqaoV0aO3VVEcio1yKrUVWp0Gkz2P+T3+z7XKO/VdioFzqhgjhSztlWOgmjhdEjVRzU32aJG7f8AJVV3/JbwI/cNvN/ZA1OzKxY7Fk1ps1uyu9vmbTUE+RRw0LY4kRXyyVk0TEamjmJupGrlc5ERF0VUo9i22a37Y7VdpqaGnp6601ngVZFR18ddTq5WNe18VRH5MjFa5OOiKio5FRFQkL5sz2j5DUYjlkz8Vp84xx1RSpRJJUS2yvo5o40e171jSSN+/HvtVrXImiIu9qpY0ua1OAWSkTOaaOO7VkkrkjxKzV9fTsY1U0a50cLna6OTynIze46JwUtEzjrGX7J7MslwLZBX3fFUibc2VlHC6aSZI1ijkqY2OVusb0crt5GdCaI9XIurURfN82u5JarlYMZpsNprlnlypJ7hUWqC8btHRUsciM5V9U6FFXeVzEREi11VU4Imq+jPpaH2QuznIsWx6ouFtuTmQVENReLHW0cLZYp2SxoqzRM3kV0aIu7qqIuuhw7lgu0ibKLHnlG3Fosxp7fUWevtklTUrQVFK+VksbmTclyjXtezXjGqKjlTh0iccdQ/LPZKLc7PZaazYrVV+cXO41lqXGpqtkPgs9Iv+1OlqNHNSNiK1UeiKruUZomq6JJYLtuvuO0ueT320VtwyWtzhLHacbS5JM2OV1HTvSJkzk3WQoiSSK7dRERVXd1XQ51D7HrL8VksGV2W8Wasz+mulzudyjrmSxW6rSv3OWiYrUdIxGclFuO0XXcXVOOgptgeazUd2vNVcLDTZimYsyy2pTrNJRcKSOndTyq5qPRHNSRN5qL+i7TpakfuGlffZPVOJWy+U9+xBbbllrraCkdbfbRjqN7KxytgqFrFYiMi1a9HOcxFardNOKHb+IXS7XrH6arvdojsdxkV3KUUNY2rY1EcqNc2VrWo5HIiOTgi6LxRF1Q6lp9lueyVWX5RcosQumU5AykoFstXy8lqioIN/WJZFZvve90r3K5Y9E4JuqhX7B9nFy2W4GtludVSyyurqmrio6Bz3UtBFJIrmU0Cv8pY2IuiaonSvBC0Y46x2IZeB/79lv1v/wCVpzUMvA/9+y363/8AK05qX71Wv+vvDJR7VYADjWQAAAj71/SPaPqms/fUxYEfev6R7R9U1n76mPX8letR8Kv9ZROxsEhku0Hxd2h4Zi/gHhHjGlavhfLbvg/g8TZPcbq7+9vadKaaecrzrjalgt/vmT4ZlOMSW193xyep/wBiu0kkUFTDUQ8m9OUjY9zHJo1UXdVOCop1E4+xgTV89kVW0E1ZS27E23KuizVuGwwvuSQtlctIlQlQrliXdTjuqzReCa6r7k9svsj2Y5bsvjy3HJ7NkeOJSqtpoaptYlelU5WU3g0m6ze35EVio5rd1U48Dq/NcCzXBorFU1lRYqzJL/tVhvNI2F0zaRiPtz40jkVW76aLE5NU3uGi+dWpY3b2PWV51R5ffMkvFpoM3usluktntWySWityUMqzQNVXo18m/I5yvXROC8E4GPGoe3Gc9zu5eyCmpb9js1i5LC5q2CwU97bV09TKlXGjXa7rGNl4qxVVOCLwcqHNw32SFbmF/v8Ai/tFaabKaO0TXSjgt+Rw3Cnl5NyMdFNLHHrBIjnx6orHcFVU10M67bGdo20O/ZDcsouGPWOW5YdU41C+wT1EropZZWvSVeUYzVnBdURUVOjjrqnMwTY3l1q2hY5frpS4labZbrFU2CS2Y+sybsb1ie2VjnxtRyq6JE3FRN1FVd56roIxGds79kFecc9jdZc52g0lKs9VBSR0VRFcY0fc5pl3Wul3o4o6biuq+U5rWo5deGhVbH/ZE0W1DLK/GJqe1QXimokuLHWO+w3elkg30jdrLGjVY9rnN1a5qcHIqKpKW3YBmsux63YNcbnYqefFamjq8Zu9Kk0qyy00jnRrVwuaiIit3WqjHO6XL8SHYVivuS4Zba667QKGy0sOsUNNDh9FW3CTVd7fc9Gxb6ovk6IjNG6Lq5dU0RjqxFHtNzPm52eZJlPgfth7T2+au8E5XkuW5Niu3d/R27rpprounxEbYdtN48brBY8qxBuO+MVLNUWeqprm2tZM+KNJXwyokbFjk5PVyabzV3XaO4Ezt42tY/lGw/aDbaGK+tqprBXbi1uO3Cki4QPcuss0DWN4Iumrk1XRE4qiHLxHZ3m+XZThmR5jUWGltuNUEq2uisrppH1E89OkKyzukaiMRsauRGN3uLlVXLohMzjOoejHPZNXO57KabaDc8HfbLJXwQstlLFc2z1tbWSzNhjgbHybWtY57l0kV2uiaqxD25Ft3yy12XM7XWYdT2PN7XYH36hpVuzamlqKVrtySRJkiTR8S8VjVnlLuojtHbye6h2C3ZPY3Yxgc1yo6XJrAyjqaWviR0tMysppWyxqqKjXOYqt3V4IujlPVPsgzXM7nluQ5ZU2Klvldi1TjNrobRLNJSwtm1e+aWWRjXK5z0jTRGeS1v6SqR+4SGN3nJcY2dbJau9NvdTPk1/t7q2u8bZZpGvka1YtWug0dDIm+r6du41uiIirrqm9je2mPZ1gu0bIckrqi4tpc4uFroIaqsRvF0zWQwJJK5GRRt1VVVVRrGo5fMVt82TXe54LstssVTRNqsWuVqrK173vRkjKWPdkSNdzVVVfc7yN186oS129jxkVwtmZW6K72ynbNlMeYY7WrE974KzlOUfFUxr5Lo9U3UVq6qj3apwRFYTGwUOx/wBkTRbUMsr8Ymp7VBeKaiS4sdY77Dd6WSDfSN2ssaNVj2uc3VrmpwcioqncBGbPKTM4nVsuYUeM0TlbG2mix1ZpOje5R0j5Ws118jRqN4aLqq68LMvGOGsYjP6TLT9UV376kLMjGf0mWn6orv31IWZzflb01Py/mWeNkAAPFSAACV2k/B6k+trb+MhNIzdpPwepPra2/jITSOu8m+qR80/alSvZCQ2vbQearZrkGW+Ae2ntTTLUeB8tyPK8UTTf3XbvT06KZO0Pa94hZPBZ/anw7lLBc75y3hPJ6eCJEvJbu4vu+V91r5OnQuvDW2vYDzo7MclxRKpKF91on08dSrd5I3rxa5U86I5E1T4jqDPsE2gXWouWY5W/HKamteG3m3OpLPPPK90k0bHcojpI2oqLyS+Tom7onF+vDfmZ9jEpsa9kBdZ58LnynD2Y1ZMviattucN2bVtZM6BZ2xTt5NnJq5jX7qork4cdPNEZZtwynOItnl2s2O1thwu65hboKW9pdUjnr6dZlavKUzWorYZURdEVy6ppq1EU5OzfZnmW0vFdlMmXT2Gnw2w26mr6Wltbpn1VbKtEsUSzb7UbGjWSuVUartV86Ie22bDtpdFY8ExCouGMVeL4hfaGuprhv1DK6opKaRVYx8e4sbZEYumqOVF0T3PSU/dMClyb2RsuFbTbfjN8sNvpKCvucVsp6mLIKeWuVZXIyKZ1Eib7YnOVqK7eVURdVQ9WGbTMym21bULdeqKgZh1hng/2t1x8ugg8D5VHNjSBOU5Tg9289NzeVEV6NTWVuHsdM3bDW2+glxSSmTK25THdqvl/bCuc2rSobBOqMVGbqeQkiK/VrGpuN1VUvKrZXkcW03NKynktFZh2aU8EV1iqJZYq2mWOmWndyKNY5j0c3dXylbouvSW/cJTE/ZlWbJsix6mdRWqG05BWR0VBJTZHS1VxY+XhCtRRM8qJHLoi6OcrFciOROOn0UdL7LsM2gbNqCz2O8vxa44pYKdYG3SlgqHXSpp4o1bCiwo3dbImjNVa5+9uromq6lKm3nFXKiJBk2q/HiV2/wBMTE7x11ZtuFxxezZldKvH7hcLo3Oosd9qXXxKljZJY6ZrPB3vijSOP+VavJrwRyuXf48Ne+eyUqsNsucOyTEvAL7jDaGV1DS3Js9PUxVcixwyJUOjZuNR6OR6uZ5KN18o9NfsIv8AVMvjWVltRa7aJRZbHvSycKSHwXeY7yP51eQfoiat4t8pOOm9ftnOUx57nGS2VLBVuvVot1upaO8rK6F6wyzrO2ZrW8GuZNo1UV3Hpbomi1/cJnaltT2i2zEsHr7fj1vtVxueUUdBNTMvTJ4amB7kVjWzJAvkS8UV+6jmo3gjtSjvO2LKqPaBQYTb8HprjkE9gZe6hXXrkqSmVZXRPiWVYFc5EcibrkZq7Xi1qIqpG232OmU2zZvJQ0lZY7ffKbLIcqtdpp3TLaaLk3MVKRrlaj0jdo9yq1iIjnro1DsLG8GyVdrcWbX1bTC6TGWWielt08siNqEq3y6tV7G6s3HNTVdF118nTiTGYZt09kAts2Z7SMuWw8ouHXKst/gaVmnhfIKxN/f5PyN7f6N12mnSp77ntkv1xzC92LCsJXKm2F0cN0rZ7pHQxsnexJORh3mO5V6Mc1V13GpvImpDZ9sI2gXLE9puI47W42lky+vnucdbcpZ21NO+ZGLJCsbI1arVcxdJN7VEd7hSpds72hYLmmVXHBKrG6q05LUMuFRTX9ahj6KrSJkT3x8k1eVY5I2KrXKxUVODtBjI9eGZ9nt19kDn2P1FroZ8atjaBI3OuW6+jbJDK9rmMSD+UWRyJvI56bmiaK7oMHC9uVfbcdkgSwXC7ZpeMuudppbFUXtKmNk0DnOn3al0TOTpo2scqJuKqJoiIuvC2pMEy7HdtF4yi0y2WqsWRwUMd1hrJJY6mmfTtezegRrHNejmv6Hq3RUJKPYJlFnkjvtpr7QmUW3MLvkFvjq3SrST0lcsjXwSuRm8x+49F3mo5Gub+knEjWOdW+yVrLZQy0lXhkzMupcgpMeq7FHcWOa19VGslPNHOrUa+NyadKNVPK1RNNF8Xr2S9TidizB1/wATSiyHHKugppaCC5tkpJG1jkbBMtU6NnJx67285zPJ3V6dTP5gsqu91bkt5uFoXJa7LbXfa+CkdKlLT0dExY2QQuczekfuqq7zkYiq5fc6caa77Nsqpsx2j36zx47cEyOmtVNT0N7WV0D2U6TNnbMjWLojmy6NVN/z6pw0Wf3DsDC7xd77YIay+WeGx173O1paeubWRq3XyXtla1u8jk4pq1F+Y3TrbYHs1uey3Da213OaiR9Vc6ivht9rdI6jtsUioraaBXojtxqoruhqavXREQ7JLxs1jKwj37zH60j/AAVMVpJYR795j9aR/gqYrTk/KXrVX9f6w2AAHmAAAJDIf6Qse+rbh+8pDXMjIf6Qse+rbh+8pDXO2ufqll8J/wBqmKvaAm8t2g2jCZKZlzjur3VCOcz2us9ZXoiJprvLTxPRvSmm9prx06FMDn5xTTXkMm/+Ert/pjZxhRyNse1y2bHMXp7rcEilmrKuOgooJ6qOljlnejlTfmkVGRsRrXOc9ehG8EVVRF6xp/Zg0LsZymsdZqK4XewLQSSUdhv0FxpqmGqqW06OiqWIiI9rnLqx7Wr7niiO3k38+gptvNvtbsSqay25LjFyhvNDJf7DXUlJK9qPYsUnLRM3mPY96LuaubwX9f6y7Z1ne0XZZe7Heo8Vtd3q62hmpWWqSdYGRQ1UMz0kldGjnOVI3aaMRE1RF86lJmfYOUm392M12VUeeWBMWnsdmbf9aWvSuZUUivexdF3GaSI9m7uaKmrk0cqLqS9FtFznIduWy+C943VYXarhQ3aobRJd0qEq0SKFWJURMa1GyR666LvIm+ujtdTf2q7BKnalluS1FRXQUdnu+ILj7XsVzqiGp8JWZsu5puqxPJ/S1VUVNE6TOhxXaM3OMPzPPJccWgxShuEdR4vNrKipqlmjjbyjYeS1Vf5PVWN1VNeCu6EicR3wDrxu3jFXuRqQZNqq6ccSuyJ/j4Meyk244vW1UNPHBkiSSvbG1ZMVujG6quiaudTIjU+dVRE85fGBfk9nXvNR/W1s/HQFCT2de81H9bWz8dAZaPOhanzoXIAPnjMAADHwP3jqfrO4fjJijJzA/eOp+s7h+MmKM6C4+q2Xyx9mGrzpZ2R/B66fRZfuKcXHveC2fRYvuIcrI/g9dPosv3FOLj3vBbPosX3EPNv/AKen4flejY0AAaK4RdB8OMn/AFUv7tS0Ii4VEOM5fcau4yspKG5RwcjVTORsXKMRzXRucvBrtN1URenjprount+SJjpqqfbNOrnE/hE7Jb4MvxqsvXFB2lnePGqy9cUHaWd51PR18MsOEtQGX41WXrig7SzvHjVZeuKDtLO8dHXwyYS1AZfjVZeuKDtLO8eNVl64oO0s7x0dfDJhLUBl+NVl64oO0s7x41WXrig7SzvHR18MmEtQGX41WXrig7SzvHjVZeuKDtLO8dHXwyYS1AZfjVZeuKDtLO8eNVl64oO0s7x0dfDJhLUBl+NVl64oO0s7x41WXrig7SzvHR18MmEtQy8D/wB+y363/wDK05+ZMuscTFe+829rE6XLVM0T/qcjBKSaOmutfLE+Btzrn1UUcjVa9I9xkbFci8U3kjR2i8URyIqIuqJ5vlH9l1qirVjhhzxZKI2qYAHGrgAAEfev6R7R9U1n76mLAkMsb7V5Lar3P5Nvjpp6OebzQrI+FzHu+Jv8m5FXzKrddE1U9byXMReqcfbEx9JJ2NcGWmVWRURUvFAqKmqL4Uzin+I8arL1xQdpZ3nXdHXwywYS1AZfjVZeuKDtLO8eNVl64oO0s7x0dfDJhLUBl+NVl64oO0s7x41WXrig7SzvHR18MmEtQGX41WXrig7SzvHjVZeuKDtLO8dHXwyYS1AZfjVZeuKDtLO8eNVl64oO0s7x0dfDJhLUBl+NVl64oO0s7x41WXrig7SzvHR18MmEtQGX41WXrig7SzvHjVZeuKDtLO8dHXwyYS1AZfjVZeuKDtLO8JlNlVdEu9Aq/SWd5HR17pMJehn9Jlp+qK799SFmRlikjyDMY7tRPbUW6joZaRKqNd6OWSWSJyoxycHbqQpqqcNX6a6o5EszlfK0x08U+2IjH6yzRsgAB4yQAASu0n4PUn1tbfxkJpHHzu3VFzx1zaWJZ56eppqxIm+6kSGeOVzU+dUYqJ86ocCHL7HPEj23eiRF6WvnaxzV+JzVVFaqa8UVEVPOdf5L/ddctOuYqn6xT3KV+xrgy/Gqy9cUHaWd48arL1xQdpZ3nq9HXwyx4S1AZfjVZeuKDtLO8eNVl64oO0s7x0dfDJhLUBl+NVl64oO0s7x41WXrig7SzvHR18MmEtQGX41WXrig7SzvHjVZeuKDtLO8dHXwyYS1AZfjVZeuKDtLO8eNVl64oO0s7x0dfDJhLUBl+NVl64oO0s7x41WXrig7SzvHR18MmEtQGX41WXrig7SzvHjVZeuKDtLO8dHXwyYS1AZfjVZeuKDtLO8eNVl64oO0s7x0dfDJhLUBl+NVl64oO0s7x41WXrig7SzvHR18MmEtQGX41WXrig7SzvPDsssbGq515t7Wp0qtUxET/qOjr4ZMJMI9+8x+tI/wVMVpL4LTSKl6ub43xQ3StSpgbI3ddyTYIomuVF4pvcmrkReOjk1RF1Kg4zyjVFV6qmPd9IiGcAB5wAACQyH+kLHvq24fvKQ1zKy9i2+/2a9yppQU0VRSVEvmhSVYnNkd8TUWJEVfNvIq6IiqhMrsjmo5LxQKipqipVM4p/idvcYmu6WeXXhExPamfyx1ROLVBl+NVl64oO0s7x41WXrig7SzvN3o6+GVMJagMvxqsvXFB2lnePGqy9cUHaWd46OvhkwlqAy/Gqy9cUHaWd48arL1xQdpZ3jo6+GTCWoDL8arL1xQdpZ3jxqsvXFB2lneOjr4ZMJahPZ17zUf1tbPx0BzfGqy9cUHaWd5mXiupcploLVbKmGun8OpaqZaeRHtgihmjmVz1ReGu4jUTpVXcE0RVRhNn++uMIjWtTE4wvwAfO2UAAGPgfvHU/Wdw/GTFGTmB+8dT9Z3D8ZMUZ0Fx9Vsvlj7MNXnSzsj+D10+iy/cU4uPe8Fs+ixfcQ5WR/B66fRZfuKcXHveC2fRYvuIebf/T0/D8r0bGgADRXD8yxMnjdHIxsjHJorXJqi/sP0AM7xctPVdF2dncPFy09V0XZ2dxogydLacU8xneLlp6rouzs7h4uWnqui7OzuNEDpbTinmM7xctPVdF2dncPFy09V0XZ2dxogdLacU8xneLlp6rouzs7h4uWnqui7OzuNEDpbTinmM7xctPVdF2dncPFy09V0XZ2dxogdLacU8xneLlp6rouzs7h4uWnqui7OzuNEDpbTinmM7xctPVdF2dncPFy09V0XZ2dxogdLacU8xwoLHbaaVskNvpYpG8UeyFrVT9qIc0ArVVVVrqnEAAVAAAAqIqKipqi+YADPdj1qe5XOtlG5y9KrTs1X/oePFy09V0XZ2dxogydLacU8xneLlp6rouzs7h4uWnqui7OzuNEDpbTinmM7xctPVdF2dncPFy09V0XZ2dxogdLacU8xneLlp6rouzs7h4uWnqui7OzuNEDpbTinmM7xctPVdF2dncPFy09V0XZ2dxogdLacU8xneLlp6rouzs7h4uWnqui7OzuNEDpbTinmM7xctPVdF2dncPFy09V0XZ2dxogdLacU8xneLlp6rouzs7gmOWlF1S10Wv0dncaIHS2nFPMeGtRjUa1Ea1E0RETREQ8gGMAAAAAA4dTZrfWSrJUUNNPIv6ckLXL/AIqhzATFU0zjTOAzvFy09V0XZ2dw8XLT1XRdnZ3GiC/S2nFPMZ3i5aeq6Ls7O4eLlp6rouzs7jRA6W04p5jO8XLT1XRdnZ3DxctPVdF2dncaIHS2nFPMZ3i5aeq6Ls7O4eLlp6rouzs7jRA6W04p5jO8XLT1XRdnZ3DxctPVdF2dncaIHS2nFPMZ3i5aeq6Ls7O4eLlp6rouzs7jRA6W04p5jO8XLT1XRdnZ3DxctPVdF2dncaIHS2nFPMZ3i5aeq6Ls7O4eLlp6rouzs7jRA6W04p5jO8XLT1XRdnZ3DxctPVdF2dncaIHS2nFPMZ3i5aeq6Ls7O4/cNittPI2SK30kUjeKOZA1FT9uhzgOlrn/AJTzAAGMAAAAAAz349apHK51so3OXiqrAxVX/oaALU1VU+bOAzvFy09V0XZ2dw8XLT1XRdnZ3GiC3S2nFPMZ3i5aeq6Ls7O4eLlp6rouzs7jRA6W04p5jrHYrYbbNiV0dLQ0s7kyW/NRz4GqqNS7VaI3inQiIiJ8yJoXvi5aeq6Ls7O4kNiKaYjdf5Pk/wDvNf8AhovW1Xx4/H0/tOwC9draZp/dPMhneLlp6rouzs7h4uWnqui7OzuNEFOltOKeYzvFy09V0XZ2dxzKakgoo+Tp4Y4I+nciYjU/wQ9oImuuqMJnEAAUAAAY+B+8dT9Z3D8ZMUZOYH7x1P1ncPxkxRnQXH1Wy+WPsw1edLOyP4PXT6LL9xTi497wWz6LF9xDYmhZUQyRSNR8cjVa5q9CovBUJ9uzzHWNRrbZGjUTRER7uH/U1r3drW2tKa7PDVGGuZj8StTVERrbAMjm+x7q2P13945vse6tj9d/eaehXndTznwrZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7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", 193 | "text/plain": [ 194 | "" 195 | ] 196 | }, 197 | "metadata": {}, 198 | "output_type": "display_data" 199 | } 200 | ], 201 | "source": [ 202 | "import io\n", 203 | "import threading\n", 204 | "import numpy as np\n", 205 | "import sounddevice as sd\n", 206 | "from scipy.io.wavfile import write\n", 207 | "from IPython.display import Image, display\n", 208 | "\n", 209 | "from openai import OpenAI\n", 210 | "\n", 211 | "from elevenlabs import play, VoiceSettings\n", 212 | "from elevenlabs.client import ElevenLabs\n", 213 | "\n", 214 | "from langgraph.graph import StateGraph, MessagesState, END, START\n", 215 | "\n", 216 | "# Initialize OpenAI client\n", 217 | "openai_client = OpenAI()\n", 218 | "\n", 219 | "# Initialize ElevenLabs client\n", 220 | "elevenlabs_client = ElevenLabs(api_key=os.getenv(\"ELEVENLABS_API_KEY\"))\n", 221 | "\n", 222 | "def record_audio_until_stop(state: MessagesState):\n", 223 | "\n", 224 | " \"\"\"Records audio from the microphone until Enter is pressed, then saves it to a .wav file.\"\"\"\n", 225 | " \n", 226 | " audio_data = [] # List to store audio chunks\n", 227 | " recording = True # Flag to control recording\n", 228 | " sample_rate = 16000 # (kHz) Adequate for human voice frequency\n", 229 | "\n", 230 | " def record_audio():\n", 231 | " \"\"\"Continuously records audio until the recording flag is set to False.\"\"\"\n", 232 | " nonlocal audio_data, recording\n", 233 | " with sd.InputStream(samplerate=sample_rate, channels=1, dtype='int16') as stream:\n", 234 | " print(\"Recording your instruction! ... Press Enter to stop recording.\")\n", 235 | " while recording:\n", 236 | " audio_chunk, _ = stream.read(1024) # Read audio data in chunks\n", 237 | " audio_data.append(audio_chunk)\n", 238 | "\n", 239 | " def stop_recording():\n", 240 | " \"\"\"Waits for user input to stop the recording.\"\"\"\n", 241 | " input() # Wait for Enter key press\n", 242 | " nonlocal recording\n", 243 | " recording = False\n", 244 | "\n", 245 | " # Start recording in a separate thread\n", 246 | " recording_thread = threading.Thread(target=record_audio)\n", 247 | " recording_thread.start()\n", 248 | "\n", 249 | " # Start a thread to listen for the Enter key\n", 250 | " stop_thread = threading.Thread(target=stop_recording)\n", 251 | " stop_thread.start()\n", 252 | "\n", 253 | " # Wait for both threads to complete\n", 254 | " stop_thread.join()\n", 255 | " recording_thread.join()\n", 256 | "\n", 257 | " # Stack all audio chunks into a single NumPy array and write to file\n", 258 | " audio_data = np.concatenate(audio_data, axis=0)\n", 259 | " \n", 260 | " # Convert to WAV format in-memory\n", 261 | " audio_bytes = io.BytesIO()\n", 262 | " write(audio_bytes, sample_rate, audio_data) # Use scipy's write function to save to BytesIO\n", 263 | " audio_bytes.seek(0) # Go to the start of the BytesIO buffer\n", 264 | " audio_bytes.name = \"audio.wav\" # Set a filename for the in-memory file\n", 265 | "\n", 266 | " # Transcribe via Whisper\n", 267 | " transcription = openai_client.audio.transcriptions.create(\n", 268 | " model=\"whisper-1\", \n", 269 | " file=audio_bytes,\n", 270 | " )\n", 271 | "\n", 272 | " # Print the transcription\n", 273 | " print(\"Here is the transcription:\", transcription.text)\n", 274 | "\n", 275 | " # Write to messages \n", 276 | " return {\"messages\": [HumanMessage(content=transcription.text)]}\n", 277 | "\n", 278 | "def play_audio(state: MessagesState):\n", 279 | " \n", 280 | " \"\"\"Plays the audio response from the remote graph with ElevenLabs.\"\"\"\n", 281 | "\n", 282 | " # Response from the agent \n", 283 | " response = state['messages'][-1]\n", 284 | "\n", 285 | " # Prepare text by replacing ** with empty strings\n", 286 | " # These can cause unexpected behavior in ElevenLabs\n", 287 | " cleaned_text = response.content.replace(\"**\", \"\")\n", 288 | " \n", 289 | " # Call text_to_speech API with turbo model for low latency\n", 290 | " response = elevenlabs_client.text_to_speech.convert(\n", 291 | " voice_id=\"pNInz6obpgDQGcFmaJgB\", # Adam pre-made voice\n", 292 | " output_format=\"mp3_22050_32\",\n", 293 | " text=cleaned_text,\n", 294 | " model_id=\"eleven_turbo_v2_5\", \n", 295 | " voice_settings=VoiceSettings(\n", 296 | " stability=0.0,\n", 297 | " similarity_boost=1.0,\n", 298 | " style=0.0,\n", 299 | " use_speaker_boost=True,\n", 300 | " ),\n", 301 | " )\n", 302 | " \n", 303 | " # Play the audio back\n", 304 | " play(response)\n", 305 | "\n", 306 | "# Define parent graph\n", 307 | "builder = StateGraph(MessagesState)\n", 308 | "\n", 309 | "# Add remote graph directly as a node\n", 310 | "builder.add_node(\"audio_input\", record_audio_until_stop)\n", 311 | "builder.add_node(\"todo_app\", remote_graph)\n", 312 | "builder.add_node(\"audio_output\", play_audio)\n", 313 | "builder.add_edge(START, \"audio_input\")\n", 314 | "builder.add_edge(\"audio_input\", \"todo_app\")\n", 315 | "builder.add_edge(\"todo_app\",\"audio_output\")\n", 316 | "builder.add_edge(\"audio_output\",END)\n", 317 | "graph = builder.compile()\n", 318 | "\n", 319 | "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" 320 | ] 321 | }, 322 | { 323 | "cell_type": "markdown", 324 | "id": "d5b16223", 325 | "metadata": {}, 326 | "source": [ 327 | "Optionally, you can supply a thread ID to ensure that conversation history is persisted. " 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": 6, 333 | "id": "1b7277b5", 334 | "metadata": {}, 335 | "outputs": [], 336 | "source": [ 337 | "import uuid\n", 338 | "thread_id = str(uuid.uuid4())" 339 | ] 340 | }, 341 | { 342 | "cell_type": "markdown", 343 | "id": "297defe6", 344 | "metadata": {}, 345 | "source": [ 346 | "Simply, run this cell and speak into your microphone. When you are finished, press Enter." 347 | ] 348 | }, 349 | { 350 | "cell_type": "code", 351 | "execution_count": 9, 352 | "id": "a18659da-1c49-48c1-8838-554f85b7876d", 353 | "metadata": {}, 354 | "outputs": [ 355 | { 356 | "name": "stdout", 357 | "output_type": "stream", 358 | "text": [ 359 | "================================\u001b[1m Human Message \u001b[0m=================================\n", 360 | "\n", 361 | "Follow the user's instructions:\n", 362 | "Recording your instruction! ... Press Enter to stop recording.\n" 363 | ] 364 | }, 365 | { 366 | "name": "stdin", 367 | "output_type": "stream", 368 | "text": [ 369 | " \n" 370 | ] 371 | }, 372 | { 373 | "name": "stdout", 374 | "output_type": "stream", 375 | "text": [ 376 | "Here is the transcription: Add a to-do to walk the dog tomorrow night and make sure I get it done by end of day tomorrow.\n", 377 | "================================\u001b[1m Human Message \u001b[0m=================================\n", 378 | "\n", 379 | "Add a to-do to walk the dog tomorrow night and make sure I get it done by end of day tomorrow.\n", 380 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n", 381 | "\n", 382 | "Awesome, Lance! I've added \"Walk the dog\" to your to-do list for tomorrow night, and it's set to be completed by the end of the day. 🐶✨ You're going to rock this! Remember to set a reminder on your phone, prepare the leash and dog bags in advance, and maybe plan a fun route in your neighborhood. You've got this! 🌟😊\n" 383 | ] 384 | } 385 | ], 386 | "source": [ 387 | "# Set user ID for storing memories\n", 388 | "config = {\"configurable\": {\"user_id\": \"Test-Audio-UX\", \"thread_id\": thread_id}}\n", 389 | "\n", 390 | "# Kick off the graph, which will record user input until the user presses Enter\n", 391 | "for chunk in graph.stream({\"messages\":HumanMessage(content=\"Follow the user's instructions:\")}, stream_mode=\"values\", config=config):\n", 392 | " chunk[\"messages\"][-1].pretty_print()" 393 | ] 394 | }, 395 | { 396 | "cell_type": "code", 397 | "execution_count": null, 398 | "id": "2e5532fe", 399 | "metadata": {}, 400 | "outputs": [], 401 | "source": [] 402 | } 403 | ], 404 | "metadata": { 405 | "kernelspec": { 406 | "display_name": "Python 3 (ipykernel)", 407 | "language": "python", 408 | "name": "python3" 409 | }, 410 | "language_info": { 411 | "codemirror_mode": { 412 | "name": "ipython", 413 | "version": 3 414 | }, 415 | "file_extension": ".py", 416 | "mimetype": "text/x-python", 417 | "name": "python", 418 | "nbconvert_exporter": "python", 419 | "pygments_lexer": "ipython3", 420 | "version": "3.11.6" 421 | } 422 | }, 423 | "nbformat": 4, 424 | "nbformat_minor": 5 425 | } 426 | --------------------------------------------------------------------------------