From 85180da2f42fec93ac9252567feb3f02f0a86c44 Mon Sep 17 00:00:00 2001 From: Nick Bobrowski <39348559+bonk1t@users.noreply.github.com> Date: Sat, 30 Nov 2024 15:41:43 +0000 Subject: [PATCH] Set up Ruff, Apply Ruff --- .github/workflows/docs.yml | 6 +- .github/workflows/publish.yml | 2 +- .gitignore | 2 +- .pre-commit-config.yaml | 17 + CONTRIBUTING.md | 6 +- README.md | 52 +- agency_swarm/__init__.py | 5 +- agency_swarm/agency/agency.py | 559 ++++++++++++------ .../genesis/AgentCreator/AgentCreator.py | 6 +- .../agency/genesis/AgentCreator/__init__.py | 2 +- .../genesis/AgentCreator/instructions.md | 6 +- .../AgentCreator/tools/CreateAgentTemplate.py | 47 +- .../genesis/AgentCreator/tools/ImportAgent.py | 25 +- .../AgentCreator/tools/ReadManifesto.py | 10 +- .../AgentCreator/tools/util/__init__.py | 2 +- .../AgentCreator/tools/util/get_modules.py | 24 +- agency_swarm/agency/genesis/GenesisAgency.py | 26 +- .../agency/genesis/GenesisCEO/GenesisCEO.py | 5 +- .../agency/genesis/GenesisCEO/__init__.py | 2 +- .../agency/genesis/GenesisCEO/instructions.md | 4 +- .../GenesisCEO/tools/CreateAgencyFolder.py | 37 +- .../GenesisCEO/tools/FinalizeAgency.py | 25 +- .../GenesisCEO/tools/ReadRequirements.py | 8 +- .../genesis/OpenAPICreator/OpenAPICreator.py | 5 +- .../agency/genesis/OpenAPICreator/__init__.py | 2 +- .../genesis/OpenAPICreator/instructions.md | 2 +- .../tools/CreateToolsFromOpenAPISpec.py | 35 +- .../agency/genesis/ToolCreator/ToolCreator.py | 3 +- .../agency/genesis/ToolCreator/__init__.py | 2 +- .../genesis/ToolCreator/instructions.md | 1 - .../genesis/ToolCreator/tools/CreateTool.py | 82 ++- .../genesis/ToolCreator/tools/TestTool.py | 66 ++- agency_swarm/agency/genesis/__init__.py | 2 +- agency_swarm/agency/genesis/manifesto.md | 1 - agency_swarm/agency/genesis/util.py | 29 +- .../agents/BrowsingAgent/BrowsingAgent.py | 108 ++-- agency_swarm/agents/BrowsingAgent/__init__.py | 2 +- .../agents/BrowsingAgent/requirements.txt | 2 +- .../BrowsingAgent/tools/ClickElement.py | 16 +- .../agents/BrowsingAgent/tools/ExportFile.py | 26 +- .../agents/BrowsingAgent/tools/ReadURL.py | 26 +- .../agents/BrowsingAgent/tools/Scroll.py | 19 +- .../BrowsingAgent/tools/SelectDropdown.py | 19 +- .../agents/BrowsingAgent/tools/SendKeys.py | 23 +- .../BrowsingAgent/tools/SolveCaptcha.py | 102 ++-- .../BrowsingAgent/tools/WebPageSummarizer.py | 15 +- .../agents/BrowsingAgent/tools/__init__.py | 10 +- .../BrowsingAgent/tools/util/__init__.py | 2 +- .../tools/util/get_b64_screenshot.py | 3 +- .../BrowsingAgent/tools/util/highlights.py | 50 +- .../BrowsingAgent/tools/util/selenium.py | 31 +- agency_swarm/agents/Devid/Devid.py | 24 +- agency_swarm/agents/Devid/__init__.py | 2 +- agency_swarm/agents/Devid/instructions.md | 6 +- agency_swarm/agents/Devid/tools/ChangeFile.py | 65 +- .../agents/Devid/tools/CheckCurrentDir.py | 1 + .../agents/Devid/tools/CommandExecutor.py | 22 +- .../agents/Devid/tools/DirectoryNavigator.py | 31 +- agency_swarm/agents/Devid/tools/FileMover.py | 15 +- agency_swarm/agents/Devid/tools/FileReader.py | 17 +- agency_swarm/agents/Devid/tools/FileWriter.py | 130 ++-- agency_swarm/agents/Devid/tools/ListDir.py | 56 +- .../agents/Devid/tools/util/__init__.py | 2 +- .../Devid/tools/util/format_file_deps.py | 44 +- agency_swarm/agents/__init__.py | 2 +- agency_swarm/agents/agent.py | 452 +++++++++----- agency_swarm/cli.py | 73 ++- agency_swarm/messages/__init__.py | 2 +- agency_swarm/messages/message_output.py | 56 +- agency_swarm/threads/thread.py | 471 ++++++++++----- agency_swarm/threads/thread_async.py | 79 ++- agency_swarm/tools/BaseTool.py | 16 +- agency_swarm/tools/ToolFactory.py | 163 ++--- agency_swarm/tools/__init__.py | 2 +- agency_swarm/tools/oai/FileSearch.py | 6 +- agency_swarm/tools/oai/Retrieval.py | 2 +- .../tools/send_message/SendMessage.py | 40 +- .../send_message/SendMessageAsyncThreading.py | 6 +- .../tools/send_message/SendMessageBase.py | 40 +- .../tools/send_message/SendMessageQuick.py | 15 +- .../tools/send_message/SendMessageSwarm.py | 33 +- agency_swarm/tools/send_message/__init__.py | 4 +- agency_swarm/user/__init__.py | 2 +- agency_swarm/util/__init__.py | 6 +- .../util/cli/create_agent_template.py | 38 +- agency_swarm/util/cli/import_agent.py | 6 +- agency_swarm/util/errors.py | 2 +- agency_swarm/util/files.py | 58 +- agency_swarm/util/helpers/__init__.py | 2 +- .../get_available_agent_descriptions.py | 13 +- .../util/helpers/list_available_agents.py | 9 +- agency_swarm/util/oai.py | 22 +- agency_swarm/util/openapi.py | 10 +- agency_swarm/util/schema.py | 32 +- agency_swarm/util/shared_state.py | 2 +- agency_swarm/util/validators.py | 27 +- docs/advanced-usage/agencies.md | 20 +- docs/advanced-usage/agents.md | 22 +- docs/advanced-usage/azure-openai.md | 2 +- docs/advanced-usage/open-source-models.md | 22 +- docs/advanced-usage/tools.md | 60 +- docs/api.md | 4 +- docs/contributing.md | 4 +- docs/deployment.md | 7 +- docs/examples.md | 4 +- docs/index.md | 6 +- docs/quick_start.md | 44 +- notebooks/agency_async.ipynb | 182 +++--- notebooks/azure.ipynb | 96 +-- notebooks/genesis_agency.ipynb | 17 +- .../os_models_with_astra_assistants_api.ipynb | 86 +-- notebooks/web_browser_agent.ipynb | 53 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_docs.txt | 2 +- requirements_test.txt | 2 +- run_tests.py | 8 +- setup.py | 34 +- tests/data/files/generated_data.json | 2 +- tests/data/files/test-txt.txt | 2 +- tests/data/tools/ExampleTool1.py | 9 +- tests/demos/demo_gradio.py | 55 +- tests/demos/streaming_demo.py | 33 +- tests/demos/term_demo.py | 28 +- tests/test_agency.py | 363 ++++++++---- tests/test_communication.py | 65 +- tests/test_tool_factory.py | 96 +-- 127 files changed, 3102 insertions(+), 1898 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a6d4a441..bc31bbe7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,7 @@ name: docs on: push: branches: - - master + - master - main permissions: contents: write @@ -23,7 +23,7 @@ jobs: with: python-version: "3.10" - - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - uses: actions/cache@v3 with: key: mkdocs-material-${{ env.cache_id }} @@ -31,4 +31,4 @@ jobs: restore-keys: | mkdocs-material- - run: pip install -r requirements_docs.txt - - run: mkdocs gh-deploy --force \ No newline at end of file + - run: mkdocs gh-deploy --force diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4e2d7715..06c43bad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,4 +19,4 @@ jobs: - name: Publish package uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index 822fb847..c1d54a5d 100644 --- a/.gitignore +++ b/.gitignore @@ -162,4 +162,4 @@ cython_debug/ settings.json .DS_Store tests/test_agents/ -test_agents/ \ No newline at end of file +test_agents/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..f7b8c49c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: debug-statements + language_version: python3 + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.7.4 + hooks: + - id: ruff + args: [--fix, --select=I] + - id: ruff-format diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cad9646b..2226e51c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ # Contributing to Agency Swarm -Each agent or tool you add to Agency Swarm will automatically be available for import by the Genesis Swarm, which will help us create an exponentially larger and smarter system. +Each agent or tool you add to Agency Swarm will automatically be available for import by the Genesis Swarm, which will help us create an exponentially larger and smarter system. This document provides guidelines for contributing new agents to the framework. @@ -23,7 +23,7 @@ agency_swarm/agents/AgentName/ ### Creating an Agent -1. Follow the structure below in your `AgentName.py` as a guideline. +1. Follow the structure below in your `AgentName.py` as a guideline. 2. All tools (except schemas) should be imported in `AgentName.py` from the `agency_swarm/tools/...` folder. ```python @@ -42,4 +42,4 @@ class AgentName(Agent): --- -Thank you for contributing to Agency Swarm! Your efforts help us build a more robust and versatile framework. \ No newline at end of file +Thank you for contributing to Agency Swarm! Your efforts help us build a more robust and versatile framework. diff --git a/README.md b/README.md index a0d0157a..5cd32c73 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Agency Swarm started as a desire and effort of Arsenii Shatokhin (aka VRSEN) to - **Customizable Agent Roles**: Define roles like CEO, virtual assistant, developer, etc., and customize their functionalities with [Assistants API](https://platform.openai.com/docs/assistants/overview). - **Full Control Over Prompts**: Avoid conflicts and restrictions of pre-defined prompts, allowing full customization. -- **Tool Creation**: Tools within Agency Swarm are created using [Instructor](https://github.com/jxnl/instructor), which provides a convenient interface and automatic type validation. +- **Tool Creation**: Tools within Agency Swarm are created using [Instructor](https://github.com/jxnl/instructor), which provides a convenient interface and automatic type validation. - **Efficient Communication**: Agents communicate through a specially designed "send message" tool based on their own descriptions. - **State Management**: Agency Swarm efficiently manages the state of your assistants on OpenAI, maintaining it in a special `settings.json` file. - **Deployable in Production**: Agency Swarm is designed to be reliable and easily deployable in production environments. @@ -45,37 +45,37 @@ Define your custom tools with [Instructor](https://github.com/jxnl/instructor): ```python from agency_swarm.tools import BaseTool from pydantic import Field - + class MyCustomTool(BaseTool): """ - A brief description of what the custom tool does. + A brief description of what the custom tool does. The docstring should clearly explain the tool's purpose and functionality. """ - + # Define the fields with descriptions using Pydantic Field example_field: str = Field( ..., description="Description of the example field, explaining its purpose and usage." ) - + # Additional fields as required # ... - + def run(self): """ The implementation of the run method, where the tool's main functionality is executed. This method should utilize the fields defined above to perform its task. Doc string description is not required for this method. """ - + # Your custom tool logic goes here do_something(self.example_field) - + # Return the result of the tool's operation return "Result of MyCustomTool operation" ``` - + or convert from OpenAPI schemas: - + ```python from agency_swarm.tools import ToolFactory # using local file @@ -83,7 +83,7 @@ Define your custom tools with [Instructor](https://github.com/jxnl/instructor): tools = ToolFactory.from_openapi_schema( f.read(), ) - + # using requests tools = ToolFactory.from_openapi_schema( requests.get("https://api.example.com/openapi.json").json(), @@ -94,13 +94,13 @@ Define your custom tools with [Instructor](https://github.com/jxnl/instructor): ```python from agency_swarm import Agent - + ceo = Agent(name="CEO", description="Responsible for client communication, task planning and management.", instructions="You must converse with other agents to ensure complete task execution.", # can be a file like ./instructions.md files_folder="./files", # files to be uploaded to OpenAI schemas_folder="./schemas", # OpenAPI schemas to be converted into tools - tools=[MyCustomTool], + tools=[MyCustomTool], temperature=0.5, # temperature for the agent max_prompt_tokens=25000, # max tokens in conversation history ) @@ -111,12 +111,12 @@ Define your custom tools with [Instructor](https://github.com/jxnl/instructor): ```bash agency-swarm import-agent --name "Devid" --destination "./" ``` - + This will import Devid (Software Developer) Agent locally, including all source code files, so you have full control over your system. Currently, available agents are: `Devid`, `BrowsingAgent`. -4. **Define Agency Communication Flows**: +4. **Define Agency Communication Flows**: Establish how your agents will communicate with each other. ```python @@ -124,16 +124,16 @@ Establish how your agents will communicate with each other. # if importing from local files from Developer import Developer from VirtualAssistant import VirtualAssistant - + dev = Developer() va = VirtualAssistant() - + agency = Agency([ ceo, # CEO will be the entry point for communication with the user [ceo, dev], # CEO can initiate communication with Developer [ceo, va], # CEO can initiate communication with Virtual Assistant [dev, va] # Developer can initiate communication with Virtual Assistant - ], + ], shared_instructions='agency_manifesto.md', #shared instructions for all agents temperature=0.5, # default temperature for all agents max_prompt_tokens=25000 # default max tokens in conversation history @@ -142,23 +142,23 @@ Establish how your agents will communicate with each other. In Agency Swarm, communication flows are directional, meaning they are established from left to right in the agency_chart definition. For instance, in the example above, the CEO can initiate a chat with the developer (dev), and the developer can respond in this chat. However, the developer cannot initiate a chat with the CEO. The developer can initiate a chat with the virtual assistant (va) and assign new tasks. -5. **Run Demo**: +5. **Run Demo**: Run the demo to see your agents in action! - + Web interface: ```python agency.demo_gradio(height=900) ``` - + Terminal version: - + ```python agency.run_demo() ``` - + Backend version: - + ```python completion_output = agency.get_completion("Please create a new website for our client.") ``` @@ -213,12 +213,12 @@ When you run the `create-agent-template` command, it creates the following folde └── AgentName/ # Directory for the specific agent ├── files/ # Directory for files that will be uploaded to openai ├── schemas/ # Directory for OpenAPI schemas to be converted into tools - ├── tools/ # Directory for tools to be imported by default. + ├── tools/ # Directory for tools to be imported by default. ├── AgentName.py # The main agent class file ├── __init__.py # Initializes the agent folder as a Python package ├── instructions.md or .txt # Instruction document for the agent └── tools.py # Custom tools specific to the agent - + ``` This structure ensures that each agent has its dedicated space with all necessary files to start working on its specific tasks. The `tools.py` can be customized to include tools and functionalities specific to the agent's role. diff --git a/agency_swarm/__init__.py b/agency_swarm/__init__.py index 79be039b..dbb7f5cf 100644 --- a/agency_swarm/__init__.py +++ b/agency_swarm/__init__.py @@ -1,8 +1,5 @@ from .agency import Agency from .agents import Agent from .tools import BaseTool -from .util import set_openai_key -from .util import set_openai_client -from .util import get_openai_client +from .util import get_openai_client, llm_validator, set_openai_client, set_openai_key from .util.streaming import AgencyEventHandler -from .util import llm_validator diff --git a/agency_swarm/agency/agency.py b/agency_swarm/agency/agency.py index 69c38880..d1e45270 100644 --- a/agency_swarm/agency/agency.py +++ b/agency_swarm/agency/agency.py @@ -5,7 +5,18 @@ import threading import uuid from enum import Enum -from typing import Any, Callable, Dict, List, Literal, Optional, Type, TypeVar, TypedDict, Union +from typing import ( + Any, + Callable, + Dict, + List, + Literal, + Type, + TypedDict, + TypeVar, + Union, +) + from openai.lib._parsing._completions import type_to_response_format_param from openai.types.beta.threads import Message from openai.types.beta.threads.runs import RunStep @@ -28,13 +39,14 @@ from agency_swarm.tools.send_message import SendMessage, SendMessageBase from agency_swarm.user import User from agency_swarm.util.errors import RefusalError -from agency_swarm.util.files import get_tools, get_file_purpose +from agency_swarm.util.files import get_file_purpose, get_tools from agency_swarm.util.shared_state import SharedState from agency_swarm.util.streaming import AgencyEventHandler console = Console() -T = TypeVar('T', bound=BaseModel) +T = TypeVar("T", bound=BaseModel) + class SettingsCallbacks(TypedDict): load: Callable[[], List[Dict]] @@ -47,21 +59,22 @@ class ThreadsCallbacks(TypedDict): class Agency: - def __init__(self, - agency_chart: List, - shared_instructions: str = "", - shared_files: Union[str, List[str]] = None, - async_mode: Literal['threading', "tools_threading"] = None, - send_message_tool_class: Type[SendMessageBase] = SendMessage, - settings_path: str = "./settings.json", - settings_callbacks: SettingsCallbacks = None, - threads_callbacks: ThreadsCallbacks = None, - temperature: float = 0.3, - top_p: float = 1.0, - max_prompt_tokens: int = None, - max_completion_tokens: int = None, - truncation_strategy: dict = None, - ): + def __init__( + self, + agency_chart: List, + shared_instructions: str = "", + shared_files: Union[str, List[str]] = None, + async_mode: Literal["threading", "tools_threading"] = None, + send_message_tool_class: Type[SendMessageBase] = SendMessage, + settings_path: str = "./settings.json", + settings_callbacks: SettingsCallbacks = None, + threads_callbacks: ThreadsCallbacks = None, + temperature: float = 0.3, + top_p: float = 1.0, + max_prompt_tokens: int = None, + max_completion_tokens: int = None, + truncation_strategy: dict = None, + ): """ Initializes the Agency object, setting up agents, threads, and core functionalities. @@ -102,25 +115,39 @@ def __init__(self, self.truncation_strategy = truncation_strategy # set thread type based send_message_tool_class async mode - if hasattr(send_message_tool_class.ToolConfig, "async_mode") and send_message_tool_class.ToolConfig.async_mode: + if ( + hasattr(send_message_tool_class.ToolConfig, "async_mode") + and send_message_tool_class.ToolConfig.async_mode + ): self._thread_type = ThreadAsync else: - self._thread_type = Thread + self._thread_type = Thread if self.async_mode == "threading": from agency_swarm.tools.send_message import SendMessageAsyncThreading - print("Warning: 'threading' mode is deprecated. Please use send_message_tool_class = SendMessageAsyncThreading to use async communication.") + + print( + "Warning: 'threading' mode is deprecated. Please use send_message_tool_class = SendMessageAsyncThreading to use async communication." + ) self.send_message_tool_class = SendMessageAsyncThreading elif self.async_mode == "tools_threading": Thread.async_mode = "tools_threading" - print("Warning: 'tools_threading' mode is deprecated. Use tool.ToolConfig.async_mode = 'threading' instead.") + print( + "Warning: 'tools_threading' mode is deprecated. Use tool.ToolConfig.async_mode = 'threading' instead." + ) elif self.async_mode is None: pass else: - raise Exception("Please select async_mode = 'threading' or 'tools_threading'.") + raise Exception( + "Please select async_mode = 'threading' or 'tools_threading'." + ) - if os.path.isfile(os.path.join(self._get_class_folder_path(), shared_instructions)): - self._read_instructions(os.path.join(self._get_class_folder_path(), shared_instructions)) + if os.path.isfile( + os.path.join(self._get_class_folder_path(), shared_instructions) + ): + self._read_instructions( + os.path.join(self._get_class_folder_path(), shared_instructions) + ) elif os.path.isfile(shared_instructions): self._read_instructions(shared_instructions) else: @@ -133,15 +160,18 @@ def __init__(self, self._create_special_tools() self._init_agents() - def get_completion(self, message: str, - message_files: List[str] = None, - yield_messages: bool = False, - recipient_agent: Agent = None, - additional_instructions: str = None, - attachments: List[dict] = None, - tool_choice: dict = None, - verbose: bool = False, - response_format: dict = None): + def get_completion( + self, + message: str, + message_files: List[str] = None, + yield_messages: bool = False, + recipient_agent: Agent = None, + additional_instructions: str = None, + attachments: List[dict] = None, + tool_choice: dict = None, + verbose: bool = False, + response_format: dict = None, + ): """ Retrieves the completion for a given message from the main thread. @@ -162,16 +192,18 @@ def get_completion(self, message: str, """ if verbose and yield_messages: raise Exception("Verbose mode is not compatible with yield_messages=True") - - res = self.main_thread.get_completion(message=message, - message_files=message_files, - attachments=attachments, - recipient_agent=recipient_agent, - additional_instructions=additional_instructions, - tool_choice=tool_choice, - yield_messages=yield_messages or verbose, - response_format=response_format) - + + res = self.main_thread.get_completion( + message=message, + message_files=message_files, + attachments=attachments, + recipient_agent=recipient_agent, + additional_instructions=additional_instructions, + tool_choice=tool_choice, + yield_messages=yield_messages or verbose, + response_format=response_format, + ) + if not yield_messages or verbose: while True: try: @@ -183,16 +215,17 @@ def get_completion(self, message: str, return res - - def get_completion_stream(self, - message: str, - event_handler: type(AgencyEventHandler), - message_files: List[str] = None, - recipient_agent: Agent = None, - additional_instructions: str = None, - attachments: List[dict] = None, - tool_choice: dict = None, - response_format: dict = None): + def get_completion_stream( + self, + message: str, + event_handler: type(AgencyEventHandler), + message_files: List[str] = None, + recipient_agent: Agent = None, + additional_instructions: str = None, + attachments: List[dict] = None, + tool_choice: dict = None, + response_format: dict = None, + ): """ Generates a stream of completions for a given message from the main thread. @@ -212,14 +245,16 @@ def get_completion_stream(self, if not inspect.isclass(event_handler): raise Exception("Event handler must not be an instance.") - res = self.main_thread.get_completion_stream(message=message, - message_files=message_files, - event_handler=event_handler, - attachments=attachments, - recipient_agent=recipient_agent, - additional_instructions=additional_instructions, - tool_choice=tool_choice, - response_format=response_format) + res = self.main_thread.get_completion_stream( + message=message, + message_files=message_files, + event_handler=event_handler, + attachments=attachments, + recipient_agent=recipient_agent, + additional_instructions=additional_instructions, + tool_choice=tool_choice, + response_format=response_format, + ) while True: try: @@ -228,28 +263,31 @@ def get_completion_stream(self, event_handler.on_all_streams_end() return e.value - - def get_completion_parse(self, message: str, - response_format: Type[T], - message_files: List[str] = None, - recipient_agent: Agent = None, - additional_instructions: str = None, - attachments: List[dict] = None, - tool_choice: dict = None, - verbose: bool = False) -> T: + + def get_completion_parse( + self, + message: str, + response_format: Type[T], + message_files: List[str] = None, + recipient_agent: Agent = None, + additional_instructions: str = None, + attachments: List[dict] = None, + tool_choice: dict = None, + verbose: bool = False, + ) -> T: """ Retrieves the completion for a given message from the main thread and parses the response using the provided pydantic model. Parameters: message (str): The message for which completion is to be retrieved. - response_format (type(BaseModel)): The response format to use for the completion. + response_format (type(BaseModel)): The response format to use for the completion. message_files (list, optional): A list of file ids to be sent as attachments with the message. When using this parameter, files will be assigned both to file_search and code_interpreter tools if available. It is recommended to assign files to the most sutiable tool manually, using the attachments parameter. Defaults to None. recipient_agent (Agent, optional): The agent to which the message should be sent. Defaults to the first agent in the agency chart. additional_instructions (str, optional): Additional instructions to be sent with the message. Defaults to None. attachments (List[dict], optional): A list of attachments to be sent with the message, following openai format. Defaults to None. tool_choice (dict, optional): The tool choice for the recipient agent to use. Defaults to None. verbose (bool, optional): Whether to print the intermediary messages in console. Defaults to False. - + Returns: Final response: The final response from the main thread, parsed using the provided pydantic model. """ @@ -258,21 +296,23 @@ def get_completion_parse(self, message: str, response_model = response_format response_format = type_to_response_format_param(response_format) - res = self.get_completion(message=message, - message_files=message_files, - recipient_agent=recipient_agent, - additional_instructions=additional_instructions, - attachments=attachments, - tool_choice=tool_choice, - response_format=response_format, - verbose=verbose) - + res = self.get_completion( + message=message, + message_files=message_files, + recipient_agent=recipient_agent, + additional_instructions=additional_instructions, + attachments=attachments, + tool_choice=tool_choice, + response_format=response_format, + verbose=verbose, + ) + try: return response_model.model_validate_json(res) except: parsed_res = json.loads(res) - if 'refusal' in parsed_res: - raise RefusalError(parsed_res['refusal']) + if "refusal" in parsed_res: + raise RefusalError(parsed_res["refusal"]) else: raise Exception("Failed to parse response: " + res) @@ -316,8 +356,11 @@ def demo_gradio(self, height=450, dark_mode=True, **kwargs): chatbot = gr.Chatbot(height=height) with gr.Row(): with gr.Column(scale=9): - dropdown = gr.Dropdown(label="Recipient Agent", choices=recipient_agent_names, - value=recipient_agent.name) + dropdown = gr.Dropdown( + label="Recipient Agent", + choices=recipient_agent_names, + value=recipient_agent.name, + ) msg = gr.Textbox(label="Your Message", lines=4) with gr.Column(scale=1): file_upload = gr.Files(label="OpenAI Files", type="filepath") @@ -340,23 +383,26 @@ def handle_file_upload(file_list): for file_obj in file_list: purpose = get_file_purpose(file_obj.name) - with open(file_obj.name, 'rb') as f: + with open(file_obj.name, "rb") as f: # Upload the file to OpenAI file = self.main_thread.client.files.create( - file=f, - purpose=purpose + file=f, purpose=purpose ) if purpose == "vision": - images.append({ - "type": "image_file", - "image_file": {"file_id": file.id} - }) + images.append( + { + "type": "image_file", + "image_file": {"file_id": file.id}, + } + ) else: - attachments.append({ - "file_id": file.id, - "tools": get_tools(file.filename) - }) + attachments.append( + { + "file_id": file.id, + "tools": get_tools(file.filename), + } + ) message_file_names.append(file.filename) print(f"Uploaded file ID: {file.id}") @@ -373,7 +419,7 @@ def handle_file_upload(file_list): def user(user_message, history): if not user_message.strip(): return user_message, history - + nonlocal message_file_names nonlocal uploading_files nonlocal images @@ -385,17 +431,33 @@ def check_and_add_tools_in_attachments(attachments, recipient_agent): for attachment in attachments: for tool in attachment.get("tools", []): if tool["type"] == "file_search": - if not any(isinstance(t, FileSearch) for t in recipient_agent.tools): + if not any( + isinstance(t, FileSearch) + for t in recipient_agent.tools + ): # Add FileSearch tool if it does not exist recipient_agent.tools.append(FileSearch) - recipient_agent.client.beta.assistants.update(recipient_agent.id, tools=recipient_agent.get_oai_tools()) - print("Added FileSearch tool to recipient agent to analyze the file.") + recipient_agent.client.beta.assistants.update( + recipient_agent.id, + tools=recipient_agent.get_oai_tools(), + ) + print( + "Added FileSearch tool to recipient agent to analyze the file." + ) elif tool["type"] == "code_interpreter": - if not any(isinstance(t, CodeInterpreter) for t in recipient_agent.tools): + if not any( + isinstance(t, CodeInterpreter) + for t in recipient_agent.tools + ): # Add CodeInterpreter tool if it does not exist recipient_agent.tools.append(CodeInterpreter) - recipient_agent.client.beta.assistants.update(recipient_agent.id, tools=recipient_agent.get_oai_tools()) - print("Added CodeInterpreter tool to recipient agent to analyze the file.") + recipient_agent.client.beta.assistants.update( + recipient_agent.id, + tools=recipient_agent.get_oai_tools(), + ) + print( + "Added CodeInterpreter tool to recipient agent to analyze the file." + ) return None check_and_add_tools_in_attachments(attachments, recipient_agent) @@ -407,7 +469,9 @@ def check_and_add_tools_in_attachments(attachments, recipient_agent): # Append the user message with a placeholder for bot response if recipient_agent: - user_message = f"👤 User 🗣️ @{recipient_agent.name}:\n" + user_message.strip() + user_message = ( + f"👤 User 🗣️ @{recipient_agent.name}:\n" + user_message.strip() + ) else: user_message = f"👤 User:" + user_message.strip() @@ -432,7 +496,9 @@ def on_message_created(self, message: Message) -> None: full_content = "" for content in message.content: if content.type == "image_file": - full_content += f"🖼️ Image File: {content.image_file.file_id}\n" + full_content += ( + f"🖼️ Image File: {content.image_file.file_id}\n" + ) continue if content.type == "image_url": @@ -442,13 +508,17 @@ def on_message_created(self, message: Message) -> None: if content.type == "text": full_content += content.text.value + "\n" - - self.message_output = MessageOutput("text", self.agent_name, self.recipient_agent_name, - full_content) + self.message_output = MessageOutput( + "text", + self.agent_name, + self.recipient_agent_name, + full_content, + ) else: - self.message_output = MessageOutput("text", self.recipient_agent_name, self.agent_name, - "") + self.message_output = MessageOutput( + "text", self.recipient_agent_name, self.agent_name, "" + ) chatbot_queue.put("[new_message]") chatbot_queue.put(self.message_output.get_formatted_content()) @@ -462,29 +532,40 @@ def on_tool_call_created(self, tool_call: ToolCall): if isinstance(tool_call, dict): if "type" not in tool_call: tool_call["type"] = "function" - + if tool_call["type"] == "function": tool_call = FunctionToolCall(**tool_call) elif tool_call["type"] == "code_interpreter": tool_call = CodeInterpreterToolCall(**tool_call) - elif tool_call["type"] == "file_search" or tool_call["type"] == "retrieval": + elif ( + tool_call["type"] == "file_search" + or tool_call["type"] == "retrieval" + ): tool_call = FileSearchToolCall(**tool_call) else: - raise ValueError("Invalid tool call type: " + tool_call["type"]) + raise ValueError( + "Invalid tool call type: " + tool_call["type"] + ) # TODO: add support for code interpreter and retrieval tools if tool_call.type == "function": chatbot_queue.put("[new_message]") - self.message_output = MessageOutput("function", self.recipient_agent_name, self.agent_name, - str(tool_call.function)) - chatbot_queue.put(self.message_output.get_formatted_header() + "\n") + self.message_output = MessageOutput( + "function", + self.recipient_agent_name, + self.agent_name, + str(tool_call.function), + ) + chatbot_queue.put( + self.message_output.get_formatted_header() + "\n" + ) @override def on_tool_call_done(self, snapshot: ToolCall): if isinstance(snapshot, dict): if "type" not in snapshot: snapshot["type"] = "function" - + if snapshot["type"] == "function": snapshot = FunctionToolCall(**snapshot) elif snapshot["type"] == "code_interpreter": @@ -492,8 +573,10 @@ def on_tool_call_done(self, snapshot: ToolCall): elif snapshot["type"] == "file_search": snapshot = FileSearchToolCall(**snapshot) else: - raise ValueError("Invalid tool call type: " + snapshot["type"]) - + raise ValueError( + "Invalid tool call type: " + snapshot["type"] + ) + self.message_output = None # TODO: add support for code interpreter and retrieval tools @@ -506,11 +589,17 @@ def on_tool_call_done(self, snapshot: ToolCall): try: args = eval(snapshot.function.arguments) recipient = args["recipient"] - self.message_output = MessageOutput("text", self.recipient_agent_name, recipient, - args["message"]) + self.message_output = MessageOutput( + "text", + self.recipient_agent_name, + recipient, + args["message"], + ) chatbot_queue.put("[new_message]") - chatbot_queue.put(self.message_output.get_formatted_content()) + chatbot_queue.put( + self.message_output.get_formatted_content() + ) except Exception as e: pass @@ -529,11 +618,16 @@ def on_run_step_done(self, run_step: RunStep) -> None: self.message_output = None chatbot_queue.put("[new_message]") - self.message_output = MessageOutput("function_output", tool_call.function.name, - self.recipient_agent_name, - tool_call.function.output) + self.message_output = MessageOutput( + "function_output", + tool_call.function.name, + self.recipient_agent_name, + tool_call.function.output, + ) - chatbot_queue.put(self.message_output.get_formatted_header() + "\n") + chatbot_queue.put( + self.message_output.get_formatted_header() + "\n" + ) chatbot_queue.put(tool_call.function.output) @override @@ -551,28 +645,58 @@ def bot(original_message, history): nonlocal uploading_files if not original_message: - return "", history, gr.update(value=recipient_agent.name, choices=set([*recipient_agent_names, recipient_agent.name])) + return ( + "", + history, + gr.update( + value=recipient_agent.name, + choices=set([*recipient_agent_names, recipient_agent.name]), + ), + ) if uploading_files: history.append([None, "Uploading files... Please wait."]) - yield "", history, gr.update(value=recipient_agent.name, choices=set([*recipient_agent_names, recipient_agent.name])) - return "", history, gr.update(value=recipient_agent.name, choices=set([*recipient_agent_names, recipient_agent.name])) + yield ( + "", + history, + gr.update( + value=recipient_agent.name, + choices=set([*recipient_agent_names, recipient_agent.name]), + ), + ) + return ( + "", + history, + gr.update( + value=recipient_agent.name, + choices=set([*recipient_agent_names, recipient_agent.name]), + ), + ) print("Message files: ", attachments) print("Images: ", images) - + if images and len(images) > 0: original_message = [ { "type": "text", "text": original_message, }, - *images + *images, ] - - completion_thread = threading.Thread(target=self.get_completion_stream, args=( - original_message, GradioEventHandler, [], recipient_agent, "", attachments, None)) + completion_thread = threading.Thread( + target=self.get_completion_stream, + args=( + original_message, + GradioEventHandler, + [], + recipient_agent, + "", + attachments, + None, + ), + ) completion_thread.start() attachments = [] @@ -596,7 +720,16 @@ def bot(original_message, history): if bot_message == "[change_recipient_agent]": new_agent_name = chatbot_queue.get(block=True) recipient_agent = self._get_agent_by_name(new_agent_name) - yield "", history, gr.update(value=new_agent_name, choices=set([*recipient_agent_names, recipient_agent.name])) + yield ( + "", + history, + gr.update( + value=new_agent_name, + choices=set( + [*recipient_agent_names, recipient_agent.name] + ), + ), + ) continue if new_message: @@ -605,15 +738,20 @@ def bot(original_message, history): else: history[-1][1] += bot_message - yield "", history, gr.update(value=recipient_agent.name, choices=set([*recipient_agent_names, recipient_agent.name])) + yield ( + "", + history, + gr.update( + value=recipient_agent.name, + choices=set( + [*recipient_agent_names, recipient_agent.name] + ), + ), + ) except queue.Empty: break - button.click( - user, - inputs=[msg, chatbot], - outputs=[msg, chatbot] - ).then( + button.click(user, inputs=[msg, chatbot], outputs=[msg, chatbot]).then( bot, [msg, chatbot, dropdown], [msg, chatbot, dropdown] ) dropdown.change(handle_dropdown_change, dropdown) @@ -633,7 +771,11 @@ def _recipient_agent_completer(self, text, state): """ Autocomplete completer for recipient agent names. """ - options = [agent for agent in self.recipient_agents if agent.lower().startswith(text.lower())] + options = [ + agent + for agent in self.recipient_agents + if agent.lower().startswith(text.lower()) + ] if state < len(options): return options[state] else: @@ -651,7 +793,8 @@ def _setup_autocomplete(self): import pyreadline as readline except ImportError: print( - "Module 'readline' not found. Autocomplete will not work. If you are using Windows, try installing 'pyreadline3'.") + "Module 'readline' not found. Autocomplete will not work. If you are using Windows, try installing 'pyreadline3'." + ) return if not readline: @@ -659,9 +802,11 @@ def _setup_autocomplete(self): try: readline.set_completer(self._recipient_agent_completer) - readline.parse_and_bind('tab: complete') + readline.parse_and_bind("tab: complete") except Exception as e: - print(f"Error setting up autocomplete for agents in terminal: {e}. Autocomplete will not work.") + print( + f"Error setting up autocomplete for agents in terminal: {e}. Autocomplete will not work." + ) def run_demo(self): """ @@ -669,17 +814,21 @@ def run_demo(self): """ outer_self = self from agency_swarm import AgencyEventHandler + class TermEventHandler(AgencyEventHandler): message_output = None @override def on_message_created(self, message: Message) -> None: if message.role == "user": - self.message_output = MessageOutputLive("text", self.agent_name, self.recipient_agent_name, - "") + self.message_output = MessageOutputLive( + "text", self.agent_name, self.recipient_agent_name, "" + ) self.message_output.cprint_update(message.content[0].text.value) else: - self.message_output = MessageOutputLive("text", self.recipient_agent_name, self.agent_name, "") + self.message_output = MessageOutputLive( + "text", self.recipient_agent_name, self.agent_name, "" + ) @override def on_message_done(self, message: Message) -> None: @@ -694,12 +843,15 @@ def on_tool_call_created(self, tool_call): if isinstance(tool_call, dict): if "type" not in tool_call: tool_call["type"] = "function" - + if tool_call["type"] == "function": tool_call = FunctionToolCall(**tool_call) elif tool_call["type"] == "code_interpreter": tool_call = CodeInterpreterToolCall(**tool_call) - elif tool_call["type"] == "file_search" or tool_call["type"] == "retrieval": + elif ( + tool_call["type"] == "file_search" + or tool_call["type"] == "retrieval" + ): tool_call = FileSearchToolCall(**tool_call) else: raise ValueError("Invalid tool call type: " + tool_call["type"]) @@ -707,15 +859,19 @@ def on_tool_call_created(self, tool_call): # TODO: add support for code interpreter and retirieval tools if tool_call.type == "function": - self.message_output = MessageOutputLive("function", self.recipient_agent_name, self.agent_name, - str(tool_call.function)) + self.message_output = MessageOutputLive( + "function", + self.recipient_agent_name, + self.agent_name, + str(tool_call.function), + ) @override def on_tool_call_delta(self, delta, snapshot): if isinstance(snapshot, dict): if "type" not in snapshot: snapshot["type"] = "function" - + if snapshot["type"] == "function": snapshot = FunctionToolCall(**snapshot) elif snapshot["type"] == "code_interpreter": @@ -724,7 +880,7 @@ def on_tool_call_delta(self, delta, snapshot): snapshot = FileSearchToolCall(**snapshot) else: raise ValueError("Invalid tool call type: " + snapshot["type"]) - + self.message_output.cprint_update(str(snapshot.function)) @override @@ -735,12 +891,19 @@ def on_tool_call_done(self, snapshot): if snapshot.type != "function": return - if snapshot.function.name == "SendMessage" and not (hasattr(outer_self.send_message_tool_class.ToolConfig, 'output_as_result') and outer_self.send_message_tool_class.ToolConfig.output_as_result): + if snapshot.function.name == "SendMessage" and not ( + hasattr( + outer_self.send_message_tool_class.ToolConfig, + "output_as_result", + ) + and outer_self.send_message_tool_class.ToolConfig.output_as_result + ): try: args = eval(snapshot.function.arguments) recipient = args["recipient"] - self.message_output = MessageOutputLive("text", self.recipient_agent_name, recipient, - "") + self.message_output = MessageOutputLive( + "text", self.recipient_agent_name, recipient, "" + ) self.message_output.cprint_update(args["message"]) except Exception as e: @@ -759,8 +922,12 @@ def on_run_step_done(self, run_step: RunStep) -> None: continue self.message_output = None - self.message_output = MessageOutputLive("function_output", tool_call.function.name, - self.recipient_agent_name, tool_call.function.output) + self.message_output = MessageOutputLive( + "function_output", + tool_call.function.name, + self.recipient_agent_name, + tool_call.function.output, + ) self.message_output.cprint_update(tool_call.function.output) self.message_output = None @@ -788,14 +955,21 @@ def on_end(self): recipient_agent = text.split("@")[1].split(" ")[0] text = text.replace(f"@{recipient_agent}", "").strip() try: - recipient_agent = \ - [agent for agent in self.recipient_agents if agent.lower() == recipient_agent.lower()][0] + recipient_agent = [ + agent + for agent in self.recipient_agents + if agent.lower() == recipient_agent.lower() + ][0] recipient_agent = self._get_agent_by_name(recipient_agent) except Exception as e: print(f"Recipient agent {recipient_agent} not found.") continue - self.get_completion_stream(message=text, event_handler=TermEventHandler, recipient_agent=recipient_agent) + self.get_completion_stream( + message=text, + event_handler=TermEventHandler, + recipient_agent=recipient_agent, + ) def get_customgpt_schema(self, url: str): """Returns the OpenAPI schema for the agency from the CEO agent, that you can use to integrate with custom gpts. @@ -821,7 +995,7 @@ def _init_agents(self): """ if self.settings_callbacks: loaded_settings = self.settings_callbacks["load"]() - with open(self.settings_path, 'w') as f: + with open(self.settings_path, "w") as f: json.dump(loaded_settings, f, indent=4) for agent in self.agents: @@ -847,18 +1021,24 @@ def _init_agents(self): agent.top_p = self.top_p if self.max_prompt_tokens is not None and agent.max_prompt_tokens is None: agent.max_prompt_tokens = self.max_prompt_tokens - if self.max_completion_tokens is not None and agent.max_completion_tokens is None: + if ( + self.max_completion_tokens is not None + and agent.max_completion_tokens is None + ): agent.max_completion_tokens = self.max_completion_tokens - if self.truncation_strategy is not None and agent.truncation_strategy is None: + if ( + self.truncation_strategy is not None + and agent.truncation_strategy is None + ): agent.truncation_strategy = self.truncation_strategy - + if not agent.shared_state: agent.shared_state = self.shared_state agent.init_oai() if self.settings_callbacks: - with open(self.agents[0].get_settings_path(), 'r') as f: + with open(self.agents[0].get_settings_path(), "r") as f: settings = f.read() settings = json.loads(settings) self.settings_callbacks["save"](settings) @@ -896,12 +1076,17 @@ def _init_threads(self): # create thread class self.agents_and_threads[agent_name][other_agent] = self._thread_type( self._get_agent_by_name(items["agent"]), - self._get_agent_by_name( - items["recipient_agent"])) + self._get_agent_by_name(items["recipient_agent"]), + ) # load thread id if available - if agent_name in loaded_thread_ids and other_agent in loaded_thread_ids[agent_name]: - self.agents_and_threads[agent_name][other_agent].id = loaded_thread_ids[agent_name][other_agent] + if ( + agent_name in loaded_thread_ids + and other_agent in loaded_thread_ids[agent_name] + ): + self.agents_and_threads[agent_name][ + other_agent + ].id = loaded_thread_ids[agent_name][other_agent] # init threads if threre are threads callbacks so the ids are saved for later use elif self.threads_callbacks: self.agents_and_threads[agent_name][other_agent].init_thread() @@ -964,7 +1149,10 @@ def _parse_agency_chart(self, agency_chart): other_agent = node[i + 1] if other_agent.name == agent.name: continue - if other_agent.name not in self.agents_and_threads[agent.name].keys(): + if ( + other_agent.name + not in self.agents_and_threads[agent.name].keys() + ): self.agents_and_threads[agent.name][other_agent.name] = { "agent": agent.name, "recipient_agent": other_agent.name, @@ -1019,7 +1207,7 @@ def _read_instructions(self, path): This method opens the file located at the given path, reads its contents, and stores these contents in the 'shared_instructions' attribute of the agency. This is used to provide common guidelines or instructions to all agents within the agency. """ path = path - with open(path, 'r') as f: + with open(path, "r") as f: self.shared_instructions = f.read() def _create_special_tools(self): @@ -1069,11 +1257,13 @@ def _create_send_message_tool(self, agent: Agent, recipient_agents: List[Agent]) class SendMessage(self.send_message_tool_class): recipient: recipients = Field(..., description=agent_descriptions) - @field_validator('recipient') + @field_validator("recipient") @classmethod def check_recipient(cls, value): if value.value not in recipient_names: - raise ValueError(f"Recipient {value} is not valid. Valid recipients are: {recipient_names}") + raise ValueError( + f"Recipient {value} is not valid. Valid recipients are: {recipient_names}" + ) return value SendMessage._caller_agent = agent @@ -1092,17 +1282,24 @@ def _create_get_response_tool(self, agent: Agent, recipient_agents: List[Agent]) class GetResponse(BaseTool): """This tool allows you to check the status of a task or get a response from a specified recipient agent, if the task has been completed. You must always use 'SendMessage' tool with the designated agent first.""" - recipient: recipients = Field(..., - description=f"Recipient agent that you want to check the status of. Valid recipients are: {recipient_names}") - @field_validator('recipient') + recipient: recipients = Field( + ..., + description=f"Recipient agent that you want to check the status of. Valid recipients are: {recipient_names}", + ) + + @field_validator("recipient") def check_recipient(cls, value): if value.value not in recipient_names: - raise ValueError(f"Recipient {value} is not valid. Valid recipients are: {recipient_names}") + raise ValueError( + f"Recipient {value} is not valid. Valid recipients are: {recipient_names}" + ) return value def run(self): - thread = outer_self.agents_and_threads[self._caller_agent.name][self.recipient.value] + thread = outer_self.agents_and_threads[self._caller_agent.name][ + self.recipient.value + ] return thread.check_status() diff --git a/agency_swarm/agency/genesis/AgentCreator/AgentCreator.py b/agency_swarm/agency/genesis/AgentCreator/AgentCreator.py index 35385d1f..06dfd796 100644 --- a/agency_swarm/agency/genesis/AgentCreator/AgentCreator.py +++ b/agency_swarm/agency/genesis/AgentCreator/AgentCreator.py @@ -1,8 +1,10 @@ from agency_swarm import Agent -from .tools.ImportAgent import ImportAgent + from .tools.CreateAgentTemplate import CreateAgentTemplate +from .tools.ImportAgent import ImportAgent from .tools.ReadManifesto import ReadManifesto + class AgentCreator(Agent): def __init__(self): super().__init__( @@ -10,4 +12,4 @@ def __init__(self): instructions="./instructions.md", tools=[ImportAgent, CreateAgentTemplate, ReadManifesto], temperature=0.3, - ) \ No newline at end of file + ) diff --git a/agency_swarm/agency/genesis/AgentCreator/__init__.py b/agency_swarm/agency/genesis/AgentCreator/__init__.py index 12044390..f7004d03 100644 --- a/agency_swarm/agency/genesis/AgentCreator/__init__.py +++ b/agency_swarm/agency/genesis/AgentCreator/__init__.py @@ -1 +1 @@ -from .AgentCreator import AgentCreator \ No newline at end of file +from .AgentCreator import AgentCreator diff --git a/agency_swarm/agency/genesis/AgentCreator/instructions.md b/agency_swarm/agency/genesis/AgentCreator/instructions.md index 7b056906..bdf134ca 100644 --- a/agency_swarm/agency/genesis/AgentCreator/instructions.md +++ b/agency_swarm/agency/genesis/AgentCreator/instructions.md @@ -1,13 +1,13 @@ # AgentCreator Agent Instructions -You are an agent that creates other agents as instructed by the user. +You are an agent that creates other agents as instructed by the user. The user will communicate to you each agent that needs to be created. Below are your instructions that needs to be followed for each agent communicated by the user. **Primary Instructions:** 1. First, read the manifesto using `ReadManifesto` tool if you have not already done so. This file contains the agency manifesto that describes the agency's purpose and goals. 2. If a similar agent to the requested one is accessible through the `ImportAgent` tool, import this agent and inform the user that the agent has been created. Skip the following steps. -3. If not, create a new agent using `CreateAgentTemplate` tool. +3. If not, create a new agent using `CreateAgentTemplate` tool. 4. Tell the `ToolCreator` agent to create tools or APIs for this agent. Make sure to also communicate the agent description, name and a summary of the processes that it needs to perform. CEO Agents do not need to utilize any tools, so you can skip this and the following steps. 5. If there are no issues and tools have been successfully created, notify the user that the agent has been created. Otherwise, try to resolve any issues with the tool creator before reporting back to the user. -6. Repeat this process for each agent that needs to be created, as instructed by the user. \ No newline at end of file +6. Repeat this process for each agent that needs to be created, as instructed by the user. diff --git a/agency_swarm/agency/genesis/AgentCreator/tools/CreateAgentTemplate.py b/agency_swarm/agency/genesis/AgentCreator/tools/CreateAgentTemplate.py index 28dc7ad1..ac7a2c68 100644 --- a/agency_swarm/agency/genesis/AgentCreator/tools/CreateAgentTemplate.py +++ b/agency_swarm/agency/genesis/AgentCreator/tools/CreateAgentTemplate.py @@ -12,7 +12,7 @@ web_developer_example_instructions = """# Web Developer Agent Instructions -You are an agent that builds responsive web applications using Next.js and Material-UI (MUI). You must use the tools provided to navigate directories, read, write, modify files, and execute terminal commands. +You are an agent that builds responsive web applications using Next.js and Material-UI (MUI). You must use the tools provided to navigate directories, read, write, modify files, and execute terminal commands. ### Primary Instructions: 1. Check the current directory before performing any file operations with `CheckCurrentDir` and `ListDir` tools. @@ -26,34 +26,41 @@ class CreateAgentTemplate(BaseTool): """ This tool creates a template folder for a new agent. Always use this tool first, before creating tools or APIs for the agent. """ + agent_name: str = Field( - ..., description="Name of the agent to be created. Cannot include special characters or spaces." + ..., + description="Name of the agent to be created. Cannot include special characters or spaces.", ) agent_description: str = Field( ..., description="Description of the agent to be created." ) instructions: str = Field( - ..., description="Instructions for the agent to be created in markdown format. " - "Instructions should include a decription of the role and a specific step by step process " - "that this agent need to perform in order to execute the tasks. " - "The process must also be aligned with all the other agents in the agency. Agents should be " - "able to collaborate with each other to achieve the common goal of the agency.", + ..., + description="Instructions for the agent to be created in markdown format. " + "Instructions should include a decription of the role and a specific step by step process " + "that this agent need to perform in order to execute the tasks. " + "The process must also be aligned with all the other agents in the agency. Agents should be " + "able to collaborate with each other to achieve the common goal of the agency.", examples=[ web_developer_example_instructions, - ] + ], ) default_tools: List[str] = Field( - [], description=f"List of default tools to be included in the agent. Possible values are {allowed_tools}." - f"CodeInterpreter allows the agent to execute python code in a remote python environment.", + [], + description=f"List of default tools to be included in the agent. Possible values are {allowed_tools}." + f"CodeInterpreter allows the agent to execute python code in a remote python environment.", example=["CodeInterpreter"], ) agency_name: str = Field( - None, description="Name of the agency to create the tool for. Defaults to the agency currently being created." + None, + description="Name of the agency to create the tool for. Defaults to the agency currently being created.", ) def run(self): if not self._shared_state.get("manifesto_read"): - raise ValueError("Please read the manifesto first with the ReadManifesto tool.") + raise ValueError( + "Please read the manifesto first with the ReadManifesto tool." + ) self._shared_state.set("agent_name", self.agent_name) @@ -63,11 +70,13 @@ def run(self): if os.path.exists(self.agent_name): shutil.rmtree(self.agent_name) - create_agent_template(self.agent_name, - self.agent_description, - instructions=self.instructions, - code_interpreter=True if "CodeInterpreter" in self.default_tools else None, - include_example_tool=False) + create_agent_template( + self.agent_name, + self.agent_description, + instructions=self.instructions, + code_interpreter=True if "CodeInterpreter" in self.default_tools else None, + include_example_tool=False, + ) # # create or append to init file path = self._shared_state.get("agency_path") @@ -100,4 +109,6 @@ def validate_tools(self): for tool in self.default_tools: if tool not in allowed_tools: - raise ValueError(f"Tool {tool} is not allowed. Allowed tools are: {allowed_tools}") + raise ValueError( + f"Tool {tool} is not allowed. Allowed tools are: {allowed_tools}" + ) diff --git a/agency_swarm/agency/genesis/AgentCreator/tools/ImportAgent.py b/agency_swarm/agency/genesis/AgentCreator/tools/ImportAgent.py index e61b1fac..f68b31e4 100644 --- a/agency_swarm/agency/genesis/AgentCreator/tools/ImportAgent.py +++ b/agency_swarm/agency/genesis/AgentCreator/tools/ImportAgent.py @@ -4,17 +4,22 @@ from agency_swarm import BaseTool from agency_swarm.util.cli import import_agent -from agency_swarm.util.helpers import get_available_agent_descriptions, list_available_agents +from agency_swarm.util.helpers import ( + get_available_agent_descriptions, + list_available_agents, +) class ImportAgent(BaseTool): """ This tool imports an existing agent from agency swarm framework. Please make sure to first use the GetAvailableAgents tool to get the list of available agents. """ - agent_name: str = Field(..., - description=get_available_agent_descriptions()) + + agent_name: str = Field(..., description=get_available_agent_descriptions()) agency_path: str = Field( - None, description="Path to the agency where the agent will be imported. Default is the current agency.") + None, + description="Path to the agency where the agent will be imported. Default is the current agency.", + ) def run(self): if not self._shared_state.get("default_folder"): @@ -40,18 +45,22 @@ def run(self): os.chdir(self._shared_state.get("default_folder")) - return (f"Success. {self.agent_name} has been imported. " - f"You can now tell the user to user proceed with next agents.") + return ( + f"Success. {self.agent_name} has been imported. " + f"You can now tell the user to user proceed with next agents." + ) - @field_validator("agent_name", mode='after') + @field_validator("agent_name", mode="after") @classmethod def agent_name_exists(cls, v): available_agents = list_available_agents() if v not in available_agents: raise ValueError( - f"Agent with name {v} does not exist. Available agents are: {available_agents}") + f"Agent with name {v} does not exist. Available agents are: {available_agents}" + ) return v + if __name__ == "__main__": tool = ImportAgent(agent_name="Devid") tool._shared_state.set("agency_path", "./") diff --git a/agency_swarm/agency/genesis/AgentCreator/tools/ReadManifesto.py b/agency_swarm/agency/genesis/AgentCreator/tools/ReadManifesto.py index 8fe0036f..a61c9b33 100644 --- a/agency_swarm/agency/genesis/AgentCreator/tools/ReadManifesto.py +++ b/agency_swarm/agency/genesis/AgentCreator/tools/ReadManifesto.py @@ -9,16 +9,20 @@ class ReadManifesto(BaseTool): """ This tool reads a manifesto for the agency being created from a markdown file. """ + agency_name: str = Field( - None, description="Name of the agency to create the tool for. Defaults to the agency currently being created." + None, + description="Name of the agency to create the tool for. Defaults to the agency currently being created.", ) def run(self): if not self._shared_state.get("default_folder"): - self._shared_state.set('default_folder', os.getcwd()) + self._shared_state.set("default_folder", os.getcwd()) if not self._shared_state.get("agency_path") and not self.agency_name: - raise ValueError("Please specify the agency name. Ask user for clarification if needed.") + raise ValueError( + "Please specify the agency name. Ask user for clarification if needed." + ) if self.agency_name: os.chdir("./" + self.agency_name) diff --git a/agency_swarm/agency/genesis/AgentCreator/tools/util/__init__.py b/agency_swarm/agency/genesis/AgentCreator/tools/util/__init__.py index 0de9658f..367096c4 100644 --- a/agency_swarm/agency/genesis/AgentCreator/tools/util/__init__.py +++ b/agency_swarm/agency/genesis/AgentCreator/tools/util/__init__.py @@ -1 +1 @@ -from .get_modules import get_modules \ No newline at end of file +from .get_modules import get_modules diff --git a/agency_swarm/agency/genesis/AgentCreator/tools/util/get_modules.py b/agency_swarm/agency/genesis/AgentCreator/tools/util/get_modules.py index 92e211be..0b515c10 100644 --- a/agency_swarm/agency/genesis/AgentCreator/tools/util/get_modules.py +++ b/agency_swarm/agency/genesis/AgentCreator/tools/util/get_modules.py @@ -17,13 +17,13 @@ def get_modules(module_name): try: # Using importlib.resources to access the package contents - with importlib.resources.path(module_name, '') as package_path: + with importlib.resources.path(module_name, "") as package_path: # Walk through the package directory using pathlib - for path in pathlib.Path(package_path).rglob('*.py'): - if path.name != '__init__.py': + for path in pathlib.Path(package_path).rglob("*.py"): + if path.name != "__init__.py": # Construct the module name from the file path relative_path = path.relative_to(package_path) - module_path = '.'.join(relative_path.with_suffix('').parts) + module_path = ".".join(relative_path.with_suffix("").parts) submodule_names.append(f"{module_name}.{module_path}") @@ -31,12 +31,16 @@ def get_modules(module_name): print(f"Module {module_name} not found.") return submodule_names - submodule_names = [name for name in submodule_names if not name.endswith(".agent") and - '.genesis' not in name and - 'util' not in name and - 'oai' not in name and - 'ToolFactory' not in name and - 'BaseTool' not in name] + submodule_names = [ + name + for name in submodule_names + if not name.endswith(".agent") + and ".genesis" not in name + and "util" not in name + and "oai" not in name + and "ToolFactory" not in name + and "BaseTool" not in name + ] # remove repetition at the end of the path like 'agency_swarm.agents.coding.CodingAgent.CodingAgent' for i in range(len(submodule_names)): diff --git a/agency_swarm/agency/genesis/GenesisAgency.py b/agency_swarm/agency/genesis/GenesisAgency.py index e3b0014f..c9e8c0f0 100644 --- a/agency_swarm/agency/genesis/GenesisAgency.py +++ b/agency_swarm/agency/genesis/GenesisAgency.py @@ -1,42 +1,46 @@ from agency_swarm import Agency -from .AgentCreator import AgentCreator +from agency_swarm.util.helpers import get_available_agent_descriptions +from .AgentCreator import AgentCreator from .GenesisCEO import GenesisCEO from .OpenAPICreator import OpenAPICreator from .ToolCreator import ToolCreator -from agency_swarm.util.helpers import get_available_agent_descriptions + class GenesisAgency(Agency): def __init__(self, with_browsing=True, **kwargs): if "max_prompt_tokens" not in kwargs: kwargs["max_prompt_tokens"] = 25000 - if 'agency_chart' not in kwargs: + if "agency_chart" not in kwargs: agent_creator = AgentCreator() genesis_ceo = GenesisCEO() tool_creator = ToolCreator() openapi_creator = OpenAPICreator() - kwargs['agency_chart'] = [ - genesis_ceo, tool_creator, agent_creator, + kwargs["agency_chart"] = [ + genesis_ceo, + tool_creator, + agent_creator, [genesis_ceo, agent_creator], [agent_creator, tool_creator], ] if with_browsing: from agency_swarm.agents.BrowsingAgent import BrowsingAgent + browsing_agent = BrowsingAgent() - browsing_agent.instructions += ("""\n + browsing_agent.instructions += """\n # BrowsingAgent's Primary instructions 1. Browse the web to find the API documentation requested by the user. Prefer searching google directly for this API documentation page. 2. Navigate to the API documentation page and ensure that it contains the necessary API endpoints descriptions. You can use the AnalyzeContent tool to check if the page contains the necessary API descriptions. If not, try perform another search in google and keep browsing until you find the right page. 3. If you have confirmed that the page contains the necessary API documentation, export the page with ExportFile tool. Then, send the file_id back to the user along with a brief description of the API. 4. Repeat these steps for each new agent, as requested by the user. - """) - kwargs['agency_chart'].append(openapi_creator) - kwargs['agency_chart'].append([openapi_creator, browsing_agent]) + """ + kwargs["agency_chart"].append(openapi_creator) + kwargs["agency_chart"].append([openapi_creator, browsing_agent]) - if 'shared_instructions' not in kwargs: - kwargs['shared_instructions'] = "./manifesto.md" + if "shared_instructions" not in kwargs: + kwargs["shared_instructions"] = "./manifesto.md" super().__init__(**kwargs) diff --git a/agency_swarm/agency/genesis/GenesisCEO/GenesisCEO.py b/agency_swarm/agency/genesis/GenesisCEO/GenesisCEO.py index c01ae104..e4c5888f 100644 --- a/agency_swarm/agency/genesis/GenesisCEO/GenesisCEO.py +++ b/agency_swarm/agency/genesis/GenesisCEO/GenesisCEO.py @@ -1,6 +1,7 @@ from pathlib import Path from agency_swarm import Agent + from .tools.CreateAgencyFolder import CreateAgencyFolder from .tools.FinalizeAgency import FinalizeAgency from .tools.ReadRequirements import ReadRequirements @@ -10,10 +11,8 @@ class GenesisCEO(Agent): def __init__(self): super().__init__( description="Acts as the overseer and communicator across the agency, ensuring alignment with the " - "agency's goals.", + "agency's goals.", instructions="./instructions.md", tools=[CreateAgencyFolder, FinalizeAgency, ReadRequirements], temperature=0.4, ) - - diff --git a/agency_swarm/agency/genesis/GenesisCEO/__init__.py b/agency_swarm/agency/genesis/GenesisCEO/__init__.py index 397a326d..f57cef35 100644 --- a/agency_swarm/agency/genesis/GenesisCEO/__init__.py +++ b/agency_swarm/agency/genesis/GenesisCEO/__init__.py @@ -1 +1 @@ -from .GenesisCEO import GenesisCEO \ No newline at end of file +from .GenesisCEO import GenesisCEO diff --git a/agency_swarm/agency/genesis/GenesisCEO/instructions.md b/agency_swarm/agency/genesis/GenesisCEO/instructions.md index d4384fd9..70368a27 100644 --- a/agency_swarm/agency/genesis/GenesisCEO/instructions.md +++ b/agency_swarm/agency/genesis/GenesisCEO/instructions.md @@ -11,7 +11,7 @@ As a Genesis CEO Agent within the Agency Swarm framework, your mission is to hel ### Example of communication flows -Here is an example of how communication flows are defined in agency swarm. Essentially, agents that are inside a double array can initiate communication with each other. Agents that are in the top level array can communicate with the user. +Here is an example of how communication flows are defined in agency swarm. Essentially, agents that are inside a double array can initiate communication with each other. Agents that are in the top level array can communicate with the user. ```python agency = Agency([ @@ -21,4 +21,4 @@ agency = Agency([ [dev, va] # Developer can initiate communication with Virtual Assistant ], shared_instructions='agency_manifesto.md') # shared instructions for all agents ``` -Keep in mind that this is just an example and you should replace it with the actual agents you are creating. Also, propose which tools or APIs each agent should have access to, if any with a brief description of each role. Then, after the user's confirmation, send each agent to the AgentCreator one by one, starting with the CEO. \ No newline at end of file +Keep in mind that this is just an example and you should replace it with the actual agents you are creating. Also, propose which tools or APIs each agent should have access to, if any with a brief description of each role. Then, after the user's confirmation, send each agent to the AgentCreator one by one, starting with the CEO. diff --git a/agency_swarm/agency/genesis/GenesisCEO/tools/CreateAgencyFolder.py b/agency_swarm/agency/genesis/GenesisCEO/tools/CreateAgencyFolder.py index 4df3d5e6..c8675bb2 100644 --- a/agency_swarm/agency/genesis/GenesisCEO/tools/CreateAgencyFolder.py +++ b/agency_swarm/agency/genesis/GenesisCEO/tools/CreateAgencyFolder.py @@ -1,3 +1,4 @@ +import os import shutil from pathlib import Path @@ -6,37 +7,41 @@ import agency_swarm.agency.genesis.GenesisAgency from agency_swarm import BaseTool -import os - class CreateAgencyFolder(BaseTool): """ This tool creates or modifies an agency folder. You can use it again with the same agency_name to modify a previously created agency, if the user wants to change the agency chart or the manifesto. """ + agency_name: str = Field( - ..., description="Name of the agency to be created. Must not contain spaces or special characters.", - examples=["AgencyName", "MyAgency", "ExampleAgency"] + ..., + description="Name of the agency to be created. Must not contain spaces or special characters.", + examples=["AgencyName", "MyAgency", "ExampleAgency"], ) agency_chart: str = Field( - ..., description="Agency chart to be passed into the Agency class.", - examples=["[ceo, [ceo, dev], [ceo, va], [dev, va]]"] + ..., + description="Agency chart to be passed into the Agency class.", + examples=["[ceo, [ceo, dev], [ceo, va], [dev, va]]"], ) manifesto: str = Field( - ..., description="Manifesto for the agency, describing its goals and additional context shared by all agents " - "in markdown format. It must include information about the working environment, the mission " - "and the goals of the agency. Do not add descriptions of the agents themselves or the agency structure.", + ..., + description="Manifesto for the agency, describing its goals and additional context shared by all agents " + "in markdown format. It must include information about the working environment, the mission " + "and the goals of the agency. Do not add descriptions of the agents themselves or the agency structure.", ) def run(self): if not self._shared_state.get("default_folder"): - self._shared_state.set('default_folder', Path.cwd()) + self._shared_state.set("default_folder", Path.cwd()) if self._shared_state.get("agency_name") is None: os.mkdir(self.agency_name) os.chdir("./" + self.agency_name) self._shared_state.set("agency_name", self.agency_name) self._shared_state.set("agency_path", Path("./").resolve()) - elif self._shared_state.get("agency_name") == self.agency_name and os.path.exists(self._shared_state.get("agency_path")): + elif self._shared_state.get( + "agency_name" + ) == self.agency_name and os.path.exists(self._shared_state.get("agency_path")): os.chdir(self._shared_state.get("agency_path")) for file in os.listdir(): if file != "__init__.py" and os.path.isfile(file): @@ -47,7 +52,9 @@ def run(self): # check that agency chart is valid if not self.agency_chart.startswith("[") or not self.agency_chart.endswith("]"): - raise ValueError("Agency chart must be a list of lists, except for the first agents.") + raise ValueError( + "Agency chart must be a list of lists, except for the first agents." + ) # add new lines after every comma, except for those inside second brackets # must transform from "[ceo, [ceo, dev], [ceo, va], [dev, va] ]" @@ -67,7 +74,7 @@ def run(self): with open(path, "w") as f: f.write(self.manifesto) - os.chdir(self._shared_state.get('default_folder')) + os.chdir(self._shared_state.get("default_folder")) return f"Agency folder has been created. You can now tell AgentCreator to create agents for {self.agency_name}.\n" @@ -80,7 +87,7 @@ def run(self): max_prompt_tokens=25000, # default tokens in conversation for all agents temperature=0.3, # default temperature for all agents ) - + if __name__ == '__main__': agency.demo_gradio() -""" \ No newline at end of file +""" diff --git a/agency_swarm/agency/genesis/GenesisCEO/tools/FinalizeAgency.py b/agency_swarm/agency/genesis/GenesisCEO/tools/FinalizeAgency.py index a7b4d362..324e3c2c 100644 --- a/agency_swarm/agency/genesis/GenesisCEO/tools/FinalizeAgency.py +++ b/agency_swarm/agency/genesis/GenesisCEO/tools/FinalizeAgency.py @@ -1,7 +1,7 @@ import os from typing import List -from pydantic import Field, model_validator, field_validator +from pydantic import Field, field_validator, model_validator from agency_swarm import BaseTool, get_openai_client from agency_swarm.util import create_agent_template @@ -11,8 +11,10 @@ class FinalizeAgency(BaseTool): """ This tool finalizes the agency structure and it's imports. Please make sure to use at only at the very end, after all agents have been created. """ + agency_path: str = Field( - None, description="Path to the agency folder. Defaults to the agency currently being created." + None, + description="Path to the agency folder. Defaults to the agency currently being created.", ) def run(self): @@ -33,8 +35,9 @@ def run(self): res = client.chat.completions.create( model="gpt-3.5-turbo", - messages=examples + [ - {'role': "user", 'content': agency_py}, + messages=examples + + [ + {"role": "user", "content": agency_py}, ], temperature=0.0, ) @@ -51,10 +54,12 @@ def run(self): @model_validator(mode="after") def validate_agency_path(self): if not self._shared_state.get("agency_path") and not self.agency_path: - raise ValueError("Agency path not found. Please specify the agency_path. Ask user for clarification if needed.") + raise ValueError( + "Agency path not found. Please specify the agency_path. Ask user for clarification if needed." + ) -SYSTEM_PROMPT = """"Please read the file provided by the user and fix all the imports and indentation accordingly. +SYSTEM_PROMPT = """"Please read the file provided by the user and fix all the imports and indentation accordingly. Only output the full valid python code and nothing else.""" @@ -88,12 +93,12 @@ def validate_agency_path(self): [ceo, news_curator], [market_analyst, news_curator]], shared_instructions='./agency_manifesto.md') - + if __name__ == '__main__': agency.demo_gradio()""" examples = [ - {'role': "system", 'content': SYSTEM_PROMPT}, - {'role': "user", 'content': example_input}, - {'role': "assistant", 'content': example_output} + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": example_input}, + {"role": "assistant", "content": example_output}, ] diff --git a/agency_swarm/agency/genesis/GenesisCEO/tools/ReadRequirements.py b/agency_swarm/agency/genesis/GenesisCEO/tools/ReadRequirements.py index 4a97f1ff..afbd4a2f 100644 --- a/agency_swarm/agency/genesis/GenesisCEO/tools/ReadRequirements.py +++ b/agency_swarm/agency/genesis/GenesisCEO/tools/ReadRequirements.py @@ -1,7 +1,9 @@ -from agency_swarm.tools import BaseTool -from pydantic import Field import os +from pydantic import Field + +from agency_swarm.tools import BaseTool + class ReadRequirements(BaseTool): """ @@ -21,7 +23,7 @@ def run(self): raise ValueError(f"File path does not exist: {self.file_path}") try: - with open(self.file_path, 'r', encoding='utf-8') as file: + with open(self.file_path, "r", encoding="utf-8") as file: content = file.read() return content except Exception as e: diff --git a/agency_swarm/agency/genesis/OpenAPICreator/OpenAPICreator.py b/agency_swarm/agency/genesis/OpenAPICreator/OpenAPICreator.py index 96f106fd..cefa2e05 100644 --- a/agency_swarm/agency/genesis/OpenAPICreator/OpenAPICreator.py +++ b/agency_swarm/agency/genesis/OpenAPICreator/OpenAPICreator.py @@ -1,4 +1,5 @@ from agency_swarm import Agent + from .tools.CreateToolsFromOpenAPISpec import CreateToolsFromOpenAPISpec @@ -7,5 +8,5 @@ def __init__(self): super().__init__( description="This agent is responsible for creating new tools from an OpenAPI specifications.", instructions="./instructions.md", - tools=[CreateToolsFromOpenAPISpec] - ) \ No newline at end of file + tools=[CreateToolsFromOpenAPISpec], + ) diff --git a/agency_swarm/agency/genesis/OpenAPICreator/__init__.py b/agency_swarm/agency/genesis/OpenAPICreator/__init__.py index 88a31f94..5b4bb2b6 100644 --- a/agency_swarm/agency/genesis/OpenAPICreator/__init__.py +++ b/agency_swarm/agency/genesis/OpenAPICreator/__init__.py @@ -1 +1 @@ -from .OpenAPICreator import OpenAPICreator \ No newline at end of file +from .OpenAPICreator import OpenAPICreator diff --git a/agency_swarm/agency/genesis/OpenAPICreator/instructions.md b/agency_swarm/agency/genesis/OpenAPICreator/instructions.md index 6aaa009e..a7250ea1 100644 --- a/agency_swarm/agency/genesis/OpenAPICreator/instructions.md +++ b/agency_swarm/agency/genesis/OpenAPICreator/instructions.md @@ -7,4 +7,4 @@ You are an agent that creates tools from OpenAPI schemas. User will provide you 2. Explore the provided file from the BrowsingAgent with the `myfiles_broswer` tool to determine which endpoints are needed for this agent's role. 3. If the file does not contain the actual API documentation page, please notify the BrowsingAgent. Keep in mind that you do not need the full API documentation. You can make an educated guess if some information is not available. 4. Use `CreateToolsFromOpenAPISpec` to create the tools by defining the OpenAPI schema accordingly. Make sure to include all the relevant API endpoints that are needed for this agent to execute its role from the provided file. Do not truncate the schema. -5. Repeat these steps for each new agent that needs to be created, as instructed by the user. \ No newline at end of file +5. Repeat these steps for each new agent that needs to be created, as instructed by the user. diff --git a/agency_swarm/agency/genesis/OpenAPICreator/tools/CreateToolsFromOpenAPISpec.py b/agency_swarm/agency/genesis/OpenAPICreator/tools/CreateToolsFromOpenAPISpec.py index 28387d5e..fe532be3 100644 --- a/agency_swarm/agency/genesis/OpenAPICreator/tools/CreateToolsFromOpenAPISpec.py +++ b/agency_swarm/agency/genesis/OpenAPICreator/tools/CreateToolsFromOpenAPISpec.py @@ -1,11 +1,9 @@ +import json import os from pydantic import Field, field_validator, model_validator from agency_swarm import BaseTool - -import json - from agency_swarm.agency.genesis.util import check_agency_path, check_agent_path from agency_swarm.tools import ToolFactory from agency_swarm.util.openapi import validate_openapi_spec @@ -15,18 +13,24 @@ class CreateToolsFromOpenAPISpec(BaseTool): """ This tool creates a set of tools from an OpenAPI specification. Each method in the specification is converted to a separate tool. """ + agent_name: str = Field( - ..., description="Name of the agent to create the API for. Must be an existing agent." + ..., + description="Name of the agent to create the API for. Must be an existing agent.", ) openapi_spec: str = Field( - ..., description="OpenAPI specification for the tool to be created as a valid JSON string. Only the relevant " - "endpoints must be included. Responses are not required. Each method should contain " - "an operation id and a description. Do not truncate this schema. " - "It must be a full valid OpenAPI 3.1.0 specification.", + ..., + description="OpenAPI specification for the tool to be created as a valid JSON string. Only the relevant " + "endpoints must be included. Responses are not required. Each method should contain " + "an operation id and a description. Do not truncate this schema. " + "It must be a full valid OpenAPI 3.1.0 specification.", examples=[ - '{\n "openapi": "3.1.0",\n "info": {\n "title": "Get weather data",\n "description": "Retrieves current weather data for a location.",\n "version": "v1.0.0"\n },\n "servers": [\n {\n "url": "https://weather.example.com"\n }\n ],\n "paths": {\n "/location": {\n "get": {\n "description": "Get temperature for a specific location",\n "operationId": "GetCurrentWeather",\n "parameters": [\n {\n "name": "location",\n "in": "query",\n "description": "The city and state to retrieve the weather for",\n "required": true,\n "schema": {\n "type": "string"\n }\n }\n ],\n "deprecated": false\n }\n }\n },\n "components": {\n "schemas": {}\n }\n}']) + '{\n "openapi": "3.1.0",\n "info": {\n "title": "Get weather data",\n "description": "Retrieves current weather data for a location.",\n "version": "v1.0.0"\n },\n "servers": [\n {\n "url": "https://weather.example.com"\n }\n ],\n "paths": {\n "/location": {\n "get": {\n "description": "Get temperature for a specific location",\n "operationId": "GetCurrentWeather",\n "parameters": [\n {\n "name": "location",\n "in": "query",\n "description": "The city and state to retrieve the weather for",\n "required": true,\n "schema": {\n "type": "string"\n }\n }\n ],\n "deprecated": false\n }\n }\n },\n "components": {\n "schemas": {}\n }\n}' + ], + ) agency_name: str = Field( - None, description="Name of the agency to create the tool for. Defaults to the agency currently being created." + None, + description="Name of the agency to create the tool for. Defaults to the agency currently being created.", ) def run(self): @@ -53,16 +57,20 @@ def run(self): api_name = api_name.replace("API", "Api").replace(" ", "") - api_name = ''.join(['_' + i.lower() if i.isupper() else i for i in api_name]).lstrip('_') + api_name = "".join( + ["_" + i.lower() if i.isupper() else i for i in api_name] + ).lstrip("_") with open("schemas/" + api_name + ".json", "w") as f: f.write(self.openapi_spec) - return "Successfully added OpenAPI Schema to " + self._shared_state.get("agent_name") + return "Successfully added OpenAPI Schema to " + self._shared_state.get( + "agent_name" + ) finally: os.chdir(self._shared_state.get("default_folder")) - @field_validator("openapi_spec", mode='before') + @field_validator("openapi_spec", mode="before") @classmethod def validate_openapi_spec(cls, v): try: @@ -78,4 +86,3 @@ def validate_agent_name(self): check_agency_path(self) check_agent_path(self) - diff --git a/agency_swarm/agency/genesis/ToolCreator/ToolCreator.py b/agency_swarm/agency/genesis/ToolCreator/ToolCreator.py index f9f3dc7d..1038876c 100644 --- a/agency_swarm/agency/genesis/ToolCreator/ToolCreator.py +++ b/agency_swarm/agency/genesis/ToolCreator/ToolCreator.py @@ -1,4 +1,5 @@ from agency_swarm import Agent + from .tools.CreateTool import CreateTool from .tools.TestTool import TestTool @@ -11,5 +12,3 @@ def __init__(self): tools=[CreateTool, TestTool], temperature=0, ) - - diff --git a/agency_swarm/agency/genesis/ToolCreator/__init__.py b/agency_swarm/agency/genesis/ToolCreator/__init__.py index f73acf71..e8e127f5 100644 --- a/agency_swarm/agency/genesis/ToolCreator/__init__.py +++ b/agency_swarm/agency/genesis/ToolCreator/__init__.py @@ -1 +1 @@ -from .ToolCreator import ToolCreator \ No newline at end of file +from .ToolCreator import ToolCreator diff --git a/agency_swarm/agency/genesis/ToolCreator/instructions.md b/agency_swarm/agency/genesis/ToolCreator/instructions.md index 4451ec10..3bc7d8ed 100644 --- a/agency_swarm/agency/genesis/ToolCreator/instructions.md +++ b/agency_swarm/agency/genesis/ToolCreator/instructions.md @@ -7,4 +7,3 @@ As a ToolCreator Agent within the Agency Swarm framework, your mission is to dev 2. Create these tools one at a time, using `CreateTool` tool. 3. Test each tool with the `TestTool` function to ensure it is working as expected. Do not ask the user, always test the tool yourself, if it does not require any API keys and all the inputs can be mocked. 4. Only after all the necessary tools are created, notify the user. - diff --git a/agency_swarm/agency/genesis/ToolCreator/tools/CreateTool.py b/agency_swarm/agency/genesis/ToolCreator/tools/CreateTool.py index cb48a3d6..0843ef26 100644 --- a/agency_swarm/agency/genesis/ToolCreator/tools/CreateTool.py +++ b/agency_swarm/agency/genesis/ToolCreator/tools/CreateTool.py @@ -10,7 +10,7 @@ prompt = """# Agency Swarm Overview -Agency Swarm started as a desire and effort of Arsenii Shatokhin (aka VRSEN) to fully automate his AI Agency with AI. By building this framework, we aim to simplify the agent creation process and enable anyone to create a collaborative swarm of agents (Agencies), each with distinct roles and capabilities. +Agency Swarm started as a desire and effort of Arsenii Shatokhin (aka VRSEN) to fully automate his AI Agency with AI. By building this framework, we aim to simplify the agent creation process and enable anyone to create a collaborative swarm of agents (Agencies), each with distinct roles and capabilities. # ToolCreator Agent Instructions for Agency Swarm Framework @@ -64,7 +64,7 @@ def run(self): This method should utilize the fields defined above to perform the task. \"\"\" # Your custom tool logic goes here - # Example: + # Example: # do_something(self.example_field, api_key, account_id) # Return the result of the tool's operation as a string @@ -78,51 +78,56 @@ class MyCustomTool(BaseTool): def run(self): # Access the shared state value = self._shared_state.get("key") - + # Update the shared state self._shared_state.set("key", "value") - + return "Result of MyCustomTool operation" - + # Access shared state in another tool class AnotherTool(BaseTool): def run(self): # Access the shared state value = self._shared_state.get("key") - + return "Result of AnotherTool operation" ``` -This is useful to pass information between tools or agents or to verify the state of the system. +This is useful to pass information between tools or agents or to verify the state of the system. Remember, you must output the resulting python tool code as a whole in a code block, so the user can just copy and paste it into his program. Each tool code snippet must be ready to use. It must not contain any placeholders or hypothetical examples.""" history = [ - { - "role": "system", - "content": prompt - }, - ] + {"role": "system", "content": prompt}, +] class CreateTool(BaseTool): """This tool creates other custom tools for the agent, based on your requirements and details.""" + agent_name: str = Field( ..., description="Name of the agent to create the tool for." ) - tool_name: str = Field(..., description="Name of the tool class in camel case.", examples=["ExampleTool"]) + tool_name: str = Field( + ..., + description="Name of the tool class in camel case.", + examples=["ExampleTool"], + ) requirements: str = Field( ..., - description="The comprehensive requirements explaning the primary functionality of the tool. It must not contain any code or implementation details." + description="The comprehensive requirements explaning the primary functionality of the tool. It must not contain any code or implementation details.", ) details: str = Field( - None, description="Additional details or error messages, class, function, and variable names." + None, + description="Additional details or error messages, class, function, and variable names.", ) mode: Literal["write", "modify"] = Field( - ..., description="The mode of operation for the tool. 'write' is used to create a new tool or overwrite an existing one. 'modify' is used to modify an existing tool." + ..., + description="The mode of operation for the tool. 'write' is used to create a new tool or overwrite an existing one. 'modify' is used to modify an existing tool.", ) agency_name: str = Field( - None, description="Name of the agency to create the tool for. Defaults to the agency currently being created." + None, + description="Name of the agency to create the tool for. Defaults to the agency currently being created.", ) class ToolConfig: @@ -149,17 +154,14 @@ def run(self): message += f"\nThe existing file content is as follows:" try: - with open("./tools/" + self.tool_name + ".py", 'r') as file: + with open("./tools/" + self.tool_name + ".py", "r") as file: prev_content = file.read() message += f"\n\n```{prev_content}```" except Exception as e: os.chdir(self._shared_state.get("default_folder")) - return f'Error reading {self.tool_name}: {e}' + return f"Error reading {self.tool_name}: {e}" - history.append({ - "role": "user", - "content": message - }) + history.append({"role": "user", "content": message}) messages = history.copy() @@ -181,29 +183,19 @@ def run(self): content = resp.choices[0].message.content - messages.append( - { - "role": "assistant", - "content": content - } - ) + messages.append({"role": "assistant", "content": content}) pattern = r"```(?:[a-zA-Z]+\n)?(.*?)```" match = re.findall(pattern, content, re.DOTALL) if match: code = match[-1].strip() - history.append( - { - "role": "assistant", - "content": content - } - ) + history.append({"role": "assistant", "content": content}) break else: messages.append( { "role": "user", - "content": f"Error: Could not find the python code block in the response. Please try again." + "content": f"Error: Could not find the python code block in the response. Please try again.", } ) @@ -219,20 +211,22 @@ def run(self): file.write(code) os.chdir(self._shared_state.get("default_folder")) - return f'{content}\n\nPlease make sure to now test this tool if possible.' + return f"{content}\n\nPlease make sure to now test this tool if possible." except Exception as e: os.chdir(self._shared_state.get("default_folder")) - return f'Error writing to file: {e}' + return f"Error writing to file: {e}" @field_validator("requirements", mode="after") @classmethod def validate_requirements(cls, v): if "placeholder" in v: - raise ValueError("Requirements contain placeholders. " - "Please never user placeholders. Instead, implement only the code that you are confident about.") + raise ValueError( + "Requirements contain placeholders. " + "Please never user placeholders. Instead, implement only the code that you are confident about." + ) # check if code is included in requirements - pattern = r'(```)((.*\n){5,})(```)' + pattern = r"(```)((.*\n){5,})(```)" if re.search(pattern, v): raise ValueError( "Requirements contain a code snippet. Please never include code snippets in requirements. " @@ -245,7 +239,9 @@ def validate_requirements(cls, v): @classmethod def validate_details(cls, v): if len(v) == 0: - raise ValueError("Details are required. Remember this tool does not have access to other files. Please provide additional details like relevant documentation, error messages, or class, function, and variable names from other files that this file depends on.") + raise ValueError( + "Details are required. Remember this tool does not have access to other files. Please provide additional details like relevant documentation, error messages, or class, function, and variable names from other files that this file depends on." + ) return v @model_validator(mode="after") @@ -262,4 +258,4 @@ def validate_agency_name(self): mode="write", file_path="test.py", ) - print(tool.run()) \ No newline at end of file + print(tool.run()) diff --git a/agency_swarm/agency/genesis/ToolCreator/tools/TestTool.py b/agency_swarm/agency/genesis/ToolCreator/tools/TestTool.py index 8d984062..7600752a 100644 --- a/agency_swarm/agency/genesis/ToolCreator/tools/TestTool.py +++ b/agency_swarm/agency/genesis/ToolCreator/tools/TestTool.py @@ -11,18 +11,22 @@ class TestTool(BaseTool): """ This tool tests other tools defined in tools.py file with the given arguments. Make sure to define the run method before testing. """ - agent_name: str = Field( - ..., description="Name of the agent to test the tool for." - ) + + agent_name: str = Field(..., description="Name of the agent to test the tool for.") chain_of_thought: str = Field( - ..., description="Think step by step to determine the correct arguments for testing.", exclude=True + ..., + description="Think step by step to determine the correct arguments for testing.", + exclude=True, ) tool_name: str = Field(..., description="Name of the tool to be run.") - arguments: Optional[str] = Field(..., - description="Arguments to be passed to the tool for testing " - "in serialized JSON format.") + arguments: Optional[str] = Field( + ..., + description="Arguments to be passed to the tool for testing " + "in serialized JSON format.", + ) agency_name: str = Field( - None, description="Name of the agency to create the tool for. Defaults to the agency currently being created." + None, + description="Name of the agency to create the tool for. Defaults to the agency currently being created.", ) def run(self): @@ -68,29 +72,51 @@ def validate_tool_name(self): tool_path = os.path.join(str(tool_path), "tools") tool_path = os.path.join(tool_path, self.tool_name + ".py") - # check if tools.py file exists if not os.path.isfile(tool_path): - available_tools = os.listdir(os.path.join(self._shared_state.get("agency_path"), agent_name)) + available_tools = os.listdir( + os.path.join(self._shared_state.get("agency_path"), agent_name) + ) available_tools = [tool for tool in available_tools if tool.endswith(".py")] - available_tools = [tool for tool in available_tools if - not tool.startswith("__") and not tool.startswith(".")] + available_tools = [ + tool + for tool in available_tools + if not tool.startswith("__") and not tool.startswith(".") + ] available_tools = [tool.replace(".py", "") for tool in available_tools] available_tools = ", ".join(available_tools) - raise ValueError(f"Tool {self.tool_name} not found. Available tools are: {available_tools}") + raise ValueError( + f"Tool {self.tool_name} not found. Available tools are: {available_tools}" + ) - agent_path = os.path.join(self._shared_state.get("agency_path"), self.agent_name) + agent_path = os.path.join( + self._shared_state.get("agency_path"), self.agent_name + ) if not os.path.exists(agent_path): available_agents = os.listdir(self._shared_state.get("agency_path")) - available_agents = [agent for agent in available_agents if - os.path.isdir(os.path.join(self._shared_state.get("agency_path"), agent))] - raise ValueError(f"Agent {self.agent_name} not found. Available agents are: {available_agents}") + available_agents = [ + agent + for agent in available_agents + if os.path.isdir( + os.path.join(self._shared_state.get("agency_path"), agent) + ) + ] + raise ValueError( + f"Agent {self.agent_name} not found. Available agents are: {available_agents}" + ) return True if __name__ == "__main__": - TestTool._shared_state.data = {"agency_path": "/Users/vrsen/Projects/agency-swarm/agency-swarm/TestAgency", - "default_folder": "/Users/vrsen/Projects/agency-swarm/agency-swarm/TestAgency"} - test_tool = TestTool(agent_name="TestAgent", tool_name="PrintTestTool", arguments="{}", chain_of_thought="") + TestTool._shared_state.data = { + "agency_path": "/Users/vrsen/Projects/agency-swarm/agency-swarm/TestAgency", + "default_folder": "/Users/vrsen/Projects/agency-swarm/agency-swarm/TestAgency", + } + test_tool = TestTool( + agent_name="TestAgent", + tool_name="PrintTestTool", + arguments="{}", + chain_of_thought="", + ) print(test_tool.run()) diff --git a/agency_swarm/agency/genesis/__init__.py b/agency_swarm/agency/genesis/__init__.py index 4f312bb2..f1526afb 100644 --- a/agency_swarm/agency/genesis/__init__.py +++ b/agency_swarm/agency/genesis/__init__.py @@ -1 +1 @@ -from .GenesisAgency import GenesisAgency \ No newline at end of file +from .GenesisAgency import GenesisAgency diff --git a/agency_swarm/agency/genesis/manifesto.md b/agency_swarm/agency/genesis/manifesto.md index f59f2f83..c9bd4a11 100644 --- a/agency_swarm/agency/genesis/manifesto.md +++ b/agency_swarm/agency/genesis/manifesto.md @@ -5,4 +5,3 @@ You are a part of a Genesis Agency for a framework called Agency Swarm. The goal **Agency Swarm started as a desire and effort of Arsenii Shatokhin (aka VRSEN) to fully automate his AI Agency with AI. By building this framework, we aim to simplify the AI agent creation process and enable anyone to create a collaborative swarms of agents (Agencies), each with distinct roles and capabilities. These agents must function autonomously, yet collaborate with other agents to achieve a common goal.** Keep in mind that communication with the other agents within your agency via the `SendMessage` tool is synchronous. Other agents will not be executing any tasks post response. Please instruct the recipient agent to continue its execution, if needed. Do not report to the user before the recipient agent has completed its task. If the agent proposes the next steps, for example, you must instruct the recipient agent to execute them. - diff --git a/agency_swarm/agency/genesis/util.py b/agency_swarm/agency/genesis/util.py index 1cedcc67..a8d18b9c 100644 --- a/agency_swarm/agency/genesis/util.py +++ b/agency_swarm/agency/genesis/util.py @@ -4,17 +4,25 @@ def check_agency_path(self): if not self._shared_state.get("default_folder"): - self._shared_state.set('default_folder', Path.cwd()) + self._shared_state.set("default_folder", Path.cwd()) if not self._shared_state.get("agency_path") and not self.agency_name: available_agencies = os.listdir("./") - available_agencies = [agency for agency in available_agencies if os.path.isdir(agency)] - raise ValueError(f"Please specify an agency. Available agencies are: {available_agencies}") + available_agencies = [ + agency for agency in available_agencies if os.path.isdir(agency) + ] + raise ValueError( + f"Please specify an agency. Available agencies are: {available_agencies}" + ) elif not self._shared_state.get("agency_path") and self.agency_name: if not os.path.exists(os.path.join("./", self.agency_name)): available_agencies = os.listdir("./") - available_agencies = [agency for agency in available_agencies if os.path.isdir(agency)] - raise ValueError(f"Agency {self.agency_name} not found. Available agencies are: {available_agencies}") + available_agencies = [ + agency for agency in available_agencies if os.path.isdir(agency) + ] + raise ValueError( + f"Agency {self.agency_name} not found. Available agencies are: {available_agencies}" + ) self._shared_state.set("agency_path", os.path.join("./", self.agency_name)) @@ -22,6 +30,11 @@ def check_agent_path(self): agent_path = os.path.join(self._shared_state.get("agency_path"), self.agent_name) if not os.path.exists(agent_path): available_agents = os.listdir(self._shared_state.get("agency_path")) - available_agents = [agent for agent in available_agents if - os.path.isdir(os.path.join(self._shared_state.get("agency_path"), agent))] - raise ValueError(f"Agent {self.agent_name} not found. Available agents are: {available_agents}") + available_agents = [ + agent + for agent in available_agents + if os.path.isdir(os.path.join(self._shared_state.get("agency_path"), agent)) + ] + raise ValueError( + f"Agent {self.agent_name} not found. Available agents are: {available_agents}" + ) diff --git a/agency_swarm/agents/BrowsingAgent/BrowsingAgent.py b/agency_swarm/agents/BrowsingAgent/BrowsingAgent.py index 61d4c4dd..afe11dad 100644 --- a/agency_swarm/agents/BrowsingAgent/BrowsingAgent.py +++ b/agency_swarm/agents/BrowsingAgent/BrowsingAgent.py @@ -1,10 +1,10 @@ -import json +import base64 import re +from typing_extensions import override + from agency_swarm.agents import Agent from agency_swarm.tools.oai import FileSearch -from typing_extensions import override -import base64 class BrowsingAgent(Agent): @@ -12,6 +12,7 @@ class BrowsingAgent(Agent): def __init__(self, selenium_config=None, **kwargs): from .tools.util.selenium import set_selenium_config + super().__init__( name="BrowsingAgent", description="This agent is designed to navigate and search web effectively.", @@ -24,7 +25,7 @@ def __init__(self, selenium_config=None, **kwargs): max_prompt_tokens=16000, model="gpt-4o", validation_attempts=25, - **kwargs + **kwargs, ) if selenium_config is not None: set_selenium_config(selenium_config) @@ -33,17 +34,23 @@ def __init__(self, selenium_config=None, **kwargs): @override def response_validator(self, message): - from .tools.util.selenium import get_web_driver, set_web_driver - from .tools.util import highlight_elements_with_labels, remove_highlight_and_labels from selenium.webdriver.common.by import By from selenium.webdriver.support.select import Select + from .tools.util import ( + highlight_elements_with_labels, + remove_highlight_and_labels, + ) + from .tools.util.selenium import get_web_driver, set_web_driver + # Filter out everything in square brackets - filtered_message = re.sub(r'\[.*?\]', '', message).strip() - + filtered_message = re.sub(r"\[.*?\]", "", message).strip() + if filtered_message and self.prev_message == filtered_message: - raise ValueError("Do not repeat yourself. If you are stuck, try a different approach or search in google for the page you are looking for directly.") - + raise ValueError( + "Do not repeat yourself. If you are stuck, try a different approach or search in google for the page you are looking for directly." + ) + self.prev_message = filtered_message if "[send screenshot]" in message.lower(): @@ -52,40 +59,50 @@ def response_validator(self, message): self.take_screenshot() response_text = "Here is the screenshot of the current web page:" - elif '[highlight clickable elements]' in message.lower(): + elif "[highlight clickable elements]" in message.lower(): wd = get_web_driver() - highlight_elements_with_labels(wd, 'a, button, div[onclick], div[role="button"], div[tabindex], ' - 'span[onclick], span[role="button"], span[tabindex]') - self._shared_state.set("elements_highlighted", 'a, button, div[onclick], div[role="button"], div[tabindex], ' - 'span[onclick], span[role="button"], span[tabindex]') + highlight_elements_with_labels( + wd, + 'a, button, div[onclick], div[role="button"], div[tabindex], ' + 'span[onclick], span[role="button"], span[tabindex]', + ) + self._shared_state.set( + "elements_highlighted", + 'a, button, div[onclick], div[role="button"], div[tabindex], ' + 'span[onclick], span[role="button"], span[tabindex]', + ) self.take_screenshot() - all_elements = wd.find_elements(By.CSS_SELECTOR, '.highlighted-element') + all_elements = wd.find_elements(By.CSS_SELECTOR, ".highlighted-element") all_element_texts = [element.text for element in all_elements] element_texts_json = {} for i, element_text in enumerate(all_element_texts): element_texts_json[str(i + 1)] = self.remove_unicode(element_text) - + element_texts_json = {k: v for k, v in element_texts_json.items() if v} - element_texts_formatted = ", ".join([f"{k}: {v}" for k, v in element_texts_json.items()]) + element_texts_formatted = ", ".join( + [f"{k}: {v}" for k, v in element_texts_json.items()] + ) - response_text = ("Here is the screenshot of the current web page with highlighted clickable elements. \n\n" - "Texts of the elements are: " + element_texts_formatted + ".\n\n" - "Elements without text are not shown, but are available on screenshot. \n" - "Please make sure to analyze the screenshot to find the clickable element you need to click on.") + response_text = ( + "Here is the screenshot of the current web page with highlighted clickable elements. \n\n" + "Texts of the elements are: " + element_texts_formatted + ".\n\n" + "Elements without text are not shown, but are available on screenshot. \n" + "Please make sure to analyze the screenshot to find the clickable element you need to click on." + ) - elif '[highlight text fields]' in message.lower(): + elif "[highlight text fields]" in message.lower(): wd = get_web_driver() - highlight_elements_with_labels(wd, 'input, textarea') + highlight_elements_with_labels(wd, "input, textarea") self._shared_state.set("elements_highlighted", "input, textarea") self.take_screenshot() - all_elements = wd.find_elements(By.CSS_SELECTOR, '.highlighted-element') + all_elements = wd.find_elements(By.CSS_SELECTOR, ".highlighted-element") all_element_texts = [element.text for element in all_elements] @@ -93,20 +110,24 @@ def response_validator(self, message): for i, element_text in enumerate(all_element_texts): element_texts_json[str(i + 1)] = self.remove_unicode(element_text) - element_texts_formatted = ", ".join([f"{k}: {v}" for k, v in element_texts_json.items()]) + element_texts_formatted = ", ".join( + [f"{k}: {v}" for k, v in element_texts_json.items()] + ) - response_text = ("Here is the screenshot of the current web page with highlighted text fields: \n" - "Texts of the elements are: " + element_texts_formatted + ".\n" - "Please make sure to analyze the screenshot to find the text field you need to fill.") + response_text = ( + "Here is the screenshot of the current web page with highlighted text fields: \n" + "Texts of the elements are: " + element_texts_formatted + ".\n" + "Please make sure to analyze the screenshot to find the text field you need to fill." + ) - elif '[highlight dropdowns]' in message.lower(): + elif "[highlight dropdowns]" in message.lower(): wd = get_web_driver() - highlight_elements_with_labels(wd, 'select') + highlight_elements_with_labels(wd, "select") self._shared_state.set("elements_highlighted", "select") self.take_screenshot() - all_elements = wd.find_elements(By.CSS_SELECTOR, '.highlighted-element') + all_elements = wd.find_elements(By.CSS_SELECTOR, ".highlighted-element") all_selector_values = {} @@ -122,11 +143,15 @@ def response_validator(self, message): all_selector_values[str(i + 1)] = selector_values all_selector_values = {k: v for k, v in all_selector_values.items() if v} - all_selector_values_formatted = ", ".join([f"{k}: {v}" for k, v in all_selector_values.items()]) + all_selector_values_formatted = ", ".join( + [f"{k}: {v}" for k, v in all_selector_values.items()] + ) - response_text = ("Here is the screenshot with highlighted dropdowns. \n" - "Selector values are: " + all_selector_values_formatted + ".\n" - "Please make sure to analyze the screenshot to find the dropdown you need to select.") + response_text = ( + "Here is the screenshot with highlighted dropdowns. \n" + "Selector values are: " + all_selector_values_formatted + ".\n" + "Please make sure to analyze the screenshot to find the dropdown you need to select." + ) else: return message @@ -136,8 +161,9 @@ def response_validator(self, message): raise ValueError(content) def take_screenshot(self): - from .tools.util.selenium import get_web_driver from .tools.util import get_b64_screenshot + from .tools.util.selenium import get_web_driver + wd = get_web_driver() screenshot = get_b64_screenshot(wd) screenshot_data = base64.b64decode(screenshot) @@ -153,14 +179,10 @@ def create_response_content(self, response_text): content = [ {"type": "text", "text": response_text}, - { - "type": "image_file", - "image_file": {"file_id": file_id} - } + {"type": "image_file", "image_file": {"file_id": file_id}}, ] return content # Function to check for Unicode escape sequences def remove_unicode(self, data): - return re.sub(r'[^\x00-\x7F]+', '', data) - + return re.sub(r"[^\x00-\x7F]+", "", data) diff --git a/agency_swarm/agents/BrowsingAgent/__init__.py b/agency_swarm/agents/BrowsingAgent/__init__.py index c8568f27..a500122e 100644 --- a/agency_swarm/agents/BrowsingAgent/__init__.py +++ b/agency_swarm/agents/BrowsingAgent/__init__.py @@ -1 +1 @@ -from .BrowsingAgent import BrowsingAgent \ No newline at end of file +from .BrowsingAgent import BrowsingAgent diff --git a/agency_swarm/agents/BrowsingAgent/requirements.txt b/agency_swarm/agents/BrowsingAgent/requirements.txt index 81d5f50d..09f0cc02 100644 --- a/agency_swarm/agents/BrowsingAgent/requirements.txt +++ b/agency_swarm/agents/BrowsingAgent/requirements.txt @@ -1,3 +1,3 @@ selenium webdriver-manager -selenium_stealth \ No newline at end of file +selenium_stealth diff --git a/agency_swarm/agents/BrowsingAgent/tools/ClickElement.py b/agency_swarm/agents/BrowsingAgent/tools/ClickElement.py index 651c19bf..84b82c1b 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/ClickElement.py +++ b/agency_swarm/agents/BrowsingAgent/tools/ClickElement.py @@ -4,6 +4,7 @@ from selenium.webdriver.common.by import By from agency_swarm.tools import BaseTool + from .util import get_web_driver, set_web_driver from .util.highlights import remove_highlight_and_labels @@ -14,6 +15,7 @@ class ClickElement(BaseTool): Before using this tool make sure to highlight clickable elements on the page by outputting '[highlight clickable elements]' message. """ + element_number: int = Field( ..., description="The number of the element to click on. The element numbers are displayed on the page after highlighting elements.", @@ -22,10 +24,12 @@ class ClickElement(BaseTool): def run(self): wd = get_web_driver() - if 'button' not in self._shared_state.get("elements_highlighted", ""): - raise ValueError("Please highlight clickable elements on the page first by outputting '[highlight clickable elements]' message. You must output just the message without calling the tool first, so the user can respond with the screenshot.") + if "button" not in self._shared_state.get("elements_highlighted", ""): + raise ValueError( + "Please highlight clickable elements on the page first by outputting '[highlight clickable elements]' message. You must output just the message without calling the tool first, so the user can respond with the screenshot." + ) - all_elements = wd.find_elements(By.CSS_SELECTOR, '.highlighted-element') + all_elements = wd.find_elements(By.CSS_SELECTOR, ".highlighted-element") # iterate through all elements with a number in the text try: @@ -36,7 +40,9 @@ def run(self): all_elements[self.element_number - 1].click() except Exception as e: if "element click intercepted" in str(e).lower(): - wd.execute_script("arguments[0].click();", all_elements[self.element_number - 1]) + wd.execute_script( + "arguments[0].click();", all_elements[self.element_number - 1] + ) else: raise e @@ -56,4 +62,4 @@ def run(self): self._shared_state.set("elements_highlighted", "") - return result \ No newline at end of file + return result diff --git a/agency_swarm/agents/BrowsingAgent/tools/ExportFile.py b/agency_swarm/agents/BrowsingAgent/tools/ExportFile.py index 26abc90e..6204f2f0 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/ExportFile.py +++ b/agency_swarm/agents/BrowsingAgent/tools/ExportFile.py @@ -1,7 +1,7 @@ import base64 -import os from agency_swarm.tools import BaseTool + from .util import get_web_driver @@ -11,19 +11,20 @@ class ExportFile(BaseTool): def run(self): wd = get_web_driver() from agency_swarm import get_openai_client + client = get_openai_client() # Define the parameters for the PDF params = { - 'landscape': False, - 'displayHeaderFooter': False, - 'printBackground': True, - 'preferCSSPageSize': True, + "landscape": False, + "displayHeaderFooter": False, + "printBackground": True, + "preferCSSPageSize": True, } # Execute the command to print to PDF - result = wd.execute_cdp_cmd('Page.printToPDF', params) - pdf = result['data'] + result = wd.execute_cdp_cmd("Page.printToPDF", params) + pdf = result["data"] pdf_bytes = base64.b64decode(pdf) @@ -31,11 +32,18 @@ def run(self): with open("exported_file.pdf", "wb") as f: f.write(pdf_bytes) - file_id = client.files.create(file=open("exported_file.pdf", "rb"), purpose="assistants",).id + file_id = client.files.create( + file=open("exported_file.pdf", "rb"), + purpose="assistants", + ).id self._shared_state.set("file_id", file_id) - return "Success. File exported with id: `" + file_id + "` You can now send this file id back to the user." + return ( + "Success. File exported with id: `" + + file_id + + "` You can now send this file id back to the user." + ) if __name__ == "__main__": diff --git a/agency_swarm/agents/BrowsingAgent/tools/ReadURL.py b/agency_swarm/agents/BrowsingAgent/tools/ReadURL.py index 38b6c8dc..e0de6589 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/ReadURL.py +++ b/agency_swarm/agents/BrowsingAgent/tools/ReadURL.py @@ -3,23 +3,28 @@ from pydantic import Field from agency_swarm.tools import BaseTool + from .util.selenium import get_web_driver, set_web_driver class ReadURL(BaseTool): """ -This tool reads a single URL and opens it in your current browser window. For each new source, either navigate directly to a URL that you believe contains the answer to the user's question or perform a Google search (e.g., 'https://google.com/search?q=search') if necessary. + This tool reads a single URL and opens it in your current browser window. For each new source, either navigate directly to a URL that you believe contains the answer to the user's question or perform a Google search (e.g., 'https://google.com/search?q=search') if necessary. -If you are unsure of the direct URL, do not guess. Instead, use the ClickElement tool to click on links that might contain the desired information on the current web page. + If you are unsure of the direct URL, do not guess. Instead, use the ClickElement tool to click on links that might contain the desired information on the current web page. -Note: This tool only supports opening one URL at a time. The previous URL will be closed when you open a new one. + Note: This tool only supports opening one URL at a time. The previous URL will be closed when you open a new one. """ + chain_of_thought: str = Field( - ..., description="Think step-by-step about where you need to navigate next to find the necessary information.", - exclude=True + ..., + description="Think step-by-step about where you need to navigate next to find the necessary information.", + exclude=True, ) url: str = Field( - ..., description="URL of the webpage.", examples=["https://google.com/search?q=search"] + ..., + description="URL of the webpage.", + examples=["https://google.com/search?q=search"], ) class ToolConfig: @@ -36,9 +41,14 @@ def run(self): self._shared_state.set("elements_highlighted", "") - return "Current URL is: " + wd.current_url + "\n" + "Please output '[send screenshot]' next to analyze the current web page or '[highlight clickable elements]' for further navigation." + return ( + "Current URL is: " + + wd.current_url + + "\n" + + "Please output '[send screenshot]' next to analyze the current web page or '[highlight clickable elements]' for further navigation." + ) if __name__ == "__main__": tool = ReadURL(url="https://google.com") - print(tool.run()) \ No newline at end of file + print(tool.run()) diff --git a/agency_swarm/agents/BrowsingAgent/tools/Scroll.py b/agency_swarm/agents/BrowsingAgent/tools/Scroll.py index d8e9637a..7a0de9cb 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/Scroll.py +++ b/agency_swarm/agents/BrowsingAgent/tools/Scroll.py @@ -3,6 +3,7 @@ from pydantic import Field from agency_swarm.tools import BaseTool + from .util.selenium import get_web_driver, set_web_driver @@ -10,18 +11,21 @@ class Scroll(BaseTool): """ This tool allows you to scroll the current web page up or down by 1 screen height. """ - direction: Literal["up", "down"] = Field( - ..., description="Direction to scroll." - ) + + direction: Literal["up", "down"] = Field(..., description="Direction to scroll.") def run(self): wd = get_web_driver() - height = wd.get_window_size()['height'] + height = wd.get_window_size()["height"] # Get the zoom level zoom_level = wd.execute_script("return document.body.style.zoom || '1';") - zoom_level = float(zoom_level.strip('%')) / 100 if '%' in zoom_level else float(zoom_level) + zoom_level = ( + float(zoom_level.strip("%")) / 100 + if "%" in zoom_level + else float(zoom_level) + ) # Adjust height by zoom level adjusted_height = height / zoom_level @@ -42,7 +46,9 @@ def run(self): elif self.direction == "down": if current_scroll_position + adjusted_height >= total_scroll_height: # Reached the bottom of the page - result = "Reached the bottom of the page. Cannot scroll down any further.\n" + result = ( + "Reached the bottom of the page. Cannot scroll down any further.\n" + ) else: wd.execute_script(f"window.scrollBy(0, {adjusted_height});") result = "Scrolled down by 1 screen height. Make sure to output '[send screenshot]' command to analyze the page after scrolling." @@ -50,4 +56,3 @@ def run(self): set_web_driver(wd) return result - diff --git a/agency_swarm/agents/BrowsingAgent/tools/SelectDropdown.py b/agency_swarm/agents/BrowsingAgent/tools/SelectDropdown.py index 0a7ad371..529c3f7f 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/SelectDropdown.py +++ b/agency_swarm/agents/BrowsingAgent/tools/SelectDropdown.py @@ -1,9 +1,11 @@ from typing import Dict + from pydantic import Field, model_validator from selenium.webdriver.common.by import By from selenium.webdriver.support.select import Select from agency_swarm.tools import BaseTool + from .util import get_web_driver, set_web_driver from .util.highlights import remove_highlight_and_labels @@ -15,15 +17,16 @@ class SelectDropdown(BaseTool): Before using this tool make sure to highlight dropdown elements on the page by outputting '[highlight dropdowns]' message. """ - key_value_pairs: Dict[str, str] = Field(..., + key_value_pairs: Dict[str, str] = Field( + ..., description="A dictionary where the key is the sequence number of the dropdown element and the value is the index of the option to select.", - examples=[{"1": 0, "2": 1}, {"3": 2}] + examples=[{"1": 0, "2": 1}, {"3": 2}], ) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def check_key_value_pairs(cls, data): - if not data.get('key_value_pairs'): + if not data.get("key_value_pairs"): raise ValueError( "key_value_pairs is required. Example format: " "key_value_pairs={'1': 0, '2': 1}" @@ -33,10 +36,12 @@ def check_key_value_pairs(cls, data): def run(self): wd = get_web_driver() - if 'select' not in self._shared_state.get("elements_highlighted", ""): - raise ValueError("Please highlight dropdown elements on the page first by outputting '[highlight dropdowns]' message. You must output just the message without calling the tool first, so the user can respond with the screenshot.") + if "select" not in self._shared_state.get("elements_highlighted", ""): + raise ValueError( + "Please highlight dropdown elements on the page first by outputting '[highlight dropdowns]' message. You must output just the message without calling the tool first, so the user can respond with the screenshot." + ) - all_elements = wd.find_elements(By.CSS_SELECTOR, '.highlighted-element') + all_elements = wd.find_elements(By.CSS_SELECTOR, ".highlighted-element") try: for key, value in self.key_value_pairs.items(): diff --git a/agency_swarm/agents/BrowsingAgent/tools/SendKeys.py b/agency_swarm/agents/BrowsingAgent/tools/SendKeys.py index ac7e234f..f7c7257d 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/SendKeys.py +++ b/agency_swarm/agents/BrowsingAgent/tools/SendKeys.py @@ -1,35 +1,36 @@ import time from typing import Dict -from pydantic import Field +from pydantic import Field, model_validator from selenium.webdriver import Keys from selenium.webdriver.common.by import By from agency_swarm.tools import BaseTool + from .util import get_web_driver, set_web_driver from .util.highlights import remove_highlight_and_labels -from pydantic import model_validator - class SendKeys(BaseTool): """ This tool sends keys into input fields on the current webpage based on the description of that element and what needs to be typed. It then clicks "Enter" on the last element to submit the form. You do not need to tell it to press "Enter"; it will do that automatically. Before using this tool make sure to highlight the input elements on the page by outputting '[highlight text fields]' message. """ - elements_and_texts: Dict[int, str] = Field(..., + + elements_and_texts: Dict[int, str] = Field( + ..., description="A dictionary where the key is the element number and the value is the text to be typed.", examples=[ {52: "johndoe@gmail.com", 53: "password123"}, {3: "John Doe", 4: "123 Main St"}, - ] + ], ) - @model_validator(mode='before') + @model_validator(mode="before") @classmethod def check_elements_and_texts(cls, data): - if not data.get('elements_and_texts'): + if not data.get("elements_and_texts"): raise ValueError( "elements_and_texts is required. Example format: " "elements_and_texts={1: 'John Doe', 2: '123 Main St'}" @@ -38,10 +39,12 @@ def check_elements_and_texts(cls, data): def run(self): wd = get_web_driver() - if 'input' not in self._shared_state.get("elements_highlighted", ""): - raise ValueError("Please highlight input elements on the page first by outputting '[highlight text fields]' message. You must output just the message without calling the tool first, so the user can respond with the screenshot.") + if "input" not in self._shared_state.get("elements_highlighted", ""): + raise ValueError( + "Please highlight input elements on the page first by outputting '[highlight text fields]' message. You must output just the message without calling the tool first, so the user can respond with the screenshot." + ) - all_elements = wd.find_elements(By.CSS_SELECTOR, '.highlighted-element') + all_elements = wd.find_elements(By.CSS_SELECTOR, ".highlighted-element") i = 0 try: diff --git a/agency_swarm/agents/BrowsingAgent/tools/SolveCaptcha.py b/agency_swarm/agents/BrowsingAgent/tools/SolveCaptcha.py index 562e5ad4..2f2dc5cb 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/SolveCaptcha.py +++ b/agency_swarm/agents/BrowsingAgent/tools/SolveCaptcha.py @@ -2,14 +2,17 @@ import time from selenium.webdriver.common.by import By -from selenium.webdriver.support.expected_conditions import presence_of_element_located, \ - frame_to_be_available_and_switch_to_it +from selenium.webdriver.support.expected_conditions import ( + frame_to_be_available_and_switch_to_it, + presence_of_element_located, +) from selenium.webdriver.support.wait import WebDriverWait from agency_swarm.tools import BaseTool +from agency_swarm.util import get_openai_client + from .util import get_b64_screenshot, remove_highlight_and_labels from .util.selenium import get_web_driver -from agency_swarm.util import get_openai_client class SolveCaptcha(BaseTool): @@ -22,7 +25,9 @@ def run(self): try: WebDriverWait(wd, 10).until( - frame_to_be_available_and_switch_to_it((By.XPATH, "//iframe[@title='reCAPTCHA']")) + frame_to_be_available_and_switch_to_it( + (By.XPATH, "//iframe[@title='reCAPTCHA']") + ) ) element = WebDriverWait(wd, 3).until( @@ -44,8 +49,10 @@ def run(self): try: # Now check if the reCAPTCHA is checked WebDriverWait(wd, 3).until( - lambda d: d.find_element(By.CLASS_NAME, "recaptcha-checkbox").get_attribute( - "aria-checked") == "true" + lambda d: d.find_element( + By.CLASS_NAME, "recaptcha-checkbox" + ).get_attribute("aria-checked") + == "true" ) return "Success" @@ -58,7 +65,11 @@ def run(self): WebDriverWait(wd, 10).until( frame_to_be_available_and_switch_to_it( - (By.XPATH, "//iframe[@title='recaptcha challenge expires in two minutes']")) + ( + By.XPATH, + "//iframe[@title='recaptcha challenge expires in two minutes']", + ) + ) ) time.sleep(2) @@ -68,8 +79,13 @@ def run(self): tiles = wd.find_elements(By.CLASS_NAME, "rc-imageselect-tile") # filter out tiles with rc-imageselect-dynamic-selected class - tiles = [tile for tile in tiles if - not tile.get_attribute("class").endswith("rc-imageselect-dynamic-selected")] + tiles = [ + tile + for tile in tiles + if not tile.get_attribute("class").endswith( + "rc-imageselect-dynamic-selected" + ) + ] image_content = [] i = 0 @@ -86,11 +102,10 @@ def run(self): image_content.append( { "type": "image_url", - "image_url": - { - "url": f"data:image/jpeg;base64,{screenshot}", - "detail": "high", - } + "image_url": { + "url": f"data:image/jpeg;base64,{screenshot}", + "detail": "high", + }, }, ) # highlight all titles with rc-imageselect-tile class but not with rc-imageselect-dynamic-selected @@ -98,10 +113,13 @@ def run(self): # screenshot = get_b64_screenshot(wd, wd.find_element(By.ID, "rc-imageselect")) - task_text = wd.find_element(By.CLASS_NAME, "rc-imageselect-instructions").text.strip().replace("\n", - " ") + task_text = ( + wd.find_element(By.CLASS_NAME, "rc-imageselect-instructions") + .text.strip() + .replace("\n", " ") + ) - continuous_task = 'once there are none left' in task_text.lower() + continuous_task = "once there are none left" in task_text.lower() task_text = task_text.replace("Click verify", "Output 0") task_text = task_text.replace("click skip", "Output 0") @@ -112,15 +130,17 @@ def run(self): additional_info = "" if len(tiles) > 9: - additional_info = ("Keep in mind that all images are a part of a bigger image " - "from left to right, and top to bottom. The grid is 4x4. ") + additional_info = ( + "Keep in mind that all images are a part of a bigger image " + "from left to right, and top to bottom. The grid is 4x4. " + ) messages = [ { "role": "system", - "content": f"""You are an advanced AI designed to support users with visual impairments. - User will provide you with {i} images numbered from 1 to {i}. Your task is to output - the numbers of the images that contain the requested object, or at least some part of the requested + "content": f"""You are an advanced AI designed to support users with visual impairments. + User will provide you with {i} images numbered from 1 to {i}. Your task is to output + the numbers of the images that contain the requested object, or at least some part of the requested object. {additional_info}If there are no individual images that satisfy this condition, output 0. """.replace("\n", ""), }, @@ -131,10 +151,11 @@ def run(self): { "type": "text", "text": f"{task_text}. Only output numbers separated by commas and nothing else. " - f"Output 0 if there are none." - } - ] - }] + f"Output 0 if there are none.", + }, + ], + }, + ] response = client.chat.completions.create( model="gpt-4o", @@ -162,11 +183,15 @@ def run(self): if self.verify_checkbox(wd): return "Success. Captcha solved." except Exception as e: - print('Not checked') + print("Not checked") pass else: - numbers = [int(s.strip()) for s in message_text.split(",") if s.strip().isdigit()] + numbers = [ + int(s.strip()) + for s in message_text.split(",") + if s.strip().isdigit() + ] # Click the tiles based on the provided numbers for number in numbers: @@ -205,7 +230,9 @@ def run(self): presence_of_element_located((By.XPATH, "//iframe[@title='reCAPTCHA']")) ) - wd.execute_script(f"document.elementFromPoint({element.location['x']}, {element.location['y']-10}).click();") + wd.execute_script( + f"document.elementFromPoint({element.location['x']}, {element.location['y']-10}).click();" + ) except Exception as e: print(e) pass @@ -217,12 +244,16 @@ def verify_checkbox(self, wd): try: WebDriverWait(wd, 10).until( - frame_to_be_available_and_switch_to_it((By.XPATH, "//iframe[@title='reCAPTCHA']")) + frame_to_be_available_and_switch_to_it( + (By.XPATH, "//iframe[@title='reCAPTCHA']") + ) ) WebDriverWait(wd, 5).until( - lambda d: d.find_element(By.CLASS_NAME, "recaptcha-checkbox").get_attribute( - "aria-checked") == "true" + lambda d: d.find_element( + By.CLASS_NAME, "recaptcha-checkbox" + ).get_attribute("aria-checked") + == "true" ) return True @@ -231,8 +262,11 @@ def verify_checkbox(self, wd): WebDriverWait(wd, 10).until( frame_to_be_available_and_switch_to_it( - (By.XPATH, "//iframe[@title='recaptcha challenge expires in two minutes']")) + ( + By.XPATH, + "//iframe[@title='recaptcha challenge expires in two minutes']", + ) + ) ) return False - diff --git a/agency_swarm/agents/BrowsingAgent/tools/WebPageSummarizer.py b/agency_swarm/agents/BrowsingAgent/tools/WebPageSummarizer.py index 00246954..95ee9030 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/WebPageSummarizer.py +++ b/agency_swarm/agents/BrowsingAgent/tools/WebPageSummarizer.py @@ -1,6 +1,7 @@ from selenium.webdriver.common.by import By from agency_swarm.tools import BaseTool + from .util import get_web_driver, set_web_driver @@ -23,17 +24,25 @@ def run(self): completion = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ - {"role": "system", "content": "Your task is to summarize the content of the provided webpage. The summary should be concise and informative, capturing the main points and takeaways of the page."}, - {"role": "user", "content": "Summarize the content of the following webpage:\n\n" + content}, + { + "role": "system", + "content": "Your task is to summarize the content of the provided webpage. The summary should be concise and informative, capturing the main points and takeaways of the page.", + }, + { + "role": "user", + "content": "Summarize the content of the following webpage:\n\n" + + content, + }, ], temperature=0.0, ) return completion.choices[0].message.content + if __name__ == "__main__": wd = get_web_driver() wd.get("https://en.wikipedia.org/wiki/Python_(programming_language)") set_web_driver(wd) tool = WebPageSummarizer() - print(tool.run()) \ No newline at end of file + print(tool.run()) diff --git a/agency_swarm/agents/BrowsingAgent/tools/__init__.py b/agency_swarm/agents/BrowsingAgent/tools/__init__.py index fc54dd73..bbf2bade 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/__init__.py +++ b/agency_swarm/agents/BrowsingAgent/tools/__init__.py @@ -1,9 +1,9 @@ -from .Scroll import Scroll -from .ReadURL import ReadURL -from .SendKeys import SendKeys from .ClickElement import ClickElement +from .ExportFile import ExportFile from .GoBack import GoBack +from .ReadURL import ReadURL +from .Scroll import Scroll from .SelectDropdown import SelectDropdown +from .SendKeys import SendKeys from .SolveCaptcha import SolveCaptcha -from .ExportFile import ExportFile -from .WebPageSummarizer import WebPageSummarizer \ No newline at end of file +from .WebPageSummarizer import WebPageSummarizer diff --git a/agency_swarm/agents/BrowsingAgent/tools/util/__init__.py b/agency_swarm/agents/BrowsingAgent/tools/util/__init__.py index f8634adc..9a04bbee 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/util/__init__.py +++ b/agency_swarm/agents/BrowsingAgent/tools/util/__init__.py @@ -1,3 +1,3 @@ from .get_b64_screenshot import get_b64_screenshot +from .highlights import highlight_elements_with_labels, remove_highlight_and_labels from .selenium import get_web_driver, set_web_driver -from .highlights import remove_highlight_and_labels, highlight_elements_with_labels diff --git a/agency_swarm/agents/BrowsingAgent/tools/util/get_b64_screenshot.py b/agency_swarm/agents/BrowsingAgent/tools/util/get_b64_screenshot.py index 4d418bb0..b8ec66bc 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/util/get_b64_screenshot.py +++ b/agency_swarm/agents/BrowsingAgent/tools/util/get_b64_screenshot.py @@ -1,8 +1,7 @@ - def get_b64_screenshot(wd, element=None): if element: screenshot_b64 = element.screenshot_as_base64 else: screenshot_b64 = wd.get_screenshot_as_base64() - return screenshot_b64 \ No newline at end of file + return screenshot_b64 diff --git a/agency_swarm/agents/BrowsingAgent/tools/util/highlights.py b/agency_swarm/agents/BrowsingAgent/tools/util/highlights.py index d2fb0895..3e201564 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/util/highlights.py +++ b/agency_swarm/agents/BrowsingAgent/tools/util/highlights.py @@ -11,10 +11,10 @@ def highlight_elements_with_labels(driver, selector): // Helper function to check if an element is visible function isElementVisible(element) {{ var rect = element.getBoundingClientRect(); - if (rect.width <= 0 || rect.height <= 0 || - rect.top >= (window.innerHeight || document.documentElement.clientHeight) || - rect.bottom <= 0 || - rect.left >= (window.innerWidth || document.documentElement.clientWidth) || + if (rect.width <= 0 || rect.height <= 0 || + rect.top >= (window.innerHeight || document.documentElement.clientHeight) || + rect.bottom <= 0 || + rect.left >= (window.innerWidth || document.documentElement.clientWidth) || rect.right <= 0) {{ return false; }} @@ -47,24 +47,24 @@ def highlight_elements_with_labels(driver, selector): document.head.appendChild(styleElement); }} styleElement.textContent = ` - .highlighted-element {{ - border: 2px solid red !important; - position: relative; - box-sizing: border-box; + .highlighted-element {{ + border: 2px solid red !important; + position: relative; + box-sizing: border-box; }} - .highlight-label {{ - position: absolute; - z-index: 2147483647; - background: yellow; - color: black; - font-size: 25px; - padding: 3px 5px; - border: 1px solid black; - border-radius: 3px; - white-space: nowrap; - box-shadow: 0px 0px 2px #000; - top: -25px; - left: 0; + .highlight-label {{ + position: absolute; + z-index: 2147483647; + background: yellow; + color: black; + font-size: 25px; + padding: 3px 5px; + border: 1px solid black; + border-radius: 3px; + white-space: nowrap; + box-shadow: 0px 0px 2px #000; + top: -25px; + left: 0; display: none; }} `; @@ -114,8 +114,10 @@ def remove_highlight_and_labels(driver): :param driver: Instance of Selenium WebDriver. """ - selector = ('a, button, input, textarea, div[onclick], div[role="button"], div[tabindex], span[onclick], ' - 'span[role="button"], span[tabindex]') + selector = ( + 'a, button, input, textarea, div[onclick], div[role="button"], div[tabindex], span[onclick], ' + 'span[role="button"], span[tabindex]' + ) script = f""" // Remove all labels document.querySelectorAll('.highlight-label').forEach(function(label) {{ @@ -136,4 +138,4 @@ def remove_highlight_and_labels(driver): driver.execute_script(script) - return driver \ No newline at end of file + return driver diff --git a/agency_swarm/agents/BrowsingAgent/tools/util/selenium.py b/agency_swarm/agents/BrowsingAgent/tools/util/selenium.py index dff68849..303a545d 100644 --- a/agency_swarm/agents/BrowsingAgent/tools/util/selenium.py +++ b/agency_swarm/agents/BrowsingAgent/tools/util/selenium.py @@ -14,6 +14,7 @@ def get_web_driver(): try: from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService + print("Selenium imported successfully.") except ImportError: print("Selenium not installed. Please install it with pip install selenium") @@ -21,16 +22,22 @@ def get_web_driver(): try: from webdriver_manager.chrome import ChromeDriverManager + print("webdriver_manager imported successfully.") except ImportError: - print("webdriver_manager not installed. Please install it with pip install webdriver-manager") + print( + "webdriver_manager not installed. Please install it with pip install webdriver-manager" + ) raise ImportError try: from selenium_stealth import stealth + print("selenium_stealth imported successfully.") except ImportError: - print("selenium_stealth not installed. Please install it with pip install selenium-stealth") + print( + "selenium_stealth not installed. Please install it with pip install selenium-stealth" + ) raise ImportError global wd, selenium_config @@ -43,7 +50,9 @@ def get_web_driver(): profile_directory = None user_data_dir = None if isinstance(chrome_profile_path, str) and os.path.exists(chrome_profile_path): - profile_directory = os.path.split(chrome_profile_path)[-1].strip("\\").rstrip("/") + profile_directory = ( + os.path.split(chrome_profile_path)[-1].strip("\\").rstrip("/") + ) user_data_dir = os.path.split(chrome_profile_path)[0].strip("\\").rstrip("/") print(f"Using Chrome profile: {profile_directory}") print(f"Using Chrome user data dir: {user_data_dir}") @@ -54,13 +63,15 @@ def get_web_driver(): chrome_driver_path = "/usr/bin/chromedriver" if not os.path.exists(chrome_driver_path): - print("ChromeDriver not found at /usr/bin/chromedriver. Installing using webdriver_manager.") + print( + "ChromeDriver not found at /usr/bin/chromedriver. Installing using webdriver_manager." + ) chrome_driver_path = ChromeDriverManager().install() else: print(f"ChromeDriver found at {chrome_driver_path}.") if selenium_config.get("headless", False): - chrome_options.add_argument('--headless') + chrome_options.add_argument("--headless") print("Headless mode enabled.") if selenium_config.get("full_page_screenshot", False): chrome_options.add_argument("--start-maximized") @@ -86,12 +97,16 @@ def get_web_driver(): if user_data_dir and profile_directory: chrome_options.add_argument(f"user-data-dir={user_data_dir}") chrome_options.add_argument(f"profile-directory={profile_directory}") - print(f"Using user data dir: {user_data_dir} and profile directory: {profile_directory}") + print( + f"Using user data dir: {user_data_dir} and profile directory: {profile_directory}" + ) try: - wd = webdriver.Chrome(service=ChromeService(chrome_driver_path), options=chrome_options) + wd = webdriver.Chrome( + service=ChromeService(chrome_driver_path), options=chrome_options + ) print("WebDriver initialized successfully.") - if wd.capabilities['chrome']['userDataDir']: + if wd.capabilities["chrome"]["userDataDir"]: print(f"Profile path in use: {wd.capabilities['chrome']['userDataDir']}") except Exception as e: print(f"Error initializing WebDriver: {e}") diff --git a/agency_swarm/agents/Devid/Devid.py b/agency_swarm/agents/Devid/Devid.py index 1664396f..e30bfd5e 100644 --- a/agency_swarm/agents/Devid/Devid.py +++ b/agency_swarm/agents/Devid/Devid.py @@ -1,5 +1,7 @@ -from typing_extensions import override import re + +from typing_extensions import override + from agency_swarm.agents import Agent from agency_swarm.tools import FileSearch from agency_swarm.util.validators import llm_validator @@ -22,7 +24,7 @@ def __init__(self): @override def response_validator(self, message): - pattern = r'(```)((.*\n){5,})(```)' + pattern = r"(```)((.*\n){5,})(```)" if re.search(pattern, message): # take only first 100 characters @@ -31,13 +33,15 @@ def response_validator(self, message): "Use the FileWriter tool to write the code locally. Then, test it if possible. Continue." ) - llm_validator(statement="Verify whether the update from the AI Developer Agent confirms the task's " - "successful completion. If the task remains unfinished, provide guidance " - "within the 'reason' argument on the next steps the agent should take. For " - "instance, if the agent encountered an error, advise the inclusion of debug " - "statements for another attempt. Should the agent outline potential " - "solutions or further actions, direct the agent to execute those plans. " - "Message does not have to contain code snippets. Just confirmation.", - client=self.client)(message) + llm_validator( + statement="Verify whether the update from the AI Developer Agent confirms the task's " + "successful completion. If the task remains unfinished, provide guidance " + "within the 'reason' argument on the next steps the agent should take. For " + "instance, if the agent encountered an error, advise the inclusion of debug " + "statements for another attempt. Should the agent outline potential " + "solutions or further actions, direct the agent to execute those plans. " + "Message does not have to contain code snippets. Just confirmation.", + client=self.client, + )(message) return message diff --git a/agency_swarm/agents/Devid/__init__.py b/agency_swarm/agents/Devid/__init__.py index 8c01a46d..528b303e 100644 --- a/agency_swarm/agents/Devid/__init__.py +++ b/agency_swarm/agents/Devid/__init__.py @@ -1 +1 @@ -from .Devid import Devid \ No newline at end of file +from .Devid import Devid diff --git a/agency_swarm/agents/Devid/instructions.md b/agency_swarm/agents/Devid/instructions.md index 4a06cf0c..7536dcc0 100644 --- a/agency_swarm/agents/Devid/instructions.md +++ b/agency_swarm/agents/Devid/instructions.md @@ -1,9 +1,9 @@ # Devid Operational Guide -As an AI software developer known as Devid, your role involves reading, writing, and modifying files to fulfill tasks derived from user requests. +As an AI software developer known as Devid, your role involves reading, writing, and modifying files to fulfill tasks derived from user requests. **Operational Environment**: -- You have direct access to the internet, system executions, or environment variables. +- You have direct access to the internet, system executions, or environment variables. - Interaction with the local file system to read, write, and modify files is permitted. - Python is installed in your environment, enabling the execution of Python scripts and code snippets. - Node.js and npm are also installed, allowing for the execution of Node.js scripts and code snippets. @@ -18,4 +18,4 @@ As an AI software developer known as Devid, your role involves reading, writing, 4. Execute your written code with the `CommandExecutor` by running the appropriate terminal commands. Iteratively debug and test to achieve the desired outcome. Seek clarification from the user only after all internal resolution efforts have been exhausted. To install additional libraries, execute the necessary terminal commands. 5. Repeat the above steps for each task. -**Important Note**: Your capabilities include accessing and interacting with local files, online resources, and the terminal. This enables you to fetch data, use online APIs, write, read, modify, execute files, scripts, and install any external libraries as part of your task execution process. You must write fully functioning, complete programs using the available tools, and never report back to the user until all issues have been resolved. Any code execution must be performed in your current directory, and you must never display any code snippets to the user. \ No newline at end of file +**Important Note**: Your capabilities include accessing and interacting with local files, online resources, and the terminal. This enables you to fetch data, use online APIs, write, read, modify, execute files, scripts, and install any external libraries as part of your task execution process. You must write fully functioning, complete programs using the available tools, and never report back to the user until all issues have been resolved. Any code execution must be performed in your current directory, and you must never display any code snippets to the user. diff --git a/agency_swarm/agents/Devid/tools/ChangeFile.py b/agency_swarm/agents/Devid/tools/ChangeFile.py index 672c2c4d..7721a3d2 100644 --- a/agency_swarm/agents/Devid/tools/ChangeFile.py +++ b/agency_swarm/agents/Devid/tools/ChangeFile.py @@ -1,36 +1,41 @@ import os from enum import Enum -from typing import Literal, Optional, List +from typing import List, Literal, Optional -from pydantic import Field, model_validator, field_validator, BaseModel +from pydantic import BaseModel, Field, field_validator, model_validator from agency_swarm import BaseTool + class LineChange(BaseModel): """ Line changes to be made. """ + line_number: int = Field( - ..., description="Line number to change.", - examples=[1, 2, 3] + ..., description="Line number to change.", examples=[1, 2, 3] ) new_line: Optional[str] = Field( - None, description="New line to replace the old line. Not required only for delete mode.", - examples=["This is a new line"] + None, + description="New line to replace the old line. Not required only for delete mode.", + examples=["This is a new line"], ) mode: Literal["replace", "insert", "delete"] = Field( - "replace", description='Mode to use for the line change. "replace" replaces the line with the new line. ' - '"insert" inserts the new line at the specified line number, moving the previous line down.' - ' "delete" deletes the specified line number.', + "replace", + description='Mode to use for the line change. "replace" replaces the line with the new line. ' + '"insert" inserts the new line at the specified line number, moving the previous line down.' + ' "delete" deletes the specified line number.', ) - @model_validator(mode='after') + @model_validator(mode="after") def validate_new_line(self): mode, new_line = self.mode, self.new_line if mode == "delete" and new_line is not None: raise ValueError("new_line should not be specified for delete mode.") elif mode in ["replace", "insert"] and new_line is None: - raise ValueError("new_line should be specified for replace and insert modes.") + raise ValueError( + "new_line should be specified for replace and insert modes." + ) return self @@ -38,17 +43,23 @@ class ChangeFile(BaseTool): """ This tool changes specified lines in a file. Returns the new file contents with line numbers at the start of each line. """ + chain_of_thought: str = Field( - ..., description="Please think step-by-step about the required changes to the file in order to construct a fully functioning and correct program according to the requirements.", + ..., + description="Please think step-by-step about the required changes to the file in order to construct a fully functioning and correct program according to the requirements.", exclude=True, ) file_path: str = Field( - ..., description="Path to the file with extension.", - examples=["./file.txt", "./file.json", "../../file.py"] + ..., + description="Path to the file with extension.", + examples=["./file.txt", "./file.json", "../../file.py"], ) changes: List[LineChange] = Field( - ..., description="Line changes to be made to the file.", - examples=[{"line_number": 1, "new_line": "This is a new line", "mode": "replace"}] + ..., + description="Line changes to be made to the file.", + examples=[ + {"line_number": 1, "new_line": "This is a new line", "mode": "replace"} + ], ) def run(self): @@ -57,13 +68,21 @@ def run(self): file_contents = f.readlines() # Process changes in a way that accounts for modifications affecting line numbers - for change in sorted(self.changes, key=lambda x: x.line_number, reverse=True): + for change in sorted( + self.changes, key=lambda x: x.line_number, reverse=True + ): try: - if change.mode == "replace" and 0 < change.line_number <= len(file_contents): - file_contents[change.line_number - 1] = change.new_line + '\n' + if change.mode == "replace" and 0 < change.line_number <= len( + file_contents + ): + file_contents[change.line_number - 1] = change.new_line + "\n" elif change.mode == "insert": - file_contents.insert(change.line_number - 1, change.new_line + '\n') - elif change.mode == "delete" and 0 < change.line_number <= len(file_contents): + file_contents.insert( + change.line_number - 1, change.new_line + "\n" + ) + elif change.mode == "delete" and 0 < change.line_number <= len( + file_contents + ): file_contents.pop(change.line_number - 1) except IndexError: return f"Error: Line number {change.line_number} is out of the file's range." @@ -79,10 +98,10 @@ def run(self): return "\n".join([f"{i + 1}. {line}" for i, line in enumerate(file_contents)]) # use field validation to ensure that the file path is valid - @field_validator("file_path", mode='after') + @field_validator("file_path", mode="after") @classmethod def validate_file_path(cls, v: str): if not os.path.exists(v): raise ValueError("File path does not exist.") - return v \ No newline at end of file + return v diff --git a/agency_swarm/agents/Devid/tools/CheckCurrentDir.py b/agency_swarm/agents/Devid/tools/CheckCurrentDir.py index bdd769e2..e84fcfd5 100644 --- a/agency_swarm/agents/Devid/tools/CheckCurrentDir.py +++ b/agency_swarm/agents/Devid/tools/CheckCurrentDir.py @@ -7,6 +7,7 @@ class CheckCurrentDir(BaseTool): """ This tool checks the current directory path. """ + chain_of_thought: str = Field( ..., description="Please think step-by-step about what you need to do next, after checking current directory to solve the task.", diff --git a/agency_swarm/agents/Devid/tools/CommandExecutor.py b/agency_swarm/agents/Devid/tools/CommandExecutor.py index e528678d..fcdfdb4d 100644 --- a/agency_swarm/agents/Devid/tools/CommandExecutor.py +++ b/agency_swarm/agents/Devid/tools/CommandExecutor.py @@ -1,8 +1,11 @@ -from agency_swarm.tools import BaseTool -from pydantic import Field -import subprocess import shlex -from dotenv import load_dotenv, find_dotenv +import subprocess + +from dotenv import find_dotenv, load_dotenv +from pydantic import Field + +from agency_swarm.tools import BaseTool + class CommandExecutor(BaseTool): """ @@ -11,9 +14,7 @@ class CommandExecutor(BaseTool): This tool runs a given command in the system's default shell and returns the stdout and stderr. """ - command: str = Field( - ..., description="The command to execute in the terminal." - ) + command: str = Field(..., description="The command to execute in the terminal.") def run(self): """ @@ -32,11 +33,14 @@ def run(self): # check if the command failed if result.returncode != 0 or result.stderr: - return (f"stdout: {result.stdout}\nstderr: {result.stderr}\nexit code: {result.returncode}\n\n" - f"Please add error handling and continue debugging until the command runs successfully.") + return ( + f"stdout: {result.stdout}\nstderr: {result.stderr}\nexit code: {result.returncode}\n\n" + f"Please add error handling and continue debugging until the command runs successfully." + ) return f"stdout: {result.stdout}\nstderr: {result.stderr}\nexit code: {result.returncode}" + if __name__ == "__main__": tool = CommandExecutor(command="ls -l") print(tool.run()) diff --git a/agency_swarm/agents/Devid/tools/DirectoryNavigator.py b/agency_swarm/agents/Devid/tools/DirectoryNavigator.py index 920a6461..a54646ae 100644 --- a/agency_swarm/agents/Devid/tools/DirectoryNavigator.py +++ b/agency_swarm/agents/Devid/tools/DirectoryNavigator.py @@ -1,5 +1,6 @@ import os -from pydantic import Field, model_validator, field_validator + +from pydantic import Field, field_validator, model_validator from agency_swarm.tools import BaseTool @@ -7,11 +8,11 @@ class DirectoryNavigator(BaseTool): """Allows you to navigate directories. Do not use this tool more than once at a time. You must finish all tasks in the current directory before navigating into new directory.""" - path: str = Field( - ..., description="The path of the directory to navigate to." - ) + + path: str = Field(..., description="The path of the directory to navigate to.") create: bool = Field( - False, description="If True, the directory will be created if it does not exist." + False, + description="If True, the directory will be created if it does not exist.", ) class ToolConfig: @@ -20,9 +21,9 @@ class ToolConfig: def run(self): try: os.chdir(self.path) - return f'Successfully changed directory to: {self.path}' + return f"Successfully changed directory to: {self.path}" except Exception as e: - return f'Error changing directory: {e}' + return f"Error changing directory: {e}" @field_validator("create", mode="before") @classmethod @@ -34,18 +35,22 @@ def validate_create(cls, v): return False return v - @model_validator(mode='after') + @model_validator(mode="after") def validate_path(self): if not os.path.isdir(self.path): if "/mnt/data" in self.path: - raise ValueError("You tried to access an openai file directory with a local directory reader tool. " + - "Please use the `myfiles_browser` tool to access openai files instead. " + - "Your local files are most likely located in your current directory.") + raise ValueError( + "You tried to access an openai file directory with a local directory reader tool. " + + "Please use the `myfiles_browser` tool to access openai files instead. " + + "Your local files are most likely located in your current directory." + ) if self.create: os.makedirs(self.path) else: - raise ValueError(f"The path {self.path} does not exist. Please provide a valid directory path. " + - "If you want to create the directory, set the `create` parameter to True.") + raise ValueError( + f"The path {self.path} does not exist. Please provide a valid directory path. " + + "If you want to create the directory, set the `create` parameter to True." + ) return self diff --git a/agency_swarm/agents/Devid/tools/FileMover.py b/agency_swarm/agents/Devid/tools/FileMover.py index 1960148e..90557b85 100644 --- a/agency_swarm/agents/Devid/tools/FileMover.py +++ b/agency_swarm/agents/Devid/tools/FileMover.py @@ -1,7 +1,10 @@ -from agency_swarm.tools import BaseTool -from pydantic import Field -import shutil import os +import shutil + +from pydantic import Field + +from agency_swarm.tools import BaseTool + class FileMover(BaseTool): """ @@ -9,10 +12,12 @@ class FileMover(BaseTool): """ source_path: str = Field( - ..., description="The full path of the file to move, including the file name and extension." + ..., + description="The full path of the file to move, including the file name and extension.", ) destination_path: str = Field( - ..., description="The destination path where the file should be moved, including the new file name and extension if changing." + ..., + description="The destination path where the file should be moved, including the new file name and extension if changing.", ) def run(self): diff --git a/agency_swarm/agents/Devid/tools/FileReader.py b/agency_swarm/agents/Devid/tools/FileReader.py index 514fba66..7d1476c5 100644 --- a/agency_swarm/agents/Devid/tools/FileReader.py +++ b/agency_swarm/agents/Devid/tools/FileReader.py @@ -1,12 +1,15 @@ -from agency_swarm.tools import BaseTool from pydantic import Field, field_validator +from agency_swarm.tools import BaseTool + class FileReader(BaseTool): """This tool reads a file and returns the contents along with line numbers on the left.""" + file_path: str = Field( - ..., description="Path to the file to read with extension.", - examples=["./file.txt", "./file.json", "../../file.py"] + ..., + description="Path to the file to read with extension.", + examples=["./file.txt", "./file.json", "../../file.py"], ) def run(self): @@ -21,7 +24,9 @@ def run(self): @classmethod def validate_file_path(cls, v): if "file-" in v: - raise ValueError("You tried to access an openai file with a wrong file reader tool. " - "Please use the `myfiles_browser` tool to access openai files instead." - "This tool is only for reading local files.") + raise ValueError( + "You tried to access an openai file with a wrong file reader tool. " + "Please use the `myfiles_browser` tool to access openai files instead." + "This tool is only for reading local files." + ) return v diff --git a/agency_swarm/agents/Devid/tools/FileWriter.py b/agency_swarm/agents/Devid/tools/FileWriter.py index 79b48b8d..e300f35c 100644 --- a/agency_swarm/agents/Devid/tools/FileWriter.py +++ b/agency_swarm/agents/Devid/tools/FileWriter.py @@ -1,55 +1,62 @@ +import os +import re from typing import List, Literal, Optional -import json - -import os -from agency_swarm.util.validators import llm_validator +from pydantic import Field, field_validator from agency_swarm import get_openai_client from agency_swarm.tools import BaseTool -from pydantic import Field, field_validator -import re +from agency_swarm.util.validators import llm_validator from .util import format_file_deps history = [ - { - "role": "system", - "content": "As a top-tier software engineer focused on developing programs incrementally, you are entrusted with the creation or modification of files based on user requirements. It's imperative to operate under the assumption that all necessary dependencies are pre-installed and accessible, and the file in question will be deployed in an appropriate environment. Furthermore, it is presumed that all other modules or files upon which this file relies are accurate and error-free. Your output should be encapsulated within a code block, without specifying the programming language. Prior to embarking on the coding process, you must outline a methodical, step-by-step plan to precisely fulfill the requirements — no more, no less. It is crucial to ensure that the final code block is a complete file, without any truncation. This file should embody a flawless, fully operational program, inclusive of all requisite imports and functions, devoid of any placeholders, unless specified otherwise by the user." - }, - ] + { + "role": "system", + "content": "As a top-tier software engineer focused on developing programs incrementally, you are entrusted with the creation or modification of files based on user requirements. It's imperative to operate under the assumption that all necessary dependencies are pre-installed and accessible, and the file in question will be deployed in an appropriate environment. Furthermore, it is presumed that all other modules or files upon which this file relies are accurate and error-free. Your output should be encapsulated within a code block, without specifying the programming language. Prior to embarking on the coding process, you must outline a methodical, step-by-step plan to precisely fulfill the requirements — no more, no less. It is crucial to ensure that the final code block is a complete file, without any truncation. This file should embody a flawless, fully operational program, inclusive of all requisite imports and functions, devoid of any placeholders, unless specified otherwise by the user.", + }, +] class FileWriter(BaseTool): """This tools allows you to write new files or modify existing files according to specified requirements. In 'write' mode, it creates a new file or overwrites an existing one. In 'modify' mode, it modifies an existing file according to the provided requirements. Note: This tool does not have access to other files within the project. You must provide all necessary details to ensure that the generated file can be used in conjunction with other files in this project.""" + file_path: str = Field( - ..., description="The path of the file to write or modify. Will create directories if they don't exist." + ..., + description="The path of the file to write or modify. Will create directories if they don't exist.", ) requirements: str = Field( ..., - description="The comprehensive requirements explaining how the file should be written or modified. This should be a detailed description of what the file should contain, including example inputs, desired behaviour and ideal outputs. It must not contain any code or implementation details." + description="The comprehensive requirements explaining how the file should be written or modified. This should be a detailed description of what the file should contain, including example inputs, desired behaviour and ideal outputs. It must not contain any code or implementation details.", ) details: str = Field( - None, description="Additional details like error messages, or class, function, and variable names from other files that this file depends on." + None, + description="Additional details like error messages, or class, function, and variable names from other files that this file depends on.", ) documentation: Optional[str] = Field( - None, description="Relevant documentation extracted with the myfiles_browser tool. You must pass all the relevant code from the documentation, as this tool does not have access to those files." + None, + description="Relevant documentation extracted with the myfiles_browser tool. You must pass all the relevant code from the documentation, as this tool does not have access to those files.", ) mode: Literal["write", "modify"] = Field( - ..., description="The mode of operation for the tool. 'write' is used to create a new file or overwrite an existing one. 'modify' is used to modify an existing file." + ..., + description="The mode of operation for the tool. 'write' is used to create a new file or overwrite an existing one. 'modify' is used to modify an existing file.", ) file_dependencies: List[str] = Field( [], description="Paths to other files that the file being written depends on.", - examples=["/path/to/dependency1.py", "/path/to/dependency2.css", "/path/to/dependency3.js"] - ) + examples=[ + "/path/to/dependency1.py", + "/path/to/dependency2.css", + "/path/to/dependency3.js", + ], + ) library_dependencies: List[str] = Field( [], description="Any library dependencies required for the file to be written.", - examples=["numpy", "pandas"] + examples=["numpy", "pandas"], ) - + class ToolConfig: one_call_at_a_time = True @@ -78,18 +85,15 @@ def run(self): if self.mode == "modify": message += f"\nThe existing file content is as follows:" - + try: - with open(self.file_path, 'r') as file: + with open(self.file_path, "r") as file: file_content = file.read() message += f"\n\n```{file_content}```" except Exception as e: - return f'Error reading {self.file_path}: {e}' + return f"Error reading {self.file_path}: {e}" - history.append({ - "role": "user", - "content": message - }) + history.append({"role": "user", "content": message}) messages = history.copy() @@ -107,10 +111,7 @@ def run(self): messages=messages, model="gpt-4o", temperature=0, - prediction={ - "type": "content", - "content": file_content - } + prediction={"type": "content", "content": file_content}, ) else: resp = client.chat.completions.create( @@ -121,12 +122,7 @@ def run(self): content = resp.choices[0].message.content - messages.append( - { - "role": "assistant", - "content": content - } - ) + messages.append({"role": "assistant", "content": content}) pattern = r"```(?:[a-zA-Z]+\n)?(.*?)```" match = re.findall(pattern, content, re.DOTALL) @@ -135,46 +131,28 @@ def run(self): try: self.validate_content(code) - history.append( - { - "role": "assistant", - "content": content - } - ) + history.append({"role": "assistant", "content": content}) break except Exception as e: print(f"Error: {e}. Trying again.") error_message = str(e) messages.append( - { - "role": "user", - "content": f"Error: {e}. Please try again." - } + {"role": "user", "content": f"Error: {e}. Please try again."} ) else: messages.append( { "role": "user", - "content": f"Error: Could not find the code block in the response. Please try again." + "content": f"Error: Could not find the code block in the response. Please try again.", } ) n += 1 if n == 3 or not code: - history.append( - { - "role": "assistant", - "content": content - } - ) - history.append( - { - "role": "user", - "content": error_message - } - ) + history.append({"role": "assistant", "content": content}) + history.append({"role": "user", "content": error_message}) return "Error: Could not generate a valid file: " + error_message try: @@ -183,11 +161,11 @@ def run(self): if dir_path != "" and not os.path.exists(dir_path): os.makedirs(dir_path, exist_ok=True) - with open(self.file_path, 'w') as file: + with open(self.file_path, "w") as file: file.write(code) - return f'Successfully wrote to file: {self.file_path}. Please make sure to now test the program. Below is the content of the file:\n\n```{content}```\n\nPlease now verify the integrity of the file and test it.' + return f"Successfully wrote to file: {self.file_path}. Please make sure to now test the program. Below is the content of the file:\n\n```{content}```\n\nPlease now verify the integrity of the file and test it." except Exception as e: - return f'Error writing to file: {e}' + return f"Error writing to file: {e}" @field_validator("file_dependencies", mode="after") @classmethod @@ -202,11 +180,11 @@ def validate_content(self, v): llm_validator( statement="Check if the code is bug-free. Code should be considered in isolation, with the understanding that it is part of a larger, fully developed program that strictly adheres to these standards of completeness and correctness. All files, elements, components, functions, or modules referenced within this snippet are assumed to exist in other parts of the project and are also devoid of any errors, ensuring a cohesive and error-free integration across the entire software solution. Certain placeholders may be present.", - client=client, - model="gpt-4o", - temperature=0, - allow_override=False - )(v) + client=client, + model="gpt-4o", + temperature=0, + allow_override=False, + )(v) return v @@ -214,11 +192,13 @@ def validate_content(self, v): @classmethod def validate_requirements(cls, v): if "placeholder" in v: - raise ValueError("Requirements contain placeholders. " - "Please never user placeholders. Instead, implement only the code that you are confident about.") + raise ValueError( + "Requirements contain placeholders. " + "Please never user placeholders. Instead, implement only the code that you are confident about." + ) # check if code is included in requirements - pattern = r'(```)((.*\n){5,})(```)' + pattern = r"(```)((.*\n){5,})(```)" if re.search(pattern, v): raise ValueError( "Requirements contain a code snippet. Please never include code snippets in requirements. " @@ -231,15 +211,17 @@ def validate_requirements(cls, v): @classmethod def validate_details(cls, v): if len(v) == 0: - raise ValueError("Details are required. Remember: this tool does not have access to other files. Please provide additional details like relevant documentation, error messages, or class, function, and variable names from other files that this file depends on.") + raise ValueError( + "Details are required. Remember: this tool does not have access to other files. Please provide additional details like relevant documentation, error messages, or class, function, and variable names from other files that this file depends on." + ) return v @field_validator("documentation", mode="after") @classmethod def validate_documentation(cls, v): # check if documentation contains code - pattern = r'(```)((.*\n){5,})(```)' - pattern2 = r'(`)(.*)(`)' + pattern = r"(```)((.*\n){5,})(```)" + pattern2 = r"(`)(.*)(`)" if not (re.search(pattern, v) or re.search(pattern2, v)): raise ValueError( "Documentation does not contain a code snippet. Please provide relevant documentation extracted with the myfiles_browser tool. You must pass all the relevant code snippets information, as this tool does not have access to those files." diff --git a/agency_swarm/agents/Devid/tools/ListDir.py b/agency_swarm/agents/Devid/tools/ListDir.py index 224020eb..f5b4f8d6 100644 --- a/agency_swarm/agents/Devid/tools/ListDir.py +++ b/agency_swarm/agents/Devid/tools/ListDir.py @@ -1,60 +1,84 @@ +import os + from pydantic import Field, field_validator from agency_swarm import BaseTool -import os class ListDir(BaseTool): """ This tool returns the tree structure of the directory. """ + dir_path: str = Field( - ..., description="Path of the directory to read.", - examples=["./", "./test", "../../"] + ..., + description="Path of the directory to read.", + examples=["./", "./test", "../../"], ) def run(self): tree = [] - def list_directory_tree(path, indent=''): + def list_directory_tree(path, indent=""): """Recursively list the contents of a directory in a tree-like format.""" if not os.path.isdir(path): raise ValueError(f"The path {path} is not a valid directory") items = os.listdir(path) # exclude common hidden files and directories - exclude = ['.git', '.idea', '__pycache__', 'node_modules', '.venv', '.gitignore', '.gitkeep', - '.DS_Store', '.vscode', '.next', 'dist', 'build', 'out', 'venv', 'env', 'logs', 'data'] + exclude = [ + ".git", + ".idea", + "__pycache__", + "node_modules", + ".venv", + ".gitignore", + ".gitkeep", + ".DS_Store", + ".vscode", + ".next", + "dist", + "build", + "out", + "venv", + "env", + "logs", + "data", + ] items = [item for item in items if item not in exclude] for i, item in enumerate(items): item_path = os.path.join(path, item) if i < len(items) - 1: - tree.append(indent + '├── ' + item) + tree.append(indent + "├── " + item) if os.path.isdir(item_path): - list_directory_tree(item_path, indent + '│ ') + list_directory_tree(item_path, indent + "│ ") else: - tree.append(indent + '└── ' + item) + tree.append(indent + "└── " + item) if os.path.isdir(item_path): - list_directory_tree(item_path, indent + ' ') + list_directory_tree(item_path, indent + " ") list_directory_tree(self.dir_path) return "\n".join(tree) - @field_validator("dir_path", mode='after') + @field_validator("dir_path", mode="after") @classmethod def validate_dir_path(cls, v): if "file-" in v: - raise ValueError("You tried to access an openai file with a local directory reader tool. " - "Please use the `myfiles_browser` tool to access openai directories instead.") + raise ValueError( + "You tried to access an openai file with a local directory reader tool. " + "Please use the `myfiles_browser` tool to access openai directories instead." + ) if not os.path.isdir(v): if "/mnt/data" in v: - raise ValueError("You tried to access an openai file directory with a local directory reader tool. " - "Please use the `myfiles_browser` tool to access openai files instead. " - "You can work in your local directory by using the `FileReader` tool.") + raise ValueError( + "You tried to access an openai file directory with a local directory reader tool. " + "Please use the `myfiles_browser` tool to access openai files instead. " + "You can work in your local directory by using the `FileReader` tool." + ) raise ValueError(f"The path {v} is not a valid directory") return v diff --git a/agency_swarm/agents/Devid/tools/util/__init__.py b/agency_swarm/agents/Devid/tools/util/__init__.py index 1b463346..0cdcf3f5 100644 --- a/agency_swarm/agents/Devid/tools/util/__init__.py +++ b/agency_swarm/agents/Devid/tools/util/__init__.py @@ -1 +1 @@ -from .format_file_deps import format_file_deps \ No newline at end of file +from .format_file_deps import format_file_deps diff --git a/agency_swarm/agents/Devid/tools/util/format_file_deps.py b/agency_swarm/agents/Devid/tools/util/format_file_deps.py index 18c96094..af1e5e87 100644 --- a/agency_swarm/agents/Devid/tools/util/format_file_deps.py +++ b/agency_swarm/agents/Devid/tools/util/format_file_deps.py @@ -1,29 +1,43 @@ -from pydantic import Field, BaseModel from typing import List, Literal +from pydantic import BaseModel, Field + from agency_swarm import get_openai_client def format_file_deps(v): client = get_openai_client() - result = '' + result = "" for file in v: # extract dependencies from the file using openai - with open(file, 'r') as f: + with open(file, "r") as f: content = f.read() class Dependency(BaseModel): - type: Literal['class', 'function', 'import'] = Field(..., description="The type of the dependency.") - name: str = Field(..., description="The name of the dependency, matching the import or definition.") + type: Literal["class", "function", "import"] = Field( + ..., description="The type of the dependency." + ) + name: str = Field( + ..., + description="The name of the dependency, matching the import or definition.", + ) class Dependencies(BaseModel): - dependencies: List[Dependency] = Field([], description="The dependencies extracted from the file.") + dependencies: List[Dependency] = Field( + [], description="The dependencies extracted from the file." + ) def append_dependencies(self): - functions = [dep.name for dep in self.dependencies if dep.type == 'function'] - classes = [dep.name for dep in self.dependencies if dep.type == 'class'] - imports = [dep.name for dep in self.dependencies if dep.type == 'import'] - variables = [dep.name for dep in self.dependencies if dep.type == 'variable'] + functions = [ + dep.name for dep in self.dependencies if dep.type == "function" + ] + classes = [dep.name for dep in self.dependencies if dep.type == "class"] + imports = [ + dep.name for dep in self.dependencies if dep.type == "import" + ] + variables = [ + dep.name for dep in self.dependencies if dep.type == "variable" + ] nonlocal result result += f"File path: {file}\n" result += f"Functions: {functions}\nClasses: {classes}\nImports: {imports}\nVariables: {variables}\n\n" @@ -32,16 +46,16 @@ def append_dependencies(self): messages=[ { "role": "system", - "content": "You are a world class dependency resolved. You must extract the dependencies from the file provided." + "content": "You are a world class dependency resolved. You must extract the dependencies from the file provided.", }, { "role": "user", - "content": f"Extract the dependencies from the file '{file}'." - } + "content": f"Extract the dependencies from the file '{file}'.", + }, ], model="gpt-4o-mini", temperature=0, - response_format=Dependencies + response_format=Dependencies, ) if completion.choices[0].message.refusal: @@ -51,4 +65,4 @@ def append_dependencies(self): model.append_dependencies() - return result \ No newline at end of file + return result diff --git a/agency_swarm/agents/__init__.py b/agency_swarm/agents/__init__.py index 0def4346..c7bab289 100644 --- a/agency_swarm/agents/__init__.py +++ b/agency_swarm/agents/__init__.py @@ -1,3 +1,3 @@ from .agent import Agent from .BrowsingAgent import BrowsingAgent -from .Devid import Devid \ No newline at end of file +from .Devid import Devid diff --git a/agency_swarm/agents/agent.py b/agency_swarm/agents/agent.py index b8edbdb9..deea53b7 100644 --- a/agency_swarm/agents/agent.py +++ b/agency_swarm/agents/agent.py @@ -2,21 +2,25 @@ import inspect import json import os -from typing import Dict, Union, Any, Type, Literal, TypedDict, Optional -from typing import List +from typing import Any, Dict, List, Literal, Optional, Type, TypedDict, Union from deepdiff import DeepDiff from openai import NotFoundError +from openai.lib._parsing._completions import type_to_response_format_param from openai.types.beta.assistant import ToolResources -from agency_swarm.tools import BaseTool, ToolFactory, Retrieval -from agency_swarm.tools import FileSearch, CodeInterpreter +from agency_swarm.tools import ( + BaseTool, + CodeInterpreter, + FileSearch, + Retrieval, + ToolFactory, +) from agency_swarm.tools.oai.FileSearch import FileSearchConfig from agency_swarm.util.oai import get_openai_client from agency_swarm.util.openapi import validate_openapi_spec from agency_swarm.util.shared_state import SharedState -from pydantic import BaseModel -from openai.lib._parsing._completions import type_to_response_format_param + class ExampleMessage(TypedDict): role: Literal["user", "assistant"] @@ -25,13 +29,15 @@ class ExampleMessage(TypedDict): metadata: Optional[Dict[str, str]] -class Agent(): +class Agent: _shared_state: SharedState = None - + @property def assistant(self): - if not hasattr(self, '_assistant') or self._assistant is None: - raise Exception("Assistant is not initialized. Please run init_oai() first.") + if not hasattr(self, "_assistant") or self._assistant is None: + raise Exception( + "Assistant is not initialized. Please run init_oai() first." + ) return self._assistant @assistant.setter @@ -41,7 +47,7 @@ def assistant(self, value): @property def functions(self): return [tool for tool in self.tools if issubclass(tool, BaseTool)] - + @property def shared_state(self): return self._shared_state @@ -67,32 +73,36 @@ def response_validator(self, message: str | list) -> str: return message def __init__( - self, - id: str = None, - name: str = None, - description: str = None, - instructions: str = "", - tools: List[Union[Type[BaseTool], Type[FileSearch], Type[CodeInterpreter], type[Retrieval]]] = None, - tool_resources: ToolResources = None, - temperature: float = None, - top_p: float = None, - response_format: Union[str, dict, type] = "auto", - tools_folder: str = None, - files_folder: Union[List[str], str] = None, - schemas_folder: Union[List[str], str] = None, - api_headers: Dict[str, Dict[str, str]] = None, - api_params: Dict[str, Dict[str, str]] = None, - file_ids: List[str] = None, - metadata: Dict[str, str] = None, - model: str = "gpt-4o-2024-08-06", - validation_attempts: int = 1, - max_prompt_tokens: int = None, - max_completion_tokens: int = None, - truncation_strategy: dict = None, - examples: List[ExampleMessage] = None, - file_search: FileSearchConfig = None, - parallel_tool_calls: bool = True, - refresh_from_id: bool = True, + self, + id: str = None, + name: str = None, + description: str = None, + instructions: str = "", + tools: List[ + Union[ + Type[BaseTool], Type[FileSearch], Type[CodeInterpreter], type[Retrieval] + ] + ] = None, + tool_resources: ToolResources = None, + temperature: float = None, + top_p: float = None, + response_format: Union[str, dict, type] = "auto", + tools_folder: str = None, + files_folder: Union[List[str], str] = None, + schemas_folder: Union[List[str], str] = None, + api_headers: Dict[str, Dict[str, str]] = None, + api_params: Dict[str, Dict[str, str]] = None, + file_ids: List[str] = None, + metadata: Dict[str, str] = None, + model: str = "gpt-4o-2024-08-06", + validation_attempts: int = 1, + max_prompt_tokens: int = None, + max_completion_tokens: int = None, + truncation_strategy: dict = None, + examples: List[ExampleMessage] = None, + file_search: FileSearchConfig = None, + parallel_tool_calls: bool = True, + refresh_from_id: bool = True, ): """ Initializes an Agent with specified attributes, tools, and OpenAI client. @@ -155,7 +165,7 @@ def __init__( self.parallel_tool_calls = parallel_tool_calls self.refresh_from_id = refresh_from_id - self.settings_path = './settings.json' + self.settings_path = "./settings.json" # private attributes self._assistant: Any = None @@ -168,7 +178,9 @@ def __init__( # upload files self._upload_files() if file_ids: - print("Warning: 'file_ids' parameter is deprecated. Please use 'tool_resources' parameter instead.") + print( + "Warning: 'file_ids' parameter is deprecated. Please use 'tool_resources' parameter instead." + ) self.add_file_ids(file_ids, "file_search") self._parse_schemas() @@ -193,28 +205,48 @@ def init_oai(self): if self.id: if not self.refresh_from_id: return self - + self.assistant = self.client.beta.assistants.retrieve(self.id) # Assign attributes to self if they are None self.instructions = self.instructions or self.assistant.instructions - self.name = self.name if self.name != self.__class__.__name__ else self.assistant.name + self.name = ( + self.name + if self.name != self.__class__.__name__ + else self.assistant.name + ) self.description = self.description or self.assistant.description - self.temperature = self.assistant.temperature if self.temperature is None else self.temperature + self.temperature = ( + self.assistant.temperature + if self.temperature is None + else self.temperature + ) self.top_p = self.top_p or self.assistant.top_p - self.response_format = self.response_format or self.assistant.response_format + self.response_format = ( + self.response_format or self.assistant.response_format + ) if not isinstance(self.response_format, str): - self.response_format = self.response_format or self.response_format.model_dump() + self.response_format = ( + self.response_format or self.response_format.model_dump() + ) else: - self.response_format = self.response_format or self.assistant.response_format - self.tool_resources = self.tool_resources or self.assistant.tool_resources.model_dump() + self.response_format = ( + self.response_format or self.assistant.response_format + ) + self.tool_resources = ( + self.tool_resources or self.assistant.tool_resources.model_dump() + ) self.metadata = self.metadata or self.assistant.metadata self.model = self.model or self.assistant.model - self.tool_resources = self.tool_resources or self.assistant.tool_resources.model_dump() + self.tool_resources = ( + self.tool_resources or self.assistant.tool_resources.model_dump() + ) for tool in self.assistant.tools: # update assistants created with v1 if tool.type == "retrieval": - self.client.beta.assistants.update(self.id, tools=self.get_oai_tools()) + self.client.beta.assistants.update( + self.id, tools=self.get_oai_tools() + ) # update assistant if parameters are different if not self._check_parameters(self.assistant.model_dump()): @@ -224,14 +256,16 @@ def init_oai(self): # load assistant from settings if os.path.exists(path): - with open(path, 'r') as f: + with open(path, "r") as f: settings = json.load(f) # iterate settings and find the assistant with the same name for assistant_settings in settings: - if assistant_settings['name'] == self.name: + if assistant_settings["name"] == self.name: try: - self.assistant = self.client.beta.assistants.retrieve(assistant_settings['id']) - self.id = assistant_settings['id'] + self.assistant = self.client.beta.assistants.retrieve( + assistant_settings["id"] + ) + self.id = assistant_settings["id"] # update assistant if parameters are different if not self._check_parameters(self.assistant.model_dump()): @@ -239,7 +273,9 @@ def init_oai(self): self._update_assistant() if self.assistant.tool_resources: - self.tool_resources = self.assistant.tool_resources.model_dump() + self.tool_resources = ( + self.assistant.tool_resources.model_dump() + ) self._update_settings() return self @@ -280,8 +316,8 @@ def _update_assistant(self): No output parameters are returned, but the method updates the assistant's details on the OpenAI server and locally updates the settings file. """ tool_resources = copy.deepcopy(self.tool_resources) - if tool_resources and tool_resources.get('file_search'): - tool_resources['file_search'].pop('vector_stores', None) + if tool_resources and tool_resources.get("file_search"): + tool_resources["file_search"].pop("vector_stores", None) params = { "name": self.name, @@ -293,7 +329,7 @@ def _update_assistant(self): "top_p": self.top_p, "response_format": self.response_format, "metadata": self.metadata, - "model": self.model + "model": self.model, } params = {k: v for k, v in params.items() if v} self.assistant = self.client.beta.assistants.update( @@ -322,7 +358,11 @@ def get_id_from_file(f_path): else: return None - files_folders = self.files_folder if isinstance(self.files_folder, list) else [self.files_folder] + files_folders = ( + self.files_folder + if isinstance(self.files_folder, list) + else [self.files_folder] + ) file_search_ids = [] code_interpreter_ids = [] @@ -350,7 +390,7 @@ def get_id_from_file(f_path): ".jpg", # JPEG ".gif", # GIF ".png", # PNG - ".zip" # ZIP + ".zip", # ZIP ] for f_path in f_paths: @@ -359,14 +399,21 @@ def get_id_from_file(f_path): f_path = f_path.strip() file_id = get_id_from_file(f_path) if file_id: - print("File already uploaded. Skipping... " + os.path.basename(f_path)) + print( + "File already uploaded. Skipping... " + + os.path.basename(f_path) + ) else: print("Uploading new file... " + os.path.basename(f_path)) - with open(f_path, 'rb') as f: - file_id = self.client.with_options( - timeout=80 * 1000, - ).files.create(file=f, purpose="assistants").id - f.close() # fix permission error on windows + with open(f_path, "rb") as f: + file_id = ( + self.client.with_options( + timeout=80 * 1000, + ) + .files.create(file=f, purpose="assistants") + .id + ) + f.close() # fix permission error on windows add_id_to_file(f_path, file_id) if file_ext in code_interpreter_file_extensions: @@ -374,15 +421,22 @@ def get_id_from_file(f_path): else: file_search_ids.append(file_id) else: - print(f"Files folder '{f_path}' is not a directory. Skipping...", ) + print( + f"Files folder '{f_path}' is not a directory. Skipping...", + ) else: - print("Files folder path must be a string or list of strings. Skipping... ", files_folder) + print( + "Files folder path must be a string or list of strings. Skipping... ", + files_folder, + ) if FileSearch not in self.tools and file_search_ids: print("Detected files without FileSearch. Adding FileSearch tool...") self.add_tool(FileSearch) if CodeInterpreter not in self.tools and code_interpreter_ids: - print("Detected files without CodeInterpreter. Adding CodeInterpreter tool...") + print( + "Detected files without CodeInterpreter. Adding CodeInterpreter tool..." + ) self.add_tool(CodeInterpreter) self.add_file_ids(file_search_ids, "file_search") @@ -401,7 +455,7 @@ def add_tool(self, tool): if not any(issubclass(t, subclass) for t in self.tools): self.tools.append(tool) return - + if issubclass(tool, BaseTool): if tool.__name__ == "ExampleTool": print("Skipping importing ExampleTool...") @@ -419,22 +473,25 @@ def get_oai_tools(self): raise Exception("Tool must not be initialized.") if issubclass(tool, FileSearch): - tools.append(tool(file_search=self.file_search).model_dump(exclude_none=True)) + tools.append( + tool(file_search=self.file_search).model_dump(exclude_none=True) + ) elif issubclass(tool, CodeInterpreter): tools.append(tool().model_dump()) elif issubclass(tool, Retrieval): tools.append(tool().model_dump()) elif issubclass(tool, BaseTool): - tools.append({ - "type": "function", - "function": tool.openai_schema - }) + tools.append({"type": "function", "function": tool.openai_schema}) else: raise Exception("Invalid tool type.") return tools def _parse_schemas(self): - schemas_folders = self.schemas_folder if isinstance(self.schemas_folder, list) else [self.schemas_folder] + schemas_folders = ( + self.schemas_folder + if isinstance(self.schemas_folder, list) + else [self.schemas_folder] + ) for schemas_folder in schemas_folders: if isinstance(schemas_folder, str): @@ -452,9 +509,9 @@ def _parse_schemas(self): f_paths = [os.path.join(f_path, f) for f in f_paths] for f_path in f_paths: - with open(f_path, 'r') as f: + with open(f_path, "r") as f: openapi_spec = f.read() - f.close() # fix permission error on windows + f.close() # fix permission error on windows try: validate_openapi_spec(openapi_spec) except Exception as e: @@ -467,28 +524,42 @@ def _parse_schemas(self): headers = self.api_headers[os.path.basename(f_path)] if os.path.basename(f_path) in self.api_params: params = self.api_params[os.path.basename(f_path)] - tools = ToolFactory.from_openapi_schema(openapi_spec, headers=headers, params=params) + tools = ToolFactory.from_openapi_schema( + openapi_spec, headers=headers, params=params + ) except Exception as e: - print("Error parsing OpenAPI schema: " + os.path.basename(f_path)) + print( + "Error parsing OpenAPI schema: " + + os.path.basename(f_path) + ) raise e for tool in tools: self.add_tool(tool) else: - print("Schemas folder path is not a directory. Skipping... ", f_path) + print( + "Schemas folder path is not a directory. Skipping... ", f_path + ) else: - print("Schemas folder path must be a string or list of strings. Skipping... ", schemas_folder) + print( + "Schemas folder path must be a string or list of strings. Skipping... ", + schemas_folder, + ) def _parse_tools_folder(self): if not self.tools_folder: return if not os.path.isdir(self.tools_folder): - self.tools_folder = os.path.join(self.get_class_folder_path(), self.tools_folder) + self.tools_folder = os.path.join( + self.get_class_folder_path(), self.tools_folder + ) self.tools_folder = os.path.normpath(self.tools_folder) if os.path.isdir(self.tools_folder): f_paths = os.listdir(self.tools_folder) - f_paths = [f for f in f_paths if not f.startswith(".") and not f.startswith("__")] + f_paths = [ + f for f in f_paths if not f.startswith(".") and not f.startswith("__") + ] f_paths = [os.path.join(self.tools_folder, f) for f in f_paths] for f_path in f_paths: if not f_path.endswith(".py"): @@ -498,17 +569,22 @@ def _parse_tools_folder(self): tool = ToolFactory.from_file(f_path) self.add_tool(tool) except Exception as e: - print(f"Error parsing tool file {os.path.basename(f_path)}: {e}. Skipping...") + print( + f"Error parsing tool file {os.path.basename(f_path)}: {e}. Skipping..." + ) else: print("Items in tools folder must be files. Skipping... ", f_path) else: - print("Tools folder path is not a directory. Skipping... ", self.tools_folder) + print( + "Tools folder path is not a directory. Skipping... ", self.tools_folder + ) def get_openapi_schema(self, url): """Get openapi schema that contains all tools from the agent as different api paths. Make sure to call this after agency has been initialized.""" if self.assistant is None: raise Exception( - "Assistant is not initialized. Please initialize the agency first, before using this method") + "Assistant is not initialized. Please initialize the agency first, before using this method" + ) return ToolFactory.get_openapi_schema(self.tools, url) @@ -527,49 +603,77 @@ def _check_parameters(self, assistant_settings, debug=False): This method compares the current agent's parameters such as name, description, instructions, tools, file IDs, metadata, and model with the given assistant settings. It uses DeepDiff to compare complex structures like tools and metadata. If any parameter does not match, it returns False; otherwise, it returns True. """ - if self.name != assistant_settings['name']: + if self.name != assistant_settings["name"]: if debug: print(f"Name mismatch: {self.name} != {assistant_settings['name']}") return False - if self.description != assistant_settings['description']: + if self.description != assistant_settings["description"]: if debug: - print(f"Description mismatch: {self.description} != {assistant_settings['description']}") + print( + f"Description mismatch: {self.description} != {assistant_settings['description']}" + ) return False - if self.instructions != assistant_settings['instructions']: + if self.instructions != assistant_settings["instructions"]: if debug: - print(f"Instructions mismatch: {self.instructions} != {assistant_settings['instructions']}") + print( + f"Instructions mismatch: {self.instructions} != {assistant_settings['instructions']}" + ) return False def clean_tool(tool): if isinstance(tool, dict): - if 'function' in tool and 'strict' in tool['function'] and not tool['function']['strict']: - tool['function'].pop('strict', None) + if ( + "function" in tool + and "strict" in tool["function"] + and not tool["function"]["strict"] + ): + tool["function"].pop("strict", None) return tool local_tools = [clean_tool(tool) for tool in self.get_oai_tools()] - assistant_tools = [clean_tool(tool) for tool in assistant_settings['tools']] + assistant_tools = [clean_tool(tool) for tool in assistant_settings["tools"]] # find file_search and code_interpreter tools in local_tools and assistant_tools # Find file_search tools in local and assistant tools - local_file_search = next((tool for tool in local_tools if tool['type'] == 'file_search'), None) - assistant_file_search = next((tool for tool in assistant_tools if tool['type'] == 'file_search'), None) + local_file_search = next( + (tool for tool in local_tools if tool["type"] == "file_search"), None + ) + assistant_file_search = next( + (tool for tool in assistant_tools if tool["type"] == "file_search"), None + ) if local_file_search: # If local file_search doesn't have a 'file_search' key, use assistant's if available - if 'file_search' not in local_file_search and assistant_file_search and 'file_search' in assistant_file_search: - local_file_search['file_search'] = assistant_file_search['file_search'] - elif 'file_search' in local_file_search: + if ( + "file_search" not in local_file_search + and assistant_file_search + and "file_search" in assistant_file_search + ): + local_file_search["file_search"] = assistant_file_search["file_search"] + elif "file_search" in local_file_search: # Update max_num_results if not set locally but available in assistant - if 'max_num_results' not in local_file_search['file_search'] and assistant_file_search and \ - assistant_file_search['file_search'].get('max_num_results') is not None: - local_file_search['file_search']['max_num_results'] = assistant_file_search['file_search']['max_num_results'] - + if ( + "max_num_results" not in local_file_search["file_search"] + and assistant_file_search + and assistant_file_search["file_search"].get("max_num_results") + is not None + ): + local_file_search["file_search"]["max_num_results"] = ( + assistant_file_search["file_search"]["max_num_results"] + ) + # Update ranking_options if not set locally but available in assistant - if 'ranking_options' not in local_file_search['file_search'] and assistant_file_search and \ - assistant_file_search['file_search'].get('ranking_options') is not None: - local_file_search['file_search']['ranking_options'] = assistant_file_search['file_search']['ranking_options'] + if ( + "ranking_options" not in local_file_search["file_search"] + and assistant_file_search + and assistant_file_search["file_search"].get("ranking_options") + is not None + ): + local_file_search["file_search"]["ranking_options"] = ( + assistant_file_search["file_search"]["ranking_options"] + ) local_tools.sort(key=lambda x: json.dumps(x, sort_keys=True)) assistant_tools.sort(key=lambda x: json.dumps(x, sort_keys=True)) @@ -582,12 +686,14 @@ def clean_tool(tool): print("Assistant tools:", assistant_tools) return False - if self.temperature != assistant_settings['temperature']: + if self.temperature != assistant_settings["temperature"]: if debug: - print(f"Temperature mismatch: {self.temperature} != {assistant_settings['temperature']}") + print( + f"Temperature mismatch: {self.temperature} != {assistant_settings['temperature']}" + ) return False - if self.top_p != assistant_settings['top_p']: + if self.top_p != assistant_settings["top_p"]: if debug: print(f"Top_p mismatch: {self.top_p} != {assistant_settings['top_p']}") return False @@ -596,41 +702,49 @@ def clean_tool(tool): tool_resources_settings = copy.deepcopy(self.tool_resources) if tool_resources_settings is None: tool_resources_settings = {} - if tool_resources_settings.get('file_search'): - tool_resources_settings['file_search'].pop('vector_stores', None) - if tool_resources_settings.get('file_search') is None: - tool_resources_settings['file_search'] = {'vector_store_ids': []} - if tool_resources_settings.get('code_interpreter') is None: - tool_resources_settings['code_interpreter'] = {"file_ids": []} - - assistant_tool_resources = assistant_settings['tool_resources'] + if tool_resources_settings.get("file_search"): + tool_resources_settings["file_search"].pop("vector_stores", None) + if tool_resources_settings.get("file_search") is None: + tool_resources_settings["file_search"] = {"vector_store_ids": []} + if tool_resources_settings.get("code_interpreter") is None: + tool_resources_settings["code_interpreter"] = {"file_ids": []} + + assistant_tool_resources = assistant_settings["tool_resources"] if assistant_tool_resources is None: assistant_tool_resources = {} - if assistant_tool_resources.get('code_interpreter') is None: - assistant_tool_resources['code_interpreter'] = {"file_ids": []} - if assistant_tool_resources.get('file_search') is None: - assistant_tool_resources['file_search'] = {'vector_store_ids': []} + if assistant_tool_resources.get("code_interpreter") is None: + assistant_tool_resources["code_interpreter"] = {"file_ids": []} + if assistant_tool_resources.get("file_search") is None: + assistant_tool_resources["file_search"] = {"vector_store_ids": []} - tool_resources_diff = DeepDiff(tool_resources_settings, assistant_tool_resources, ignore_order=True) + tool_resources_diff = DeepDiff( + tool_resources_settings, assistant_tool_resources, ignore_order=True + ) if tool_resources_diff != {}: if debug: print(f"Tool resources mismatch: {tool_resources_diff}") print("Local tool resources:", tool_resources_settings) - print("Assistant tool resources:", assistant_settings['tool_resources']) + print("Assistant tool resources:", assistant_settings["tool_resources"]) return False - metadata_diff = DeepDiff(self.metadata, assistant_settings['metadata'], ignore_order=True) + metadata_diff = DeepDiff( + self.metadata, assistant_settings["metadata"], ignore_order=True + ) if metadata_diff != {}: if debug: print(f"Metadata mismatch: {metadata_diff}") return False - if self.model != assistant_settings['model']: + if self.model != assistant_settings["model"]: if debug: print(f"Model mismatch: {self.model} != {assistant_settings['model']}") return False - response_format_diff = DeepDiff(self.response_format, assistant_settings['response_format'], ignore_order=True) + response_format_diff = DeepDiff( + self.response_format, + assistant_settings["response_format"], + ignore_order=True, + ) if response_format_diff != {}: if debug: print(f"Response format mismatch: {response_format_diff}") @@ -642,14 +756,14 @@ def _save_settings(self): path = self.get_settings_path() # check if settings.json exists if not os.path.isfile(path): - with open(path, 'w') as f: + with open(path, "w") as f: json.dump([self.assistant.model_dump()], f, indent=4) else: settings = [] - with open(path, 'r') as f: + with open(path, "r") as f: settings = json.load(f) settings.append(self.assistant.model_dump()) - with open(path, 'w') as f: + with open(path, "w") as f: json.dump(settings, f, indent=4) def _update_settings(self): @@ -657,18 +771,22 @@ def _update_settings(self): # check if settings.json exists if os.path.isfile(path): settings = [] - with open(path, 'r') as f: + with open(path, "r") as f: settings = json.load(f) for i, assistant_settings in enumerate(settings): - if assistant_settings['id'] == self.id: + if assistant_settings["id"] == self.id: settings[i] = self.assistant.model_dump() break - with open(path, 'w') as f: + with open(path, "w") as f: json.dump(settings, f, indent=4) # --- Helper Methods --- - def add_file_ids(self, file_ids: List[str], tool_resource: Literal["code_interpreter", "file_search"]): + def add_file_ids( + self, + file_ids: List[str], + tool_resource: Literal["code_interpreter", "file_search"], + ): if not file_ids: return @@ -679,33 +797,34 @@ def add_file_ids(self, file_ids: List[str], tool_resource: Literal["code_interpr if CodeInterpreter not in self.tools: raise Exception("CodeInterpreter tool not found in tools.") - if tool_resource not in self.tool_resources or self.tool_resources[ - tool_resource] is None: - self.tool_resources[tool_resource] = { - "file_ids": file_ids - } + if ( + tool_resource not in self.tool_resources + or self.tool_resources[tool_resource] is None + ): + self.tool_resources[tool_resource] = {"file_ids": file_ids} - self.tool_resources[tool_resource]['file_ids'] = file_ids + self.tool_resources[tool_resource]["file_ids"] = file_ids elif tool_resource == "file_search": if FileSearch not in self.tools: raise Exception("FileSearch tool not found in tools.") - if tool_resource not in self.tool_resources or self.tool_resources[ - tool_resource] is None: + if ( + tool_resource not in self.tool_resources + or self.tool_resources[tool_resource] is None + ): self.tool_resources[tool_resource] = { - "vector_stores": [{ - "file_ids": file_ids - }] + "vector_stores": [{"file_ids": file_ids}] } - elif not self.tool_resources[tool_resource].get('vector_store_ids'): - self.tool_resources[tool_resource]['vector_stores'] = [{ - "file_ids": file_ids - }] + elif not self.tool_resources[tool_resource].get("vector_store_ids"): + self.tool_resources[tool_resource]["vector_stores"] = [ + {"file_ids": file_ids} + ] else: - vector_store_id = self.tool_resources[tool_resource]['vector_store_ids'][0] + vector_store_id = self.tool_resources[tool_resource][ + "vector_store_ids" + ][0] self.client.beta.vector_stores.file_batches.create( - vector_store_id=vector_store_id, - file_ids=file_ids + vector_store_id=vector_store_id, file_ids=file_ids ) else: raise Exception("Invalid tool resource.") @@ -714,14 +833,19 @@ def get_settings_path(self): return self.settings_path def _read_instructions(self): - class_instructions_path = os.path.normpath(os.path.join(self.get_class_folder_path(), self.instructions)) + class_instructions_path = os.path.normpath( + os.path.join(self.get_class_folder_path(), self.instructions) + ) if os.path.isfile(class_instructions_path): - with open(class_instructions_path, 'r') as f: + with open(class_instructions_path, "r") as f: self.instructions = f.read() elif os.path.isfile(self.instructions): - with open(self.instructions, 'r') as f: + with open(self.instructions, "r") as f: self.instructions = f.read() - elif "./instructions.md" in self.instructions or "./instructions.txt" in self.instructions: + elif ( + "./instructions.md" in self.instructions + or "./instructions.txt" in self.instructions + ): raise Exception("Instructions file not found.") def get_class_folder_path(self): @@ -761,13 +885,17 @@ def _delete_files(self): return file_ids = [] - if self.tool_resources.get('code_interpreter'): - file_ids = self.tool_resources['code_interpreter'].get('file_ids', []) + if self.tool_resources.get("code_interpreter"): + file_ids = self.tool_resources["code_interpreter"].get("file_ids", []) - if self.tool_resources.get('file_search'): - file_search_vector_store_ids = self.tool_resources['file_search'].get('vector_store_ids', []) + if self.tool_resources.get("file_search"): + file_search_vector_store_ids = self.tool_resources["file_search"].get( + "vector_store_ids", [] + ) for vector_store_id in file_search_vector_store_ids: - files = self.client.beta.vector_stores.files.list(vector_store_id=vector_store_id, limit=100) + files = self.client.beta.vector_stores.files.list( + vector_store_id=vector_store_id, limit=100 + ) for file in files: file_ids.append(file.id) @@ -785,11 +913,11 @@ def _delete_settings(self): # check if settings.json exists if os.path.isfile(path): settings = [] - with open(path, 'r') as f: + with open(path, "r") as f: settings = json.load(f) for i, assistant_settings in enumerate(settings): - if assistant_settings['id'] == self.id: + if assistant_settings["id"] == self.id: settings.pop(i) break - with open(path, 'w') as f: + with open(path, "w") as f: json.dump(settings, f, indent=4) diff --git a/agency_swarm/cli.py b/agency_swarm/cli.py index 6a746e76..7b3cf7f8 100644 --- a/agency_swarm/cli.py +++ b/agency_swarm/cli.py @@ -1,56 +1,93 @@ import argparse import os + from dotenv import load_dotenv + from agency_swarm.util.helpers import list_available_agents def main(): - parser = argparse.ArgumentParser(description='Agency Swarm CLI.') + parser = argparse.ArgumentParser(description="Agency Swarm CLI.") - subparsers = parser.add_subparsers(dest='command', help='Utility commands to simplify the agent creation process.') + subparsers = parser.add_subparsers( + dest="command", help="Utility commands to simplify the agent creation process." + ) subparsers.required = True # create-agent-template - create_parser = subparsers.add_parser('create-agent-template', help='Create agent template folder locally.') - create_parser.add_argument('--path', type=str, default="./", help='Path to create agent folder.') - create_parser.add_argument('--use_txt', action='store_true', default=False, - help='Use txt instead of md for instructions and manifesto.') - create_parser.add_argument('--name', type=str, help='Name of agent.') - create_parser.add_argument('--description', type=str, help='Description of agent.') + create_parser = subparsers.add_parser( + "create-agent-template", help="Create agent template folder locally." + ) + create_parser.add_argument( + "--path", type=str, default="./", help="Path to create agent folder." + ) + create_parser.add_argument( + "--use_txt", + action="store_true", + default=False, + help="Use txt instead of md for instructions and manifesto.", + ) + create_parser.add_argument("--name", type=str, help="Name of agent.") + create_parser.add_argument("--description", type=str, help="Description of agent.") # genesis-agency - genesis_parser = subparsers.add_parser('genesis', help='Start genesis agency.') - genesis_parser.add_argument('--openai_key', default=None, type=str, help='OpenAI API key.') - genesis_parser.add_argument('--with_browsing', default=False, action='store_true', - help='Enable browsing agent.') + genesis_parser = subparsers.add_parser("genesis", help="Start genesis agency.") + genesis_parser.add_argument( + "--openai_key", default=None, type=str, help="OpenAI API key." + ) + genesis_parser.add_argument( + "--with_browsing", + default=False, + action="store_true", + help="Enable browsing agent.", + ) # import-agent - import_parser = subparsers.add_parser('import-agent', help='Import pre-made agent by name to a local directory.') + import_parser = subparsers.add_parser( + "import-agent", help="Import pre-made agent by name to a local directory." + ) available_agents = list_available_agents() - import_parser.add_argument('--name', type=str, required=True, choices=available_agents, help='Name of the agent to import.') - import_parser.add_argument('--destination', type=str, default="./", help='Destination path to copy the agent files.') + import_parser.add_argument( + "--name", + type=str, + required=True, + choices=available_agents, + help="Name of the agent to import.", + ) + import_parser.add_argument( + "--destination", + type=str, + default="./", + help="Destination path to copy the agent files.", + ) args = parser.parse_args() if args.command == "create-agent-template": from agency_swarm.util import create_agent_template + create_agent_template(args.name, args.description, args.path, args.use_txt) elif args.command == "genesis": load_dotenv() - if not os.getenv('OPENAI_API_KEY') and not args.openai_key: - print("OpenAI API key not set. " - "Please set it with --openai_key argument or by setting OPENAI_API_KEY environment variable.") + if not os.getenv("OPENAI_API_KEY") and not args.openai_key: + print( + "OpenAI API key not set. " + "Please set it with --openai_key argument or by setting OPENAI_API_KEY environment variable." + ) return if args.openai_key: from agency_swarm import set_openai_key + set_openai_key(args.openai_key) from agency_swarm.agency.genesis import GenesisAgency + agency = GenesisAgency(with_browsing=args.with_browsing) agency.run_demo() elif args.command == "import-agent": from agency_swarm.util import import_agent + import_agent(args.name, args.destination) diff --git a/agency_swarm/messages/__init__.py b/agency_swarm/messages/__init__.py index 2b9332c4..26a48fc8 100644 --- a/agency_swarm/messages/__init__.py +++ b/agency_swarm/messages/__init__.py @@ -1 +1 @@ -from .message_output import MessageOutput \ No newline at end of file +from .message_output import MessageOutput diff --git a/agency_swarm/messages/message_output.py b/agency_swarm/messages/message_output.py index 17107fb1..a64c37d6 100644 --- a/agency_swarm/messages/message_output.py +++ b/agency_swarm/messages/message_output.py @@ -1,14 +1,22 @@ -from typing import Literal import hashlib -from rich.markdown import Markdown +from typing import Literal + from rich.console import Console, Group from rich.live import Live +from rich.markdown import Markdown console = Console() + class MessageOutput: - def __init__(self, msg_type: Literal["function", "function_output", "text", "system"], sender_name: str, - receiver_name: str, content, obj=None): + def __init__( + self, + msg_type: Literal["function", "function_output", "text", "system"], + sender_name: str, + receiver_name: str, + content, + obj=None, + ): """Initialize a message object with sender, receiver, content and type. Args: @@ -36,7 +44,12 @@ def hash_names_to_color(self): hash_obj = hashlib.md5(encoded_str) hash_int = int(hash_obj.hexdigest(), 16) colors = [ - 'green', 'yellow', 'blue', 'magenta', 'cyan', 'bright_white', + "green", + "yellow", + "blue", + "magenta", + "cyan", + "bright_white", ] color_index = hash_int % len(colors) return colors[color_index] @@ -96,10 +109,26 @@ def get_sender_emoji(self): hash_obj = hashlib.md5(encoded_str) hash_int = int(hash_obj.hexdigest(), 16) emojis = [ - '🐶', '🐱', '🐭', '🐹', '🐰', '🦊', - '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', - '🐷', '🐸', '🐵', '🐔', '🐧', '🐦', - '🐤'] + "🐶", + "🐱", + "🐭", + "🐹", + "🐰", + "🦊", + "🐻", + "🐼", + "🐨", + "🐯", + "🦁", + "🐮", + "🐷", + "🐸", + "🐵", + "🐔", + "🐧", + "🐦", + "🐤", + ] emoji_index = hash_int % len(emojis) @@ -109,8 +138,13 @@ def get_sender_emoji(self): class MessageOutputLive(MessageOutput): live_display = None - def __init__(self, msg_type: Literal["function", "function_output", "text", "system"], sender_name: str, - receiver_name: str, content): + def __init__( + self, + msg_type: Literal["function", "function_output", "text", "system"], + sender_name: str, + receiver_name: str, + content, + ): super().__init__(msg_type, sender_name, receiver_name, content) # Initialize Live display if not already done self.live_display = Live(vertical_overflow="visible") diff --git a/agency_swarm/threads/thread.py b/agency_swarm/threads/thread.py index 07eaa313..e372df26 100644 --- a/agency_swarm/threads/thread.py +++ b/agency_swarm/threads/thread.py @@ -2,24 +2,22 @@ import inspect import json import os +import re import time -from typing import List, Optional, Type, Union +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import List, Optional, Union from openai import APIError, BadRequestError from openai.types.beta import AssistantToolChoice from openai.types.beta.threads.message import Attachment -from openai.types.beta.threads.run import TruncationStrategy -from agency_swarm.tools import FileSearch, CodeInterpreter -from agency_swarm.util.streaming import AgencyEventHandler from agency_swarm.agents import Agent from agency_swarm.messages import MessageOutput +from agency_swarm.tools import CodeInterpreter, FileSearch from agency_swarm.user import User from agency_swarm.util.oai import get_openai_client +from agency_swarm.util.streaming import AgencyEventHandler -from concurrent.futures import ThreadPoolExecutor, as_completed - -import re class Thread: async_mode: str = None @@ -27,8 +25,8 @@ class Thread: @property def thread_url(self): - return f'https://platform.openai.com/playground/assistants?assistant={self.recipient_agent.id}&mode=assistant&thread={self.id}' - + return f"https://platform.openai.com/playground/assistants?assistant={self.recipient_agent.id}&mode=assistant&thread={self.id}" + @property def thread(self): self.init_thread() @@ -53,9 +51,15 @@ def __init__(self, agent: Union[Agent, User], recipient_agent: Agent): self._num_run_retries = 0 # names of recepient agents that were called in SendMessage tool # needed to prevent agents calling the same recepient agent multiple times - self._called_recepients = [] + self._called_recepients = [] - self.terminal_states = ["cancelled", "completed", "failed", "expired", "incomplete"] + self.terminal_states = [ + "cancelled", + "completed", + "failed", + "expired", + "incomplete", + ] def init_thread(self): self._called_recepients = [] @@ -63,7 +67,7 @@ def init_thread(self): if self.id: return - + self._thread = self.client.beta.threads.create() self.id = self._thread.id if self.recipient_agent.examples: @@ -73,42 +77,46 @@ def init_thread(self): **example, ) - def get_completion_stream(self, - message: Union[str, List[dict], None], - event_handler: type(AgencyEventHandler), - message_files: List[str] = None, - attachments: Optional[List[Attachment]] = None, - recipient_agent:Agent=None, - additional_instructions: str = None, - tool_choice: AssistantToolChoice = None, - response_format: Optional[dict] = None): - - return self.get_completion(message, - message_files, - attachments, - recipient_agent, - additional_instructions, - event_handler, - tool_choice, - yield_messages=False, - response_format=response_format) - - def get_completion(self, - message: Union[str, List[dict], None], - message_files: List[str] = None, - attachments: Optional[List[dict]] = None, - recipient_agent: Union[Agent, None] = None, - additional_instructions: str = None, - event_handler: type(AgencyEventHandler) = None, - tool_choice: AssistantToolChoice = None, - yield_messages: bool = False, - response_format: Optional[dict] = None - ): + def get_completion_stream( + self, + message: Union[str, List[dict], None], + event_handler: type(AgencyEventHandler), + message_files: List[str] = None, + attachments: Optional[List[Attachment]] = None, + recipient_agent: Agent = None, + additional_instructions: str = None, + tool_choice: AssistantToolChoice = None, + response_format: Optional[dict] = None, + ): + return self.get_completion( + message, + message_files, + attachments, + recipient_agent, + additional_instructions, + event_handler, + tool_choice, + yield_messages=False, + response_format=response_format, + ) + + def get_completion( + self, + message: Union[str, List[dict], None], + message_files: List[str] = None, + attachments: Optional[List[dict]] = None, + recipient_agent: Union[Agent, None] = None, + additional_instructions: str = None, + event_handler: type(AgencyEventHandler) = None, + tool_choice: AssistantToolChoice = None, + yield_messages: bool = False, + response_format: Optional[dict] = None, + ): self.init_thread() if not recipient_agent: recipient_agent = self.recipient_agent - + if not attachments: attachments = [] @@ -121,8 +129,12 @@ def get_completion(self, recipient_tools.append({"type": "code_interpreter"}) for file_id in message_files: - attachments.append({"file_id": file_id, - "tools": recipient_tools or [{"type": "file_search"}]}) + attachments.append( + { + "file_id": file_id, + "tools": recipient_tools or [{"type": "file_search"}], + } + ) if event_handler: event_handler.set_agent(self.agent) @@ -130,20 +142,28 @@ def get_completion(self, # Determine the sender's name based on the agent type sender_name = "user" if isinstance(self.agent, User) else self.agent.name - print(f'THREAD:[ {sender_name} -> {recipient_agent.name} ]: URL {self.thread_url}') + print( + f"THREAD:[ {sender_name} -> {recipient_agent.name} ]: URL {self.thread_url}" + ) # send message if message: message_obj = self.create_message( - message=message, - role="user", - attachments=attachments + message=message, role="user", attachments=attachments ) if yield_messages: - yield MessageOutput("text", self.agent.name, recipient_agent.name, message, message_obj) + yield MessageOutput( + "text", self.agent.name, recipient_agent.name, message, message_obj + ) - self._create_run(recipient_agent, additional_instructions, event_handler, tool_choice, response_format=response_format) + self._create_run( + recipient_agent, + additional_instructions, + event_handler, + tool_choice, + response_format=response_format, + ) error_attempts = 0 validation_attempts = 0 @@ -155,8 +175,10 @@ def get_completion(self, if self._run.status == "requires_action": self._called_recepients = [] tool_calls = self._run.required_action.submit_tool_outputs.tool_calls - tool_outputs_and_names = [] # list of tuples (name, tool_output) - sync_tool_calls, async_tool_calls = self._get_sync_async_tool_calls(tool_calls, recipient_agent) + tool_outputs_and_names = [] # list of tuples (name, tool_output) + sync_tool_calls, async_tool_calls = self._get_sync_async_tool_calls( + tool_calls, recipient_agent + ) def handle_output(tool_call, output): if inspect.isgenerator(output): @@ -169,23 +191,50 @@ def handle_output(tool_call, output): output = e.value else: if yield_messages: - yield MessageOutput("function_output", tool_call.function.name, recipient_agent.name, output, tool_call) + yield MessageOutput( + "function_output", + tool_call.function.name, + recipient_agent.name, + output, + tool_call, + ) for tool_output in tool_outputs_and_names: if tool_output[1]["tool_call_id"] == tool_call.id: tool_output[1]["output"] = output - + return output if len(async_tool_calls) > 0 and self.async_mode == "tools_threading": - max_workers = min(self.max_workers, os.cpu_count() or 1) # Use at most 4 workers or the number of CPUs available + max_workers = min( + self.max_workers, os.cpu_count() or 1 + ) # Use at most 4 workers or the number of CPUs available with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = {} for tool_call in async_tool_calls: if yield_messages: - yield MessageOutput("function", recipient_agent.name, self.agent.name, str(tool_call.function), tool_call) - futures[executor.submit(self.execute_tool, tool_call, recipient_agent, event_handler, tool_outputs_and_names)] = tool_call - tool_outputs_and_names.append((tool_call.function.name, {"tool_call_id": tool_call.id})) + yield MessageOutput( + "function", + recipient_agent.name, + self.agent.name, + str(tool_call.function), + tool_call, + ) + futures[ + executor.submit( + self.execute_tool, + tool_call, + recipient_agent, + event_handler, + tool_outputs_and_names, + ) + ] = tool_call + tool_outputs_and_names.append( + ( + tool_call.function.name, + {"tool_call_id": tool_call.id}, + ) + ) for future in as_completed(futures): tool_call = futures[future] @@ -200,16 +249,34 @@ def handle_output(tool_call, output): # execute sync tool calls for tool_call in sync_tool_calls: if yield_messages: - yield MessageOutput("function", recipient_agent.name, self.agent.name, str(tool_call.function), tool_call) - output, output_as_result = self.execute_tool(tool_call, recipient_agent, event_handler, tool_outputs_and_names) - tool_outputs_and_names.append((tool_call.function.name, {"tool_call_id": tool_call.id, "output": output})) + yield MessageOutput( + "function", + recipient_agent.name, + self.agent.name, + str(tool_call.function), + tool_call, + ) + output, output_as_result = self.execute_tool( + tool_call, + recipient_agent, + event_handler, + tool_outputs_and_names, + ) + tool_outputs_and_names.append( + ( + tool_call.function.name, + {"tool_call_id": tool_call.id, "output": output}, + ) + ) output = yield from handle_output(tool_call, output) if output_as_result: self._cancel_run() return output # split names and outputs - tool_outputs = [tool_output for _, tool_output in tool_outputs_and_names] + tool_outputs = [ + tool_output for _, tool_output in tool_outputs_and_names + ] tool_names = [name for name, _ in tool_outputs_and_names] # await coroutines @@ -224,7 +291,7 @@ def handle_output(tool_call, output): if event_handler: event_handler.set_agent(self.agent) event_handler.set_recipient_agent(recipient_agent) - + # submit tool outputs try: self._submit_tool_outputs(tool_outputs, event_handler) @@ -232,22 +299,38 @@ def handle_output(tool_call, output): if 'Runs in status "expired"' in e.message: self.create_message( message="Previous request timed out. Please repeat the exact same tool calls in the exact same order with the same arguments.", - role="user" + role="user", ) - self._create_run(recipient_agent, additional_instructions, event_handler, 'required', temperature=0) + self._create_run( + recipient_agent, + additional_instructions, + event_handler, + "required", + temperature=0, + ) self._run_until_done() if self._run.status != "requires_action": - raise Exception("Run Failed. Error: ", self._run.last_error or self._run.incomplete_details) + raise Exception( + "Run Failed. Error: ", + self._run.last_error or self._run.incomplete_details, + ) # change tool call ids - tool_calls = self._run.required_action.submit_tool_outputs.tool_calls + tool_calls = ( + self._run.required_action.submit_tool_outputs.tool_calls + ) if len(tool_calls) != len(tool_outputs): tool_outputs = [] for i, tool_call in enumerate(tool_calls): - tool_outputs.append({"tool_call_id": tool_call.id, "output": "Error: openai run timed out. You can try again one more time."}) + tool_outputs.append( + { + "tool_call_id": tool_call.id, + "output": "Error: openai run timed out. You can try again one more time.", + } + ) else: for i, tool_name in enumerate(tool_names): for tool_call in tool_calls[:]: @@ -262,22 +345,37 @@ def handle_output(tool_call, output): # error elif self._run.status == "failed": full_message += self._get_last_message_text() - common_errors = ["something went wrong", "the server had an error processing your request", "rate limit reached"] + common_errors = [ + "something went wrong", + "the server had an error processing your request", + "rate limit reached", + ] error_message = self._run.last_error.message.lower() - if error_attempts < 3 and any(error in error_message for error in common_errors): + if error_attempts < 3 and any( + error in error_message for error in common_errors + ): if error_attempts < 2: time.sleep(1 + error_attempts) else: self.create_message(message="Continue.", role="user") - - self._create_run(recipient_agent, additional_instructions, event_handler, - tool_choice, response_format=response_format) + + self._create_run( + recipient_agent, + additional_instructions, + event_handler, + tool_choice, + response_format=response_format, + ) error_attempts += 1 else: - raise Exception("OpenAI Run Failed. Error: ", self._run.last_error.message) + raise Exception( + "OpenAI Run Failed. Error: ", self._run.last_error.message + ) elif self._run.status == "incomplete": - raise Exception("OpenAI Run Incomplete. Details: ", self._run.incomplete_details) + raise Exception( + "OpenAI Run Incomplete. Details: ", self._run.incomplete_details + ) # return assistant message else: message_obj = self._get_last_assistant_message() @@ -285,7 +383,13 @@ def handle_output(tool_call, output): full_message += last_message if yield_messages: - yield MessageOutput("text", recipient_agent.name, self.agent.name, last_message, message_obj) + yield MessageOutput( + "text", + recipient_agent.name, + self.agent.name, + last_message, + message_obj, + ) if recipient_agent.response_validator: try: @@ -304,15 +408,21 @@ def handle_output(tool_call, output): content = str(e) message_obj = self.create_message( - message=content, - role="user" + message=content, role="user" ) if yield_messages: for content in message_obj.content: - if hasattr(content, 'text') and hasattr(content.text, 'value'): - yield MessageOutput("text", self.agent.name, recipient_agent.name, - content.text.value, message_obj) + if hasattr(content, "text") and hasattr( + content.text, "value" + ): + yield MessageOutput( + "text", + self.agent.name, + recipient_agent.name, + content.text.value, + message_obj, + ) break if event_handler: @@ -322,27 +432,43 @@ def handle_output(tool_call, output): validation_attempts += 1 - self._create_run(recipient_agent, additional_instructions, event_handler, tool_choice, response_format=response_format) + self._create_run( + recipient_agent, + additional_instructions, + event_handler, + tool_choice, + response_format=response_format, + ) continue return last_message - def _create_run(self, recipient_agent, additional_instructions, event_handler, tool_choice, temperature=None, response_format: Optional[dict] = None): + def _create_run( + self, + recipient_agent, + additional_instructions, + event_handler, + tool_choice, + temperature=None, + response_format: Optional[dict] = None, + ): try: if event_handler: with self.client.beta.threads.runs.stream( - thread_id=self.id, - event_handler=event_handler(), - assistant_id=recipient_agent.id, - additional_instructions=additional_instructions, - tool_choice=tool_choice, - max_prompt_tokens=recipient_agent.max_prompt_tokens, - max_completion_tokens=recipient_agent.max_completion_tokens, - truncation_strategy=recipient_agent.truncation_strategy, - temperature=temperature, - extra_body={"parallel_tool_calls": recipient_agent.parallel_tool_calls}, - response_format=response_format + thread_id=self.id, + event_handler=event_handler(), + assistant_id=recipient_agent.id, + additional_instructions=additional_instructions, + tool_choice=tool_choice, + max_prompt_tokens=recipient_agent.max_prompt_tokens, + max_completion_tokens=recipient_agent.max_completion_tokens, + truncation_strategy=recipient_agent.truncation_strategy, + temperature=temperature, + extra_body={ + "parallel_tool_calls": recipient_agent.parallel_tool_calls + }, + response_format=response_format, ) as stream: stream.until_done() self._run = stream.get_final_run() @@ -357,7 +483,7 @@ def _create_run(self, recipient_agent, additional_instructions, event_handler, t truncation_strategy=recipient_agent.truncation_strategy, temperature=temperature, parallel_tool_calls=recipient_agent.parallel_tool_calls, - response_format=response_format + response_format=response_format, ) self._run = self.client.beta.threads.runs.poll( thread_id=self.id, @@ -365,44 +491,54 @@ def _create_run(self, recipient_agent, additional_instructions, event_handler, t # poll_interval_ms=500, ) except APIError as e: - match = re.search(r"Thread (\w+) already has an active run (\w+)", e.message) + match = re.search( + r"Thread (\w+) already has an active run (\w+)", e.message + ) if match: - self._cancel_run(thread_id=match.groups()[0], run_id=match.groups()[1], check_status=False) - elif "The server had an error processing your request" in e.message and self._num_run_retries < 3: + self._cancel_run( + thread_id=match.groups()[0], + run_id=match.groups()[1], + check_status=False, + ) + elif ( + "The server had an error processing your request" in e.message + and self._num_run_retries < 3 + ): time.sleep(1) - self._create_run(recipient_agent, additional_instructions, event_handler, tool_choice, response_format=response_format) + self._create_run( + recipient_agent, + additional_instructions, + event_handler, + tool_choice, + response_format=response_format, + ) self._num_run_retries += 1 else: raise e def _run_until_done(self): - while self._run.status in ['queued', 'in_progress', "cancelling"]: + while self._run.status in ["queued", "in_progress", "cancelling"]: time.sleep(0.5) self._run = self.client.beta.threads.runs.retrieve( - thread_id=self.id, - run_id=self._run.id + thread_id=self.id, run_id=self._run.id ) def _submit_tool_outputs(self, tool_outputs, event_handler=None, poll=True): if not poll: self._run = self.client.beta.threads.runs.submit_tool_outputs( - thread_id=self.id, - run_id=self._run.id, - tool_outputs=tool_outputs + thread_id=self.id, run_id=self._run.id, tool_outputs=tool_outputs ) else: if not event_handler: self._run = self.client.beta.threads.runs.submit_tool_outputs_and_poll( - thread_id=self.id, - run_id=self._run.id, - tool_outputs=tool_outputs + thread_id=self.id, run_id=self._run.id, tool_outputs=tool_outputs ) else: with self.client.beta.threads.runs.submit_tool_outputs_stream( - thread_id=self.id, - run_id=self._run.id, - tool_outputs=tool_outputs, - event_handler=event_handler() + thread_id=self.id, + run_id=self._run.id, + tool_outputs=tool_outputs, + event_handler=event_handler(), ) as stream: stream.until_done() self._run = stream.get_final_run() @@ -410,11 +546,10 @@ def _submit_tool_outputs(self, tool_outputs, event_handler=None, poll=True): def _cancel_run(self, thread_id=None, run_id=None, check_status=True): if check_status and self._run.status in self.terminal_states and not run_id: return - + try: self._run = self.client.beta.threads.runs.cancel( - thread_id=self.id, - run_id=self._run.id + thread_id=self.id, run_id=self._run.id ) except BadRequestError as e: if "Cannot cancel run with status" in e.message: @@ -427,10 +562,7 @@ def _cancel_run(self, thread_id=None, run_id=None, check_status=True): raise e def _get_last_message_text(self): - messages = self.client.beta.threads.messages.list( - thread_id=self.id, - limit=1 - ) + messages = self.client.beta.threads.messages.list(thread_id=self.id, limit=1) if len(messages.data) == 0 or len(messages.data[0].content) == 0: return "" @@ -438,10 +570,7 @@ def _get_last_message_text(self): return messages.data[0].content[0].text.value def _get_last_assistant_message(self): - messages = self.client.beta.threads.messages.list( - thread_id=self.id, - limit=1 - ) + messages = self.client.beta.threads.messages.list(thread_id=self.id, limit=1) if len(messages.data) == 0 or len(messages.data[0].content) == 0: raise Exception("No messages found in the thread") @@ -451,39 +580,44 @@ def _get_last_assistant_message(self): if message.role == "assistant": return message - raise Exception("No assistant message found in the thread") + raise Exception("No assistant message found in the thread") - def create_message(self, message: str, role: str = "user", attachments: List[dict] = None): + def create_message( + self, message: str, role: str = "user", attachments: List[dict] = None + ): try: return self.client.beta.threads.messages.create( - thread_id=self.id, - role=role, - content=message, - attachments=attachments + thread_id=self.id, role=role, content=message, attachments=attachments ) except BadRequestError as e: regex = re.compile( r"Can't add messages to thread_([a-zA-Z0-9]+) while a run run_([a-zA-Z0-9]+) is active\." ) match = regex.search(str(e)) - + if match: thread_id, run_id = match.groups() thread_id = f"thread_{thread_id}" run_id = f"run_{run_id}" - + self._cancel_run(thread_id=thread_id, run_id=run_id) return self.client.beta.threads.messages.create( thread_id=thread_id, role=role, content=message, - attachments=attachments + attachments=attachments, ) else: raise e - def execute_tool(self, tool_call, recipient_agent=None, event_handler=None, tool_outputs_and_names={}): + def execute_tool( + self, + tool_call, + recipient_agent=None, + event_handler=None, + tool_outputs_and_names={}, + ): if not recipient_agent: recipient_agent = self.recipient_agent @@ -492,7 +626,10 @@ def execute_tool(self, tool_call, recipient_agent=None, event_handler=None, tool tool = next((func for func in funcs if func.__name__ == tool_name), None) if not tool: - return f"Error: Function {tool_call.function.name} not found. Available functions: {[func.__name__ for func in funcs]}", False + return ( + f"Error: Function {tool_call.function.name} not found. Available functions: {[func.__name__ for func in funcs]}", + False, + ) try: # init tool @@ -503,13 +640,22 @@ def execute_tool(self, tool_call, recipient_agent=None, event_handler=None, tool # check if the tool is already called for tool_name in [name for name, _ in tool_outputs_and_names]: if tool_name == tool_name and ( - hasattr(tool, "ToolConfig") and hasattr(tool.ToolConfig, "one_call_at_a_time") and tool.ToolConfig.one_call_at_a_time): - return f"Error: Function {tool_name} is already called. You can only call this function once at a time. Please wait for the previous call to finish before calling it again.", False - + hasattr(tool, "ToolConfig") + and hasattr(tool.ToolConfig, "one_call_at_a_time") + and tool.ToolConfig.one_call_at_a_time + ): + return ( + f"Error: Function {tool_name} is already called. You can only call this function once at a time. Please wait for the previous call to finish before calling it again.", + False, + ) + # for send message tools, don't allow calling the same recepient agent multiple times if tool_name.startswith("SendMessage"): if tool.recipient.value in self._called_recepients: - return f"Error: Agent {tool.recipient.value} has already been called. You can only call each agent once at a time. Please wait for the previous call to finish before calling it again.", False + return ( + f"Error: Agent {tool.recipient.value} has already been called. You can only call each agent once at a time. Please wait for the previous call to finish before calling it again.", + False, + ) self._called_recepients.append(tool.recipient.value) tool._caller_agent = recipient_agent @@ -522,7 +668,7 @@ def execute_tool(self, tool_call, recipient_agent=None, event_handler=None, tool if "For further information visit" in error_message: error_message = error_message.split("For further information visit")[0] return error_message, False - + def _await_coroutines(self, tool_outputs): async_tool_calls = [] for tool_output in tool_outputs: @@ -539,13 +685,15 @@ def _await_coroutines(self, tool_outputs): asyncio.set_event_loop(loop) loop = asyncio.get_event_loop() - results = loop.run_until_complete(asyncio.gather(*[call["output"] for call in async_tool_calls])) - + results = loop.run_until_complete( + asyncio.gather(*[call["output"] for call in async_tool_calls]) + ) + for tool_output, result in zip(async_tool_calls, results): tool_output["output"] = str(result) - + return tool_outputs - + def _get_sync_async_tool_calls(self, tool_calls, recipient_agent): async_tool_calls = [] sync_tool_calls = [] @@ -554,37 +702,40 @@ def _get_sync_async_tool_calls(self, tool_calls, recipient_agent): sync_tool_calls.append(tool_call) continue - tool = next((func for func in recipient_agent.functions if func.__name__ == tool_call.function.name), None) + tool = next( + ( + func + for func in recipient_agent.functions + if func.__name__ == tool_call.function.name + ), + None, + ) - if (hasattr(tool.ToolConfig, "async_mode") and tool.ToolConfig.async_mode) or self.async_mode == "tools_threading": + if ( + hasattr(tool.ToolConfig, "async_mode") and tool.ToolConfig.async_mode + ) or self.async_mode == "tools_threading": async_tool_calls.append(tool_call) else: sync_tool_calls.append(tool_call) return sync_tool_calls, async_tool_calls - + def get_messages(self, limit=None): all_messages = [] after = None while True: - response = self.client.beta.threads.messages.list(thread_id=self.id, limit=100, after=after) + response = self.client.beta.threads.messages.list( + thread_id=self.id, limit=100, after=after + ) messages = response.data if not messages: break all_messages.extend(messages) - after = messages[-1].id # Set the 'after' cursor to the ID of the last message + after = messages[ + -1 + ].id # Set the 'after' cursor to the ID of the last message if limit and len(all_messages) >= limit: break return all_messages - - - - - - - - - - diff --git a/agency_swarm/threads/thread_async.py b/agency_swarm/threads/thread_async.py index 6f1ebd70..113d9035 100644 --- a/agency_swarm/threads/thread_async.py +++ b/agency_swarm/threads/thread_async.py @@ -1,5 +1,5 @@ import threading -from typing import Union, Optional, List +from typing import List, Optional, Union from openai.types.beta import AssistantToolChoice @@ -13,42 +13,48 @@ def __init__(self, agent: Union[Agent, User], recipient_agent: Agent): super().__init__(agent, recipient_agent) self.pythread = None self.response = None - self.async_mode = False - - def worker(self, - message: str, - message_files: List[str] = None, - attachments: Optional[List[dict]] = None, - recipient_agent=None, - additional_instructions: str = None, - tool_choice: AssistantToolChoice = None - ): - self.async_mode = False - - gen = self.get_completion(message=message, - message_files=message_files, - attachments=attachments, - recipient_agent=recipient_agent, - additional_instructions=additional_instructions, - tool_choice=tool_choice) + self.async_mode = False + + def worker( + self, + message: str, + message_files: List[str] = None, + attachments: Optional[List[dict]] = None, + recipient_agent=None, + additional_instructions: str = None, + tool_choice: AssistantToolChoice = None, + ): + self.async_mode = False + + gen = self.get_completion( + message=message, + message_files=message_files, + attachments=attachments, + recipient_agent=recipient_agent, + additional_instructions=additional_instructions, + tool_choice=tool_choice, + ) while True: try: next(gen) except StopIteration as e: - self.response = f"""{self.recipient_agent.name}'s Response: '{e.value}'""" + self.response = ( + f"""{self.recipient_agent.name}'s Response: '{e.value}'""" + ) break return - def get_completion_async(self, - message: str, - message_files: List[str] = None, - attachments: Optional[List[dict]] = None, - recipient_agent=None, - additional_instructions: str = None, - tool_choice: AssistantToolChoice = None, - ): + def get_completion_async( + self, + message: str, + message_files: List[str] = None, + attachments: Optional[List[dict]] = None, + recipient_agent=None, + additional_instructions: str = None, + tool_choice: AssistantToolChoice = None, + ): if self.pythread and self.pythread.is_alive(): return "System Notification: 'Agent is busy, so your message was not received. Please always use 'GetResponse' tool to check for status first, before using 'SendMessage' tool again for the same agent.'" elif self.pythread and not self.pythread.is_alive(): @@ -58,11 +64,20 @@ def get_completion_async(self, run = self.get_last_run() - if run and run.status in ['queued', 'in_progress', 'requires_action']: + if run and run.status in ["queued", "in_progress", "requires_action"]: return "System Notification: 'Agent is busy, so your message was not received. Please always use 'GetResponse' tool to check for status first, before using 'SendMessage' tool again for the same agent.'" - self.pythread = threading.Thread(target=self.worker, - args=(message, message_files, attachments, recipient_agent, additional_instructions, tool_choice)) + self.pythread = threading.Thread( + target=self.worker, + args=( + message, + message_files, + attachments, + recipient_agent, + additional_instructions, + tool_choice, + ), + ) self.pythread.start() @@ -76,7 +91,7 @@ def check_status(self, run=None): return "System Notification: 'Agent is ready to receive a message. Please send a message with the 'SendMessage' tool.'" # check run status - if run.status in ['queued', 'in_progress', 'requires_action']: + if run.status in ["queued", "in_progress", "requires_action"]: return "System Notification: 'Task is not completed yet. Please tell the user to wait and try again later.'" if run.status == "failed": diff --git a/agency_swarm/tools/BaseTool.py b/agency_swarm/tools/BaseTool.py index 51cdaeb3..78a81c85 100644 --- a/agency_swarm/tools/BaseTool.py +++ b/agency_swarm/tools/BaseTool.py @@ -2,8 +2,8 @@ from typing import Any, ClassVar, Literal, Union from docstring_parser import parse - from pydantic import BaseModel + from agency_swarm.util.shared_state import SharedState @@ -17,15 +17,15 @@ def __init__(self, **kwargs): if not self.__class__._shared_state: self.__class__._shared_state = SharedState() super().__init__(**kwargs) - + # Ensure all ToolConfig variables are initialized config_defaults = { - 'strict': False, - 'one_call_at_a_time': False, - 'output_as_result': False, - 'async_mode': None + "strict": False, + "one_call_at_a_time": False, + "output_as_result": False, + "async_mode": None, } - + for key, value in config_defaults.items(): if not hasattr(self.ToolConfig, key): setattr(self.ToolConfig, key, value) @@ -88,7 +88,7 @@ def openai_schema(cls): if "$defs" in schema["parameters"]: for def_ in schema["parameters"]["$defs"].values(): def_["additionalProperties"] = False - + return schema @abstractmethod diff --git a/agency_swarm/tools/ToolFactory.py b/agency_swarm/tools/ToolFactory.py index 2759de8e..d36cd7ce 100644 --- a/agency_swarm/tools/ToolFactory.py +++ b/agency_swarm/tools/ToolFactory.py @@ -5,21 +5,16 @@ from importlib import import_module from typing import Any, Dict, List, Type, Union +import httpx import jsonref -from jsonref import requests -from pydantic import create_model, Field - -from .BaseTool import BaseTool -from ..util.schema import dereference_schema, reference_schema - from datamodel_code_generator import DataModelType, PythonVersion from datamodel_code_generator.model import get_data_model_types from datamodel_code_generator.parser.jsonschema import JsonSchemaParser -import httpx +from .BaseTool import BaseTool -class ToolFactory: +class ToolFactory: @staticmethod def from_langchain_tools(tools: List) -> List[Type[BaseTool]]: """ @@ -64,15 +59,15 @@ def callback(self): if len(tool_input) == 1: return tool.run(list(tool_input.values())[0]) else: - raise TypeError(f"Error parsing input for tool '{tool.__class__.__name__}' Please open an issue " - f"on github.") + raise TypeError( + f"Error parsing input for tool '{tool.__class__.__name__}' Please open an issue " + f"on github." + ) return ToolFactory.from_openai_schema( - format_tool_to_openai_function(tool), - callback + format_tool_to_openai_function(tool), callback ) - @staticmethod def from_openai_schema(schema: Dict[str, Any], callback: Any) -> Type[BaseTool]: """ @@ -86,12 +81,11 @@ def from_openai_schema(schema: Dict[str, Any], callback: Any) -> Type[BaseTool]: A BaseTool. """ data_model_types = get_data_model_types( - DataModelType.PydanticV2BaseModel, - target_python_version=PythonVersion.PY_37 + DataModelType.PydanticV2BaseModel, target_python_version=PythonVersion.PY_37 ) parser = JsonSchemaParser( - json.dumps(schema['parameters']), + json.dumps(schema["parameters"]), data_model_type=data_model_types.data_model, data_model_root_type=data_model_types.root_model, data_model_field_type=data_model_types.field_model, @@ -99,7 +93,7 @@ def from_openai_schema(schema: Dict[str, Any], callback: Any) -> Type[BaseTool]: dump_resolve_reference_action=data_model_types.dump_resolve_reference_action, use_schema_description=True, validation=False, - class_name='Model', + class_name="Model", # custom_template_dir=Path('/Users/vrsen/Projects/agency-swarm/agency-swarm/agency_swarm/tools/data_schema_templates') ) @@ -108,26 +102,34 @@ def from_openai_schema(schema: Dict[str, Any], callback: Any) -> Type[BaseTool]: # # Execute the result to extract the model exec_globals = {} exec(result, exec_globals) - model = exec_globals.get('Model') + model = exec_globals.get("Model") if not model: raise ValueError(f"Could not extract model from schema {schema['name']}") - + class ToolConfig: strict: bool = schema.get("strict", False) - - tool = type(schema['name'], (BaseTool, model), { - "__doc__": schema.get('description', ""), - "run": callback, - }) + + tool = type( + schema["name"], + (BaseTool, model), + { + "__doc__": schema.get("description", ""), + "run": callback, + }, + ) tool.ToolConfig = ToolConfig return tool @staticmethod - def from_openapi_schema(schema: Union[str, dict], headers: Dict[str, str] = None, params: Dict[str, Any] = None, strict: bool = False) \ - -> List[Type[BaseTool]]: + def from_openapi_schema( + schema: Union[str, dict], + headers: Dict[str, str] = None, + params: Dict[str, Any] = None, + strict: bool = False, + ) -> List[Type[BaseTool]]: """ Converts an OpenAPI schema into a list of BaseTools. @@ -149,9 +151,10 @@ def from_openapi_schema(schema: Union[str, dict], headers: Dict[str, str] = None headers = {k: v for k, v in headers.items() if v is not None} for path, methods in openapi_spec["paths"].items(): for method, spec_with_ref in methods.items(): + async def callback(self): url = openapi_spec["servers"][0]["url"] + path - parameters = self.model_dump().get('parameters', {}) + parameters = self.model_dump().get("parameters", {}) # replace all parameters in url for param, value in parameters.items(): if "{" + str(param) + "}" in url: @@ -160,24 +163,34 @@ async def callback(self): url = url.rstrip("/") parameters = {k: v for k, v in parameters.items() if v is not None} parameters = {**parameters, **params} if params else parameters - async with httpx.AsyncClient(timeout=90) as client: # Set custom read timeout to 10 seconds + async with httpx.AsyncClient( + timeout=90 + ) as client: # Set custom read timeout to 10 seconds if method == "get": - response = await client.get(url, params=parameters, headers=headers) + response = await client.get( + url, params=parameters, headers=headers + ) elif method == "post": - response = await client.post(url, - params=parameters, - json=self.model_dump().get('requestBody', None), - headers=headers) + response = await client.post( + url, + params=parameters, + json=self.model_dump().get("requestBody", None), + headers=headers, + ) elif method == "put": - response = await client.put(url, - params=parameters, - json=self.model_dump().get('requestBody', None), - headers=headers) + response = await client.put( + url, + params=parameters, + json=self.model_dump().get("requestBody", None), + headers=headers, + ) elif method == "delete": - response = await client.delete(url, - params=parameters, - json=self.model_dump().get('requestBody', None), - headers=headers) + response = await client.delete( + url, + params=parameters, + json=self.model_dump().get("requestBody", None), + headers=headers, + ) return response.json() # 1. Resolve JSON references. @@ -209,31 +222,37 @@ async def callback(self): param["schema"] = {"type": param["type"]} param_properties[param["name"]] = param["schema"] if "description" in param: - param_properties[param["name"]]["description"] = param["description"] + param_properties[param["name"]]["description"] = param[ + "description" + ] if "required" in param and param["required"]: required_params.append(param["name"]) if "example" in param: - param_properties[param["name"]]["example"] = param["example"] + param_properties[param["name"]]["example"] = param[ + "example" + ] if "examples" in param: - param_properties[param["name"]]["examples"] = param["examples"] - + param_properties[param["name"]]["examples"] = param[ + "examples" + ] + schema["properties"]["parameters"] = { "type": "object", "properties": param_properties, - "required": required_params + "required": required_params, } function = { "name": function_name, "description": desc, "parameters": schema, - "strict": strict + "strict": strict, } tools.append(ToolFactory.from_openai_schema(function, callback)) return tools - + @staticmethod def from_file(file_path: str) -> Type[BaseTool]: """Dynamically imports a BaseTool class from a Python file within a package structure. @@ -251,7 +270,7 @@ def from_file(file_path: str) -> Type[BaseTool]: class_name = os.path.splitext(file_name)[0] exec_globals = globals() - + # importing from agency_swarm package if "agency_swarm" in import_path: import_path = import_path.lstrip(".") @@ -262,8 +281,6 @@ def from_file(file_path: str) -> Type[BaseTool]: sys.path.append(current_working_directory) exec(f"from {import_path} import {class_name}", exec_globals) - - imported_class = exec_globals.get(class_name) if not imported_class: raise ImportError(f"Could not import {class_name} from {import_path}") @@ -275,8 +292,12 @@ def from_file(file_path: str) -> Type[BaseTool]: return imported_class @staticmethod - def get_openapi_schema(tools: List[Type[BaseTool]], url: str, title="Agent Tools", - description="A collection of tools.") -> str: + def get_openapi_schema( + tools: List[Type[BaseTool]], + url: str, + title="Agent Tools", + description="A collection of tools.", + ) -> str: """ Generates an OpenAPI schema from a list of BaseTools. @@ -291,11 +312,7 @@ def get_openapi_schema(tools: List[Type[BaseTool]], url: str, title="Agent Tools """ schema = { "openapi": "3.1.0", - "info": { - "title": title, - "description": description, - "version": "v1.0.0" - }, + "info": {"title": title, "description": description, "version": "v1.0.0"}, "servers": [ { "url": url, @@ -304,11 +321,7 @@ def get_openapi_schema(tools: List[Type[BaseTool]], url: str, title="Agent Tools "paths": {}, "components": { "schemas": {}, - "securitySchemes": { - "apiKey": { - "type": "apiKey" - } - } + "securitySchemes": {"apiKey": {"type": "apiKey"}}, }, } @@ -318,28 +331,28 @@ def get_openapi_schema(tools: List[Type[BaseTool]], url: str, title="Agent Tools openai_schema = tool.openai_schema defs = {} - if '$defs' in openai_schema['parameters']: - defs = openai_schema['parameters']['$defs'] - del openai_schema['parameters']['$defs'] + if "$defs" in openai_schema["parameters"]: + defs = openai_schema["parameters"]["$defs"] + del openai_schema["parameters"]["$defs"] - schema['paths']["/" + openai_schema['name']] = { + schema["paths"]["/" + openai_schema["name"]] = { "post": { - "description": openai_schema['description'], - "operationId": openai_schema['name'], + "description": openai_schema["description"], + "operationId": openai_schema["name"], "x-openai-isConsequential": False, "parameters": [], "requestBody": { "content": { - "application/json": { - "schema": openai_schema['parameters'] - } + "application/json": {"schema": openai_schema["parameters"]} } }, } } - schema['components']['schemas'].update(defs) + schema["components"]["schemas"].update(defs) - schema = json.dumps(schema, indent=2).replace("#/$defs/", "#/components/schemas/") + schema = json.dumps(schema, indent=2).replace( + "#/$defs/", "#/components/schemas/" + ) - return schema \ No newline at end of file + return schema diff --git a/agency_swarm/tools/__init__.py b/agency_swarm/tools/__init__.py index d77d38dd..8bca7b06 100644 --- a/agency_swarm/tools/__init__.py +++ b/agency_swarm/tools/__init__.py @@ -2,4 +2,4 @@ from .oai.CodeInterpreter import CodeInterpreter from .oai.FileSearch import FileSearch from .oai.Retrieval import Retrieval -from .ToolFactory import ToolFactory \ No newline at end of file +from .ToolFactory import ToolFactory diff --git a/agency_swarm/tools/oai/FileSearch.py b/agency_swarm/tools/oai/FileSearch.py index 03274d5b..2831c48f 100644 --- a/agency_swarm/tools/oai/FileSearch.py +++ b/agency_swarm/tools/oai/FileSearch.py @@ -1,8 +1,10 @@ -from openai.types.beta.file_search_tool import FileSearchTool from openai.types.beta.file_search_tool import FileSearch as OpenAIFileSearch +from openai.types.beta.file_search_tool import FileSearchTool + class FileSearchConfig(OpenAIFileSearch): pass + class FileSearch(FileSearchTool): - type: str = "file_search" \ No newline at end of file + type: str = "file_search" diff --git a/agency_swarm/tools/oai/Retrieval.py b/agency_swarm/tools/oai/Retrieval.py index da4076fb..58e8a695 100644 --- a/agency_swarm/tools/oai/Retrieval.py +++ b/agency_swarm/tools/oai/Retrieval.py @@ -2,4 +2,4 @@ class Retrieval(BaseModel): - type: str = "file_search" \ No newline at end of file + type: str = "file_search" diff --git a/agency_swarm/tools/send_message/SendMessage.py b/agency_swarm/tools/send_message/SendMessage.py index d5c777ac..9cb83881 100644 --- a/agency_swarm/tools/send_message/SendMessage.py +++ b/agency_swarm/tools/send_message/SendMessage.py @@ -1,11 +1,15 @@ -from typing import Optional, List -from pydantic import Field, field_validator, model_validator +from typing import List, Optional + +from pydantic import Field, model_validator + from .SendMessageBase import SendMessageBase + class SendMessage(SendMessageBase): """Use this tool to facilitate direct, synchronous communication between specialized agents within your agency. When you send a message using this tool, you receive a response exclusively from the designated recipient agent. To continue the dialogue, invoke this tool again with the desired recipient agent and your follow-up message. Remember, communication here is synchronous; the recipient agent won't perform any tasks post-response. You are responsible for relaying the recipient agent's responses back to the user, as the user does not have direct access to these replies. Keep engaging with the tool for continuous interaction until the task is fully resolved. Do not send more than 1 message to the same recipient agent at the same time.""" + my_primary_instructions: str = Field( - ..., + ..., description=( "Please repeat your primary instructions step-by-step, including both completed " "and the following next steps that you need to perform. For multi-step, complex tasks, first break them down " @@ -14,31 +18,37 @@ class SendMessage(SendMessageBase): "sent in a separate message. Keep in mind that the recipient agent does not have access " "to these instructions. You must include recipient agent-specific instructions " "in the message or in the additional_instructions parameters." - ) + ), ) message: str = Field( - ..., - description="Specify the task required for the recipient agent to complete. Focus on clarifying what the task entails, rather than providing exact instructions. Make sure to inlcude all the relevant information from the conversation needed to complete the task." + ..., + description="Specify the task required for the recipient agent to complete. Focus on clarifying what the task entails, rather than providing exact instructions. Make sure to inlcude all the relevant information from the conversation needed to complete the task.", ) message_files: Optional[List[str]] = Field( default=None, description="A list of file IDs to be sent as attachments to this message. Only use this if you have the file ID that starts with 'file-'.", - examples=["file-1234", "file-5678"] + examples=["file-1234", "file-5678"], ) additional_instructions: Optional[str] = Field( default=None, - description="Additional context or instructions from the conversation needed by the recipient agent to complete the task." + description="Additional context or instructions from the conversation needed by the recipient agent to complete the task.", ) - @model_validator(mode='after') + @model_validator(mode="after") def validate_files(self): # prevent hallucinations with agents sending file IDs into incorrect fields - if "file-" in self.message or (self.additional_instructions and "file-" in self.additional_instructions): + if "file-" in self.message or ( + self.additional_instructions and "file-" in self.additional_instructions + ): if not self.message_files: - raise ValueError("You must include file IDs in message_files parameter.") + raise ValueError( + "You must include file IDs in message_files parameter." + ) return self - + def run(self): - return self._get_completion(message=self.message, - message_files=self.message_files, - additional_instructions=self.additional_instructions) \ No newline at end of file + return self._get_completion( + message=self.message, + message_files=self.message_files, + additional_instructions=self.additional_instructions, + ) diff --git a/agency_swarm/tools/send_message/SendMessageAsyncThreading.py b/agency_swarm/tools/send_message/SendMessageAsyncThreading.py index 38912e09..d2664bd7 100644 --- a/agency_swarm/tools/send_message/SendMessageAsyncThreading.py +++ b/agency_swarm/tools/send_message/SendMessageAsyncThreading.py @@ -1,8 +1,8 @@ -from typing import ClassVar, Type -from agency_swarm.threads.thread_async import ThreadAsync from .SendMessage import SendMessage + class SendMessageAsyncThreading(SendMessage): """Use this tool for asynchronous communication with other agents within your agency. Initiate tasks by messaging, and check status and responses later with the 'GetResponse' tool. Relay responses to the user, who instructs on status checks. Continue until task completion.""" + class ToolConfig: - async_mode = "threading" \ No newline at end of file + async_mode = "threading" diff --git a/agency_swarm/tools/send_message/SendMessageBase.py b/agency_swarm/tools/send_message/SendMessageBase.py index 26b61128..97db38c6 100644 --- a/agency_swarm/tools/send_message/SendMessageBase.py +++ b/agency_swarm/tools/send_message/SendMessageBase.py @@ -1,17 +1,23 @@ -from agency_swarm.agents.agent import Agent -from agency_swarm.threads.thread import Thread +from abc import ABC from typing import ClassVar, Union + from pydantic import Field, field_validator + +from agency_swarm.agents.agent import Agent +from agency_swarm.threads.thread import Thread from agency_swarm.threads.thread_async import ThreadAsync from agency_swarm.tools import BaseTool -from abc import ABC + class SendMessageBase(BaseTool, ABC): - recipient: str = Field(..., description="Recipient agent that you want to send the message to. This field will be overriden inside the agency class.") - + recipient: str = Field( + ..., + description="Recipient agent that you want to send the message to. This field will be overriden inside the agency class.", + ) + _agents_and_threads: ClassVar = None - @field_validator('additional_instructions', mode='before', check_fields=False) + @field_validator("additional_instructions", mode="before", check_fields=False) @classmethod def validate_additional_instructions(cls, value): # previously the parameter was a list, now it's a string @@ -19,23 +25,27 @@ def validate_additional_instructions(cls, value): if isinstance(value, list): return "\n".join(value) return value - + def _get_thread(self) -> Thread | ThreadAsync: return self._agents_and_threads[self._caller_agent.name][self.recipient.value] - + def _get_main_thread(self) -> Thread | ThreadAsync: return self._agents_and_threads["main_thread"] - + def _get_recipient_agent(self) -> Agent: - return self._agents_and_threads[self._caller_agent.name][self.recipient.value].recipient_agent - + return self._agents_and_threads[self._caller_agent.name][ + self.recipient.value + ].recipient_agent + def _get_completion(self, message: Union[str, None] = None, **kwargs): thread = self._get_thread() if self.ToolConfig.async_mode == "threading": return thread.get_completion_async(message=message, **kwargs) else: - return thread.get_completion(message=message, - event_handler=self._event_handler, - yield_messages=not self._event_handler, - **kwargs) \ No newline at end of file + return thread.get_completion( + message=message, + event_handler=self._event_handler, + yield_messages=not self._event_handler, + **kwargs, + ) diff --git a/agency_swarm/tools/send_message/SendMessageQuick.py b/agency_swarm/tools/send_message/SendMessageQuick.py index f673f909..f349aaf8 100644 --- a/agency_swarm/tools/send_message/SendMessageQuick.py +++ b/agency_swarm/tools/send_message/SendMessageQuick.py @@ -1,14 +1,15 @@ -from agency_swarm.threads.thread import Thread -from typing import ClassVar, Optional, List, Type -from pydantic import Field, field_validator, model_validator +from pydantic import Field + from .SendMessageBase import SendMessageBase + class SendMessageQuick(SendMessageBase): """Use this tool to facilitate direct, synchronous communication between specialized agents within your agency. When you send a message using this tool, you receive a response exclusively from the designated recipient agent. To continue the dialogue, invoke this tool again with the desired recipient agent and your follow-up message. Remember, communication here is synchronous; the recipient agent won't perform any tasks post-response. You are responsible for relaying the recipient agent's responses back to the user, as the user does not have direct access to these replies. Keep engaging with the tool for continuous interaction until the task is fully resolved. Do not send more than 1 message to the same recipient agent at the same time.""" + message: str = Field( - ..., - description="Specify the task required for the recipient agent to complete. Focus on clarifying what the task entails, rather than providing exact instructions. Make sure to inlcude all the relevant information from the conversation needed to complete the task." + ..., + description="Specify the task required for the recipient agent to complete. Focus on clarifying what the task entails, rather than providing exact instructions. Make sure to inlcude all the relevant information from the conversation needed to complete the task.", ) - + def run(self): - return self._get_completion(message=self.message) \ No newline at end of file + return self._get_completion(message=self.message) diff --git a/agency_swarm/tools/send_message/SendMessageSwarm.py b/agency_swarm/tools/send_message/SendMessageSwarm.py index 868b9258..3817f6d5 100644 --- a/agency_swarm/tools/send_message/SendMessageSwarm.py +++ b/agency_swarm/tools/send_message/SendMessageSwarm.py @@ -1,7 +1,8 @@ from openai import BadRequestError -from agency_swarm.threads.thread import Thread + from .SendMessage import SendMessageBase + class SendMessageSwarm(SendMessageBase): """Use this tool to route messages to other agents within your agency. After using this tool, you will be switched to the recipient agent. This tool can only be used once per message. Do not use any other tools together with this tool.""" @@ -9,8 +10,8 @@ class ToolConfig: # set output as result because the communication will be finished after this tool is called output_as_result: bool = True one_call_at_a_time: bool = True - - def run(self): + + def run(self): # get main thread thread = self._get_main_thread() @@ -20,12 +21,21 @@ def run(self): # submit tool output try: thread._submit_tool_outputs( - tool_outputs=[{"tool_call_id": self._tool_call.id, "output": "The request has been routed. You are now a " + recipient_agent.name + " agent. Please assist the user further with their request."}], - poll=False + tool_outputs=[ + { + "tool_call_id": self._tool_call.id, + "output": "The request has been routed. You are now a " + + recipient_agent.name + + " agent. Please assist the user further with their request.", + } + ], + poll=False, ) except BadRequestError as e: - raise Exception("You can only call this tool by itself. Do not use any other tools together with this tool.") - + raise Exception( + "You can only call this tool by itself. Do not use any other tools together with this tool." + ) + try: # cancel run thread._cancel_run() @@ -37,9 +47,14 @@ def run(self): if self._event_handler: if hasattr(self._event_handler, "change_recipient_agent"): self._event_handler.change_recipient_agent(self.recipient.value) - + # continue conversation with the new recipient agent - message = thread.get_completion(message=None, recipient_agent=recipient_agent, yield_messages=not self._event_handler, event_handler=self._event_handler) + message = thread.get_completion( + message=None, + recipient_agent=recipient_agent, + yield_messages=not self._event_handler, + event_handler=self._event_handler, + ) return message or "" except Exception as e: diff --git a/agency_swarm/tools/send_message/__init__.py b/agency_swarm/tools/send_message/__init__.py index d044d285..97a795a9 100644 --- a/agency_swarm/tools/send_message/__init__.py +++ b/agency_swarm/tools/send_message/__init__.py @@ -1,5 +1,5 @@ +from .SendMessage import SendMessage from .SendMessageAsyncThreading import SendMessageAsyncThreading from .SendMessageBase import SendMessageBase -from .SendMessage import SendMessage +from .SendMessageQuick import SendMessageQuick from .SendMessageSwarm import SendMessageSwarm -from .SendMessageQuick import SendMessageQuick \ No newline at end of file diff --git a/agency_swarm/user/__init__.py b/agency_swarm/user/__init__.py index b7bb9be8..ee4c00b1 100644 --- a/agency_swarm/user/__init__.py +++ b/agency_swarm/user/__init__.py @@ -1 +1 @@ -from .user import User \ No newline at end of file +from .user import User diff --git a/agency_swarm/util/__init__.py b/agency_swarm/util/__init__.py index 479b725a..54901a37 100644 --- a/agency_swarm/util/__init__.py +++ b/agency_swarm/util/__init__.py @@ -1,5 +1,5 @@ from .cli.create_agent_template import create_agent_template from .cli.import_agent import import_agent -from .oai import set_openai_key, get_openai_client, set_openai_client -from .files import get_tools, get_file_purpose -from .validators import llm_validator \ No newline at end of file +from .files import get_file_purpose, get_tools +from .oai import get_openai_client, set_openai_client, set_openai_key +from .validators import llm_validator diff --git a/agency_swarm/util/cli/create_agent_template.py b/agency_swarm/util/cli/create_agent_template.py index 01306a49..3032f686 100644 --- a/agency_swarm/util/cli/create_agent_template.py +++ b/agency_swarm/util/cli/create_agent_template.py @@ -1,13 +1,15 @@ import os -def create_agent_template(agent_name=None, - agent_description=None, - path="./", - instructions=None, - code_interpreter=False, - use_txt=False, - include_example_tool=True): +def create_agent_template( + agent_name=None, + agent_description=None, + path="./", + instructions=None, + code_interpreter=False, + use_txt=False, + include_example_tool=True, +): if not agent_name: agent_name = input("Enter agent name: ") if not agent_description: @@ -23,14 +25,18 @@ def create_agent_template(agent_name=None, # create agent file with open(path + class_name + ".py", "w") as f: - f.write(agent_template.format( - class_name=class_name, - agent_name=agent_name, - agent_description=agent_description, - ext="md" if not use_txt else "txt", - code_interpreter="CodeInterpreter" if code_interpreter else "", - code_interpreter_import="from agency_swarm.tools import CodeInterpreter" if code_interpreter else "" - )) + f.write( + agent_template.format( + class_name=class_name, + agent_name=agent_name, + agent_description=agent_description, + ext="md" if not use_txt else "txt", + code_interpreter="CodeInterpreter" if code_interpreter else "", + code_interpreter_import="from agency_swarm.tools import CodeInterpreter" + if code_interpreter + else "", + ) + ) with open(path + "__init__.py", "w") as f: f.write(f"from .{class_name} import {class_name}") @@ -75,7 +81,7 @@ def __init__(self): temperature=0.3, max_prompt_tokens=25000, ) - + def response_validator(self, message): return message """ diff --git a/agency_swarm/util/cli/import_agent.py b/agency_swarm/util/cli/import_agent.py index c33a3a23..622933b6 100644 --- a/agency_swarm/util/cli/import_agent.py +++ b/agency_swarm/util/cli/import_agent.py @@ -8,7 +8,7 @@ def import_agent(agent_name, destination): Copies the specified agent files from the package to a specified destination directory, preserving the folder structure. """ - package = 'agency_swarm.agents' + package = "agency_swarm.agents" # Construct the destination path for the agent agent_destination = os.path.join(destination, agent_name) @@ -31,4 +31,6 @@ def import_agent(agent_name, destination): print(f"Agent '{agent_name}' copied to: {agent_destination}") except Exception as e: - print(f"Error importing agent '{agent_name}'. Most likely the agent name is wrong. Error: {e}") + print( + f"Error importing agent '{agent_name}'. Most likely the agent name is wrong. Error: {e}" + ) diff --git a/agency_swarm/util/errors.py b/agency_swarm/util/errors.py index 4831a8f0..31bcf258 100644 --- a/agency_swarm/util/errors.py +++ b/agency_swarm/util/errors.py @@ -1,2 +1,2 @@ class RefusalError(Exception): - pass \ No newline at end of file + pass diff --git a/agency_swarm/util/files.py b/agency_swarm/util/files.py index 473b1f13..fbfcbcc0 100644 --- a/agency_swarm/util/files.py +++ b/agency_swarm/util/files.py @@ -1,31 +1,56 @@ import mimetypes # Register the MIME type for .xlsx files -mimetypes.add_type('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.xlsx') -mimetypes.add_type('application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.docx') -mimetypes.add_type('application/vnd.openxmlformats-officedocument.presentationml.presentation', '.pptx') +mimetypes.add_type( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx" +) +mimetypes.add_type( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx" +) +mimetypes.add_type( + "application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx" +) -image_types = [ - "image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif" -] +image_types = ["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif"] code_interpreter_types = [ - "application/csv", "image/jpeg", "image/gif", "image/png", - "application/x-tar", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/xml", "text/xml", "application/zip", "text/csv" + "application/csv", + "image/jpeg", + "image/gif", + "image/png", + "application/x-tar", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/xml", + "text/xml", + "application/zip", + "text/csv", ] dual_types = [ - "text/x-c", "text/x-csharp", "text/x-c++", "application/msword", + "text/x-c", + "text/x-csharp", + "text/x-c++", + "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "text/html", "text/x-java", "application/json", "text/markdown", - "application/pdf", "text/x-php", + "text/html", + "text/x-java", + "application/json", + "text/markdown", + "application/pdf", + "text/x-php", "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "text/x-python", "text/x-script.python", "text/x-ruby", "text/x-tex", - "text/plain", "text/css", "text/javascript", "application/x-sh", - "application/typescript" + "text/x-python", + "text/x-script.python", + "text/x-ruby", + "text/x-tex", + "text/plain", + "text/css", + "text/javascript", + "application/x-sh", + "application/typescript", ] + def get_file_purpose(file_path): mime_type, _ = mimetypes.guess_type(file_path) if not mime_type: @@ -36,6 +61,7 @@ def get_file_purpose(file_path): return "assistants" raise ValueError(f"Unsupported file type: {mime_type}") + def get_tools(file_path): """Returns the tools for the given file path""" mime_type, _ = mimetypes.guess_type(file_path) @@ -46,4 +72,4 @@ def get_tools(file_path): elif mime_type in dual_types: return [{"type": "code_interpreter"}, {"type": "file_search"}] else: - raise ValueError(f"Unsupported file type: {mime_type}") \ No newline at end of file + raise ValueError(f"Unsupported file type: {mime_type}") diff --git a/agency_swarm/util/helpers/__init__.py b/agency_swarm/util/helpers/__init__.py index 607b52b1..10bca085 100644 --- a/agency_swarm/util/helpers/__init__.py +++ b/agency_swarm/util/helpers/__init__.py @@ -1,2 +1,2 @@ from .get_available_agent_descriptions import get_available_agent_descriptions -from .list_available_agents import list_available_agents \ No newline at end of file +from .list_available_agents import list_available_agents diff --git a/agency_swarm/util/helpers/get_available_agent_descriptions.py b/agency_swarm/util/helpers/get_available_agent_descriptions.py index c4fc09da..dac6c197 100644 --- a/agency_swarm/util/helpers/get_available_agent_descriptions.py +++ b/agency_swarm/util/helpers/get_available_agent_descriptions.py @@ -1,19 +1,22 @@ import importlib -from pathlib import Path import re +from pathlib import Path + from .list_available_agents import list_available_agents + def extract_description_from_file(file_path): """ Extracts the agent's description from its Python file. """ description_pattern = re.compile( - r'\s*description\s*=\s*["\'](.*?)["\'],', re.DOTALL) - with open(file_path, 'r', encoding='utf-8') as file: + r'\s*description\s*=\s*["\'](.*?)["\'],', re.DOTALL + ) + with open(file_path, "r", encoding="utf-8") as file: content = file.read() match = description_pattern.search(content) if match: - description = ' '.join(match.group(1).split()) + description = " ".join(match.group(1).split()) return description return "Description not found." @@ -41,4 +44,4 @@ def get_available_agent_descriptions(): for name, desc in descriptions.items(): agent_descriptions += f"'{name}': {desc}\n" - return agent_descriptions \ No newline at end of file + return agent_descriptions diff --git a/agency_swarm/util/helpers/list_available_agents.py b/agency_swarm/util/helpers/list_available_agents.py index 62c6887a..2cf54610 100644 --- a/agency_swarm/util/helpers/list_available_agents.py +++ b/agency_swarm/util/helpers/list_available_agents.py @@ -1,6 +1,8 @@ import os from importlib import resources -def list_available_agents(package='agency_swarm.agents'): + + +def list_available_agents(package="agency_swarm.agents"): """ Lists available agents within the specified package directory. @@ -17,12 +19,13 @@ def list_available_agents(package='agency_swarm.agents'): # Fallback for Python 3.7 and 3.8 where resources.files is not available # This requires the importlib_resources backport from importlib_resources import files as package_files + package_dir = package_files(package) # List the contents of the agents directory if package_dir.is_dir(): for entry in package_dir.iterdir(): - if entry.is_dir() and not entry.name.startswith(('.', '_')): + if entry.is_dir() and not entry.name.startswith((".", "_")): available_agents.append(entry.name) - return available_agents \ No newline at end of file + return available_agents diff --git a/agency_swarm/util/oai.py b/agency_swarm/util/oai.py index 38e22d70..cb502ce1 100644 --- a/agency_swarm/util/oai.py +++ b/agency_swarm/util/oai.py @@ -1,8 +1,8 @@ -import httpx -import openai -import threading import os +import threading +import httpx +import openai from dotenv import load_dotenv load_dotenv() @@ -16,13 +16,17 @@ def get_openai_client(): with client_lock: if client is None: # Check if the API key is set - api_key = openai.api_key or os.getenv('OPENAI_API_KEY') + api_key = openai.api_key or os.getenv("OPENAI_API_KEY") if api_key is None: - raise ValueError("OpenAI API key is not set. Please set it using set_openai_key.") - client = openai.OpenAI(api_key=api_key, - timeout=httpx.Timeout(60.0, read=40, connect=5.0), - max_retries=10, - default_headers={"OpenAI-Beta": "assistants=v2"}) + raise ValueError( + "OpenAI API key is not set. Please set it using set_openai_key." + ) + client = openai.OpenAI( + api_key=api_key, + timeout=httpx.Timeout(60.0, read=40, connect=5.0), + max_retries=10, + default_headers={"OpenAI-Beta": "assistants=v2"}, + ) return client diff --git a/agency_swarm/util/openapi.py b/agency_swarm/util/openapi.py index 8ae2bcc2..3faf7272 100644 --- a/agency_swarm/util/openapi.py +++ b/agency_swarm/util/openapi.py @@ -5,22 +5,22 @@ def validate_openapi_spec(spec: str): spec = json.loads(spec) # Validate that 'paths' is present in the spec - if 'paths' not in spec: + if "paths" not in spec: raise ValueError("The spec must contain 'paths'.") - for path, path_item in spec['paths'].items(): + for path, path_item in spec["paths"].items(): # Ensure each path item is a dictionary if not isinstance(path_item, dict): raise ValueError(f"Path item for '{path}' must be a dictionary.") for operation in path_item.values(): # Basic validation for each operation - if 'operationId' not in operation: + if "operationId" not in operation: raise ValueError("Each operation must contain an 'operationId'.") - if 'description' not in operation: + if "description" not in operation: raise ValueError("Each operation must contain a 'description'.") # Perform any additional basic validation as needed # If the function reaches this point, the spec has passed basic validation - return spec \ No newline at end of file + return spec diff --git a/agency_swarm/util/schema.py b/agency_swarm/util/schema.py index 173f352a..68146767 100644 --- a/agency_swarm/util/schema.py +++ b/agency_swarm/util/schema.py @@ -1,14 +1,11 @@ -from typing import Dict, Any - - def dereference_schema(schema): defs = schema.get("parameters", {}).get("$defs", {}) def resolve_refs(node): if isinstance(node, dict): - if '$ref' in node: - ref_path = node['$ref'] - ref_path_parts = ref_path.split('/') + if "$ref" in node: + ref_path = node["$ref"] + ref_path_parts = ref_path.split("/") ref = defs.get(ref_path_parts[-1], {}) return ref else: @@ -27,17 +24,25 @@ def reference_schema(schema): def find_and_extract_defs(node, defs, parent_key=None, path_prefix="#/$defs/"): if isinstance(node, dict): # Extract nested properties into $defs - if parent_key == 'properties' and 'properties' in node and isinstance(node['properties'], dict): - def_name = node.get('title', None) + if ( + parent_key == "properties" + and "properties" in node + and isinstance(node["properties"], dict) + ): + def_name = node.get("title", None) if def_name: defs[def_name] = node return {"$ref": path_prefix + def_name} # Recursively process the dictionary - return {k: find_and_extract_defs(v, defs, parent_key=k) for k, v in node.items()} + return { + k: find_and_extract_defs(v, defs, parent_key=k) for k, v in node.items() + } elif isinstance(node, list): # Recursively process the list - return [find_and_extract_defs(element, defs, parent_key) for element in node] + return [ + find_and_extract_defs(element, defs, parent_key) for element in node + ] else: return node @@ -45,9 +50,6 @@ def find_and_extract_defs(node, defs, parent_key=None, path_prefix="#/$defs/"): # Extract definitions and update the schema new_schema = {k: find_and_extract_defs(v, defs) for k, v in schema.items()} if defs: - new_schema['parameters'] = new_schema.get('parameters', {}) - new_schema['parameters']['$defs'] = defs + new_schema["parameters"] = new_schema.get("parameters", {}) + new_schema["parameters"]["$defs"] = defs return new_schema - - - diff --git a/agency_swarm/util/shared_state.py b/agency_swarm/util/shared_state.py index c45b3bdc..7a46c105 100644 --- a/agency_swarm/util/shared_state.py +++ b/agency_swarm/util/shared_state.py @@ -14,4 +14,4 @@ def get(self, key, default=None): def print_data(self): for key, value in self.data.items(): - print(f"{key}: {value}") \ No newline at end of file + print(f"{key}: {value}") diff --git a/agency_swarm/util/validators.py b/agency_swarm/util/validators.py index 85ce7b4e..86130f44 100644 --- a/agency_swarm/util/validators.py +++ b/agency_swarm/util/validators.py @@ -1,20 +1,33 @@ -from openai import OpenAI from typing import Callable -from pydantic import Field, BaseModel + +from openai import OpenAI +from pydantic import BaseModel, Field + from agency_swarm.util.oai import get_openai_client + class Validator(BaseModel): """ Validate if an attribute is correct and if not, return a new value with an error message """ - reason: str = Field(..., description="Step-by-step reasoning why the attribute could be valid or not with a conclussion at the end.") - is_valid: bool = Field(..., description="Whether the attribute is valid based on the requirements.") - fixed_value: str = Field(..., description="If the attribute is not valid, suggest a new value for the attribute. Otherwise, leave it empty.") + + reason: str = Field( + ..., + description="Step-by-step reasoning why the attribute could be valid or not with a conclussion at the end.", + ) + is_valid: bool = Field( + ..., description="Whether the attribute is valid based on the requirements." + ) + fixed_value: str = Field( + ..., + description="If the attribute is not valid, suggest a new value for the attribute. Otherwise, leave it empty.", + ) + def llm_validator( statement: str, - client: OpenAI=None, + client: OpenAI = None, allow_override: bool = False, model: str = "gpt-4o-mini", temperature: float = 0, @@ -86,4 +99,4 @@ def llm(v: str) -> str: return resp.fixed_value return v - return llm \ No newline at end of file + return llm diff --git a/docs/advanced-usage/agencies.md b/docs/advanced-usage/agencies.md index c40a3a49..7b1893f3 100644 --- a/docs/advanced-usage/agencies.md +++ b/docs/advanced-usage/agencies.md @@ -1,6 +1,6 @@ -# Agencies +# Agencies -An `Agency` is a collection of Agents that can communicate with one another. +An `Agency` is a collection of Agents that can communicate with one another. ### Benefits of using an Agency @@ -8,7 +8,7 @@ Here are the primary benefits of using an Agency, instead of an individual agent 1. **Fewer hallucinations**: When agents are part of an agency, they can supervise one another and recover from mistakes or unexpected circumstances. 2. **More complex tasks**: The more agents you add, the longer the sequence of actions they can perform before returning the result back to the user. -3. **Scalability**: As the complexity of your integration increases, you can keep adding more and more agents. +3. **Scalability**: As the complexity of your integration increases, you can keep adding more and more agents. !!! tip It is recommended to start with as few agents as possible, fine-tune them until they are working as expected, and only then add new agents to the agency. If you add too many agents at first, it will be difficult to debug and understand what is going on. @@ -71,7 +71,7 @@ class EventHandler(AgencyEventHandler): response = agency.get_completion_stream("I want you to build me a website", event_handler=EventHandler) ``` -Also, there is an additional class method `on_all_streams_end` which is called when all streams have ended. This method is needed because, unlike in the official documentation, your event handler will be called multiple times and probably by even multiple agents. +Also, there is an additional class method `on_all_streams_end` which is called when all streams have ended. This method is needed because, unlike in the official documentation, your event handler will be called multiple times and probably by even multiple agents. ## Async Mode @@ -92,7 +92,7 @@ With this mode, the response from the `SendMessage` tool will be returned instan If you would like to use asynchronous execution for tools, you can specify a `async_mode` parameter to `tools_threading`. With this mode on, all tools will be executed concurrently in separate threads, which can significantly speed up the work flow of I/O bound tasks. ```python -agency = Agency([ceo], async_mode='tools_threading') +agency = Agency([ceo], async_mode='tools_threading') ``` @@ -101,7 +101,7 @@ agency = Agency([ceo], async_mode='tools_threading') You can add shared files for all agents in the agency by specifying a folder path in a `shared_files` parameter. This is useful for sharing common resources that all agents need to access. ```python -agency = Agency([ceo], shared_files='shared_files') +agency = Agency([ceo], shared_files='shared_files') ``` ### Settings Path @@ -109,7 +109,7 @@ agency = Agency([ceo], shared_files='shared_files') If you would like to use a different file path for the settings, other than default `settings.json`, you can specify a `settings_path` parameter. All your agent states will then be saved and loaded from this file. If this file does not exist, it will be created, along with new Assistants on your OpenAI account. ```python -agency = Agency([ceo], settings_path='my_settings.json') +agency = Agency([ceo], settings_path='my_settings.json') ``` ### Temperature and Max Token Controls @@ -117,7 +117,7 @@ agency = Agency([ceo], settings_path='my_settings.json') You can also specify parameters like `temperature`, `top_p`, `max_completion_tokens`, `max_prompt_tokens` and `truncation_strategy`, parameters for the entire agency. These parameters will be used as default values for all agents in the agency, however, you can still override them for individual agents by specifying them in the agent's constructor. ```python -agency = Agency([ceo], temperature=0.3, max_prompt_tokens=25000) +agency = Agency([ceo], temperature=0.3, max_prompt_tokens=25000) ``` ## Running the Agency @@ -131,13 +131,13 @@ When it comes to running the agency, you have 3 options: ### Running the Agency inside a Gradio Interface ```python -agency.demo_gradio(height=700) +agency.demo_gradio(height=700) ``` ### Get completion from the agency ```python -response = agency.get_completion("I want you to build me a website", +response = agency.get_completion("I want you to build me a website", additional_instructions="This is an additional instruction for the task.", tool_choice={"type": "function", "function": {"name": "SendMessage"}}, attachments=[], diff --git a/docs/advanced-usage/agents.md b/docs/advanced-usage/agents.md index 78852c11..1d3d68df 100644 --- a/docs/advanced-usage/agents.md +++ b/docs/advanced-usage/agents.md @@ -31,13 +31,13 @@ agent = Agent(name='MyAgent', file_search={'max_num_results': 25}) # must be bet ### Schemas Folder -You can specify the folder where the agent will look for OpenAPI schemas to convert into tools. Additionally, you can add `api_params` and `api_headers` to the schema to pass additional parameters and headers to the API call. +You can specify the folder where the agent will look for OpenAPI schemas to convert into tools. Additionally, you can add `api_params` and `api_headers` to the schema to pass additional parameters and headers to the API call. ```python from agency_swarm import Agent -agent = Agent(name='MyAgent', - schemas_folder='schemas', +agent = Agent(name='MyAgent', + schemas_folder='schemas', api_params={'my_schema.json': {'param1': 'value1'}}, api_headers={'my_schema.json': {'Authorization': 'Bearer token'}} ) @@ -49,7 +49,7 @@ agent = Agent(name='MyAgent', ### Fine Tuned models -You can use any previously fine-tuned model by specifying the `model` parameter in the agent. +You can use any previously fine-tuned model by specifying the `model` parameter in the agent. ```python from agency_swarm import Agent @@ -70,7 +70,7 @@ class MyAgent(Agent): """This function is used to validate the response before sending it to the user or another agent.""" if "bad word" in message: raise ValueError("Please don't use bad words.") - + return message ``` @@ -113,7 +113,7 @@ When it comes to creating your agent, you have 3 options: ### Defining the agent directly in the code -To define your agent in the code, you can simply instantiate the `Agent` class and pass the required parameters. +To define your agent in the code, you can simply instantiate the `Agent` class and pass the required parameters. ```python from agency_swarm import Agent @@ -148,18 +148,18 @@ When you run the `create-agent-template` command, it creates the following folde └── AgentName/ # Directory for the specific agent ├── files/ # Directory for files that will be uploaded to openai ├── schemas/ # Directory for OpenAPI schemas to be converted into tools - ├── tools/ # Directory for tools to be imported by default. + ├── tools/ # Directory for tools to be imported by default. ├── AgentName.py # The main agent class file ├── __init__.py # Initializes the agent folder as a Python package └── instructions.md or .txt # Instruction document for the agent - + ``` - `files`: This folder is used to store files that will be uploaded to OpenAI. You can use any of the [acceptable file formats](https://platform.openai.com/docs/assistants/tools/supported-files). After file is uploaded, an id will be attached to the file name to avoid re-uploading the same file twice. - `schemas`: This folder is used to store OpenAPI schemas that will be converted into tools automatically. All you have to do is put the schema in this folder, and specify it when initializing your agent. - `tools`: This folder is used to store tools in the form of Python files. Each file must have the same name as the tool class for it to be imported by default. For example, `ExampleTool.py` must contain a class called `ExampleTool`. -#### Agent Template +#### Agent Template The `AgentName.py` file will contain the following code: @@ -184,7 +184,7 @@ class AgentName(Agent): """This function is used to validate the response before sending it to the user or another agent.""" if "bad word" in message: raise ValueError("Please don't use bad words.") - + return message ``` @@ -204,4 +204,4 @@ For the most complex and requested use cases, we will be creating premade agents agency-swarm import-agent --name "AgentName" --destination "/path/to/directory" ``` -This will copy all your agent source files locally. You can then import the agent as shown above. To check available agents, simply run this command without any arguments. \ No newline at end of file +This will copy all your agent source files locally. You can then import the agent as shown above. To check available agents, simply run this command without any arguments. diff --git a/docs/advanced-usage/azure-openai.md b/docs/advanced-usage/azure-openai.md index 18b14d09..649a1168 100644 --- a/docs/advanced-usage/azure-openai.md +++ b/docs/advanced-usage/azure-openai.md @@ -48,4 +48,4 @@ agency.run_demo() ## Example Notebook -You can find an example notebook for using Azure OpenAI in the [notebooks folder](https://github.com/VRSEN/agency-swarm/blob/main/notebooks/azure.ipynb). \ No newline at end of file +You can find an example notebook for using Azure OpenAI in the [notebooks folder](https://github.com/VRSEN/agency-swarm/blob/main/notebooks/azure.ipynb). diff --git a/docs/advanced-usage/open-source-models.md b/docs/advanced-usage/open-source-models.md index bf7a9aa8..f4123cb4 100644 --- a/docs/advanced-usage/open-source-models.md +++ b/docs/advanced-usage/open-source-models.md @@ -18,7 +18,7 @@ To use agency-swarm with Astra Assistants API, follow these steps: ![Astra Assistants API Example](https://firebasestorage.googleapis.com/v0/b/vrsen-ai/o/public%2Fgithub%2FScreenshot%202024-07-01%20at%208.19.00%E2%80%AFAM.png?alt=media&token=b4f1a7ad-3b77-40fa-a5da-866a4f1410bd) -**2. Add Astra DB Token to your .env file:** +**2. Add Astra DB Token to your .env file:** Copy token from the file that starts with "AstraCS:" and paste it into your .env file. ```env @@ -35,11 +35,11 @@ GROQ_API_KEY=your_groq_api_key ``` **4. Install the Astra Assistants API and gradio:** - + ```bash pip install astra-assistants-api gradio ``` - + **5. Patch the OpenAI client:** ```python @@ -55,14 +55,14 @@ client = patch(OpenAI()) set_openai_client(client) ``` -**6. Create an agent:** +**6. Create an agent:** Create an agent and replace the model parameter with the name of the model you want to use. With Astra Assistants you can upload files like usual using `files_folder`. ```python from agency_swarm import Agent -ceo = Agent(name="ceo", - description="I am the CEO", +ceo = Agent(name="ceo", + description="I am the CEO", model='ollama/llama3', # model = 'perplexity/llama-3-8b-instruct' # model = 'anthropic/claude-3-5-sonnet-20240620' @@ -72,7 +72,7 @@ ceo = Agent(name="ceo", ) ``` -**7. Create an agency:** +**7. Create an agency:** You can add more agents as needed, just make sure all manager agents support function calling. @@ -82,7 +82,7 @@ from agency_swarm import Agency agency = Agency([ceo]) ``` -**8. Start gradio:** +**8. Start gradio:** To utilize your agency in gradio, apply a specific non-streaming `demo_gradio` method from the [agency-swarm-lab](https://github.com/VRSEN/agency-swarm-lab/blob/main/OpenSourceSwarm/demo_gradio.py) repository: @@ -126,7 +126,7 @@ from agency_swarm import Agent ceo = Agent(name="ceo", description="I am the CEO", model='ollama/llama3') ``` -**4. Start Gradio:** +**4. Start Gradio:** To utilize your agency in gradio, apply a specific non-streaming `demo_gradio` method from the [agency-swarm-lab](https://github.com/VRSEN/agency-swarm-lab/blob/main/OpenSourceSwarm/demo_gradio.py) repository: @@ -153,6 +153,6 @@ agency.get_completion("I am the CEO") ## Future Plans -Updates will be provided as new open-source assistant API implementations stabilize. +Updates will be provided as new open-source assistant API implementations stabilize. -If you successfully integrate other projects with agency-swarm, please share your experience through an issue or pull request. \ No newline at end of file +If you successfully integrate other projects with agency-swarm, please share your experience through an issue or pull request. diff --git a/docs/advanced-usage/tools.md b/docs/advanced-usage/tools.md index d9f3e60a..bf442b1b 100644 --- a/docs/advanced-usage/tools.md +++ b/docs/advanced-usage/tools.md @@ -1,12 +1,12 @@ # Advanced Tools -All tools in Agency Swarm are created using [Instructor](https://github.com/jxnl/instructor). +All tools in Agency Swarm are created using [Instructor](https://github.com/jxnl/instructor). The only difference is that you must extend the `BaseTool` class and implement the `run` method with your logic inside. For many great examples on what you can create, checkout [Instructor Cookbook](https://jxnl.github.io/instructor/examples/). --- -## Example: Converting [Answering Questions with Validated Citations Example](https://jxnl.github.io/instructor/examples/exact_citations/) from Instructor +## Example: Converting [Answering Questions with Validated Citations Example](https://jxnl.github.io/instructor/examples/exact_citations/) from Instructor This is an example of how to convert an extremely useful tool for RAG applications from instructor. It allows your agents to not only answer questions based on context, but also to provide the exact citations for the answers. This way your users can be sure that the information is always accurate and reliable. @@ -23,7 +23,7 @@ import re class Fact(BaseModel): fact: str = Field(...) substring_quote: List[str] = Field(...) - + @model_validator(mode="after") def validate_sources(self, info: FieldValidationInfo) -> "Fact": text_chunks = info.context.get("text_chunk", None) @@ -66,24 +66,24 @@ To allow your agents to retrieve the context themselves, we must split `Question class QueryDatabase(BaseTool): """Use this tool to query a vector database to retrieve the relevant context for the question.""" question: str = Field(..., description="The question to be answered") - + def run(self): - # Check if context is already retrieved + # Check if context is already retrieved if self._shared_state.get("context", None) is not None: raise ValueError("Context already retrieved. Please proceed with the AnswerQuestion tool.") - + # Your code to retrieve the context here context = "This is a test context" - + # Then, save the context to the shared state self._shared_state.set("context", context) - + return f"Context retrieved: {context}.\n\n Please proceed with the AnswerQuestion tool." ``` !!! note "Shared State" - `shared_state` is a state that is shared between all tools, across all agents. It allows you to control the execution flow, share data, and provide instructions to the agents based on certain conditions or actions performed by other agents. + `shared_state` is a state that is shared between all tools, across all agents. It allows you to control the execution flow, share data, and provide instructions to the agents based on certain conditions or actions performed by other agents. #### The `AnswerQuestion` tool will: @@ -96,15 +96,15 @@ class QueryDatabase(BaseTool): class AnswerQuestion(BaseTool): answer: str = Field(..., description="The answer to the question, based on context.") sources: List[Fact] = Field(..., description="The sources of the answer") - + def run(self): # Remove the context after question is answered self._shared_state.set("context", None) - + # additional logic here as needed, for example save the answer to a database - + return "Success. The question has been answered." # or return the answer, if needed - + @model_validator(mode="after") def validate_sources(self) -> "QuestionAnswer": # In "Agency Swarm", context is directly extracted from `shared_state` @@ -114,7 +114,7 @@ class AnswerQuestion(BaseTool): raise ValueError("Please retrieve the context with the QueryDatabase tool first.") self.answer = [fact for fact in self.answer if len(fact.substring_quote) > 0] return self - + ``` @@ -126,13 +126,13 @@ The `Fact` tool will stay primarily the same. The only difference is that we mus class Fact(BaseTool): fact: str = Field(...) substring_quote: List[str] = Field(...) - + def run(self): pass - + @model_validator(mode="after") def validate_sources(self) -> "Fact": - context = self._shared_state.get("context", None) + context = self._shared_state.get("context", None) text_chunks = context.get("text_chunk", None) spans = list(self.get_spans(text_chunks)) self.substring_quote = [text_chunks[span[0] : span[1]] for span in spans] @@ -150,7 +150,7 @@ To implement tools with Instructor in Agency Swarm, generally, you must: 1. Extend the `BaseTool` class. 2. Add fields with types and clear descriptions, plus the tool description itself. 3. Implement the `run` method with your execution logic inside. -4. Add validators and checks based on various conditions. +4. Add validators and checks based on various conditions. 5. Split tools into smaller tools to give your agents more control, as needed. @@ -163,23 +163,23 @@ Tool factory is a class that allows you to create tools from different sources. ### Import from Langchain -!!! warning "Not recommended" +!!! warning "Not recommended" This method is not recommended, as it does not provide the same level of type checking, error correction and tool descriptions as Instructor. However, it is still possible to use this method if you prefer. ```python from langchain.tools import YouTubeSearchTool from agency_swarm.tools import ToolFactory - + LangchainTool = ToolFactory.from_langchain_tool(YouTubeSearchTool) ``` - + ```python from langchain.agents import load_tools - + tools = load_tools( ["arxiv", "human"], ) - + tools = ToolFactory.from_langchain_tools(tools) ``` @@ -210,7 +210,7 @@ tools = ToolFactory.from_openapi_schema( ```python class RunCommand(BaseTool): command: Literal["start", "stop"] = Field(...) - + def run(self): if command == "start": subprocess.run(["start", "your_command"]) @@ -226,23 +226,23 @@ tools = ToolFactory.from_openapi_schema( ```python class QueryDatabase(BaseTool): question: str = Field(...) - + def run(self): # query your database here context = query_database(self.question) - + if context is None: raise ValueError("No context found. Please propose to the user to change the topic.") else: self._shared_state.set("context", context) return "Context retrieved. Please proceed with explaining the answer." - ``` + ``` 3. Use `shared_state` to validate actions taken by other agents, before allowing them to proceed with the next action. ```python class Action2(BaseTool): input: str = Field(...) - + def run(self): if self._shared_state.get("action_1_result", None) is "failure": raise ValueError("Please proceed with the Action1 tool first.") @@ -257,7 +257,7 @@ tools = ToolFactory.from_openapi_schema( class ToolConfig: one_call_at_a_time = True - + def run(self): # your code here ``` @@ -276,4 +276,4 @@ tools = ToolFactory.from_openapi_schema( def run(self): return f"The weather in {self.location} is 30 degrees." - ``` \ No newline at end of file + ``` diff --git a/docs/api.md b/docs/api.md index f0227879..b7192a5c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,7 +1,7 @@ -# API Reference +# API Reference ::: agency_swarm.agents.agent ::: agency_swarm.agency.agency -::: agency_swarm.tools.ToolFactory \ No newline at end of file +::: agency_swarm.tools.ToolFactory diff --git a/docs/contributing.md b/docs/contributing.md index cd2a36b4..93065b1d 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -62,7 +62,7 @@ class AgentName(Agent): # Set instructions kwargs['instructions'] = "./instructions.md" - + # Add more kwargs as needed # Initialize the parent class @@ -70,4 +70,4 @@ class AgentName(Agent): ``` -Thank you for contributing to Agency Swarm! Your efforts help us build a more robust and versatile framework. \ No newline at end of file +Thank you for contributing to Agency Swarm! Your efforts help us build a more robust and versatile framework. diff --git a/docs/deployment.md b/docs/deployment.md index fdc6f563..d475e402 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -8,7 +8,7 @@ To deploy your Agency on a production server, typically, you need to do the foll ## Loading Agents and Threads dynamically -To load agents and threads dynamically, based on specific conditions, you will need to implement `threads_callbacks` and `settings_callbacks` in your agency. +To load agents and threads dynamically, based on specific conditions, you will need to implement `threads_callbacks` and `settings_callbacks` in your agency. ### Settings Callbacks @@ -49,11 +49,11 @@ def save_threads(new_threads: Dict): ### Example Below is an example of how you initialize an agency with these callbacks. You will typically need to get some info like `user_id` or `chat_id` beforehand, and pass them into these callbacks, depending on your use case or business logic: - + ```python agency = Agency([ceo], threads_callbacks={ - 'load': lambda: load_threads(chat_id), + 'load': lambda: load_threads(chat_id), 'save': lambda new_threads: save_threads(new_threads) }, settings_callbacks={ @@ -67,4 +67,3 @@ agency = Agency([ceo], ## Deploy each agent as a separate microservice ... coming soon ... - diff --git a/docs/examples.md b/docs/examples.md index c0f45fa7..c17d1191 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -12,7 +12,7 @@ Examples of Agencies can be found in the [agency-swarm-lab](https://github.com/V ## Videos with Notebooks -- [Browsing Agent for QA Testing Agency](https://youtu.be/Yidy_ePo7pE?si=WMuWpb9_DVckIkP6) - This video shows how to use BrowsingAgent with GPT-4 vision inside a QA testing agency. It can also break captcha, as shown in [this video](https://youtu.be/qBs_50SzyBQ?si=w7e3GOhEztG8qDPE). The notebook is available [here](https://github.com/VRSEN/agency-swarm/blob/main/notebooks/web_browser_agent.ipynb). +- [Browsing Agent for QA Testing Agency](https://youtu.be/Yidy_ePo7pE?si=WMuWpb9_DVckIkP6) - This video shows how to use BrowsingAgent with GPT-4 vision inside a QA testing agency. It can also break captcha, as shown in [this video](https://youtu.be/qBs_50SzyBQ?si=w7e3GOhEztG8qDPE). The notebook is available [here](https://github.com/VRSEN/agency-swarm/blob/main/notebooks/web_browser_agent.ipynb). - [Genesis Agency](https://youtu.be/qXxO7SvbGs8?si=uosmTSzzz6id_lLl) - This agency creates your agents for you. The notebook is available [here](https://github.com/VRSEN/agency-swarm/blob/main/notebooks/genesis_agency.ipynb). -### ... more coming soon \ No newline at end of file +### ... more coming soon diff --git a/docs/index.md b/docs/index.md index ea198abf..647e304a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,14 +13,14 @@ An open source agent orchestration framework built on top of the latest [OpenAI ## What is Agency Swarm? -Agency Swarm started as a desire and effort of Arsenii Shatokhin (aka VRSEN) to fully automate his AI Agency with AI. By building this framework, we aim to simplify the agent creation process and enable anyone to create collaborative swarm of agents (Agencies), each with distinct roles and capabilities. By thinking about automation in terms of **real world entities**, such as agencies and specialized agent roles, we make it a lot more intuitive for both the agents and the users. +Agency Swarm started as a desire and effort of Arsenii Shatokhin (aka VRSEN) to fully automate his AI Agency with AI. By building this framework, we aim to simplify the agent creation process and enable anyone to create collaborative swarm of agents (Agencies), each with distinct roles and capabilities. By thinking about automation in terms of **real world entities**, such as agencies and specialized agent roles, we make it a lot more intuitive for both the agents and the users. ### Key Features - **Customizable Agent Roles**: Define roles like CEO, virtual assistant, developer, etc., and customize their functionalities with [Assistants API](https://platform.openai.com/docs/assistants/overview). - **Full Control Over Prompts**: Avoid conflicts and restrictions of pre-defined prompts, allowing full customization. -- **Tool Creation**: Tools within Agency Swarm are created using [Instructor](https://github.com/jxnl/instructor), which provides a convenient interface and automatic type validation. +- **Tool Creation**: Tools within Agency Swarm are created using [Instructor](https://github.com/jxnl/instructor), which provides a convenient interface and automatic type validation. - **Efficient Communication**: Agents communicate through a specially designed "send message" tool based on their own descriptions. - **State Management**: Agency Swarm efficiently manages the state of your assistants on OpenAI, maintaining it in a special `settings.json` file. - **Deployable in Production**: Agency Swarm is designed to be reliable and easily deployable in production environments. @@ -37,7 +37,7 @@ Unlike other frameworks, Agency Swarm: ### **AutoGen** vs Agency Swarm -In AutoGen, by default, the next speaker is determined with an extra call to the model that emulates "role play" between the agents. [[1]](https://microsoft.github.https://microsoft.github.io/autogen/blog/2023/12/29/AgentDescriptionsio/autogen/blog/2023/12/29/AgentDescriptions) Not only this is very inefficient, but it also makes the system less controllable and less customizable, because you cannot control which agent can communicate with which other agent. +In AutoGen, by default, the next speaker is determined with an extra call to the model that emulates "role play" between the agents. [[1]](https://microsoft.github.https://microsoft.github.io/autogen/blog/2023/12/29/AgentDescriptionsio/autogen/blog/2023/12/29/AgentDescriptions) Not only this is very inefficient, but it also makes the system less controllable and less customizable, because you cannot control which agent can communicate with which other agent. Recently, autogen has added support for [determining the next speaker based on certain hardcoded conditions](https://microsoft.github.io/autogen/docs/notebooks/agentchat_groupchat_customized/). While this does make your system more customizable, it completely undermines the main benefit of agentic systems - adaptability. In my opinion, **you should only determine the boundaries for your agents, not the conditions themselves, as you are unlikely to account for every single condition in the real world.** ([#113](https://github.com/VRSEN/agency-swarm/issues/113)) diff --git a/docs/quick_start.md b/docs/quick_start.md index ccc1344d..95784ced 100644 --- a/docs/quick_start.md +++ b/docs/quick_start.md @@ -20,38 +20,38 @@ pip install agency-swarm from agency_swarm import set_openai_key set_openai_key("YOUR_API_KEY") ``` - -2. **Create Tools**: Define your custom tools with [Instructor](https://github.com/jxnl/instructor). -All tools must extend the `BaseTool` class and implement the `run` method. + +2. **Create Tools**: Define your custom tools with [Instructor](https://github.com/jxnl/instructor). +All tools must extend the `BaseTool` class and implement the `run` method. ```python from agency_swarm.tools import BaseTool from pydantic import Field - + class MyCustomTool(BaseTool): """ - A brief description of what the custom tool does. + A brief description of what the custom tool does. The docstring should clearly explain the tool's purpose and functionality. It will be used by the agent to determine when to use this tool. """ - + # Define the fields with descriptions using Pydantic Field example_field: str = Field( ..., description="Description of the example field, explaining its purpose and usage for the Agent." ) - + # Additional Pydantic fields as required # ... - + def run(self): """ The implementation of the run method, where the tool's main functionality is executed. This method should utilize the fields defined above to perform the task. Doc string is not required for this method and will not be used by your agent. """ - + # Your custom tool logic goes here do_something(self.example_field) - + # Return the result of the tool's operation as a string return "Result of MyCustomTool operation" ``` @@ -61,7 +61,7 @@ All tools must extend the `BaseTool` class and implement the `run` method. ```python from agency_swarm import Agent - + ceo = Agent(name="CEO", description="Responsible for client communication, task planning and management.", instructions="You must converse with other agents to ensure complete task execution.", # can be a file like ./instructions.md @@ -73,39 +73,39 @@ All tools must extend the `BaseTool` class and implement the `run` method. tools=[MyCustomTool]) ``` -4. **Create Agency**: Define your agency chart. +4. **Create Agency**: Define your agency chart. Any agents that are listed in the same list (eg. `[[ceo, dev]]`) can communicate with each other. The top-level list (`[ceo]`) defines agents that can communicate with the user. ```python from agency_swarm import Agency - + agency = Agency([ ceo, # CEO will be the entry point for communication with the user [ceo, dev], # CEO can initiate communication with Developer ], shared_instructions='You are a part of an ai development agency.\n\n') # shared instructions for all agents ``` - + !!! note "Note on Communication Flows" In Agency Swarm, communication flows are directional, meaning they are established from left to right in the agency_chart definition. For instance, in the example above, the CEO can initiate a chat with the developer (dev), and the developer can respond in this chat. However, the developer cannot initiate a chat with the CEO. - 5. **Run Demo**: + 5. **Run Demo**: Run the demo to see your agents in action! - + Web interface: ```python agency.demo_gradio(height=900) ``` - + Terminal version: - + ```python agency.run_demo() ``` - + Backend version: - + ```python completion_output = agency.get_completion("Please create a new website for our client.", yield_messages=False) ``` @@ -125,7 +125,7 @@ All tools must extend the `BaseTool` class and implement the `run` method. - The agents you want to involve and their communication flows. - Which tools or APIs each agent should have access to, if any. -3. **Fine Tune**: After Genesis has created your agents for you, you will see all the agent folders in the same directory where you ran the `genesis` command. You can then fine-tune the agents and tools as per your requirements. To do so, follow these steps: +3. **Fine Tune**: After Genesis has created your agents for you, you will see all the agent folders in the same directory where you ran the `genesis` command. You can then fine-tune the agents and tools as per your requirements. To do so, follow these steps: 1. **Adjust Tools**: Modify the tools in the `tools` directories of each agent as per your requirements. @@ -140,5 +140,3 @@ All tools must extend the `BaseTool` class and implement the `run` method. - Learn how to create more Tools, Agents and Agencies - Deploy in Production - - diff --git a/notebooks/agency_async.ipynb b/notebooks/agency_async.ipynb index 3af4e7f3..58d7fdb0 100644 --- a/notebooks/agency_async.ipynb +++ b/notebooks/agency_async.ipynb @@ -2,123 +2,145 @@ "cells": [ { "cell_type": "markdown", - "source": [ - "# Make sure you have the latest version of agency-swarm installed\n", - "You can uninstall the old version with `pip uninstall agency-swarm` and install the latest version with `pip install agency-swarm`" - ], + "id": "2c4b19dbe5ae6302", "metadata": { "collapsed": false }, - "id": "2c4b19dbe5ae6302" + "source": [ + "# Make sure you have the latest version of agency-swarm installed\n", + "You can uninstall the old version with `pip uninstall agency-swarm` and install the latest version with `pip install agency-swarm`" + ] }, { "cell_type": "code", - "outputs": [], - "source": [ - "from agency_swarm import Agent, Agency\n", - "\n", - "ceo = Agent(name=\"CEO\",\n", - " description=\"Responsible for client communication, task planning and management.\",\n", - " instructions=\"You must converse with other agents to ensure complete task execution.\", # can be a file like ./instructions.md\n", - " tools=[])\n", - "\n", - "test = Agent(name=\"Test Agent\",\n", - " description=\"Test agent\",\n", - " instructions=\"Please always respond with 'test complete'\", # can be a file like ./instructions.md\n", - " tools=[])" - ], + "execution_count": 1, + "id": "a16ee4220f5ab03a", "metadata": { - "collapsed": false, "ExecuteTime": { "end_time": "2024-01-29T03:34:23.557863Z", "start_time": "2024-01-29T03:34:23.157979Z" - } + }, + "collapsed": false }, - "id": "a16ee4220f5ab03a", - "execution_count": 1 + "outputs": [], + "source": [ + "from agency_swarm import Agency, Agent\n", + "\n", + "ceo = Agent(\n", + " name=\"CEO\",\n", + " description=\"Responsible for client communication, task planning and management.\",\n", + " instructions=\"You must converse with other agents to ensure complete task execution.\", # can be a file like ./instructions.md\n", + " tools=[],\n", + ")\n", + "\n", + "test = Agent(\n", + " name=\"Test Agent\",\n", + " description=\"Test agent\",\n", + " instructions=\"Please always respond with 'test complete'\", # can be a file like ./instructions.md\n", + " tools=[],\n", + ")" + ] }, { "cell_type": "markdown", - "source": [ - "## Loading agents and threads from DB example" - ], + "id": "8d99382d99b7a8ac", "metadata": { "collapsed": false }, - "id": "8d99382d99b7a8ac" + "source": [ + "## Loading agents and threads from DB example" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "ba3f9da173f7a0b6", + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "# threads is an object\n", "threads = {}\n", "\n", + "\n", "def load_threads():\n", " # your code to load threads from DB here\n", " # we simply use a global variable for this example\n", " global threads\n", " return threads\n", "\n", + "\n", "def save_threads(new_threads):\n", " # your code to save new_threads to DB here\n", " global threads\n", " threads = new_threads" - ], - "metadata": { - "collapsed": false - }, - "id": "ba3f9da173f7a0b6" + ] }, { "cell_type": "code", + "execution_count": null, + "id": "7920db25caf29803", + "metadata": { + "collapsed": false + }, "outputs": [], "source": [ "# settings is an array of objects with your agent settings\n", "settings = []\n", "\n", + "\n", "def load_settings():\n", " # your code to load settings from DB here\n", " # we simply use a global variable for this example\n", " global settings\n", " return settings\n", "\n", + "\n", "def save_settings(new_settings):\n", " # your code to save new_settings to DB here\n", " global settings\n", " settings = new_settings" - ], - "metadata": { - "collapsed": false - }, - "id": "7920db25caf29803" + ] }, { "cell_type": "markdown", - "source": [ - "## Creating agency with loaded agents and threads" - ], + "id": "88734e5628b23466", "metadata": { "collapsed": false }, - "id": "88734e5628b23466" + "source": [ + "## Creating agency with loaded agents and threads" + ] }, { "cell_type": "code", - "outputs": [], - "source": [ - "agency = Agency([ceo, [ceo, test]], \n", - " async_mode='threading', # only threading is supported for now\n", - " threads_callbacks={'load': load_threads, 'save': save_threads},\n", - " settings_callbacks={'load': load_settings, 'save': save_settings})" - ], + "execution_count": null, + "id": "a65a5acb4bc6bbc3", "metadata": { "collapsed": false }, - "id": "a65a5acb4bc6bbc3" + "outputs": [], + "source": [ + "agency = Agency(\n", + " [ceo, [ceo, test]],\n", + " async_mode=\"threading\", # only threading is supported for now\n", + " threads_callbacks={\"load\": load_threads, \"save\": save_threads},\n", + " settings_callbacks={\"load\": load_settings, \"save\": save_settings},\n", + ")" + ] }, { "cell_type": "code", + "execution_count": 3, + "id": "f578f37d8b261559", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-23T04:17:09.976706Z", + "start_time": "2024-01-23T04:17:01.254030Z" + }, + "collapsed": false + }, "outputs": [ { "name": "stdout", @@ -139,19 +161,19 @@ ], "source": [ "agency.get_completion(\"Say hi to test agent\", yield_messages=False)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-01-23T04:17:09.976706Z", - "start_time": "2024-01-23T04:17:01.254030Z" - } - }, - "id": "f578f37d8b261559", - "execution_count": 3 + ] }, { "cell_type": "code", + "execution_count": 4, + "id": "fc798bc6c58c9c16", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-23T04:17:37.434522Z", + "start_time": "2024-01-23T04:17:30.450008Z" + }, + "collapsed": false + }, "outputs": [ { "name": "stdout", @@ -171,19 +193,19 @@ ], "source": [ "agency.get_completion(\"Check status\", yield_messages=False)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-01-23T04:17:37.434522Z", - "start_time": "2024-01-23T04:17:30.450008Z" - } - }, - "id": "fc798bc6c58c9c16", - "execution_count": 4 + ] }, { "cell_type": "code", + "execution_count": 7, + "id": "6ef9050ecc718655", + "metadata": { + "ExecuteTime": { + "end_time": "2024-01-21T03:36:13.946078Z", + "start_time": "2024-01-21T03:36:13.843445Z" + }, + "collapsed": false + }, "outputs": [ { "name": "stdout", @@ -196,8 +218,8 @@ }, { "data": { - "text/plain": "", - "text/html": "
" + "text/html": "
", + "text/plain": "" }, "metadata": {}, "output_type": "display_data" @@ -213,25 +235,17 @@ ], "source": [ "agency.demo_gradio()" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-01-21T03:36:13.946078Z", - "start_time": "2024-01-21T03:36:13.843445Z" - } - }, - "id": "6ef9050ecc718655", - "execution_count": 7 + ] }, { "cell_type": "code", - "outputs": [], - "source": [], + "execution_count": null, + "id": "94e0b7064cfb2c62", "metadata": { "collapsed": false }, - "id": "94e0b7064cfb2c62" + "outputs": [], + "source": [] } ], "metadata": { diff --git a/notebooks/azure.ipynb b/notebooks/azure.ipynb index 770a25e7..7fdcf042 100644 --- a/notebooks/azure.ipynb +++ b/notebooks/azure.ipynb @@ -5,16 +5,18 @@ "execution_count": 1, "id": "initial_id", "metadata": { - "collapsed": true, "ExecuteTime": { "end_time": "2024-02-27T05:09:13.406911Z", "start_time": "2024-02-27T05:09:12.975080Z" - } + }, + "collapsed": true }, "outputs": [], "source": [ "import os\n", + "\n", "from openai import AzureOpenAI\n", + "\n", "from agency_swarm import set_openai_client\n", "\n", "client = AzureOpenAI(\n", @@ -32,44 +34,55 @@ }, { "cell_type": "code", - "outputs": [], - "source": [ - "from agency_swarm import Agent\n", - "\n", - "agent1 = Agent(name=\"agent1\", description=\"I am a simple agent\", model='assistants-test')\n", - "\n", - "ceo = Agent(name=\"ceo\", description=\"I am the CEO\", model='assistants-test')" - ], + "execution_count": 2, + "id": "6ed63cac3adfd958", "metadata": { - "collapsed": false, "ExecuteTime": { "end_time": "2024-02-27T05:09:14.574852Z", "start_time": "2024-02-27T05:09:14.570556Z" - } + }, + "collapsed": false }, - "id": "6ed63cac3adfd958", - "execution_count": 2 - }, - { - "cell_type": "code", "outputs": [], "source": [ - "from agency_swarm import Agency\n", + "from agency_swarm import Agent\n", "\n", - "agency = Agency([ceo, [ceo, agent1]])" - ], + "agent1 = Agent(\n", + " name=\"agent1\", description=\"I am a simple agent\", model=\"assistants-test\"\n", + ")\n", + "\n", + "ceo = Agent(name=\"ceo\", description=\"I am the CEO\", model=\"assistants-test\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8d34677515d0b414", "metadata": { - "collapsed": false, "ExecuteTime": { "end_time": "2024-02-27T05:09:17.184056Z", "start_time": "2024-02-27T05:09:15.516931Z" - } + }, + "collapsed": false }, - "id": "8d34677515d0b414", - "execution_count": 3 + "outputs": [], + "source": [ + "from agency_swarm import Agency\n", + "\n", + "agency = Agency([ceo, [ceo, agent1]])" + ] }, { "cell_type": "code", + "execution_count": 6, + "id": "49d28043d85f925c", + "metadata": { + "ExecuteTime": { + "end_time": "2024-02-27T05:09:50.392702Z", + "start_time": "2024-02-27T05:09:39.272522Z" + }, + "collapsed": false + }, "outputs": [ { "name": "stdout", @@ -81,20 +94,22 @@ } ], "source": [ - "response = agency.get_completion(\"Say hi to agent1. Let me know his response.\", yield_messages=False)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-02-27T05:09:50.392702Z", - "start_time": "2024-02-27T05:09:39.272522Z" - } - }, - "id": "49d28043d85f925c", - "execution_count": 6 + "response = agency.get_completion(\n", + " \"Say hi to agent1. Let me know his response.\", yield_messages=False\n", + ")" + ] }, { "cell_type": "code", + "execution_count": 7, + "id": "57d25ceeb2860261", + "metadata": { + "ExecuteTime": { + "end_time": "2024-02-27T05:09:50.398665Z", + "start_time": "2024-02-27T05:09:50.394964Z" + }, + "collapsed": false + }, "outputs": [ { "name": "stdout", @@ -106,16 +121,7 @@ ], "source": [ "print(response)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2024-02-27T05:09:50.398665Z", - "start_time": "2024-02-27T05:09:50.394964Z" - } - }, - "id": "57d25ceeb2860261", - "execution_count": 7 + ] } ], "metadata": { diff --git a/notebooks/genesis_agency.ipynb b/notebooks/genesis_agency.ipynb index 700ac972..e0c5fd52 100644 --- a/notebooks/genesis_agency.ipynb +++ b/notebooks/genesis_agency.ipynb @@ -2,19 +2,19 @@ "cells": [ { "cell_type": "code", - "outputs": [], - "source": [ - "!pip install agency-swarm selenium webdriver-manager selenium_stealth gradio" - ], + "execution_count": 1, + "id": "dfd7b2e0ab798deb", "metadata": { - "collapsed": false, "ExecuteTime": { "end_time": "2024-01-20T02:55:04.842809Z", "start_time": "2024-01-20T02:55:03.742371Z" - } + }, + "collapsed": false }, - "id": "dfd7b2e0ab798deb", - "execution_count": 1 + "outputs": [], + "source": [ + "!pip install agency-swarm selenium webdriver-manager selenium_stealth gradio" + ] }, { "cell_type": "code", @@ -30,6 +30,7 @@ "outputs": [], "source": [ "from agency_swarm import set_openai_key\n", + "\n", "set_openai_key(\"YOUR_OPENAI_API_KEY\")" ] }, diff --git a/notebooks/os_models_with_astra_assistants_api.ipynb b/notebooks/os_models_with_astra_assistants_api.ipynb index 2c081b32..f3ff337f 100644 --- a/notebooks/os_models_with_astra_assistants_api.ipynb +++ b/notebooks/os_models_with_astra_assistants_api.ipynb @@ -128,7 +128,8 @@ "source": [ "# add agency swarm from local\n", "import sys\n", - "sys.path.append('../agency-swarm')" + "\n", + "sys.path.append(\"../agency-swarm\")" ] }, { @@ -159,10 +160,11 @@ } ], "source": [ - "from openai import OpenAI\n", "from astra_assistants import patch\n", - "from agency_swarm import set_openai_client\n", "from dotenv import load_dotenv\n", + "from openai import OpenAI\n", + "\n", + "from agency_swarm import set_openai_client\n", "\n", "load_dotenv()\n", "\n", @@ -214,7 +216,7 @@ } ], "source": [ - "from agency_swarm import Agent, Agency\n", + "from agency_swarm import Agency, Agent\n", "from agency_swarm.tools import BaseTool\n", "\n", "\n", @@ -222,6 +224,7 @@ " \"\"\"\n", " A simple tool that prints input.\n", " \"\"\"\n", + "\n", " input: str\n", "\n", " def run(self):\n", @@ -231,23 +234,28 @@ " print(self.input)\n", " return f\"{self.input} has been printed.\"\n", "\n", - "ceo = Agent(name=\"CEO\",\n", - " description=\"Responsible for client communication, task planning, and management.\",\n", - " instructions=\"You must say 'I am using test tool' and then use test tool in the same message.\",\n", - " # model=\"perplexity/llama-3-8b-instruct\",\n", - " # model=\"anthropic/claude-3-haiku-20240307\",\n", - " # model=\"groq/mixtral-8x7b-32768\",\n", - " model=\"claude-3-5-sonnet-20240620\",\n", - " # model=\"gpt-4o\",\n", - " # files_folder=\"./files\",\n", - " temperature=0,\n", - " tools=[PrintTool])\n", - "\n", - "agent2 = Agent(name=\"Agent2\",\n", - " description=\"Test agent for demo purposes\",\n", - " instructions=\"You are a test agent for demo purposes\",\n", - " # files_folder=\"./files\",\n", - " model=\"claude-3-5-sonnet-20240620\")\n", + "\n", + "ceo = Agent(\n", + " name=\"CEO\",\n", + " description=\"Responsible for client communication, task planning, and management.\",\n", + " instructions=\"You must say 'I am using test tool' and then use test tool in the same message.\",\n", + " # model=\"perplexity/llama-3-8b-instruct\",\n", + " # model=\"anthropic/claude-3-haiku-20240307\",\n", + " # model=\"groq/mixtral-8x7b-32768\",\n", + " model=\"claude-3-5-sonnet-20240620\",\n", + " # model=\"gpt-4o\",\n", + " # files_folder=\"./files\",\n", + " temperature=0,\n", + " tools=[PrintTool],\n", + ")\n", + "\n", + "agent2 = Agent(\n", + " name=\"Agent2\",\n", + " description=\"Test agent for demo purposes\",\n", + " instructions=\"You are a test agent for demo purposes\",\n", + " # files_folder=\"./files\",\n", + " model=\"claude-3-5-sonnet-20240620\",\n", + ")\n", "\n", "agency = Agency([ceo, [ceo, agent2]])" ] @@ -302,12 +310,15 @@ " recipient_agents = [agent.name for agent in agency.main_recipients]\n", " recipient_agent = agency.main_recipients[0]\n", "\n", - " with (gr.Blocks(js=js) as demo):\n", + " with gr.Blocks(js=js) as demo:\n", " chatbot = gr.Chatbot(height=height)\n", " with gr.Row():\n", " with gr.Column(scale=9):\n", - " dropdown = gr.Dropdown(label=\"Recipient Agent\", choices=recipient_agents,\n", - " value=recipient_agent.name)\n", + " dropdown = gr.Dropdown(\n", + " label=\"Recipient Agent\",\n", + " choices=recipient_agents,\n", + " value=recipient_agent.name,\n", + " )\n", " msg = gr.Textbox(label=\"Your Message\", lines=4)\n", " with gr.Column(scale=1):\n", " file_upload = gr.Files(label=\"Files\", type=\"filepath\")\n", @@ -325,11 +336,10 @@ " if file_list:\n", " try:\n", " for file_obj in file_list:\n", - " with open(file_obj.name, 'rb') as f:\n", + " with open(file_obj.name, \"rb\") as f:\n", " # Upload the file to OpenAI\n", " file = agency.main_thread.client.files.create(\n", - " file=f,\n", - " purpose=\"assistants\"\n", + " file=f, purpose=\"assistants\"\n", " )\n", " message_file_ids.append(file.id)\n", " message_file_names.append(file.filename)\n", @@ -352,13 +362,17 @@ "\n", " # Append the user message with a placeholder for bot response\n", " if recipient_agent:\n", - " user_message = f\"👤 User @{recipient_agent.name}:\\n\" + user_message.strip()\n", + " user_message = (\n", + " f\"👤 User @{recipient_agent.name}:\\n\" + user_message.strip()\n", + " )\n", " else:\n", " user_message = f\"👤 User:\" + user_message.strip()\n", "\n", " nonlocal message_file_names\n", " if message_file_names:\n", - " user_message += \"\\n\\n:paperclip: Files:\\n\" + \"\\n\".join(message_file_names)\n", + " user_message += \"\\n\\n:paperclip: Files:\\n\" + \"\\n\".join(\n", + " message_file_names\n", + " )\n", "\n", " return original_user_message, history + [[user_message, None]]\n", "\n", @@ -368,8 +382,12 @@ " nonlocal recipient_agent\n", " print(\"Message files: \", message_file_ids)\n", " # Replace this with your actual chatbot logic\n", - " gen = agency.get_completion(message=original_message, message_files=message_file_ids,\n", - " recipient_agent=recipient_agent, yield_messages=True)\n", + " gen = agency.get_completion(\n", + " message=original_message,\n", + " message_files=message_file_ids,\n", + " recipient_agent=recipient_agent,\n", + " yield_messages=True,\n", + " )\n", "\n", " message_file_ids = []\n", " message_file_names = []\n", @@ -399,11 +417,7 @@ " # Handle the end of the conversation if necessary\n", " pass\n", "\n", - " button.click(\n", - " user,\n", - " inputs=[msg, chatbot],\n", - " outputs=[msg, chatbot]\n", - " ).then(\n", + " button.click(user, inputs=[msg, chatbot], outputs=[msg, chatbot]).then(\n", " bot, [msg, chatbot], [msg, chatbot]\n", " )\n", " dropdown.change(handle_dropdown_change, dropdown)\n", diff --git a/notebooks/web_browser_agent.ipynb b/notebooks/web_browser_agent.ipynb index 0419751b..3f9c8c4d 100644 --- a/notebooks/web_browser_agent.ipynb +++ b/notebooks/web_browser_agent.ipynb @@ -142,7 +142,8 @@ "outputs": [], "source": [ "import sys\n", - "sys.path.insert(0, '../')" + "\n", + "sys.path.insert(0, \"../\")" ] }, { @@ -154,6 +155,7 @@ "source": [ "# don't run this cell if you have already set the key in environment variables\n", "from agency_swarm import set_openai_key\n", + "\n", "set_openai_key(\"YOUR_OPENAI_API_KEY\")" ] }, @@ -200,8 +202,8 @@ } ], "source": [ - "from agency_swarm.agents import BrowsingAgent, Devid\n", "from agency_swarm import Agency, Agent\n", + "from agency_swarm.agents import BrowsingAgent, Devid\n", "\n", "selenium_config = {\n", " # your profile path\n", @@ -214,7 +216,7 @@ "\n", "agency = Agency([browsing_agent])\n", "\n", - "demo = agency.demo_gradio(height=700) # reload the notebook each time you run this cell" + "demo = agency.demo_gradio(height=700) # reload the notebook each time you run this cell" ] }, { @@ -254,7 +256,8 @@ "outputs": [], "source": [ "import sys\n", - "sys.path.insert(0, '../')" + "\n", + "sys.path.insert(0, \"../\")" ] }, { @@ -266,6 +269,7 @@ "source": [ "# don't run this cell if you have already set the key in environment variables\n", "from agency_swarm import set_openai_key\n", + "\n", "set_openai_key(\"YOUR_OPENAI_API_KEY\")" ] }, @@ -281,8 +285,8 @@ }, "outputs": [], "source": [ - "from agency_swarm.agents import BrowsingAgent\n", - "from agency_swarm import Agency, Agent" + "from agency_swarm import Agency, Agent\n", + "from agency_swarm.agents import BrowsingAgent" ] }, { @@ -297,9 +301,11 @@ }, "outputs": [], "source": [ - "report_manager = Agent(name=\"Report Manager\",\n", - " description=\"The Report Manager Agent is responsible for supervising data collection from various weather websites and compiling reports as necessary.\",\n", - " instructions=\"As a Report Manager Agent, your role involves direct interaction and oversight of the BrowsingAgent's operations. Your primary duty is to guarantee that the user's task is comprehensively and accurately completed. Achieve this by methodically breaking down each task from the user into smaller steps required to complete it. Then, issue each step of the task as a distinct message to the BrowsingAgent. Make sure to always tell the browsing agent to go back to google search results before proceeding to the the next source. After the necessary data is collection, compile a report and send it to the user. Make sure to ask the browsing agent for direct links to the sources and include them into report. Try to trouble shoot any issues that may arise along the way with the other agents first, before reporting back to the user. Do not respond to the user until the report is complete or you have encountered an issue that you cannot resolve yourself.\")" + "report_manager = Agent(\n", + " name=\"Report Manager\",\n", + " description=\"The Report Manager Agent is responsible for supervising data collection from various weather websites and compiling reports as necessary.\",\n", + " instructions=\"As a Report Manager Agent, your role involves direct interaction and oversight of the BrowsingAgent's operations. Your primary duty is to guarantee that the user's task is comprehensively and accurately completed. Achieve this by methodically breaking down each task from the user into smaller steps required to complete it. Then, issue each step of the task as a distinct message to the BrowsingAgent. Make sure to always tell the browsing agent to go back to google search results before proceeding to the the next source. After the necessary data is collection, compile a report and send it to the user. Make sure to ask the browsing agent for direct links to the sources and include them into report. Try to trouble shoot any issues that may arise along the way with the other agents first, before reporting back to the user. Do not respond to the user until the report is complete or you have encountered an issue that you cannot resolve yourself.\",\n", + ")" ] }, { @@ -354,9 +360,10 @@ } ], "source": [ - "agency = Agency([report_manager,\n", - " [report_manager, browsing_agent]],\n", - " shared_instructions=\"You are a part of a data collection agency with the goal to find the most relevant information about people on the web. Your core value is autonomy and you are free to use any means necessary to achieve your goal. You do not stop until you have found the information you need or you have exhausted all possible means. You always to to compile a comprehensive report with as much information from the web pages as possible.\")" + "agency = Agency(\n", + " [report_manager, [report_manager, browsing_agent]],\n", + " shared_instructions=\"You are a part of a data collection agency with the goal to find the most relevant information about people on the web. Your core value is autonomy and you are free to use any means necessary to achieve your goal. You do not stop until you have found the information you need or you have exhausted all possible means. You always to to compile a comprehensive report with as much information from the web pages as possible.\",\n", + ")" ] }, { @@ -694,7 +701,8 @@ "outputs": [], "source": [ "import sys\n", - "sys.path.insert(0, '../')" + "\n", + "sys.path.insert(0, \"../\")" ] }, { @@ -711,6 +719,7 @@ "source": [ "# don't run this cell if you have already set the key in environment variables\n", "from agency_swarm import set_openai_key\n", + "\n", "set_openai_key(\"YOUR_OPENAI_API_KEY\")" ] }, @@ -726,8 +735,8 @@ }, "outputs": [], "source": [ - "from agency_swarm.agents import BrowsingAgent\n", - "from agency_swarm import Agency" + "from agency_swarm import Agency\n", + "from agency_swarm.agents import BrowsingAgent" ] }, { @@ -751,10 +760,12 @@ } ], "source": [ - "browsing_agent = BrowsingAgent(selenium_config={\n", - " #\"chrome_profile_path\": \"/Users/vrsen/Library/Application Support/Google/Chrome Canary/Profile 5\", # path to your canary chrome profile\n", - " \"headless\": False, # set to True if you don't want to see the browser\n", - "})" + "browsing_agent = BrowsingAgent(\n", + " selenium_config={\n", + " # \"chrome_profile_path\": \"/Users/vrsen/Library/Application Support/Google/Chrome Canary/Profile 5\", # path to your canary chrome profile\n", + " \"headless\": False, # set to True if you don't want to see the browser\n", + " }\n", + ")" ] }, { @@ -777,7 +788,7 @@ } ], "source": [ - "agency = Agency([browsing_agent],shared_instructions=\"\")" + "agency = Agency([browsing_agent], shared_instructions=\"\")" ] }, { @@ -878,7 +889,7 @@ } ], "source": [ - "# Reload the notebook each time you run this cell \n", + "# Reload the notebook each time you run this cell\n", "# Additionally, do not change browser window size, or it will not work\n", "agency.demo_gradio(height=600)" ] diff --git a/pyproject.toml b/pyproject.toml index 4055e836..35ef1f36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,4 +31,4 @@ urls = { homepage = "https://github.com/VRSEN/agency-swarm" } [project.scripts] agency-swarm = "agency_swarm.cli:main" -[tool.setuptools_scm] \ No newline at end of file +[tool.setuptools_scm] diff --git a/requirements.txt b/requirements.txt index c66fce38..4d8c1b5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,4 @@ deepdiff==6.7.1 termcolor==2.4.0 python-dotenv==1.0.1 rich==13.7.1 -jsonref==1.1.0 \ No newline at end of file +jsonref==1.1.0 diff --git a/requirements_docs.txt b/requirements_docs.txt index ffbc3d5a..d54cc309 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,4 +1,4 @@ mkdocs-material mkdocs-jupyter mkdocstrings[python] -mistune==3.0.2 \ No newline at end of file +mistune==3.0.2 diff --git a/requirements_test.txt b/requirements_test.txt index 6f29b3a1..09d187c0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1 +1 @@ -langchain==0.0.345 \ No newline at end of file +langchain==0.0.345 diff --git a/run_tests.py b/run_tests.py index b4617f51..b2ed3f46 100644 --- a/run_tests.py +++ b/run_tests.py @@ -1,16 +1,16 @@ -import unittest import os import sys +import unittest -if __name__ == '__main__': +if __name__ == "__main__": os.environ["DEBUG_MODE"] = "True" # Change the current working directory to 'tests' - os.chdir('tests') + os.chdir("tests") # Create a test suite combining all test cases loader = unittest.TestLoader() - suite = loader.discover(start_dir='.', pattern='test*.py') + suite = loader.discover(start_dir=".", pattern="test*.py") # Create a test runner that will run the test suite runner = unittest.TextTestRunner() diff --git a/setup.py b/setup.py index 2dc7a02f..ed4ee56a 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,27 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup # Read the contents of your requirements file -with open('requirements.txt') as f: +with open("requirements.txt") as f: requirements = f.read().splitlines() setup( - name='agency-swarm', - version='0.4.0', - author='VRSEN', - author_email='me@vrsen.ai', - description='An opensource agent orchestration framework built on top of the latest OpenAI Assistants API.', - long_description=open('README.md', encoding='utf-8').read(), - long_description_content_type='text/markdown', - url='https://github.com/VRSEN/agency-swarm', - packages=find_packages(exclude=['tests', 'tests.*']), + name="agency-swarm", + version="0.4.0", + author="VRSEN", + author_email="me@vrsen.ai", + description="An opensource agent orchestration framework built on top of the latest OpenAI Assistants API.", + long_description=open("README.md", encoding="utf-8").read(), + long_description_content_type="text/markdown", + url="https://github.com/VRSEN/agency-swarm", + packages=find_packages(exclude=["tests", "tests.*"]), install_requires=requirements, classifiers=[ - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Build Tools', - 'License :: OSI Approved :: MIT License', + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: MIT License", ], - entry_points = { - 'console_scripts': ['agency-swarm=agency_swarm.cli:main'], + entry_points={ + "console_scripts": ["agency-swarm=agency_swarm.cli:main"], }, - python_requires='>=3.7', + python_requires=">=3.7", ) diff --git a/tests/data/files/generated_data.json b/tests/data/files/generated_data.json index e8490dbb..5d30527b 100644 --- a/tests/data/files/generated_data.json +++ b/tests/data/files/generated_data.json @@ -1 +1 @@ -{"C72aUUla9W": "GFp2jqKZlBCJpwANZlHZBK4KxtAUQSL22PlnKil17U4DY1OzAP", "fIZ5sGdtDr": "G9rfk7kn7Np7ZzObJr2SWidOE1seJH0KanyGsZSt7x934gnfb0", "CCp7UfWbxS": "dtkYHMLwwyxyVvayZanPM0wOE9GopivwF76XTwA1OHpDbgxNQX", "rFNe0bCaGk": "lUMKCptIzFVdxyPTLYYPEZnnGSE6ZAXxOykKYRV5N6QJejjzpQ", "BYx7YYou1O": "B20YUQemcd6KDxJ8Ro40hvcHT2KXHEjrtE33kv1W66ZtVrco3C", "C3bgGJNfjR": "jkTQ5gBwjVWExA1jJ6LE8BDLcK6TzMJLjYhhT21lS1S6wxrQ5T", "lQD4SCdbah": "e2in1CsSWSU5OMZTVQdzSQdO23t7R8Ryr8FRYOkwsF5EdOijeq", "6OgrE4BG3U": "tFtK2jgIyAkHWoEcL7rIJwN0VYCczXxffOZo3GZ4oCuO8xkOE0", "nXUBzCFTiI": "3RNTzGe1pMghXowbMA4JAstYmWFv1x9brH6INXETkEQytbGZTs", "bJWlwCyjpX": "tAlXIOztQtP98jK10t4oPhHws66rbSHTAng7itXOYcuImeoCRp", "osOVD3sm8D": "44pnbwzFEL09LyCrwvHPouvfm85rXonwct2bGMSCPEXoUeanSt", "s09rKtTtvX": "HVlDMGdNvTNRAIsRC2wHw4dWnpCtKzBEtPPqp1q3bhArhH1wn4", "wOS150cHku": "2uoHFIq2KLXc8cK0V6HzDkWq92nB6IUMLFnuF2e4qluksLIptG", "Q4YqJGk8LM": "MvSWVhvgK8TNIWzh6lMSZj0wqIQ7BT9jnZZ1glqFrVKIeBdnbY", "7uZD6nTOjy": "p1dHRjmG3gPPoZ5UhPEtDnagQW37sfNGVHD4VgPdvqOO0A7NTs", "ir25ceDFTK": "9nQD1q7nf4TUctAQSmulxwXw6DJoZfFkqriPHhASmC4GPN7zl7", "fGEQfvcof8": "an1yNP0uVATJkKpVCmwVUPw6kNYUCLt2y8ldyClWuYpD5sSAOG", "41Ut499l4P": "Sny2W8obXvydkM7jIWMwK7pFSLveQUKWM1PU6qsWYnSSPqFo1H", "bk6eDhQnYy": "XHaLkylJ8Fj6wee9gwA46ykdnMYQm7pnHuMswfmcFGibRr3RND", "dmIdHbFd6m": "lFR3thGRApNO42tOZbXIjE3Q30so0LITLnyWPbO9hr1cPFX3sw", "VJYqnKzvUV": "yfGvypMdYJNi3bGd0buruziJu1S2DWEUlKFU5XinDXsO17SQQo", "oeLSXHZ2Tw": "Q1XqgVSNzNqcGIDgM2dChb83ttL0DWCCCUV0BSgXp3A4QqfX36", "gMDcoQNzNN": "3vSgG8MHQWJfn7QQFXJz78rDl0gQFnRrTvJyS0nBEoTg1FHmGw", "uQfgJqFW4m": "0WxYQ4ixtcJ1ZEzAUxnPastXSjRD3cvlbwBgzZtS53zhQkDRGX", "W6OePnoI9w": "6C3nTKMIo5HVmpm45MhvPaRSiOaEpDoLBA1mEw9IiznZEv2BZB", "m6cGbU8KOj": "75s88xTIlYOjycxYd4PORbXtdDZtqgrxUT18E7PfDdSFq5Xmc2", "FXmXLCG6Uq": "Fp6JFT5OhEEXbzUTGFQBYxKLkWlxZNjrIKzWog712yJEtBVECW", "OHZ1mtyquy": "CgBCXlDB2WJxQJuxn8TIu4DwZCmFDwKJqPsV3pqnDJydO29psm", "a8Zd2CST6R": "zuOwceQQvR0sPTgqs3IDDN6CYBkKwpZVuTZBPucO3KNM6MCGNZ", "IKMnpt2ng2": "Y6NUHPz0gOHQBOHf7AlhnuLKJQblLY71D0IYumlCbmFFsf84zj", "pwKEIRyNTX": "cd1QQ9lrX2t6gdkMZhWpM6NPgwq7vftuacDy0hO8gg83BB8pWc", "ko1HyoFecQ": "GWxOonlL8fCVWaAHl9Zbh3HxUXgG7Ito2b7dQrBICd8OsjHvlD", "JzipnmKEEK": "AgzUp6VIogKpPWp7SP6xjjoyWSmR46wxX9qW5IY8CX4bd11IrP", "CIVidRQNCn": "2HlABNeU4v2UdGAXcFMRztYo6KFHfAAZLiY7v3A0ZsR7mu8NnP", "x3JS6j3Nrm": "02XmqIBExx2L1wF7qS4ufeMlGm2d6idzOyZVjrs71H97RigBwj", "GAlf9lFOYd": "6F6jTBXYccb46UHBkzHXt4iK3zXnJlPj9avaYk4drK3OIsD4Vv", "5ZNdSVqzjH": "eO2x8iNM29tqtGgabpPkmHNk4PJ9jTQGjP9wY3hXISYTfzkwkn", "hXxEKE4nt9": "f8Xo1jNwPbAv3BHDuO4zilAVguju9Jok4Nciqjjwcaf4Ytq86h", "21tKnbwId1": "JrPOmcW4qpffaemiTYCL4G8Jq1yKGfWlsAMTK04hxaFZe6QMyF", "MrxKOIj38U": "fNVTVQySjgnATvhE1VywIaPiVFHuUp49Nwbg49Mv2Gh6qJub54", "KohrGPIXfI": "TIQRfxSYndzrllYlgR7ynG5evnmNWzIBfcJ6qs7a831fYOlTva", "8eVOTkLXO7": "QjymESApOqRbM3Wxf1nCaaHuJNtieK2uWxuneg9DjgC8ctQRTT", "VzXJuW8ysO": "aVGLZG74zHRqGN8Jn2N0kt7Brt571c8lp0VEiwtEoLsZal5o3X", "LhXGmZk8ij": "yRNA7DmhIuQkBY1aEK2FYXp1ILIpgRAdP5ASRvK8tJV7dGEWYH", "wh4GHDMFUL": "LpwRAxd8MLH2kO5UAkYzNZDdxMSAxpEp4R2AZMbMzYC3FKTpqB", "AmMczTt11m": "OSzip2uDhSLHOwtT0dMbGe0RE2UXfAmeeYpzkVw9iRCDkEhxT0", "Z5ck9hDAbR": "uD220S55VhuhDGQ0u5KDuyhg06aLVOMRExDrzbn0WQ6YyGtaxT", "e7q37wroCp": "pjm0JInEEZA47xAvT2MJxj8qjZSFjVZ08daTKfKpClO3l7VObq", "ocRfgkZ0BV": "zfodqtUm16srzVYi95TC4otn2RXP5rihxFKKkq2fvnjL75Yg9y", "yCaI03Eybe": "gyO9jgRZWeHS83Ooihu95k9y88VCqDWYfffFZ0OnJJXeqlbCHF", "ON1gLsGb00": "VURBxIqhR2xTWgavi1N6KvqBPT6yJ2fuge1gs4uCVgp2pavdrl", "pMXdUZz64C": "CgsDcuAFfUOlGHnwbO3495fgw1WmkJeytUWyGBJxBxFJHgBFQv", "9xf9Zs0dD3": "I1GRkjkmEi1sQTYgAVq1aFu9TudqshhiE4Ij3qNUYUxIJ2yODs", "o9EYQBL2bd": "4MiJKVDEh5SQr8o99GwJckOkHVsnL0dwNK1ZG6lHFFNJORqBP1", "DKlf9GirJg": "EKIQZPu7sS3lAPQ7gU9G5f8SAkJrSueTSZKs8DKRxmK9kXCsE5", "ApRt7EOXvE": "QMo4KhubgpLKaWC8rmp5nj2ccgx9bCYZGa6vKCnAl9fZYSiemS", "mOEFLwmwJI": "JfM7uyOhZnb5utP48jZJCSLVVJOfL3pEbRDcgUfpNPguCl2ZIf", "3PelCFOdX9": "qYKU8l9sleAcLsDZ8K4TpCF5nBGjS4FL9AggzpzQTYutfERYVl", "0MwRoKpHr9": "TJqQDFkezPlziUfTeuD6Ufj9LoMAolCHNKl7W2DqEhFtr667HT", "hW3Py64ViM": "f52jLjPspPN39SifxdyMt6PWfZUxmIN5EYKyWd7Ox0gymwBctM", "3v6DsTOqXY": "mQsz62l5vz7IUaltG0H7xcIvLWjjxApeDAvz6t0J6gSbINvVZ7", "sTnBjc3bSz": "qSicqFvEhdLHtTwyQS0i7sPxw8cjJx30RTJTXWt7ALBnWBib3J", "o1may7TMLk": "cSmrbzZuOWTyzR79DlyWhJTvQRkmA9hJmmK1hwGRebcAy8mGKy", "ay2dK4ZwuW": "Jd9F3cqt8siCqcdAKhbyQovMSUHr7tRaIZueG0j4MjoGgEp85q", "L4hDx88kMA": "FHU0jSYVbyq1VkqpujkO3xeLAM0JxoK4fgHz1mnnoFu3KDVQT1", "VzlHaSebTy": "vnjCy4r4JCz3iG8nEVna8UexfaEIQwPuenfqZxfgbbuY3bvMgT", "zHeBaV50K5": "6yu6zCl1u6QPpcNKvimL3fwvzOPyMRb9kYK9gx2NVbYgsvORiT", "k6XZLkA20G": "4Bdz6cYANvDHQbKnJlQUQvAXEIcwE87UcCX07KzZBX51QY0Ozr", "TbGXwaWd8t": "resZEllurht3CCwF4rX04JIvYPDq3ct807oFWkxFSUgxrevsOz", "Y7WIazgkxa": "31NHHBRltzQSAxeLMP5RrP0bKC2BHMef5i2svYfv9SzYTei2gW", "n2d3HoTwQu": "qa5NzzJfVsp527BCF1wMC34oXepaq1eFanR3zghojWJStqAXeG", "0Ij3w63kL6": "ReX1qVsbuupyaGwKHNgEKYpfBqtwRJKOE7f1UuRImTYWdCITGQ", "wMfeA5lm4X": "K2FeYpOfMtNJvFDGksyR65gCd93o5vEyZ9mJAoUE4hcygoRg9r", "WiYE9YffzX": "mnfFEhs4vgug7sCiDn0SL2bqJHzoMF2rWuPlhFsY5sAtAcoDC0", "bg2XUpkzRt": "HTGncqAOpCAEcvjBjULbTMILAcaFcNiVhv0tv9qzPVS46MzosW", "stEXHT8WOB": "yi8E1Q0rQr9FlfoRw01yc3uUkOZdChzYsOolCUQy1hqbOZYpL1", "F9tEbQnUyP": "QadIrxzMPsFQjYbqOjXcMI3HjT4qbO3P7BSuqXmJsfB30e6KD2", "ii2Xkinxlv": "40yh7N3GSUaW6TC4pPBdKL0KigLL2XbF0co0jTeqI1zzI6QKv8", "1zwECHyjeu": "HxbJqDQFntyff931OKmxb2KJjZ9QlDp3Dk5JKr0VcIQ2YS4X0T", "8rPBRYt8PI": "w9dgFKwD0R9avUDasmUQ2Ea7ZYchLR6aZE2b3H5zHHYw3JxIJV", "2EuNVs4JLy": "6fEwJLdSP58my44DEb868SPZZCeddYbpkDmZfhb4KcoEZ0HRic", "B8CzdNkpIP": "CVWerWOXqxZZ63ELE6chIgzvF9K3u5uuHDbJeGcq4DQ2YY5CSM", "r3w2u1ecuC": "Qk9CWfYjxt2j90PddoebKiX62rqdeRx4CtvH2gwQuuUh1mBRrq", "B97uvtFho4": "39wY1CCstM4pxquaN3zjniNNkQBajpql7Sm5VdvwQGUE5BHnXO", "aGTixKv1pc": "ZKW5x8gJNmjEkhZ2RXCsADD1gpNs2N3PvEPMNumGDMfhUwU9rM", "pQu0o1za6m": "6d1isOShZwQv1vGSGty8cUf9IX2KdCxP5xN8iztrHLZVCzUiHg", "fK5rVp3IF9": "tS8lfKMWkK2RzzSCnbRk8wFRmclEZbJunAwrTDMwVPEtPU0zlH", "vsSiGI5c3x": "kWn1ccQjvW4BXBmiX9gOVlvHn06qSOvJSerFD1LSZeZWyniMTT", "FO4iIaBkvQ": "hIxUymXWbszGvVJu7Om6H89QxV89kFYqOC3vK3izwGmx8AeD4f", "RyoJBCVra9": "gz8Y1dzWID4QFJgL848HaB3wz45muqIEjLOnN9iMezKQjXSghs", "j1sdb4hyrI": "OF84jZi5XhYYy2gLzOEAOfOhYPK3zNsKcWuMwhwx01D7wJPCrN", "AQ4dO5Rf9l": "dBqATKQE9a9rt0TAA9JhI1B9fDELjiwEOpIcxF9KWGsOUeUjR6", "yrZtqL97qu": "6Kbz8eBq5DwaVhQ4L40sWiS0D65e96iRxJttV0zFH4hPj9tHQF", "HeFkkHhdfB": "mpvUcXNOUICxc1WKvScqr1R0EvgtdZ8o13rrIxXCej5VCTfSNh", "HkkrhBXbyU": "RE92M0RMBQwHmJuzXksGgP92g7VDhd8wXf8m6Pha84RgLoNQ7X", "BfX8Ejzn02": "99bhU5m62OPYfGbGCmxmOeIPtXIDoeYCZf5y1eFydADhrWsabD", "fjzkNCnsGM": "j2Dlq6Dacouo1B4aZ6Vcc9yokEqukCdor2NH5cy4cZWKKCvirx", "w49wetYHv9": "HbJ9IUMLbDIhKXV4m9sDJ1HTHUYMfWq88Nxqga1xBUQk4Wn9XX", "Z9f0sIcf4S": "zeL1IS01V6fQhTMj0v6eQCTOrfkq1JCOPbyq6MapaVpz9NiEsC", "RgCoQzb38G": "CbeVh0N1H0fglbZx8CLU99GdHuKIDQvMwuryZEONH6Mr9Zjgj3", "6zYOOMghIM": "x2RNiNOZ8cGZwKp3m7GclVh8R6edWqzwrOFyIUy8VR28SEGaGC", "m0LnbokJxn": "VzU9ePSvFTsGo6JtWdaE4YKVR6w3hOvZgJ7GdFixUG14hYKtnY", "Bai5RKdLDc": "WkBZkzfPKiU3Yar7QbtEHYb7cZePEvT0ndX5pHLB8D4Ts82ZCZ", "6WomHwhSab": "JbstDYO76I0HJjuQZhskbc82EB91Cqe5O30f6caPkx2Vl7IVH3", "VPyWjtaCgd": "hsHN4jqIeUOuPbBMK6WQS3mONKBzHpYf0tIqXxHyitCWPuOf7Q", "nSAtwGq3wr": "iq12sXkEehPkO8AMvWGxkdvfgWAObTIc9FHw6lUhqbKDgl8lym", "hF2692Yf2X": "drRsXvzCJCpGK0Q5IQVS3WNeTy6VynlCyrnW20OwDmBhtrdHZi", "aOjXWBeB3O": "s4u8rDy7MUSlLLLP2TJrDuQmPL9Nyu4CvPqoAiWXZRByiQri3s", "or9KLsMrpQ": "N32HkOs1Mt0AWGiKOBMt7Sf0GALF7lAtoQ1x09Cb0qU0O5yKaU", "y1Gg6uigHO": "j923zv3f7MB8cTLpF0v0vDHO73SiZYOOMpFy85ISiGVb9oySNv", "ZELA2m42od": "1y93Plv2OB40VwhtIajdLSkmEBUzzbze2CClP93MnBqbqlvPFJ", "MsIT8xH1xV": "RHUr2fJFqj8d0hVxF3UdCKEdCqr2ZDS6ARl7trVALpvzyorYni", "525aPsJrOj": "AoSYKCmW6RvlNK0JMajpuNkC9DV3xmdeuR94P9cJCyhymGfa6o", "sr1dPOnP3e": "5A5JlgzHTppswwITzLTuK85Uc8XU0X4mEu80JG686XCdQWHIWZ", "q39SaBQ5aA": "jmHtlanUmKgZHXwzbIUJhXfOyjCUFuR3mw2JFW59nYUkc9M1iQ", "N8TvJbPjyQ": "aDBB3rtynidAn4PxU6bxSGGd5FQeMLs0oY8RPyMKN7ywKtFMCS", "Vb1I0K1ZNG": "ktdfumcthHRxbA48YOEmSLKIn3YFfhORn0Vav4VWRW0hNqxl26", "imhs3Xdvyr": "IrR7ydvToFeQ2fNnDyYjRxRijXV7W3SmmXYkl5HgEi8wjjd7lQ", "Zoe8Sbb20H": "J6zhHnDDzH9dmEDYUyeU851YlPdMCWtmuhbGNALpu7qH1esUMz", "LR9PTANQYN": "AHDilSn8Nz5E1WsrUgLHomSCIpcryNI7v4nsVAHnjO7zSV8CVQ", "rESBOCvGo8": "wjLy4FsGzqsNpN6N91ugVLbR7btdqtouCJwZCFeO3JbJSbFO1e", "JCDDuX5NWH": "aYT4D0V2rY1DXxrW7ZeJbODT9tTNMP0mRDsjW61a9SX1M6omkf", "09eFNUNA8X": "6bi3Lmc8AXGZnyZXWSX1ZwkpJLbpKxj0b5vUqolLo6lAAe8ITZ", "v7c1L7Op0k": "IgoiHSlz6SiSIiRaEl6eHtxDEZYPfooeZSNBhF3RZRUVtb3Vyz", "U3VQy856pG": "5h0ZyeOBFQtFynZsbOAqfG4dmoYxKAzKZ0zscxhaIaa2rqRcXT", "CFmBcXy87x": "a7CS0sCNgcc8siR0KxFVYGG5YLzVlyWWmxcycnhpE9nztpl6XP", "gPhKk2INBO": "g6rqKdlpIhaiROIkoTSo3CSVbemkQF73wKgBD96I26jVSyyl8E", "ziySGmJGIc": "OCUtoi4HKcHxsc4cwCZV78RIS1chfSoKc1kpPHiv6ZHunJGQcb", "MBKYRd9i1q": "GEBhJHKusbspmiz8xxOwWPAgyk4yFuShBwa2O9V001q2a51yA9", "DLwpsDC2Np": "hm3J1eSyOEbggyIwW87pTVuU42Nx7wh3FoSY255xprXh9sZ4YA", "r5HYzaAGOL": "nuGFYRhsq6hox9iI2YZeTna0vzaLiK1sg1Rpp4wndAqJPoMEXk", "LqXUoEXnug": "Y3ukD9z8xxqknN0H5BAd3kHb9OQXOsVlDlGmBgOLiblmjX8Qho", "m5v2a43Ipi": "ALjywwC5K2A4sJro09Z8RICfc45SzgsLdZ7e1KRftq0zr8q7nh", "cEhmv6K6eM": "OcDXQsSfNUhNXKhS8UfqfxhFpm9U6mIq8CQMeZ1kl5ae4C04h1", "2Tp7pLjCag": "3hyfYa6NUbFNbGjxvGvDnyvd1VbBhusqw4oX7MaWhLpLkCzozu", "SOZ7oPoc4G": "86OKOYAWGCA3hCHCYY7kwrZIJHQE6xnXDyaR9iLJ89BjKMoq0Y", "n5Bm9um4dJ": "iTlpFmtxkAogllKxaBlt57RswfLiPv97NRKFmYgcd64FoXjt7B", "jaPdYTneyw": "5zHs2NhmMVvBhdG47je3pwR69ZhxIr9Zn5Xgb38biQvYUcZ50G", "sk7WRVyHub": "r25DMHmEO3J47HC0L6mYAhVwr3Z5JcCxNFRx5b4cQOm82WvjRj", "yq4wNYYnBv": "LNi9Ov4pZ1W3jEuQwzkvwgDdZ3BJW27w2qtkAiiUdvN267ApJO", "0xr9ylDowV": "nKL6idWSYEcsavWECx7uwqqQpv2paZe5W3TspamSq3THQCsKP7", "fuBhBXt9cA": "Eg79nvGFz9R0fEUIqTSJ7sFiHLJ3oWC3vN0miRajU8QK5Njlao", "Q7e422OfO6": "lW3BxSR8eycnwZRSnpGpQXc8luR5PqxmcyQasmGlNd3yFZvvZH", "V8rRDIHeYb": "O133MLzxRa6wqTViHYqDCsrWQA38UQtCip10SEqSGfPcKwXCtT", "hvZJtReRnK": "woclFGXqVnblxoxUmQyPgJo37oMWWJzTmITHmSbFOqMIWlb1Oj", "1PXmeHXg0Y": "bQUCisOJ36jqgLT6Ie4wJlsGUvG7qqAVLC6Jj88h6jwiW9zdyV", "vykrCJRX2z": "8NyqZ0JYpFz0UZPCnd2NU60EJ619SCHqXinAT5hAeYBl7frgf1", "E0ifr04Oiz": "XpNXFEioSOkaI8FU5lkh8ftFY3r9s4UgtzBi5ZVqGpaTbLGrZJ", "cU2LZitX8y": "qqyl7ucz4yMi6SKsmO3jjrvIYYxGc99yQjGLpjAVxueXYGYUbk", "S2ORMA9dmH": "BEMYdRjkjttCxfnqw8nWfbH3Yj3278SeXM3MZMmfiYKzOGb5Bg", "AvolcQ01oO": "AShXDo2mNA1yqeXM9aykzdHO9I5mWQosJlPTVm7UIwhcD7DX6y", "A7vRasyCMb": "w0ZjwYqLRJe3MoDHELyq2olOneP88uFOTpj8RMeoGP17rRFPEj", "efsNe97ES0": "HETrTEHxidQ9H6VTEOHYXnD1c6KvcMBHxDWQeCuE40QXOd2yZJ", "vf3cxs1jz9": "fJQjOm7cPhgoPloWkC3sm3CyHyYbPMQZhNzSvCVxrmwBloXm0a", "yVTvV62uja": "hv9UI9xRWR4WIDmRf9WUzaYG122r4MAXfsQDFiyk0KG6eSLXCV", "UiChZbQfTA": "bbWU52vmSPB1jMS1SfNkG6idZ7ZaN1aG2iAbkizpr8reEsrJb9", "MOF9b7xlWr": "JNZCRiiE6YV0e5EpoieC5bhPVqdnJBL3kSS8YV0eyXdFsfyYgk", "vyt31HOq1E": "q86HzbafsN6V9eCi2UxZD5DgHy2ChlGvWmKchHuhNBHhAmXBW9", "HVBpcL8Vm6": "CVi7iLWuEar9eKjjLSIAS2l2GzWT7HgXUV3LZQjArViRxU9vZs", "4yyxLKP8J8": "z91xnk6PuYqKz6wAK8Xb3nrRJwZScVFHCXn1etPJ6BvcFJOO7n", "QNcWwgPx7P": "EaUklX17V4IuOXKabieOLkXcVPnIE0mTvqLf3Tb69RtDU0B4HI", "USOtIFdcFG": "tnegvTE55UcVrrPg1vnjrfw2f0UNOYiM28ryTNhCBNBDZPhw9K", "Dmcs6VxfuI": "tHfwEIJXXFZF4U43NRSXQvaFHhCf2ir6cOkW7TAxeHQKmOHUoy", "HclNrtq5Wv": "soZBfbPzqW16MRYbFW7lLuW5708E0OgV1tcsniGeSQWWiGwgsJ", "LmHfd9ItBG": "EQrSvHEuDh4zEXPTrRirdMCHkMc26Nh7XSQrzIQakYxHjC5ID3", "FBoi0RGMVy": "cdbizX9EJzLkcHLnsWXOJhOT2sSoLji8DOKKc3AB6DsL8wYnNx", "ix7Ls3Is82": "F9PJsrsucPCLXsVhN0jbuORpxx0TZnGJQPV0akiyXTm09gQ8Nm", "F7KzYfPRMo": "8PwukdUWCJZ4Wi6UenHL8bR9g9k30fKQtyP9SY3kxjX3omENcL", "c3gse9xVcM": "oj4otjttmvRfT2hvl1L1dkKr6WuRHpwpil9rjiRzeDlJzlBiXD", "hIY4UiRRE5": "grGHaEpZlT5gyKrUZNdmWN6YxJyonrPQgregOqxTZJ3YPdsKEu", "hMcUYN4HQt": "w4LH6NMNd0cJFqc6myOUtyuDVGJ3xJ9J5Eg0FCx2WkFQDR2JKd", "LorCHjVK18": "83DjNZa0i54wOEOG8vpGdAxorj0eI2CVCvMXOq6WUMts9ZW3JG", "AvR949SEGq": "BbGfTe5mSbELTQpaWg33Jf8r3dN6gw500BINFyG6J29x8FZYrs", "T8it1hOCe0": "DUMSRlcwFts2JMyLforbPxk0pfyf3ZeWwgTCrjSY1XjtrRrDQi", "q2XpdY3GDe": "OLzki4YoerfkSEOUFmdkePCrk2KtGZP2F6ukVEvRHEXk2bQAis", "6mMXVdqARe": "Uo1hM2yf10NuxqvCuVsBkpFN42OJRKglys8TCKYqxB4vhFJCaX", "Ukt9B7TLLg": "QUjKe5CGdzQZX2xBZCWX4KG5njvwy4lccgKohd8BeabmAChNu0", "GNLtBKw22L": "syIfP5VPDt7bBZ2N8BX8L0NWSUfseJKgHZn2DYEKcRrQpJuHyg", "6N3S7FqQk4": "lpt9bGFikhi0cjBd5n4pEY0nE6m7ceMmlByy5JX43qZqhXGzRL", "36n8eS4qpp": "BWtBXn8XJvT1pRqm0v0XxLA3aUpqy1c6YoF6luNdc1u7onx1fd", "I2JwvCeNnK": "hM6rskNdjsJRDuVpAHWn3Z6DluTjpzzqFvdqhGWGIVYpyOexnv", "FC7j4inMdz": "9rUVwEoZfYMRctJbFCNSnO9p3UhAkcKwmzhHMAkVggNk2FMNVI", "nP1HHMxp8C": "T547daYmNBqEHqJQMwxWZVD0i5YEKvMjUEPSo3EjI9Oij5uSwl", "ek53NLTV2V": "jJolXmh8L6ll3sM6uO2LRkgcaNtn2OJp9BlMxjd86DmdCaTcQT", "PlZOZtZ1x3": "Ti33dfMVTKloLi3vFKUZpG4Y8ELfI33fhKXCgZRTztdrsG8q79", "Q5PNnDOKWk": "fpYukoLr3Hkgg8qhy7KHAXndPWPX1dBDzhGKwdNSgycZqmPZQZ", "TclnuC1Ltg": "oC7lcjzPGz8Ql7ZzO2alePzU4VyHSmvEan6wOGX5IH8JwqqfpH", "kalaIDmTLj": "NSLYAaVorVbI7iJKlnGwvKfEprndoZP8wtgMpFSwnWINVbonjV", "ylYjFYQWO2": "6Hzs9f19TfTYLENrWofWexJwwsaSSajW6brdz7lsvbkgzBqeNE", "50jg2tIoj9": "wFkk6yWf2pd4YsHaGK9yjjBD2jxv2u5coKuRxbhwcmm3qc4geR", "TzX7zp4ES8": "uuqEFqkNpXLqGDe3Wg9ljgqLBoTEDWPRqDpqs5vCKJFEo7kVaG", "q70fe0ZHaf": "sRiRQ8gyA7lqXEZLnwz2GuDmIL8VMwKbeG80cWFHx3TzTaFpbM", "oJaePOrF3X": "ugLYPi79ljo72cXBmla6IJKF0gSkCr2cGJKx7XKnGHhLO9GrdZ", "66Dpp37YfP": "ZFf5wlqt4K8dSRMPYF2bskChvNgn6HwYmmFpcJXqPxxohqJ5pl", "17n3GGSzF3": "6Xta8kWjdmIvlpeP73J1WXE9b1VuZXEXRnNBqHzWT64HjjC2IJ", "ZWY2ej82d1": "0dKBwvkET1u5aCAHdhReMieoqmxTdRIVXHO4H7T1PJOvjI5Xms", "AuLzJazRei": "RCSepealBg8L3fQsJouqQNkjc0yn00LNMFy9DaPjJ7zyNo3hPI", "nckBTDCqpp": "sjEcGeyAVOusSZLswImNCYJ77ANqJ5YBg8jfXnzDK1JBlyZ4Fr", "UWrb5dOdKl": "tlIF5IwK4VqCTqInNYfnYxpQGS0928HmU2ebfkYumiHUOUW7vL", "88CZbu2LMM": "DWyWtZZDyKqHiel86HmbFkoHcJtYxFIF7lo2Pc8Yhv3WXax7Hs", "l6VAwa9myI": "P89aK5VMfvJJ8idivUJboz5XQEMMp9st5B4foUq4zC6QBR9vYd", "uOn4tmpJSB": "TPg2FUx1aaOTmWo4DR14UEJu549heWpz3xUAbcNp3EfGvIokcg", "ncFMWXjniv": "kz7mdKNi6QuLVMaevLUQtG3TMijJoKKZJfQPJKiWp3u4IqqNb0", "PwTyG4Xdd6": "0JkU1DAYJsriWSfRZMwOZrKSQuEwLEWjjkhMOBek6208vAboXH", "vDxxGQv42K": "F0Y1L0IwdnYu7kAmklovaXaxa60cSt2CATNJfkgMs9f8rh2TUJ", "q6lHJRsKeu": "q8qpOzapvjYqaLZaetsNM7L1mMscQtmFQZF8ZghO8Rnez1LEgB", "T5QoxAIh97": "s5uwO4SvPMvJscxq18KyK6xp9I8AEosjYQSfQmaS930Ft7L7C0", "zGlnH78tJC": "C2nlefrttCpHkk6gStlgTSRd2T7evwDG999u8eKr9DJ2IAO8fA", "OYkDxfUlE5": "UianZ1zzw7U0c3rePMHH733xATKIaRyvxUg4J5r8hnIhH8IGVR", "NsaSUHoMxt": "dgYCcOb15bAbBGKHHibyPDcgdl9ATHEWbb9pueFM1qeb38wWf5", "M9yfp7AiiG": "zXLfwLT33H8pgpNp5F6Tmq1W3Y4ikmK3DVgYXZ8CF0M0qz3nuC", "K7zQaq3Kzd": "1lEhi48oyX252q3JNdVXLniXLAZsvJjv3grM79jPz2z8SDfrTs", "sUxQ3VC3Lv": "MvMBD9ozMhHXXjTtjk9HipZ0v5GcS76abkKqf3kgDnsZ0RC4yj", "QgFEm4KXXz": "XURMPPoALkhM0ULoTwo1dfsuFe6wPzqLJJzJBKfzgQOmowUE8c", "VNNfuRhKRY": "ZJ0Aq9sNimxwwiAxbAGqws8HBwhavzj4ePQET2lAPEAzl78jI1", "29sSKsuGhj": "yyteb55mN57xEwR9pR3J5NtfDx1SJJ1z0hqzKb0L5dPOoQXl31", "bXDgb3uOq2": "cySmxOXnmoGsw24WGunQAWwpArqEHRv3lmBn2y9pdFMmymjLzx", "63H8pvby55": "74AKV8QnzS991bD5PYBlhINaisv9CT73aXWVZOM3QPruTxdoEX", "I8MzHSm3zZ": "P8Fj7DqbtoIqjH4FU9vkxmO8XPGqeYt3YzEddfae6EqFgZRJTz", "nFiwjFRLsw": "Z2LIvcVX1JSOhpBqZbgh8N5j7gawVfbD0a1yyLOBiKv8iy9aUD", "cRmmFKUHgW": "VzpQA4sXbGbPB2aahe88RiYO8mBgDxSLNhx5IWnnHI80Mgc8y9", "HK9ositiT7": "nX6Y9I5HyN0sTOURAiTFXblOfInW2TwDvj5GecvANHy1ONqAs6", "Sb8DygoEuz": "Rsgd7Zt8IGIVqkemzuxqO3snnrPLxikYxKeVfYq7g4pHWtq8JZ", "cKYZDnHGpe": "TF5qHx5MQKs4UFfuNEhvkRVddxil12tdKIoU0fypaEKcTI6WUP", "Dzj18JuOY7": "cdOwWvel8vzJxuqWQ1MFPXnvAOxLOFsf7ibMLIIkVAO2x4gdUc", "sj9PP3tq1I": "r5CpjgY5KlTiJK9aQTla3DRmUytMsTNIrdWhcbtmDI8f2cPGcn", "JkSNcpw7Vw": "Y19h9k04iR3qBQbLjJFtMeJMz8gfFHKEH55Rm4MkPs6HKADGG4", "rQUZE8ISt1": "xw27DGZecaRDRf0dwEtyOdF5j8QyWU0AWSWHal9wKh56BTJZph", "ZXJAwHt8dt": "OwCq0U6TCx3LtE3oS0skYD4QbRL6vXdq1bfJ8CfJvvBzn8AmMn", "nZOo42kyTr": "VMeh1mcemK1XAhIrAvQINEpMlZWTE54WJqOOzsqI081G8BAEUs", "GzVL91eody": "i2NGQ7Q7abR7nxDNRi1So5sXwxdTjiIqS5EQYIBOCUFmBltZ6i", "peIeJTfNEZ": "TuUk4iRFJ3WJptqttTwgq9JjaymgN0Wfshmj26UqEBC1Fo1Jfb", "3Y1kkAMQ1B": "JHA6bMeczXkiOZhNO1RYnMC16TnxYqAYpO3kRDixTFOkmqBTeK", "6Efl3BvPxJ": "foiqBveWwIb8acwZDlVhaWjY4LRLaiExwT7ypMR0E29O0Y7Ulp", "SqMOK5sqRc": "iPCDULPo14v879lYZLxZw7oQKumnilQFFCj1FPowen2y1aue2G", "F8KV20P3Xn": "NANP03IuYd80nF9neyApYeRkvaII1xRL4CcQC2ugAeSwRBcwdE", "tgmHnzUS6p": "o57xTY1SxxbrbTq7A3VcR4cr86ef2UFl9hzbf6TeYglQhii27A", "ZMsqijS71h": "rQHNAddwjagj5Z7TperGluQCvoxaooPsa2WOjXWTxnUCRjzXim", "UkJhu1Su3N": "YmtkpF1h0XNKiFZUHk4WxIdMUoNkl9HIeHU7x6YhQKyoALG1Fa", "wLO3xeXH62": "RAITrteiEAY5lSqkrU7eoX4mEYAkAJ1STXYtmEXMAzULzk92C9", "egyxD6NTTM": "VndoK3fqjZZEknSOqZmbNTFcq1klIkSA8mSVqGz4dE9tCQ1FKu", "GCyJngKziv": "eJI4MDQ6OuXAxWYegbNQc8bricnk7TRMppDDSpolNkiE1EGTfE", "34HZOIX2iV": "P06ZKyOjvtPUC24URqxbglQUM3mxlZmKethxcPDeCit4ri6Mt3", "bTgivOLRF5": "gXnhlHPjNkQKEspAlZSno7235vbLLjqUNQQM0NA5YCXqnhlviY", "2iDoIVZ2hD": "79O6wR5JJRsFMLBhYdb545B3lMqpM7zcH7rPtwCqBhADgost9R", "MWipjd2uv6": "HhYLDJ7F2sdgUTEfjxSR4qW2qqZZvaJ908mC9oxtHFT1J12T2n", "zTsBKSQk9v": "tIYbx39zaJ4xTVX4VJ2DmF6oMnlrpcecy7zeDgW2KqTPPtVKkO", "zMP2rzrwqu": "gFmSE3svtXYd59ZKb9STnhcDEWgmRXcinBTFkEcCIjPafcVKwR", "jigf9DzeZH": "l9l6NDXBfBOrP9q3smm7b3bWebEYu2niwyzKBSM3eTAvCvwRAk", "0OZBp4i9sY": "cArugC2NVNPq7xGuuWoCMU9jjEmo8uAXk6QH2ugihcEuyd9vKY", "YfRF1zcsva": "AvrwCoHB1VcYioHlXYvm6ntHdgNxrA11RZG6C1DmZuxMQ9TaNb", "r18WQMppM5": "QeDwtqGr5zRTxfMsq89mnIFLt2UMojTCS8o9rugQtV0TkkBMBs", "jpPfuNqDDv": "b5MZkSRwXdCOADDMOrUAGGt7suz5OqhbXdeRULmqUSxEYNmCgR", "MtzL5xRIu5": "jKWc0GJ2eSXcK2uhDfnxy359V2RLHe9z1sdj3qmcovEChjqpfd", "foxLy8EHX5": "9w3cokgt2q1JWatFT8YRRha3Kt2jvLXnYYnqzK1qeeqXbD4jQu", "dWhMWvr9ex": "NZht9b0fUaBUlE1caoPfTG4kap7yBkTrIvYMIWsxAimJVkkEIZ", "xz6NiwYMBP": "oWkdfNWmhKE9iYnaF8TaHbaash2f6Nw71braEMPCjl37joqsXo", "Ku1HCrjHHx": "u2dVru9S2bLbgUngkF3wn0ACVBB5lvJGPn9PZ71j7dxFzpcqDV", "tDJT5d9K4A": "RWIwChefWPKMsdFGW6Pa3K47uYQ9hIwyvyBXGACk798KbwWmH6", "Tc455KuXIU": "JHgFvesJUGX4vlhCbQq6P1v8nsaXeI2tWB5WTLZtuqFfNraz4h", "VoZpea6CwN": "GvDRDrupDOSjjxmmYQk435HBFKlvVrpGWqKME0IlkP3r706j6I", "BH5Q0ZSxOu": "QMBIZyxaUpbZeISliBIeuMcO2C5XJVbGlkQdpVNLye5wadTHcQ", "jeknnBU5P6": "lplZXEmAqA9bCRjcBkcxVQ5YcUB9GdOUvgRc3GI5nZ2ze4aFTH", "vWtAtWg5pa": "HICNHyisLNdwBK82iG9qx161XboXK4yEGC6HvWXxwTm9zJnYhM", "NnSOM5FODH": "8Wx2NiKvVmyQSREuQBmoy58lAyihboKGFrwzKPZfiW64uoKkew", "y4f5eTJQzZ": "1Rn7VyYHisu9XsWKJjuc9iB1KtvmBoUDw6knJIwwjlgJixMhya", "zTi0rsguc3": "D89dEFFb5uNgrEgMRvjVyuPYsWA7IgLDTJ38nq31GGxQ1h5fdL", "OaMGojT9vs": "EYhINLCprkKyHACrOU2UPtMz5bJuVFIfGpFq4HLZXI5ulpJfIR", "HlET7aypwJ": "rE2Mdzu2zfsSJZ28fgNkBPjg7GsKkvnOCGbtXccfOjNaXwxVLs", "nOOOV0MZhZ": "VpgxsGCPkfKeDXTUbCJAqEDEpRE5BB3UOsuv7C4ECSzQNz0cy6", "Pf8rcxOn7w": "orrRtesBroVPM1uFXWFVUUsnUfZa0cTfM9PulAt5hu8KH9kCSf", "VvWx1a80In": "1dXhRxHYUKXxG907y0hpXH94x5wIeoYNY2atdoGT1MvugXLwEp", "0WX7iWjl69": "DTtuKJXjbMgi99lWrjFxHhHqf8XdcMnUaQQpQ5QQBNTkGy49O7", "Nk7ANE4LIw": "P6NSWiMbzPWzzGS5kVV6YTNwsel9g3y0echISFf9k5Y44l81q5", "10Eeg6Lr9k": "tTnGnz9lq9eEEQt2D8XeaITMn9kNWSN6cAUBN62o9As5P4hsfM", "eAaTKau7ZE": "oZYKExDPzusfRBRjfxphDb9CmfrFM1wdXcdQEaghZpYBDWD6kX", "8v0mDZct2w": "37SUu2KQvmJK4C0u5nQUlWNl0utlCjDoKmIGd4KV6tL2FLJ8cd", "nN1NMvT6db": "Sado3km8D6ppnTglN0vCRu4CEj8Ky9siJy0tgL7yolObaj3m0v", "E7tw375V8I": "GvYxGGKFIduzzC2thnz8TJp4v3Tc9AQED1LuULIdTnOiiufUUu", "vIvBl1GjXQ": "OTU3YNY7cagNFYJ9k8D7gUaAj6qzWSaLykqZfjehMcHQhocnmZ", "XW8rhcdjUu": "OsAmNUwA2GxzBG2wc96EDugfTudOCQGgK98txRrrNAu865JPIE", "LrmdzIFVyt": "T3xMBLo6ObPB98OeJIB96UKcZzfv9WEeE6WcdOzm1eWKqb1ZEL", "GGGOkqCJjJ": "qM0c4YGkeaFqzp4DbNtnmur2aCOL9tOhe5EFSTPAbWme4wJPit", "wfnd3sxkf5": "gTUM7jJSKy44r5E7c5s4vm1JSyEb1g0aRaieh0qTGRTQT4Nmqs", "8Rg2Yq6UP6": "dYEjIR39mRKC85GWqKAWuDXRxfgAjNE1VWNZGRI3WJrwmaHqMS", "RttX6OjBxK": "Ofc1MHwcBjYAN5XlWmh2L80uqygPUDrHuYBoPuHGfhFcPhKKuh", "xjRDozazPC": "jx8nhdVyzQyIS4C4syarO2J6BWaFpcm5XEqU1LF8OPML4pGGMp", "AsVV6QSeRC": "ionqBBL6W8PnELiTJeUs5jGdveyUz1xj7zP82Zqb8nuBmTNdfu", "02qmBC01aB": "UGpmd6ZLNuuu9lIHRFT80X1HcV8rDtQ4WFrWKyF3ucVZv5Xasx", "1VZCPvhr8c": "7DYLIZiN3GgkCbUtUHXLavvcy9Sl9u1rwi8wmC3W1evwGnC8n1", "c3b72d5T0X": "DBCj6wVwoMwk2L59aHGXeyjHnHoiWJgcHeEqDlNqY6wfhqw7i2", "hxrNqjZvNd": "CoTM079XWohZQAp01NvArevpkltfpI7QzDhwcqK0l2AoEkdUdS", "eAU6PbHCbd": "17Wwyf3rG8i9RJMHNuqMXuSSsPZ0jD8YivacB3eJpxYlcZXYxc", "aFgdtiLN2e": "VpwWYDad2D8nn6Mzdu6eVqVe0tCTltBU5RtqgnMEiQnFVWzrg3", "2wjjQ0qIVU": "wZZP5ZSlrg679ygdpavC4suYLyBHyTPBWxy8Sxh9TVIat571zc", "hvJCdyWJxW": "uEXCipI3tVpFxwLxUdu3lhSSFWK1iF5kMMLeP7b4nDrZltfb5u", "42dppeT05q": "GrqjsyjStKf4hx2KJCsAxkxsd898q2YR9xtYxqOKGTFcp61gyC", "vuikCrNUnG": "j9LunRZuZNcHkGtKEoE6SMRIWlLKE3wavyBTlyYR53mx5ajd0J", "VKsVRLea7f": "TC7uyEmb4cSbvMYbLK9jXm42jyWkR1gtvJx4zesXussS0LEKqU", "bxfj0tcqwb": "NHLZN8AJu3ELHktpkJWpPgTqJg7NeUEzHr5l0nIeBXOcAKEpYA", "RddnrQ4Lst": "qYkXsXdT3vKguUGjzvRTsXB0CJczunT1pW5046bqTZjz314Qi4", "EtraExrXrG": "GahtQuVRk1SFPChe3ivaYbropCBOvGaB2BdgHM9N7ObcbpNMBt", "n8EbzU3H4k": "vXyQCXUkYAV6dR3P6FCMh0lSNhPvC43UDX0jwacK9OQfq5BNRz", "KveucSyH5G": "4z7FfddXfwfohmBcgrJPbgEJpgWG80cFHChaRkjiY6BDQk6T6Y", "7Dawkj1ydb": "IXBShr82HaemGXBZbIbQYtlCU7dP3mZ6116yEjjBiv6IHHZjfV", "5QbKa0GPKt": "YI3Tou58lO70njQHCZUiyTrSDxIOekjUU4mV0CygKYUbg3jLqp", "Gag4WW8dat": "VRY0tQL5Ayzutb7ERqmjKlQ9mXqNd4IHQv9QNXl5MrDnop9WxB", "VfjSxfzIlR": "B2KeD8WZDigfWdnYuh8SISGVfMjtlQEHAZrdge0gOoLqJzIneO", "prZLYYelEN": "LIYSzFyXCrVIPV2gQm36RNNc10ln3MSK0LO8AmbBks5DYRbFu2", "Lp1qrLbxfi": "NdDnTCSyKoLmITTD0hgMxEjqYcZD2yTGgXwMI6KOXOHWlWQMci", "SLWgMPysgU": "qFdI6WlcGtlHrNyHZTKh2QQPr79gNMhQE1dCb0TRd2laJGdddx", "fPwjObuevP": "W0G6agN2NwgJ0SCf5YpEtxlMFI500c0j2UGSHoTeMomUY5g05o", "XzAvXvND7G": "9TuCYgeSIk5a1ZVZxBfY48BgzzHVeGXkyJEuWLNfbpoVoUTEvG", "TLvEd6tAdK": "JOH76Gx1bOydYpycxYcQVu54hDPjrLsDssIcUFSIKLjepFcwMe", "uQM6pBrWrh": "kwZcwpcVQfqxi8H5NJoJB4mkx29UITtRftBSGUthsnVWQQ66N3", "rj3Juaf4Ie": "IOCkpqpt6F9smtqdlOrcwYNduzQPFJMIjODphxB3zefmrRjx5X", "Yymzsxa1i9": "bgtA7YyD5lfMKLrVnVaMW7mD0ED2lFfgVpLrXbyLMVUXAkp8Pl", "Kws9t3YEvk": "eO4TQ5VIh0o1G3ODyfQDpbkv3kDmD3q6IlQcoRdFsrTtPjXFVK", "00Ai8REVEc": "wnvONqDkuXAN7kAsvvHaBzcR3xQQdRlFDpAyXBZ2IDAi5jXwEZ", "lSi8hbDOLi": "RNfnTpSsdWQzXFJOVl8FKkT0yZzbowRbY7Ac74AqafkgT42N2S", "OBthYOFw8N": "jwgLfSLHUINfZs6T36WpkHdRzYtDVsUWjIb0Ml86s0J5aO4hIe", "XrfM7CSUMM": "5Alqb3zzypzC6I0Qnso7cBdl3DCFCv61QsYQyxnCvpx9jgSR7G", "5bx8pCN9hB": "XJiyrrHiYU7NwHNFyAcbYGhQYQCKYuXDeRi8VLWsYZlMGcj58x", "ikGxJUSgT9": "izzWjBrEx5xyfyL7jPBPVjZdxXh8oWpGh19km8Di18Hm3Dg01C", "cacABJnEyZ": "NolJpkVsZLoLIv43Iz5niSFR69tsGglHuhBmpnl8OJWvDGTcLv", "AEGlltEliV": "YWeA4rOlFEUfl1wTDuxxDgeMVmALHdw3obLdRAR6xOHZbCJAYb", "FPZhHXpFKB": "MwYebovxWsNz6V3SHZ9Vuutc43DPsbS03QHwfH8Hqg7MDPnm91", "B3Em8kVgtX": "AkMhFnJcqtaRfNZmDnixMiKoxR3uY863mxomVn5HEw2OKt7F5G", "brrcRjTsK9": "wZV0mFxSIOrxRFKKysHBbjfX5GCLxYyDYHEcm0kMcoMpn2m7d8", "692h86xIQc": "oXkyz6MZJmfScskptw7q9RkxCh3PSmHD0KzcWmhYKClH1A2Lwr", "3RKI7LeCGe": "SC1vQf5pBtzuanPgDIOTLtuqiy2lTFbvE0UfESbPyMvgUaEjqR", "W5iNi4CUxx": "kXXOYQXLT1SOqAA0nDADwE5EQqo36egcQWWZ49T3Y3OQpK3p0L", "P1rmJLq1yR": "9x7GiMRVRKSKNvtJFuNCrgyn9XOORSmmwOiJp0rD1H4NIGq6qO", "gBefd6unfB": "t47ffDiB24BJhd7ufwwetUBj0Oj9p0s2DKWXaAAh2o5eDBBLBY", "dR14jbpTdP": "pHvCuJtyPnFRxcHyyldfJO2Iv7CpZngVe3IS7KMYvgMNynGLs9", "549zjov2xF": "5EkS9wikbDHRTQIOxBFg9Eb8Bg8ovH2nDD31xbdK423r0fJr66", "PVaA6i0tBv": "wk5XeOjhjdUpivW7W48DXFwxZtW0wzKyz865YAaqArnw6VIGRO", "eEqrohFrP2": "pNKYCgFb81hqO65vtGt6YzQbjIMeceU1SM75LXJakDDM5q9qew", "Pvtw1RPTuu": "uBPIisahM8CKWCSAjAjciz2OFJYueeRCTCbx0WbPB6igevEdgx", "wD2v4vL18l": "MS1xQhK6EiUafUHHNrQcY4XEf98CQBuG5MgtCHubyDXkFd2fs9", "99zD9PWbwW": "JFfYr4wEOg9ki6gcaQW9B5CWboOvLdujBmj9DabrLhP9GgyAjK", "qCaAAWp8aF": "Xe1mQGeZqbYUysYozyClXDKMoxCSGPGRZ75L9VYDkjXUbzaPvv", "JKo1H6HE0r": "KEZGxpMAUC4IIb8CXy9dtdJLASbmFHf6hJXYNnwNFl8pilEazL", "xVRzTNSC0f": "27c47l3fYwQULZv5OoyxhVzxgEv38A25pV29NwL0YdytKcTkBf", "n4ZHJnkE1i": "RbWhrv5TfqWuO7yd2hXsq8eetw3DLzWv1TFmDuROXUUoY6MwOV", "6qOcBmL2p1": "3V3N4MWJAzqAjljxvzKqmEXXU74ifsDNyu7DKlsQTqKz8EYdWM", "3PqBbpQlK6": "xkdnQkqq86xmSeRxmzPONO0fPeOPK2zhs9mCDuPpsdr8422pcJ", "puxHaswNr3": "T5u9twip12G7zbNsH6jRx10VTYVxx6kfzvE7zU8jyLFWeI7ib8", "dIAQh5szvL": "eBZopoc0T6OYdT8raS6o8KSQjxApL4g37KpeZLDNlDLsFaJEct", "onkxVHIEIg": "aaLdZMAcKwHYQti9Xgi0j6ZcTGFQ7rahOiEDUmKSyaFcC33xcs", "UlNxTc1BHK": "DDPkf8ldilDemyIQzpftYalUAnBAl9o2EOxcIWQd8hjeVmLY2I", "ow7Nj7n2Qe": "ujvL5RLcEG1eQAxAbpId5g6QYok2Ozv2Pu8MjmEY281KJnsMSz", "VyUf41d1R1": "eZqKqJNmeERxqtKibPnGyUkPlcNUjcqr7Hu0BFcZUj3IPvuUYS", "KvLBIjuVcU": "g0bbaZKXVBuBKtO4mBw2Hhlwyvmii88eGV1UqN01o3KZDAfgDj", "17zqC6U0Vw": "qFLRJ238iJLaHvCnplMJEVwKoocUxeGmn5x2JmEU2elt7uBWbU", "yvwcdnEJZp": "OV0ndvLL32uOkH9jdmyI6QIBuhCNI40dLa8ElmsEOMYKOzx2ja", "SUhz2hTHpg": "CBr3oIy1pul8XahddKRooIA2kvariJvJRMR4slyds3Je0hv7Qq", "4xg0OvrkSA": "bDaELajhKso4CUx98kmL0ULghhh7mo1jphdN5R86ec4WqywmPy", "VFGE2m0cjH": "OU8SOLQRaEg7PlaT33jw0qxP3yd7B8GgCAzAWZtY1AawzhIbhG", "5walgsXSXU": "pW2MnULYjqOJKfJg2eMU6gqEeKtNIo0i0o3XruKSS9qhT76vA1", "xkhTcOiUdF": "rNWtj137uTg17Zno4h2jmSNBYthYYbN7rmv5Ev9qyasxqqwmdU", "6oqweJCYTq": "dmWYrVn0oEpLOmfRgNM9QcSYDNHkQKsfDqcmGvYJQTrZrPR4Mk", "TOAB3Fm52d": "oH936EHH1hLj4a2Lk9eiB6PuJ7sZrH7wUoiC08B7jxzUWBclBC", "mKBEb9wLPl": "6Q2V3hor5vG4tpESeF5QM99cwsTTHf1MHZnXdlVlYBCrazBGP1", "oyXGOKxPHW": "ZGMTRKOXJQ7kH3CQlELh1NLSIiux9LeMjE17FoqjIFz2SAURoL", "KZG5L1KJAC": "XoELEjQOHeQR8GLm4QyWyWrHFnu7liKeLeeXyrb1GYQQOTMbH5", "qWkz0ZvEGf": "ZNyny7rpBEKqq65e7RWsZhyH65ZtNNGJ2HJpM1QfxWx7GllM6Z", "lamDbRC6gL": "WTVJZhcaNjRLxbalN9Jujl55PCmT5g3gwu4acIBLfKlHzoTJaH", "ry1by3yJmw": "pUhl3XAb2ab976QzUWI2DaKPZzX7fjfLo9lKo8QnuypTCE9e72", "yJqYyHdMTw": "9qWii0VXnXpTMcc513cSKtqe9UswZqSDzVE9SWVaWIEHK6mkYh", "KmFbP37WA5": "Yi01ag8YeRuWZ3z1stsirTVd44mho7RCs7J2augF5K3IWJ4KuI", "w875zwCo1X": "SXkxCkB9irH54SWxXJyRgUqrjGzEQhqvPbzYToN56igVFgeCmh", "xqcjtfA2H8": "P7ZnDnRR1elWw7QMcKuymq0NOEIWJPPTSwCTrdMsMp11lCMP53", "jD1LHcYU70": "WhBlHP2ALzX8bERPekkhhEpgYZfHC9ZNMWqtzg7yhCHPETBpJG", "x6eKOoejUe": "YmKr3gJXgbIpVnWVErIejosUA62v3qI0LzUt203XYQccegXAzj", "deHyIfgbni": "wF3hlK3L5BO1G8ZaIwsH6nNoEWNuSKUOH57gfEU7Z6llwla71H", "f0trVUXqo2": "LBs7APVZYNsf4JeQifkbhfmiZSZZHHtZpzRjsHITfNdCiZKbyn", "x7nysjqOyv": "cXrTkFg7Ly5qt8seLUDmH9X3BZsKa4hQkfkehyiFC4oGenTaEp", "jnTs9y39Ux": "X4S5v4XysvBR44XYSdFLQztftFAJAM0nV9IdoBvbW5nEhnZ1YO", "2MDUOEcb6h": "Mbgu9GCa0ScBeQ6Z3axtyYAQT0Vi0oKX7yDdnQrSLahvMyJVg7", "YWXX3tKrok": "wQdOSLyQ91Uvalr7XwcxCj31wQr9rPd3AtFmgE99WP7Rf8gK32", "i3Lwy1ftoW": "nnrP53oOELQEhrcC7Bbrk4iwxjR7AufTYzQHUkADJ7YYed34Rp", "Ae7eGzy5g4": "aES9GIasMvF9GpGrwwFMMKMXkPHqEQaz0lhqT1HgPaMDdKrtGb", "cHoAlPAY30": "J6mQUgKBO63uuIxuJWR116yDicNEvyAElh1ClrIqmBeo2kMnvw", "aAvEsP1PjS": "JdPmRs22Wa0BYh2cHGoRmY541WnFKMgh3tAMVEilSZkRXwZLDi", "LZIvJebO4F": "E9DEPg6m0Y1jfNBtIhkGsCdFobTuhqlsuOnGJzdcHpu57c8jrV", "m0t3u7gfmp": "31nt1jvx2hwWHPARKZTtFc7wZnXVvXcvnOp20TZ8usEjl39FZ6", "tMiQyXgAH4": "9j00rvj2mLZwVAaROduX41q4nPqw8avgCoJDVKXtzqw8xUfIHr", "1k3dQzwl8A": "YIPwDItnFf66cvQ0s8baxTWSguwnpozBR20fY6IEFn7gOs0Ouc", "RuDHMBgyiR": "FEcCk29faBHB8tIjWx9vcYtitKeRLCfxDhv3fx0IWCmwq4Q0Uy", "MShSspWfyA": "ZLjwEenWYKOLPc6AXlm1xXUMAUP1KL55Xa3C2DD42PWYwOzRC9", "WP3vN8Lxpt": "Or4mk98qAJtj8mng7FPbfNuTelnFte6NP1urvVyI2WE4ciPiCj", "DuO0gSR5HB": "4ZgYK6vP47cW2D02hCEdbdUYplwZtJN4O7JuusO2Y3vOAKgWCt", "QZoKa49m0g": "Seoh8n8jRPALwd5wxK1ME1OZP27X7eAIPZFXnkG27VeEo11iMC", "16BkSh8RxC": "VTrEws6V2fmf7FaGBx13vu0j7Uy6JxXzgGll4NPndXy76X3KbB", "WRLAQGWEy0": "vLOPxqrv7ZLDcxoCyXf8zosr5wRD2AIurW4aDWR2mePDywnpAo", "YcDDndHyDs": "EaLMEAbFrJZpWqpP1Q6uSNHs9IUjIOCF2lBi6cVGaW0R0ZBSvH", "4AeE1nlHdp": "hQNYzieDA02JdEgCFkjmNrv4EqXlTcI6wVNHaTIKieuVx6yA0P", "h1H8ev8dwC": "P09dDv6pJmsIJsUF5a0a1D7FXH7LwOk3BYwjNmzeDwRC2yTHg8", "ojcdZn7qCa": "t6rXWKFp8C5VSTlglFwf3EtW6XFfVpVwbw1L9EYKz6CbZwsEo7", "M2JmCQxzMM": "ImkhlJ3ExX57C3QP02hrT5q6oNVZCcmG021hSRD8v5hVv4kDmU", "J2yRRbYwZV": "uvgcMgbxRlJjNalt2bWv0T4EOz0J2halMBlkTbOg9UcHVqPXa9", "6wTvNgckbF": "UOFxUWh70gqkxNv0X6fQMpE9AvyMzKzAeZFFbI54wgsIoQstW3", "VRaVkHfkPs": "ZqCIpA9Fy8JJCSgBK4lQBhAoWXyC7mPWTa1GwBspW4xUolSSad", "KI3EpVBX5P": "BccSNBX2yq835MEDkAA9Y1czHw14vlP2vqRRbMwWsTr5Bsm7Eg", "zeClARljma": "ZRc5zsLkv2daKm4yUZgQFsjS7DtyskkDxPi97zrefTSRW2Hnss", "aMLmQ083jS": "P2y0iJ10vpR5C5UTMiBdvOhRNef3Fp2JT4S8vcMN7pEQ2vMk69", "FVgKfLFYGB": "JQ1JMFl5jH5HPU6L0ERcbhECzN4pzM0VXXzipGLsmEMoVB41dr", "zeUQX8TP42": "c6bGi9UEbbMdMODFo4tvimpHxNYsw07ATi21YpFzc5kw1sh0MI", "mNLzi49mqK": "oMQbMAbqf0zlr9gDBjF3Y69Mg2OkP7MFYlaaIfdvtgTpHjrCY0", "btXAjzxWr6": "1ELPUHmAKBJbEzbvHnThaZhttPe03wo7sJBLd6LIHvkYbPMj9s", "g9nvQCRrQ0": "lpT5VuxZkRE3Hz1wBMljfunpLYN2QNUukV1KNX4a1938CoM3zs", "cCvhemCmDU": "eSQojVqjeUW0uU2iZ8w9Uani78vOnYWZAZbXcvjYWH8O1bWS3x", "svpjSeLQO8": "INc3PzvCYKmGEUOIzqhSmm82m6Z5GQelbUEqWYRa1ELPvBZdNO", "H9CnJWDIcR": "w7B88aYld9BgghJCPNBUx428HAQnZrFpOA7B9qW3Od20XXElot", "NVAxLFs5kM": "0OzdOiFlC6RyjKOCjLSfULFsMScXXfA4wCEgcXyQPKi83w521E", "GG4K7BclZm": "Java1R9LRqaIAQ1AYQpvAzaVdipwpIkWtnclVomSOvAgkwpPhF", "ejeJH57ZJ2": "QF8xCdBAQ0SniHtdcoGHoyubdXaABqk9zN9GpDDIcaeWQRVvOg", "60ie7tSk1I": "yQYB6DofN7Rtk8EkW9ep4xgZNKlrGS1mw5F0YZFDmmOh9sfgX1", "QydxlAWoDL": "nz3LYSS8iMS9k6L0sqdXHpl6LM8VSK4xdQuAEspNttdE0c9n5B", "N9ejZueepZ": "A6Uc1v5Ddg7kzjh8zHtsxaNJiQSRswvCeg7215qkqNDDj5Dlvg", "wIf6AqEAEd": "p95YRa7DEhLNafDIuqLoQ6m1q9Z5iCp61jEeidNX89T2Yp3ZjJ", "GMA81v0knl": "U0JGBOEHXznx5xaslCBREpVZx1OthUvi7mLiE11fMLkssuDM4i", "OfSCczZkRy": "DxxotrwH0804ODUjDbb0qkqt3i8uP27o1BGq0DBlp4OB5YAniS", "NnFIhdbTMU": "Rr8pVAiibM6tQ560AONaLbQCv1tJ4EIGlI70AXhJU6xSUPzwxe", "JHoy8e03ks": "2mXFAnGE2NbmZmqCa9zjNKZzjz9JzqJKj2wU3SJAQcB3u9hsex", "0dUip365VC": "gsUkRprq7m5hxFVHjVJmBGgexcqKaxZShDXQABS6DCJ6yfISXu", "HsQxMZJ0Ft": "PhVu65Xpk9Qzx4qIW1YeARyNp7yg4EvJPHsmazPGxP692lqUh5", "rKTHGJhDiJ": "YFzi8H1K92XyRrKrzTUZFPd4emDETunHEpK9IcjbjQPRRa4ANv", "NWNniJmojq": "NTUOAvtpDVWyeknALz0yCgR4WgpPYdM1Qz2K28SWOXKR54Bm9V", "cNBOsPjDjd": "O6uxzo2N0ouoYQwzJpSeXOZDDgadaHatFtqllktccSZHLc0QWR", "cJc9bH6xlJ": "sztZ1ydU9LHTnQAhE7j60y1nQ6AWazdXxLZBTSgZimxncpDF2w", "YGiGS8QfFk": "NdKBmZZsQD1oq4e9w2l5iGAsdfnAHyDE65KEnS95dicxBYEU4d", "AlIBMPhtCz": "BtAkzB2sO2ZCFguAitVeufQQL4izT6KpEB06S6WLVuTn3xjnoh", "DoZw9hwqTO": "KJbDaGrRDpDTRJlSFuFBWGnAA1TLahbdoaRl7PFM9z6LmGO4fY", "laeB9OWDJD": "ayZp4fEh7lcoRID6hOFritRpgpQnoC2hDVhnkZMuOKXbIlT3Tl", "tetgZLKIIQ": "wKI9ibpMlnYmma0Y8dCQF8QTzQ2VNJ79RHgo0Dc4xRFDKH5iaE", "j45tXKTi3f": "VeoTN5oNDBPS3cDoED3WL5uuMqXPCr4w6bT2GxVrGnM6mXBUvd", "s4nnsO8QWr": "8TSiCoCetom2A9qpIkBx12LtPbtgljf3aPx3Eti1mT6BU67KsZ", "dBowwF0gBs": "VYhQBUpbGyBsFrLlCoWHRORs4LtFH8OL9PXA2KfiqvwHuY9KBg", "VknqHj9Cd5": "rRYjL5xjtzFXK8VogAtn5s9YZ9FH6gcqbcOluluPmEHloZRfna", "oCv7RB49b9": "CheR0Pza91k51JlvdfnKRXOs1rlcZMmuMoHaSCLQxD7OHA5Zev", "8CZzKWfWy6": "4vwczsOpFBsWSUWGAgcF2p8wUvnbpEwGvCd8Rz4Hdj87e5McCr", "TetIdAtSuX": "vm1KJL0gtBJFwA89BQKhNcT46Gphw9ZcvlcARjUCU3GD91ShGV", "RNdZgJmKKH": "v0Gd8Ti3QKasK9UHMYlUnTe3GUg5lZ2UxDNUpOqQCRovdgkPEK", "1QsIQc3upi": "4XbojOuOOetqIVnStZtQJUQDDCmTiCnR58IQ4HZXgMgcLOobTN", "gj27ZCPvoK": "fZUxVCVuJvb4p6kvCIh2I6tQCHmsMrJCKjxZtn3WI3xlYCZYWP", "BONr357l0r": "1tdGjVgYunWhqxMpicxdEvG7ORhUYUQrFicQfMkehKsSrNcMbs", "6TOj3cxRwq": "8xNii7hD75lIangWluH8G3LpvDfaNFc5EQ432QILFfwWf1yDJ6", "VfiAOIw3Qv": "535TGw69HYjKPBomef1zaMSXGlRjWMLJxNs1j3j7TkRQazX3bm", "wSbMOQ2590": "cIpQqVp0fF6yZ5JDpyCXLyZrWcb6d7GCJwKIy8aUKlySehb7Ur", "cdKu9ri3g1": "dNk7Fw0CvqFbOIf1c5WfBXVDmT2zxkTH075p9X9Pc6CFoTc659", "2xOeODE2pq": "4IHKKvLFhu0qlPlwYGoWMaFa3msMQn1mjtPWrbRtRzJEJZR4dq", "QjzB9dKYKm": "1IGBR1hY6ZU0BawmDAKeSTgsu9qIyfJhwFrGtvkXVBFuuobP8R", "nmEe0slPGP": "YlpculF5aj2vardvUJzc4iFiA6RopsczfZcAtSq3dcuWjJF6Tp", "1QmW5Vfqbv": "gDCkANh56KXZLVcq2n3NjPWsOoOMxfx8vTvwMeHE2dcUssR5eN", "Xdqw7q8Y1q": "6AaBKopz7SdtcAxWO00t43K7GiYPgQxgGCJgerHvLHhvlajoaD", "aUzbnH6686": "LFnirlwwSudKqm0rZh6N0rMrcuNYQeh3DPhN1WTxl3cO46CHbB", "wTKBvoGu2V": "HNKPbxoAFfUzwVzF6O6Cgi9SP903nAVCqsTvW0oxhwhf64I6ZK", "5WT5Xf9vya": "EQpfd9RKnIq8hivjouJdDxEQ8uobL1f8089Hl32uGUsWlAn2zg", "9yF2qtRuQt": "LL2bSLkEU4bkIND90v7RxAMB06xNCClp9a9G6oA3cBXJBX1Upp", "9lezJFMRZd": "7Vit6m6C1t83KyOfrnMUkMZczaNuykJePBu543LyZVLbLKXPFA", "KbL0BFHSSp": "8FZgyPbxpILw0WwiyMQQ2RwB7O7zI0PGIEB9ZmUFXyPzw6Lsfz", "m4aDjZAfeM": "kT34QdFUO6FdMVq3nEMRFhaOjGPbF63EMdCmY5bFIzTYIimc8R", "G1eNij7Iw6": "9r0lZtvczz82A4Hw0XrX84IyymbYFMiEn2FINMYLngZNyHaiaW", "y1rTCQEaOh": "EqLre89YfKfpvVhsjsH6nOGqtWKLeRJNkDt0ZYlD9yS7viHlu8", "lsatFHvSKH": "F6eZmW6ArvYoeRNlfUWer8hbz2wqgjWxp3gwj6Gl6UxZFSrjkP", "ShYSgfHWHb": "BzFFBlumowlv6ZYeWC5Fm5PSb3bdgEi0vmf6EXuO94aJkX6isX", "KO9CN1pVPs": "Y1NR665elGswCg1DW14Q5PV6lZvn7YRkmvZzGzNohYPAcgjwYw", "2glqwFikZ4": "2GZYlcpHUIlDULr1j5BhnR1sY9rP3DK59fbM3v7mSnrTq0pbWE", "Vax3Z7WCJZ": "1VjJiCehMpScGgd11gddpYoI7yQ3AtoeN3Sw8AHgiQhTnzZZA2", "xt2qMhElXy": "soJ4z5JT9pAcf4BDrlQYFFa00iJez49mglPMnZZAbVGnXp0zvj", "TqCna0FBvI": "uFvEDEJY29kDFdIc5jxELqPsePV8MpunGcXyTRpJwcRbe0tro8", "UpHCtBo56J": "rIk6T1BusRiwtf0oH732NmpkakTVSnH8CHMvSvvLs1CjybwiSz", "Ah09Q2qHGJ": "Hxgm0xKkrcgJqA1EbahcdBI1A7hz1IBzzmMF7ZDfk6iDoeh7tF", "NstRPjHilQ": "KOKENRFKcxIuGu0s8FG3fpDBVvTawKMyyMARaqGUN3CNT4CgjT", "NFww1quInt": "O985T2gwarOSzoO0eqjilXuMpwDv2D93xIM7VKm9aMTNZ0loX8", "xSDZzgNyAg": "RcVCt7IizeRXtPcYkHl1eLLHRbXs95Oik1QBDMtUdEtzzQHbKR", "r1F0dVhJ6q": "xfuNUTCXh5QBrofY2kfcoydEHKO4SmOxskNyH1kTSF55zFbaDU", "JNA26fkXH7": "wFXV0pdGDnS541PnS41znw4OECP3nP1NsXAMLSuukdeJlRS5kt", "ynkPCkAAFN": "DSs8wuGb4kYAhZxEjuxztyzNtdCfazG7NnNDzKXuMhdivCfvOF", "uKWOnhcDpq": "RGBOruC6GtiM2v0zC1pw2Cjyn2TJZqnw17YiwfYJxfKJarImwh", "SA8pIy8reZ": "6LytHmo6J83YCANzIJYOq2HvowrhCl3hFi7c8GgoUjhAkdb5dy", "S2EqDpNpx5": "fatA1IIIfLGMJDPLrOGu1U9IjfoswJUOw45f1NUCbXObf0UwY6", "rBdGvcfh1w": "VDXwLhIyRojmBXTCOcYEQZmOGdf7cMurYSARVHuAhQWlVRI2zg", "kDKtzRc1RF": "EsRPSfCdlAnp4teODR63dTS8HOBsNMxQZ9gnyWc42OvnhG4J0o", "sRpnn85aWh": "NP9mFIHqMDNfQjNju8riSUzY5RWdHIgkRUK6eypKZZdhwDxJgB", "VjxaNP7vvs": "lgIcPyCX9VuCxLcUv79OTkgTl8fsV30aKvvvpAYKxqNFFoVvAN", "kHGVEaQVRT": "2DNRrJUkGHjxi2EvBZzwZ5oSD9FY0PentPIxMjXdnQdV2tLWcb", "7Jx7Ea7In4": "4C7P2Mn6bqtl8tlJI2am9VkSYHVUtEV1HAKewKNpDHXzeRvNER", "4LP2zUwafF": "XJJs7ubIsi3yVinoHJMfQLay11ht3EDDmtZQQVqLPFlzsj5Ifu", "r6l2yDpqKE": "AwZxRsBhjlymmW29iDKBjyMWGY954rR1wNrsDoNiO7Cadb3NpY", "4eLel19Hrb": "EngkByji79ToApyr0f8UgPMtQc36PT7bucfhCVfbeE1cRjgS9x", "ZU38gorO2L": "gzEKTPxfZN5u8DxoxTXcWqxdxF9PG7KEhespdZ3ibAPHOCY7IC", "JyBCM7l2dn": "TdAV5Fdfd9sgNXzSD112Vqe9MsoJuxI3v5AMtAI0EQsWMXN7en", "uSypJntU51": "4pbhXVQxngyLT3jYrTOHilRWOznKhFS1I1KOuohLv82w5ZbFWs", "LQbxEi6zRn": "0jvP92S9gdGbmjnVcjLZ4uASyoiBIhpIjlVh2UU1fVegYhr33i", "PqzNDnUqJT": "I4NiptgrkdJgo1OKaDewGkgTHtkJ5qOG8mE6eRe6a3J9CDPcV1", "19mhmnsNRE": "OS3CqBu43lTKBjNDIa5sq69RmZaW1o60Z2bwjleYhxpglwLIPX", "G2uCUqLT5I": "g1pbJThcp6AMI2srsGzGEK2C2gTlaPkTkcRLTHvu740Obef9y9", "vM81VZddQP": "HJ138A90grPSsCqTdUtOud8JkVnnJBQRHBja13Mw0FwvHmet9U", "uOj2LiZ3Ox": "Bbn5ztq3dNlEYfK9Sug4r8Vmt4ApJQZb8vlCKurvsUpimLZiq8", "hgF4PE3jh8": "E7X0HcVjMvwBS302mzHpXj9TFoRhyDr139TcHYJc1xjygugV4n", "9lyQo0XCIa": "tpYuJrBhzyBbvehDNM1x0dfRMjalHZBN7luOVitDwT315RYC5I", "A1xTFSmE7k": "gqNHmlPPnHnBVqPG68HJw5v6XmZdgDRwgFOEW8oeAIycjDFEW4", "Zvch0Zis7V": "hiBgVzAZLx6HJSlbPcC9IWplzuLOAv6i5zYVlfTX9XS301sCNk", "LqtAu1KyAV": "ZVtArV0OxOnAPkPGm0a3LbMQQ4ijbwQT0MZ1CEKTmL0pwqjyY7", "bir0WUiJ4e": "Y5cIy0UhE0ZgbiCqoiZAQO91dh6MrKXka2LxLMlToOP3KesY9a", "HDcZ0eoUHx": "KW2RERsp0xZKNYlVc0TxvWYmRjH9F8kYeqWUPVgTODXR5iUnjx", "iZX551JBa5": "wXfFjd3aPiAiV4omp9l0SUwVC4SxrpxmDKXfFviFKgjjxhNY62", "YkW9xNEjbt": "t9ByKfHPux6ECrrIattsFJuZxXZD4Kakz14fqbjW3ZNsEOEGSs", "HQpujorWTH": "o2FccGx4nC7UaI5VbfVZqYdbfqZa8ZxyBokcH3FSFFTuCO1Q2c", "zGDOiDqRAL": "L30v8wkGVO8ZqtSgjJu3EuQumz28FW9StCFIb6oSlzTSgrzk7L", "iVQdgJkWka": "cPRfVXMYrEFnmzxsxsfspDyiqNoxRxKdT1YwZm5Xl5iqnwTcsj", "JqtjtT88zK": "86ei1Y04BBZD9fytBoGecUfVZuOEw40RVEefe0eHpRYK5ndkfY", "Ln3DBi3lzf": "asMow15ZDNtkc7wW9wBow3ZHccpr47fxvCRzO1mnO2oBW8anzu", "WeTkvwgM1e": "dgMlcbxQm4ds7irc7XyPW05jHatoa8xLCGASb3ycplWWvyXHcw", "C8GPsMwbag": "fgoZcgMGMd6sFF4pPHKiyTX1PRwUUfOCl7io9VI1PU8I402Yub", "WNsLFBL7CB": "BP5roDrERISXqkAp6Okb2q9nkPAJk4S5sj08UnJTzwdEiseRYN", "70sun4m99K": "NmmQIAnZwfeIFrCxk5agJc3FckJ9yX3LH9qKuh9R8fWJ3zRFc3", "bZG2MoLQlS": "9pxO6FzJ8RqbkZUuxnLvyj14qhIitszYgSzAFdKB6f7yVlAtdt", "F2YExtZqYS": "NiLjLPYtUauRVYB3OEIX1JaZOKjEkHHrstgWRaOd9iOxoHCqgh", "J7dJLLNPgJ": "7t0gXRWmtPK7Fh1PcfjT5T3uxRYVLHqSuNnyE0HoPD5t8BaOmP", "JbcDKPYwWF": "7yd4JngOjQoPzpIkrEqhxUxucMqNB7sLCceuMKoGdsLNEEkExR", "RKtYmlbGRf": "o3ZsN8heYrXUfTY2a93VHc64TrxRIWatSTUqom6K5p0S3PQ4O2", "mN3DaTbWdY": "9rc94NOTN3U2Wp83qAF4Cdmn8sfYL0m0mgtAbMNWm47RwWUcUz", "TfQOJoiwbb": "6o8Va9PvKXmUsDbfUXmOZOSsaBVyj9wRZi3CxCBUxLO26HAdNQ", "twYamqBbF9": "OwHXLXqW0roryzITHOhkhDM8tDr4dwrokz5rbXwwchduvtivQW", "jw1oXRMJ2G": "CSOZF2BO8GywAJNfqps80NQ7QUFdZ1TRxWWrq5jjNUXMINnRoh", "iTq96k7ACD": "W8oPOSHtZ06hEWiro2GV3kE2P8bzfKZN40wdYx9iAxv8sdCtvq", "rr6TqLbpJL": "0O7wNPubbxArWTM0JwVgUTNuNrZJKzYE2gjEyjyitr66qMiW49", "GqJTQYVXdV": "lUcf5JsTiqXeDtvRBmaVITjMTHIeMa6ggkjer6fbQ00KudQJNI", "DsrgevZthH": "ioLzRIbbehEuE9MpcyO5dUnoIUkrXg2cQrrmomV8LQcMByXsfx", "1cmahP3HDT": "URCHRkHRIc42GI7gFwP55AMJBYP33JGZmSYwDVbctY0edPCPk7", "mJ2CiLnqgd": "UlYVZHaTOR4zockmVVwwh8XHPMgQmyMLiDDHX2UNNNoYaX6iSF", "XtF3oH866E": "O1xSxjYWywgwjX9dLJFnm6VK4m0hGZVamnd3IqquZpY2yNkX3B", "BnqVcIRzqp": "Hm3iMWZShfzjix86lNq8CddtdA6b2Y8HXu9HvJjSz0ejRiyqF9", "9gdLsQUuBX": "1JBSDIWLxy3L0dKZU9Rnu7Kp8BBfZzQ7rLNWkxAY4pppXjc64g", "h2SLqcwHq1": "VKqTQj1tuD4I6LVvgKYUCltHfz9U7i30hiqrnkkKZ5S0WURmJo", "Wy2BHg6zG4": "lLgngZ78EFPjZAkH2l8Yv4vBb0GrKCcthbZ6be3M8O755uKtsZ", "zaAxRUJfzj": "umjnQse6kZSXQpgkKjLADO2eZs9Dt52CZn3an3k31a6pfEpXMj", "Q1bd0Ytm20": "sUYNQVgEwyanEcl1hh7rWqiZE9bd17pPnI02CNEQNnVHb3dkSR", "dkH5kN10nL": "angc4HYQFsdSwi9A2KJdF734JJRsiwlDzSNYjv4C8CjpcBWLum", "ed5vbfZQff": "ZfL0TQbB6bOzVQSh4hoJUr8eSh829lqYgr9knx9miSfC7D3oli", "X6Oq6Bzoz0": "0zYNYrQkCkOGWzYoyg75mIQC11g4Wco9zQDi20DPP8f8ORh2eS", "MPZFdpyW6p": "9Y8JJkZEmiTkJGUuWIjgEtNhG95JlwaPlgDwcv1npm1QcHzCbG", "fdlxCNkAgQ": "68BOvq9LUPYAvjASfed6FpEoF6vRynzvZMNwFVdqRUVOyzseX9", "LWXQUhO9Rx": "m2aBrXfu3lmyiOevxfcbH8zQsHynIXZyt4JGmIEsfdO7C8YdWr", "2PhOuKveYO": "lXUUAVG6wy0uR053rvK5lAUJWTfpzY3Nc4TZV5aV3fwPyyv06C", "a8zxnk90HY": "7KLmU8n9IIicqYCTQL0Bf9LwM4m9syrFOjCYK6Tb9MR6IhryzC", "I6yQIPZieR": "YhAIqMI0HGvzRU9iw4GbJvaYn8flGmQULLXXj7vRyilToW32DP", "UpuKqcRSkF": "9Pa92qxKGsWRz8amdGzpdiLh7nLFaC3BI7iGXJ2xSdaljCdfzK", "AVv8ZtdNBy": "nK7VrIptHdddQyCWWXhGLM1By2jq9O1xCrnhqXOGI8BKUPqbD3", "LZNP8OXQmJ": "hvBd8ssCUkcWSF1HxPP2tPaGrDoRcLprN3yN3nux20my2gmlTs", "wFxk0Oyyc8": "3TkL6Y6l3yp2GzihmDubVWRn1EfQKK6XUVUg544GjxokntCDet", "bG8TBRJEI5": "J2eeibRGABnJeCjBhQG1SbFX3l2XY3ablJhJW6b381Vbez0tIX", "lXRx3EGVFE": "l2w8zwIA94aMLbLdDYSeQkR4UG7S2IfGFQWNTfo58WSqMG5JCf", "6zRLA1N3Y7": "lBN3HAALxUThF9JinFVBZQYbLuE9fmQrBfSVj2eYAAOKrM6jJo", "BncILNLsjl": "sOCPQgDQUtfrcAyHQyKe5g5HNKrNjdhBDSGW3YYsePiTgHOrKv", "cGCUMCwXMc": "7I8KPPg7ntrfJK3SvZA0QstpVoTHhr4LA25wbZXRG8ivIB4UWK", "6aJGN6Nsjx": "4GGAjOe6wuQziJaOUZB0UHiAqgAHguFPCNjyqbIHwNpcyFrHay", "hxc2U5Sxih": "fhexxdJrhUj4HEMf1jf9G6G6yqkwRGPxCY3z33mh1yhS2pjaff", "cl4WyVYNd3": "52B8J0gQIhzaVazeQ7YuHt2q4ZlMqCOLXPQm8C3i5T37TzS4Rk", "Xe643H2IIk": "mUM4KFXUeed0ic62rDvw7HecOpKPAXz1FVD1rL4UEZPkqC3mXl", "KMKiLXreMQ": "PIwyggHPH0KXCHFV7nIDjJhllNeVEowiNDdWoVXB7rK1UPvZ50", "12qc1tlDd8": "gY8tdmX6PPst0fPttecnD7fYq9KLiRfga4TJ0XUIIqflqOCaDn", "zdWRSy8nNK": "rwtec8x7kq3EWYWklYvfil37UWFDgoF0MG6mPRswvERulFZ1br", "6rBUUsD8E7": "bTFoLmKNfBJLVvrEsHw4Mvhbav0cGmLOXHsnUXYn0qnzplTjmc", "VmcOzHqUVL": "5UlLZTJa4d6s8l9833ifBArKU8zWIvIMd1MXvH3yi3n8QBt3AZ", "SDFR1JopqZ": "PXvANOK56ItWQpvvfdkV2wDC91o18osEpKxVU1L1tDVtSb1jVY", "r4lVDKATxq": "RQUx60uLC7Sal0N4J4pdasFb7qHYcInh080WCq6wq9Ul6yWuKU", "EOxJRxA5ih": "2nK79TgYIESU9h5K5iTWRJa5BFKpO9QJFmkbUkeYFaIdhb7sZs", "wfz3eCVlbt": "urcHDZhDdM2hEaJMvJY8V0O5ChZVg6tli1ZtmqFYGoP9L6uFXp", "iE68UoNXsa": "TNUqZlOF01F05Axt6ni4J9qjbuJ3EUrkr8Z0GHFL16iTeHdgQ3", "ZPQEtjT4ui": "M40chKych3WSDLfeXfqmUygPNSurXCfXvaLXplbPbKs7y19i5d", "DJnYgkha3K": "ZtDKAhxvgcoykyntGKPicrvzDKaJOuGnDQ8t5io5vEMD0QpB2d", "eHZcQNB5Tu": "lFAbFe1VFwWw0iQSCsDKxWOzWb2pRuPPL9etlGsEdhRqLjCLrh", "dLMXKjNyBA": "7leNwmiOH2OFnOU4WHdTJUlF1dU1ANeBsyqmg3U8MP4twhwx2C", "vyE1i9fJQl": "rI551piWMX3hTpBTYCNGIha4NRNKUiozclvNi2DMwneELw79Bf", "a0qTiAnQX5": "9T6WJOLasumX4I671yKcekhXdF8ycZgaj9k9nRPzkiySOdXM0s", "PFlwK09qbn": "zqB15R61nW5cN9Pc9zhiu7iJAuRNjLI2xUMedAAjimGDvxFnwe", "T1rzb4NePB": "Ppt1q6TcGFcZaiwTZrUXrO00F3OvWbdjPevn6ULPNbdXStLJks", "5CxSkU59DK": "DaHMKlIT61IJ4J32BvDYazHL9S4PLnemuS8qJuAzXCCShZXx8Y", "QlXJQsn05l": "Bs99HOBmvB1BmZXZr6ncACHkK8vsa3COIHqgZuHUkICT9JsvoM", "FAt6ooL5QK": "U6wYmbhsKMDu5ekdPTTY1MFCAWzxo7BbPeSHRdNKByhPYBQmA3", "dGw0R5sZUn": "mqvI9cmJB8RV5takwH64khcv57m0eFlhZEpPeZYJa0qQi8iYy7", "nda2C9dCkA": "yZ11GaJeLgKsI8l1ZcmhGyP0DewfVeiqfDDjiuUIQdGwUtLuJK", "yDmbXa1IbX": "hGBgx7G5fLtCWqYZqaTJxhaQbs5rC5t3ocKjmY4nG5j2etIHBO", "9FpUn4XgBy": "ax5HJjvhxeK06fmg7W4RRFr4l1WGgnOgBN0AB4M1S23J2nKTZi", "EqXwIlH0nY": "07JIfnIOHwGiPvN79cQhfJZAE4OOkZIYoytSiffPSgJrylWzF0", "zb89qeAdnW": "CjT5sCpaDFDh4Acq3M24zpYBpwJOoZ4XXsYZS9rIg8060Wbzki", "r14nRtbc27": "wdP7RSpY8YOKUKSob07buhp9BOPnnkW9vHetEOxtNXtruoDlGW", "xs1LeSwoCg": "K5D8HbKQ1TezJLvWDluT1ehirUIvHaO3DOwZnt03hf2yGD3ge4", "QHjcCoh1t8": "YqJeHuNu4WW7bzIJ548Dww8RTPZir8nSsCYYD1SRkOztn11hGG", "kxcX1h0sNi": "95lTRDAqcvUYZ0FJ6PwRuKPhVbtSdLL1cK0gpfkvTCYKW2owkk", "bmpVS4mL9n": "nAO2eRw5NRjbGVcYk7ixVWCuEUNfMxQAzvJg7q59SSE2ObOWa4", "WhntFUArLQ": "cKAMsC783qetNxB9tYxC3tz5A2yOo2stS4umIxnTSO9c6xlOpu", "YyKaLdqcaR": "BHOGOHRCg8hc980yDBgv2lw9U6ypwprMhB9sW4dPK6oJsiseCT", "HN7t9HkCSd": "muhd5mJBRMBuhi5BINRMc8biW9mgsYDWeG94xzjdQmgkNsfMdT", "C5jGnaAi9V": "SUiOK2Val7u0sdbxgI9l5sMtlhEWLQnkLBecitkC1bzIzqPE5p", "WaGxeHLm64": "Zj9K1W6N6st9888c2vde9mDMuXtYP4SbpoC68Kz3SqB2nvqBue", "hVY95Pc6FS": "YTacS1dwJa1HomkLoawEVIiSOO2DHyc0qm40oeCnpy2eMRe9Zk", "FxjCnXNzhU": "Obk6uJ64pqhzyGqtxCiUpfDLo9ULKZMB6cKl6KQGNYrh3HFTsY", "2OYnV65Awr": "eTMJE61bU5ugnMHPKCCwJ5uXCL5mECYOEXKEi4MRzIEySwu8vO", "c1bqmEcAXC": "oFAu8L6qEWt5sii2cdRRA1EW2uuprYmwU3b4LqsiFWpnqWv3WT", "esiXZaG98E": "4aWMSzeLNVFWzw7mhDoaVD0d2VWSCrRFMOY2TfS7W92ModQuGA", "uyECRxF43w": "EKNCVfg7GNC0tyt9FgWtW9AwtxPwDwAejlTSR4aS9tmRmGTsTK", "RzXf1YDZt9": "nuxNx7NUdV6skzD74wpuCC0u9GCJS1MfYaoCGVjwJrWivnKsv5", "nzcMZ7vUq2": "jUUQHXfSDGqlqCdaMu282EZpg91zlXQ9HGACCu2u2qWTlsmMn5", "jfEAbkKUhs": "P8Tdj26oFodufEKFWunw0gjf5oFeaqnv8DsZ2MHVUWWsZ2rH4N", "mJr2rmIm99": "Pss85BbB5OWQxFsgSnfIoyeTlpoPxBoqMUKoxPom4GenxRNcNR", "FD8Sxp5Ho8": "jC1jHWGCL0uNacng2IyB2aey4N9JrIBSCBD1Et4o2cmNJyTCNV", "hJFghxFvUM": "NGe5s1WJnwBzZC5T6CJduZyDs46qUwLcj67ddy2aFSk7Xmi9NA", "I2CERGMdbP": "IaWgOGeu5hYC8we3w2ZXvOFcjfqOo2ABj6zDSK2S1rfrQoefux", "XRbSvRO80O": "IIeQ5mGvoB6fG1psL5ibqd6KgtkdarBi4udbBf34VNJNng0gca", "9P9yQHCpsA": "xbZIZhpiywAdNut0Uhp6eMQSovRGgwuc2OidhtMocD322aFrWV", "S8Q1zXS8mc": "M14kAdptJ0RSmberFnbA7qRmYmRUp7kQk8QJKiZa6naCP9abG6", "QJZ15OavaU": "FKkuzjwi3GK8Efo9C7FB3fOVCXDbI74c7yPhRgDhIHjUvvYhR4", "feUgOML7df": "fUrmnem7UOXr52dRIbjgOynrNeMykLk0Qicl035F9J6nQD8RIo", "57q65gmfeW": "FQZAMecu2RHWJuzn8Zb01wVrXmQ1fqFXOx3kqLQF1lF28QC5Bj", "YQ0A55LsxZ": "cR1qpbUph9G1o8nDqupeErdvwd6NO70lZ5rg0VozyACyx6bIVJ", "sCcNkcvFVH": "VTClKs1XatrfFCn6kT7z0sNBShFgpk9lLcEPkscsvTcG9oT4aa", "42uwYvD0ua": "TabrGJsU0N1vpwXSnRNhZsQOWddHXawvBag8Ji9UuHPFcSnb2Y", "DZzyOIR14h": "ERzikQCoLlqE2OkgLxcIHSMoRrSLDomLX2kz4pITwxv2p1C77z", "JupNKZEDaX": "9bk6MNGZy2tzdZUBonKeVQ6HlbI6Y5OjhDg9rKYBzl7S2ali7P", "3Nyh53op3E": "mil0dVb3fkHta3r8J1rEXubN7oFnzisg1RUPXh6KF6Wtaiuq0k", "sm7AAZvZ5s": "7fpmZSrM523CYC08TlCUVikqNkGO1WVJLJOhvNv7JWWbuKpGDa", "QzULr8oQu5": "uy7iQKMaLaMgHHzGyh7PhPnbuo5YdEeV1ICVcKG6Y7ILnVOaIn", "uDYscNBQtz": "ZCvadgugCNEs4PbIe6Keg43uAxIyecwjQM1QSEcGA5FfduiFYh", "TaPmm1rbe4": "XyOkJjqpISyHFHOLZeqK1PFVORY1wG3MQWx5o9T34UsyrzIAhE", "nGwn1hnxvk": "lGe3ntVX9KLMRrZMyakRKBwbrCCfC5WXSrbIu4tvIZtVysx12d", "Fyz3QvlOS3": "hRdb1jy0OUtgQwDL7WRFZNUMUblD2u59JkTcDF0KszJaXokkVD", "Wps85hLhKI": "OqUk8t60xjKYsnL2hNzgbNAMi3X4thjRP4oGYrx6kdq16au9eU", "nmYLUz6kas": "hJPbGW0KgkBQhkyn5UEYFIgsiB1HJxFHUoweV6UmvmZVAVhtui", "dV1r6zdIgQ": "AiatqxhvyfSGF9zVRa4Ccm2H8sbuBbHYGTr0c5AbaftkHrXdk1", "Red494IwM2": "ramEfGtGuvKbICBtHl2bofYZbVRWMr53LutOe8oNbZsTJNg8EL", "haPEDfEYTy": "wZuFgDsT4uNPprcquSGb7JfaWsXr0hHzzkVhpFcTGYIHI0Iw0x", "vJisJS75Yh": "DGBVsGEbCaDwv4jflfg9M18LKZxWHgYVKaDFXB7vUduuoiFARl", "FdKIaVT05N": "7KQpDsCKgrufXY0o7dcNaJapGhIcWJAINRCH0gNfAAyYuAtZij", "8uJLWXuMNT": "AVqzO6J8Py5Jjk4pnq8pCPZPmX2Hkp1Czb3jnWWxtfgPUTdVya", "NH4mOqyPY0": "Imx61MpyocKmtRD711mbKxHVsDcJbs7w0l9l0NcXaIhUzs1egd", "O5VSpGoGvI": "v9bPj0nq1BAmjaVtwtmDciu5PNsgHvVn8FglvQmBcMDP8vsg2I", "ODFMEo6yeo": "aihLpIzEKaXOpTworN65m0mCd4yyMEXqT0Vz8bZFXFMmdLec6M", "8z56wrLBhW": "Hfmtyr4xwXYxylQYrd7PGJRuR5Y1ksdRFJe20e1Ql8j1khYnNB", "4uCDfzoDo3": "20FGIVJVgPOGEmE48Hc3LufZ9huzUDKbu81aCdoOpAIMNswk2f", "DFbRPDbbvQ": "NunxRSFM4SEE039XhUGZfQ0kcxULpmnFSjpBOeXvI7xKR3EEVv", "H7WUWy4JJ7": "WFZbZehQ685rsjpAIi9iPMwRSHXONANNBwgnXbqqObW6lcQJvo", "VGTOe2RWRX": "n8VM7DAnBSCrkUyQZk2ZFFQbVE0yQzTMTVBOMXd88OPauydXKg", "afMICJjpSW": "WHcSSGJu6diXAcDIVGRu46l0vblb4PTbvfrPBV9WHwRZj9YHq0", "QECKgoIPic": "oYUG7MKpm7i5L9XBOwUxvkIGceNX6nQGuZeldj5QwnFGctGs9S", "LSxpBUT7Cl": "rYBDN6c8IJW9zypmc3jIkGjj1OVslbJkW65FI36wdHJaeHFPP0", "n6PJfVTFMk": "mhACO60lG0isZp891P2Bj36LximbHrja7UgJfJpaTfN4nYsU6u", "5AjqlnLmUb": "J3LgO7XZZSeQ04G2XHvncmdoSJUjlyLe4aslRnP1MH6ZjzHPOm", "WoNV3uFqTw": "99potNbkEdUboBANn72Yx0bejF86MFvfaJzcw1wg9MzuYnwGue", "9p5plZMZWF": "vu4wRNPei0Cn5QpO1gL2p4E3sBWbT36ZkXYP9kmWi7w72dOIob", "D3prGgqd2z": "LgS2uCicGg5axyuQRLyzGBOCsAppcgT1sriR8fMQjhC9pd9RB1", "E1Kjdkpdbh": "NIXHEscbNQ86Q7d2vjjst9NiWWKgi8Qm5JdASvfPTzxM7rZvbd", "ksDf0cJMlf": "eRRpz5x0PIlBA1bo8Y91Wr7mqHGsp2cUBZr7qsV5kFPvfaMVix", "OVZksdJKKq": "n2fyCNP82CIod7EOu0x2BytXVA1nuk91DPuxstfHfH8yL68rnA", "Q9SwXX7lGp": "gb4F0t9f8nrpjprgD5XAuO1rVqo5PZfWrldnIxAGW2dwE1fuUE", "7YQw3Hnkm3": "Xp3a7QP7YZr135U1TOk3IIGNGLd4VYT4bVdpz7XBfzVf474sSn", "UHAjFIaA7y": "enjOfrOwqJNP7dadlZdeqXbQFIDcMCQPhsEW0531YgWRZBpLuq", "GyHWUjiwao": "KY7qtTBWla7ylZ3sR3pUNMCQgTYH2oKRqoQKdFYnEEcpdtL1BR", "96EuzFaIbU": "GsBN15V4cUn3aOb0FQx3iUibBYg3GhKHIqHrqLcN99pqCn209F", "2C35ffPnT2": "3IT2DyVIQspZO3CDOaOkGBSzipn83giYAPEV2UFmrPEnufGaJK", "knCxk5Vnx3": "VPIcO8eaMeCF8nTAlp731nhBPKkPp5oNGiBOTGsFZaCWLJOms4", "fPoWAMLvPI": "Ilu2e6Hdz5nqRdlu7WZ43cSRW1XweQqtqVAobiLGPDSiEYhHsa", "Ita76koxTD": "iqxWIlY15p4DgWi68R4vpwwXRB0LCQ88FaWrHwrE3cWE6EhKo9", "csl8tA5BXv": "vC9zJpxNOHDmPBnfJymrMIzRP5UcHE0auDt5PRmlMwChoeeD9L", "CbxZFYSXil": "PmWDHVI1tjECZQX2u3sHnuG2nPEBN4BYid4bKgsz9HzZv3D2Q3", "ci56ykHJtR": "XodpSDCsPosJnYeIk5Sr5PuHnWDU2Wz2rDJVjir0Hi5MC0VeNd", "rrevagbTbD": "beWKp311KQUWrQOb9AiC0HMHw2lhNi0vndXVEOOPkW6EMQllNw", "Jr68oTIk68": "pX6oXgjUohRRVWaAqDeZwq55Ge3Rhcrxzhn6Knty66dVJr8K8M", "kEjyC4b6gS": "iBnVsDeUM6KKHnGkjEvVbH0JJaJqOoGkUJTu9gbBsaTCgUzQNf", "z47GcB7pcK": "GWEOIfRVw10AAOS9hfutqRz47BNSt0NqMDckn5D2wxbSXtNjfv", "zEsKXz6bLp": "h1cOFENXGKZqremYOVcY7P8YKhexzVNVgxnq2mfoWN0dem38LE", "1Uw6gjQgpD": "5AFrAnxCDd5XPQI1mqftVLh2BurrjhLgWs93e1gSkAlUUYkgsn", "OGff4VQqPY": "KMHq0jvxtR7t0dmPQ9t9IAOoFjDPecga8hpKovx0jUb8zWkGLT", "iO09Hq8UYH": "ViOcIn5SH09WOusQ4dVzKBX1ak4BEo9VNT94Gy97IJMHP11LVn", "NMI3022Kr9": "zXfOKgLU6E10FGFZYBcoObLVmDzv5ik8OgqDRGQYmPv6coxS4Q", "tH4alLlDal": "pUOqGvEWL1G4mvUO0etuHDaU9CNkD6swMxK3jBrxouJ2rLsIzi", "M1nseVN69D": "t1uN1oWcCDlZNql3iKTyFo32Sh29ee0xkiYu70MGrbTkLRuCFw", "cqAUOSQOPX": "GWKpy25c8fujd5gPtbhbNTcOWZLGutfQH7N6xHsePCEIVLqbxI", "gMSigwRiJb": "pWK6cSGP88hgjGB2H77olN9rPvtQprZkMCFZa6cCYQHXMkkBtz", "9nM67yNqM3": "u4dnzIqEdjrNcEDEDmIq8gYK0tTtLVtlJfJE5TBZaa5ULr6Hey", "7ukXp8Ak0z": "WC9ENp2tNEeSlRERMWftupo2f4z5tOFggkJkJGSPlax2KTwTOU", "QjDi6F9VkD": "ELppgFx4kvb1GFKyrpAJCsyKXSC5wlBjuN73S4JwlI6Y9oTrHT", "ofSWVEzdEa": "6mxVItkpP34xH7e81WWpv00VqyQKVzLN3sJpX1jioIUTdC7cHm", "AcGL8DvLc1": "9GnjvUcdRKIFfXNE2l8wI1xHapxHKnb0cXQIm2SOSQ7KT3fWzf", "kSrMqJvmRV": "RFdw0IljeDw0ulB4JsQqAHMna0l27Pm18VBkCovuZgI7IumY8R", "AGV10XIaOx": "KQx75cdqA7BlKSwodllYcKzNnWqMB99Ng3lrCtUPjBrzTh6bxX", "3sqRKOe2U2": "ZxjlydrSacyNvU8qApEASaQBKWNy8152fgKztGEESWSrW4TI0Y", "BQS314QvBV": "86UNC8fwVFeZhJHp6mpyxAW7NR9HTaa2MiEvpLcYC8DpawFdEC", "sD2veWAGhG": "98JNPEItZe586Hk1d1yKQpc37pFmgJfNFzbDfoRueq2B6of0bo", "ipxCn4jvav": "lqF64ti35zXtRg58R11hy5KqcxUHb6JAeUed2tC5gIgrcqJAJS", "Za13pzg9Ez": "9mVZklcYAIf8HTosNwu1Rc61LtY3oUENATWXVhvTkc14hmiDcN", "285OC6PVWt": "p67vuipvYHmt99SPjarQASVQZPuuieMReZxQCKuCBXy7u77y6W", "AqzqUwvZa5": "mMkrV3biBwuWCDYsd9b57jpX9MIbvksLvN78Z2s8C8S2QGEkVF", "jYTsnyi3CC": "fV1pXwlwGAKP3XfA14IoWipEfg9R7MM3izLKlTOljkbOEd57Cd", "Dq1U2tnIvx": "6c51ivYXkVYzuURn56HlQcQ40yJ2Netko5O8VgoFfUEKu9tknj", "fkrAPAZAtq": "KGa1av9y07aFQ6Ya9XR1nugkeQO3VKqNPsu10ZvLMs5cROBxrf", "CmmoxPVv9O": "pgCDmUAzSaGK67sam6OIbaXx3rEtKB2m2Rxa7M8RPH7abV67Nz", "Spcr3VGuyF": "vd8gHOcK32tp4f7fJcW2D2OaqBTvNBSpVFj4vDJPTzP3sxp6Oc", "P2JLrycqwa": "2WjRqeED34ARmeh8Wo3TL2kqR4aNu4gkPuD5N7i0YnwLMYgOsy", "HB8aE6I23h": "XRmAqfUaulyU0kKa6o1vW6MwZVushmcAs5ZvG88UHi6ZurRNfc", "iFQCJpOOQ1": "KRhad9SDt33iHjuJ20oEV5i4Zwj2O7J16RXNVqlgvp1x5H4HYA", "00sqFNdGUh": "3iXsnPLyAzuG8tO1W15PX60mIQOTXtg0Pozon95eR9jWgDN0gY", "FnuILwctzl": "FvAeGNVNbWbVe9iEXs9ySktjWdllceILYWeRDLG7qX4e5wwCEu", "807lzrEcOW": "rpXl5XyxXBNcYly2Zsite4fcW05S1VReq2IUbTUBEAVucqU5qA", "DUdLzP9K3M": "YqlMecrXREnvdqrMXXgInYrE5og5me9fvzGIFk4okkBv1tQ5N5", "WuBcMkbLBF": "Mhihy2df7LidlYWYRQ4yNA3OQVl5TN7abHgtZkTm4busaCxN3z", "bf05TP74Qs": "UVc4AXPJzrUrYN2x7X5s2mdLKNva3PvdPAEZXvfjikqIrhIi9k", "HvrQlOqIP7": "YfUXYB8gKdMo7hp0sfSBuhXjpDBJ4XUbypLGvv3UhMUNsoCOPh", "6mrcHssWkR": "y1Kc9HxlyyTD6BW64QTXRg5j2HtVDL59aYsgLkscsuiLVHqrg8", "BajRTKJyKm": "0vNRqtN5RbayCs1MbviRY5AghTZ1zPBt3OHWsVC9TM1UClYSVy", "wqZLRmlVcM": "P8qH1ItS0GpUAkEXNLeXTFHbIR0ypZ2og8qBI8zo62eGKQbP6j", "4O2KXR8IvX": "ADiXKak7wmYnjlzQmSSP9LEur6CD4OI577ato4zjghiY37ZwSQ", "7Ul5xBT0vZ": "15o9rFn7odtRmRonf3fR2cCzKaLkWX2kXokmFFCg251hM6Jotm", "U4oLc9od7V": "5p5u6tEamMKhkmVb0vNnlBs9Sq8ZEhJMIS7mkUtQFqNn5NFEs2", "3liqkW1VDO": "mncHLdVx6XaMpPPPK2iAuW4uhmHIb8VD5jsolEQ0pkQrQtTQir", "XQYFGx1Zak": "u4QE807q1drPyG13x07GpV9uywVMxcB90ZvIWrcuyjc9MUeE0Z", "YLpbQKr7qU": "u4HSoE02PlSeCEagE1vUQ9H6O1dqUM472fMwhQwEmEMhnr7rJO", "lUCOtOW0CS": "SV5uXWFJYstmL0SSAWEJebIXgfJ2ms1XtnTWMZUecV9h94nPBG", "Pe2gBb7okf": "xussUtvhvRPM9rpYpZSLtS1WkcNSXXHpcot6uJFLnQDKBAZXDI", "hhbikz5xNu": "lAQqwGvOtlXdPU2VWEr6NXUdGUg7I8dyWeTYwcQqgCaqUm8erx", "z7AKUDFIs5": "AcxsQYbpxqPrpjPi5DW7OkcigWKfLHv0cVlHmd4EkYehg7tz19", "9b6RiLqOyu": "h0KTLG5nukf0x2fD3XYU1tU4JBxBb8frgdjgRJ2nQl1ClXAFUe", "gTe7ClqexF": "KzuVxphABOwPMmoRYtSC81zHWplRyBkqMZF4fiAeGOLob7nMYK", "cfpvsEiNdt": "hjfURGfjKzruWLZ0xQGfkfxz9l4AsFauuqHoDkXzgIP0L41Q9v", "cKvnsBvUE5": "2AZ2704LpPaCxroczCLLn4ezJ2BvJsOCN0h0PGAjCu4dziGP0c", "o1fU080dJZ": "f53G7e02aF3ZADhX5R7u2k3n9876lScqNyidLrGk4c0v5UJJhf", "RuJVPAw1zO": "17JbucQ08Q85IWWdy0TftRqdU9BBaAtu54T5bJFhwq04WP7C3O", "F0Z6Q2m8Yg": "NSeJ0Ehdkmj10YvEpuhxQzIdsQZ5czWz18yYCjuvfRIIcWAxQU", "IPBsqBwvPv": "lY8SzORLARVer7av5R21r7xQatTGJl1mg9QG0UVYNoUdMMNLzE", "JQxfx8Vp2V": "8MwklQzET1YPBjkQ2HINilwWdmvqVRiM0OZBdgrAOUPOR6NhHn", "rhnDFaNjPf": "1rybLNbx0I1HgTCp91ngPZIOze8L6IGlZxw00T9939xgk5BMyT", "ktww2XrbIe": "x7H6ucenUKWPCLtklLQbWXTQorUgaDj6oRwZwr36DVBOaPdNvT", "SypyYuAfjC": "pZOSYC7b5Ev9mhSiQO3NyVxYKN0BbtxwMhWvMFXnVq3iwMrQq8", "JCiHAacUv8": "GP2O98xPnUm9FgFYwql5k5EprYGWju9lwesPuf0vrYBze8yjUc", "wE6PrGyvog": "VX2hpkZGaPaSeetrDCc7pq9nYuTUz3ws9kUSENMNku07LRPiPz", "FFBuDPcPuq": "qw4v2mBYYCL1PihgYiRJtSKDlW3vH2C71HVidhw2XeA9NS7Qtx", "qhr1kwOFj7": "dTsRZH4SF1R6JWwDq2uHsB0RRJebCYkmJDCssJVWa6mdtpPWae", "XPgyxh2Aox": "Qihtc5TxJWHtOUU3RKYf1jLV0lbbCaonn6zmkhkjFsOPoNfWhQ", "7SWPySZIQQ": "Xp2yThjkoijH8OcQoTNfqtsYx4tx5lBF38YrLsTRTJCyOJFsvT", "63jmpCvPcO": "crHlRBcl7hneLvoXVVZJT8OHZMPCiEeqTFm0sUupJ6SBTrcEDC", "h9eK2M81KR": "peXVFgHHFnvSBDukN0vhzGyUc3UykMtD8QbPNpWReIMX6kmII2", "CMLXLzmoj6": "2aaNULJZdmpboxEGUBh4CctcrWOKLbTnclkFTXecwfwxt767KS", "PG1MJiqmUK": "SsEBXqMoI3zzwc69AjFktt2hm94MIwUYMdyEODUC8eEx2NBXPw", "frB37OkxPz": "4PxARGJ47sqaKaTgeq4LFbyujR8N9fLeSxzSrcMrRl4djY3Z0A", "lpb2VnzOZT": "a22kJMUtl5FYYeE2dXr5ViqMkyX9HwmoOORFjKUcK3LDpRnW6E", "t1lHz9sLkS": "jQ6hu8THdRiLN09UpbWPjCecNGHpH1vodPxCVEuGhw5FgV87TN", "d5rq51JOmj": "Jh95szSVVwmVNXzfXxVM0uZVoMA8O1GvUXwEtp7YHOwQIdMU1D", "ifkSMt06FZ": "YIv9JUGTZVMCMhRqHG6Q7IHChK5dgPZ0RPTecqfLEPBpqvroTC", "8Xtiz8Nvph": "LmAkI3MJpPnItUSbPXkeI3ms8TOjbpFx99HDKF3xBNx2pKAa1f", "xQ3hBbhfJr": "694grKjxOqfTLHk4FzUn6TOnZSrlmQGwelnHVCaUbxN8ErEqIJ", "oq01nroT52": "VSwvzOoGZt2merFBUAyFdRDSPWUFaVRASN651DOlc2BNzR0U8b", "ZigqmS8EWp": "az7TGY1ssjf24XmbueNb5K1AAewEoJ07Vuh2fHacRnzhN4upSD", "p9IFQDFhqn": "cEh4FDzFJ3UH1eAP8JvCwxAAT1dIf5uwQOHCGxUjC0oAtAbOWB", "tA4c9Kz9Af": "cD1XA7BVlBUtu5We0En1AkKRpYHJjF0du4i4CZhWATN4djwWwy", "AX7r9TtWbk": "jA7RDDDkgiZm9B5fJ0Vcf0IrcEC5pYS4MEzQv4qEmD1YavZ4QO", "QXopfR2kxf": "dhhtjA4gYjwhJzmcAZ9OzElOUWNDfetqmrqoDtTPH44olPqegE", "3Jrn7KvWzo": "tI6h3w3S6gErEeLMtWlhOErUFOZapr2SrRBGtw92pBN720Q7j5", "HVLgb6QQoa": "TGkn8WdiOqMmGDlew9lATq0W5ivBHY9INJyJOzxGLsvGnlNTir", "1xZlrB96hf": "b3vJpvqTGV5XJHKKJuf1ThW6KK0dLWOXfQxRatCrtbDXfsIebn", "7X8WjsQKKI": "pZT2y1bHu8DIvT32qb6r2T4P0LMkAajuCRzvTE9kFf6w4hW4es", "QSzQdmrroG": "ez45rvXdsEVlI79UOCUERKQ2ffqoxVWfBE2egzEcD8dGcJ6kKh", "1hugxQCf23": "q2kzcYEeZDHhXUtV5ZjFqijpcwV08pTYwRKKq083GGSm3hB0YW", "ifejDqncEg": "Y0UQV44ft8AEebPzbdV3wNDrqIQwcXMtUFyYaAApgeQ4hlV4Nj", "ezf3qVUXA1": "7jJvi5n4iTj1dZpMp9dfq5A9R2DkVX5FTL10eZrISJ2lJjpBcW", "eKTiN1T8M6": "NdFlb6AbpPnjwTkQhl0EyOeycGCr4shM7Thr5kmUf7tkmjiqDc", "TWBEgtbS1N": "wkssvkoAqNfYDhU3HbZUubuFsdprxUvQNLYylcyu80ZYXGQoju", "SsRcK9FU0j": "87u6BpYYAlWWJavjRMAUce1BxMcjx6Pc6NLFhgWFa35BQaqH5X", "QKMtOAzIDH": "gpp76Gnzs6Ag93uHUz8sDQh9mbRBI9G9wRBaI1i78mLRLBZSo1", "74YEEvDilr": "EJDJ2j5GqeIJoIEBd60nrLTViGC8FgAboSJeaVDkq8bP83F9pm", "Y6dhk7ZYsH": "O3FT9f9l8BZ9RuHSjBIrQcTDXR9GfF7fYyCAg2T4mQlc9bPJf9", "D8K0IwmdPV": "CFeCOMxHGYrzudY2sXf2vENEOFcvVI78J4q6ibwsNRKE8rTpgN", "iArab4Lg78": "2oUQABvl75YamDZ9DSwG9jaaJrG8xKOzR2AvP1ydQCU3sxg6ED", "Vo5N6vVHUy": "HfiM67Z7gHjBQoTcPEV9uF9B4NeDVccS9NMqTyPhZiTEQF2jNj", "fxJlQtSW1c": "7luOLlZT6lexNugGGgnARPs2GNJ7B0K46hSNsToOZf618hPzxY", "cRrzWABORb": "k87I2JJZ5vjnXQ1adf4gSOrfZX18xSZjw0N2uscJC9N1npE1WV", "xgfjiFLrvd": "c4NSxU5YAsBRL8LBqVJgw1AMhxLmbW02zkdca7S7UgSZTRM3hi", "y8oa5IyoTU": "IIzMRGEn1pO2UiFBgRMjAHl6PTm88ahFfsbGrLSAvcwinUkmeh", "oRXw8S5bao": "tVrrP7BNKTsHHpaLiabOWQr0JddjKKYaRU6kDoXOHnbrBd80c1", "CHbuvg3aqz": "D4UL5D0YnJggrZcD2TaVPPb969j3Wd40TNkWEd23gnM2CTlpaF", "RIV5oI82jm": "N35qtwFvJxgNVIs7HdWR7YrBZ7YyzBmCEpZ4rrh3H4x0OhhoXh", "7TA20gF1ez": "dWifFXTs25OFSeG6NJmJMNuNHCH3LXuvtwuR5LtzcDfNFYfCYu", "kxxYRGM1Xg": "i0CdJTRMloeIRtvUi0hBKjdGb9SWWqYLD1OVqstqlEFgGISlft", "uxUUBex4DF": "LpdzpbosDiwVpbMFN6IzgakHm4gLmBT4aV2gJCpCLnRacrcaEC", "kPwo9qANYN": "kUcnVZMCpNVeX9gw5xzxHV3dZJHmXC0Ey1HEJskVQuebdUQdwG", "DEcWZLYuAG": "1gDAGquRYbDhVAx7OT2HB1w4a5A4DQSUItpuq1Yj3L3OLHpc7i", "iiHt29jd0T": "G1PbklNFZ5GEr0fTdYkJGZSmIqvLYUj63AXc8sBfdcZR3jvArk", "0C2vAiB4dx": "gvMM9vGkXBjIUIZM3ouYjw1X0Aljg6UWimdyfZQTsYG7QZLj4m", "fT4Lgb1Xmr": "a2b6vPfsRgSEMKBXgboYB46AaX1XPHgDRoPwTwpKLivcwbpLnS", "KMCIMuYsEQ": "eU1Yb6BtIYWHatQKaTiOj5yNeAMBr6US4ccMXMsPKW2tjcCcZW", "QedJglhz8b": "2yf4eIVPHDgjctO5XOOiDAoqW9WpNdFZkEtjcTZAQ07AWmGIg1", "bCNweGvPoo": "sQWNgrhtPDoYKhVBh4reGQcZpzZXqHnmknyESTyDwneWVX6767", "so39FU6LWS": "DudrfxdnDuw16LVPJ7yelFawCTYN5fdXahqqk52GT38TAFR3pm", "zzHcPRIj8T": "bQGuFDA0KdDXgyBRzEAo5EGwgNQTpmMZbUzpJ4BV7uZ4xBuG83", "Z4nSSS9Cqn": "5QvWz4KkfSt0xuK56Q6OvPuTEeinQOIFQmdBacplMSQP5PG1M0", "0X0ZPbGzm0": "Y5TSEVG3KnasYm648TvWloO49leQnso91eu7kssNVWu7k6XzB8", "O5fcFpnsbZ": "BmUBm0dAc5Nqf759AAdqC1Ocvlg6thi8fmhDlFFuMquxVJNYqV", "Ft6Iaimvg2": "54Oy83BuKHAP1mG9EdX6d1sxwwOK8OwFgzBHLtMrHse0iPWO84", "xVBwrOv6i5": "UiwMFahNBMXBFp5O8YSFrCb9P7YrZMoqXYZuL7tvLj1ycHlIgz", "TsO18xkMIA": "uixtkkRDnFUGwwx4TgAnezE42Nxdssj7yiYPek2MaaTEBNj4Wb", "icv4UBHZoq": "CfHO2Plakcn9cqLv31IapysNfAHpH5Fiyf2tFAmW1o8iA3larJ", "F1bQa6V2d3": "DX1H8odt7qEes42Msm4OHKtEXY5tfhWItBDhDXn1fkE7JNHxRD", "staoey9d9x": "lYyRVda3YctSm9krmFrQQyxb2Lifl38fmkvlYeRe6Y1PXTuOKN", "cppUYIzftV": "HFeLJZPvfKZG34MitMrQ5i3dyLfxnnVzFanUXmiQWGepBLgk23", "jewxF967yR": "d6tNAl1a01q3qIWSprm86OJ2QpEt5YALktp2njVfJ99O2tbwaB", "bdyUTCKkHS": "eD4F3Nuv6ug73xIimV6FmDswOD6Ixb52wGVVAuNtetIFeeVrjA", "AuLnfizK35": "Kc04yf5xSyBGe8hVxYtMDXg1sx706rQmgzGvq5E1tjRecola3i", "x3ie6x8Atd": "K927NnVVxuYEqupyd176f0aAfJok8ljyiP21XIIgegqUJs58bC", "Usymd0frp9": "YDcHIj2EjZMsBFpZjChFZlgrrfINSdA4AS8toYKepzqqshEPMU", "y8DT0hrpuW": "p9ujZMaXd5opfawgzjXQb4ORFW7Lb1sdFMuC6wgqNagiK1Lhm2", "Fvagg8ik3V": "nP1ynGQ7HWkmZgaj5Ql4l2yprVvAHef5QJ80jlI4FsS978POxg", "qiJtYOqj23": "mtnzo7XQAI4zwsLAKvsGkMAYbPqXghCzKjJBpQCQ9nrK6X53xp", "x5UffXfCe7": "wypA8Hn14W0KaT5dIZCropxAppGOUgSVOKCZdGgsWL7fkbJd9f", "W7BJpiP7HY": "Q9l2nNc4JZU9fntKJP5lI3eUP07TYVOgBg3TewGuPS5qUinhWl", "AkZjN10mag": "QPZlvz9vUcPtQx5ubxNfHXpkuaLf4Fmis8qSp3O6lh1klyKxZw", "1iV2UvxD9z": "nVInbRkU8Fq2j3834N9B0XYGVL3La6HAkK8mP1Bbz3mcnt1J5L", "huZ38Zsn4U": "SFurPG7YQcb1RK4WcJIW1zYrSlA9T4aFI5d0hjCCyqp9EM5LAG", "Nm6BLHIFew": "TDdsswXTxeYUYyAcmU5cS6FchM4tsyc123Kqrv6TxFBPHWGqe2", "cb2kCYouwl": "0jt32Umy30QGQM6a1nKz3odVF0QhPWXPzXhfPYquifUAgc8Wc9", "PhxC5kH9tP": "ZsytZoQu0VcdafgS4zzJrRSqbAlIaUikFGknkjmt30MQ4lVE4Z", "syjPONcxT4": "7AFjSgkk45yU7Dn2wbsnZzrZ32PknwtKBMsP2HbCaFltlbJg27", "LTvs1fKYo1": "wVnWjqPPeIOGaexHhO4zjJW889H1OLdKH5VqUUj0J2DG7P2mPK", "qsQcT7XJm7": "vsgXbGVMRM0TzwfyruLfBCd3eRVLxSq4nFksRNzU8GI8XJXXjJ", "exWjd73uV8": "8u2bVgQ7ZUl0YJd51Xf9cumclDFcnC0PshgN7j8LHVmszaGVtf", "TgEgfJQ3t9": "uCesqV08BHoD2wOH1zFECd00WKRY1AUHBvVk2BpqjTkNVtdSa3", "ylnFPqCUML": "cNTsZkhwjhRPtFQBQSEpSDZ5UHGpsFr3lACpi98ihz9ArJZ64G", "bob79EvfT8": "hCKtogcRXflFAF6Uf3gGE6YhR1zFSj80F9tnyqCpW8SJILSzlD", "ZfNVTSaQMz": "48wcznrBo4C5dCkjIjvyrNC5OCRXGkwaRwBcUoaWTeZ9IPYYtX", "bIvmYp51KG": "aADPUGqx5CoupULfS8at9bLJgoXEsRiqMtKg29vLu4dFJqwJGP", "gEPndSjKYt": "UNN9956fWj2lSLRul79GAX1N4X8J0RP6xFNdyKyqdy5CdAvJM6", "MjFd9EsmPK": "7Fd1WOGOJewSY0A3PDGxcG9koXr7V1Dp01E3s55PNYQIiOyE5Q", "VVkQJsFyO3": "I7yNzTovcOMdXxnkIuDl8dePWGdho0K2WudiMvscLUwlVwx3yi", "uyblqn65Mr": "fnlcQTLXKLQskwz2WINVFCg6VrrdnR01RBO4tquHdmo7EsP54C", "AxzC047Q8w": "yUuaeiPedFeck44FF6ShcOUmi9CB2iMZe4zfuJs433b6fsWzGf", "I4hoVOwvMt": "6DnjBE5Gssj86oLrYEZkb3plaUcWcfNzt2H5rXh6PrOdN8fSSV", "ilcJ64B9vK": "VRZs1BsF9yLCfcM2eZ39f8t6JPyxarpuCTWjG3zubPaeS5fxs0", "lPhPxoldC9": "UDYFaRv4jmdrKJTAEs1qT3De1BXqQdDk5TC6FoNMg3iLkmyfyi", "SIFGtAONGg": "iEyX74arnmpXodDMpTZ7jhG5RGVBaDtR7nfZvCkx0yQMQXrPd1", "jTbNFqF6g4": "47znDNxMyLaneYVi8xbqrljIxQR6uXcVI4CGye4GN1iNL53jII", "EvcC6gtpUL": "YQUW8maJxkcaxBAuKszlTQSokzkxqu7WdpazvuLHAD7qQi36Cw", "T5iZLln6vD": "90YtAuuVEf2paCerJ74Q4WyjHjHTQZ9lXbYKMH3VxzD1iJw1D1", "IvH3rlsAAE": "ltdI5OrdQCkSj0CAcVpBCtAqKdfX9v96ZlIVdjPXjIQwOxOV5t", "fNoty9Lb8j": "48Vh6tWihjNvdF7XyS4N4biO2DyfGgHNZmuPkAVBqgsJR9KoKa", "Js7BTqn1lB": "mYQ8bQJos2fE6oF8OOkFVAgL517Gr638sTTv2a9GM8zHwyVVbg", "67mJOzNSkc": "FjFhaprasU36ZEiFCKXCZFTnPm4VASymoZK7NA3Nw7GEykA3ko", "AawnS3rdIx": "RzQCJCnCMDm6vo3iRqDS3Ona1lgoAtTpQSuLyuolByOhkhVeou", "2TYKiHQnvd": "gDmJI30y6aVIUEHo2Y8iqK9XlSBldK4sjaQ97vihW0TZ1KugiO", "2iXiofde5y": "Ks46Wcm1zMGvHGygn1a32ZBLgBJusDgHuSvFVR6mVWu7iFlAWc", "W9agxxMUUA": "YpYud2ZjVNYMKVSgOzQrjuAhqXcdy0rVe0y5mV0F2pVmIl5MO5", "eb2lqVapn7": "i0EjYqvevIvyViVRcwRJRlf8FuiOpidAjaVsWAV3uMROh21zLc", "TuZrUR1eUh": "oC2gZ1eAU5KD9bNxQ9zYQlXmxgwDThVGfD7FuCvJgBBPozcavQ", "IEwUKKbSVr": "FQXy8Lecf9SGLypSkICLUpIpB3WFDfO8HQ1n3MS8kbV2zoQAMD", "R9qRZpqyTs": "xJpr6zGagLWjHgLsMkRWgkBPY6bJmawz1jwqVz0AUsAjK3e1Ei", "6stOOpD4kt": "XQ97JBhzmQrazqtzD8SVkPFB7hfaDhXOZf0takQiimOcKlD1KZ", "Ne1xibK5hK": "lkbSsJgfV7TnWhhBBazZh9kzImHJfBQrkNhh6Tv4DlcvTQ2me3", "73cR8C7Dfu": "fTbnz69ab2ubPEC7kuixliKB5Eu6z7SDLFQLujBD5rKZrZZgZu", "W6X1yhXlyY": "ONeRnkjJ5sUfG41D9SSbr58owWK7v1rOyufiCtpUsqBxavnLez", "7zDOuRDbNl": "LUxW8o236F9S3RpuyjOyzLxPSTuCO2KDPJZKwg8u7ENG8LHnKM", "M6Iu6yvMS3": "XkjlFq0BMHIsVaZwVJ7K2F8F161IGiLBuPg3BpWZbwpaDPAJUi", "s2EvNz8vcL": "tm0k1Ls2XbMEInQs9fzpHxffpjDPVytJ1ZYvn5JVK4fkBlAj9p", "MSv7uYYn28": "hbRy3PH1bakyr5gxU7pLYT0FOB0DfK820YSid8TjrtKXky8CNh", "w9JlSralSu": "9V1aIXC1DdShhyKbwTbgpAuLPrHHVRQUmOkiWiYHqQVOrWHNuR", "CU7SOPVPv7": "svtcHNfAQyEaovbQviijBNE7ILD2Ig16Ns9m5MlaWpaGIE8F01", "xsImbEPBw9": "EUxzOU8XH6YBcc0bEuU8rwr6xrCjcANtNLzktot31enw8i4vRR", "fbRN63H18h": "6tFP0gNdusCzgF3CU9JzzHXHUzPaKrdW7dA5IiO910LZdzogE0", "LDuz0NeIeP": "LcgKZDL8Xqms3MyJI4Luyccke2qr2wx4hsFcpDEeQJ6dJm8HXQ", "18vQ08Dyao": "sgXubvd5n9TL5R8yDalQFJjP9iplGUTZhP2tc26sIHtOfWWPwj", "FgPaWt36IH": "DIGT26YORPSkBwZZFCi7qPOfBHoEQhwTevz6sbvsgv9BuT8xaM", "Tpd8yavBaR": "Qj1FLpQuqTCaOKSytlSceRO9G1JZseSTbzfAZc641kCBLrL2cc", "HEKsOQGGhA": "FOaJyqdWYG87Os93uiVMVNRHAej9NESyQZxGNnvwsUvjozJpVx", "GWsF1Ein4N": "07ohK6w82Wre6tSKdcPw7zbEB8W4KrZMh31OOG9JammhzF8R8I", "44zU1FkBkF": "5jukhYs2qFZe72LlM5f86pYj8xHmmGWCMZFK1zSnFLCml2eZWN", "wqF3ezxwyC": "hBcYZJf9fm7aLL5OPjyUmRwJsRJ5sNZbdgEHLQA6nh3NJVtSDX", "iLBYZUm5fY": "5NBfdWn22uLYzKJkxoSvWHgay6hHaU36z9DrBMYsG8z7RnSQCc", "xAufy8hCv3": "EvCq1rsZkTYSkLjliCKiJoKLWm7hGYI0iaBcwAZ5EqxSq7advz", "PKGYrmKiiz": "5YQYbnFgnUbYF382l57fVUGRlu47iMtcH35ERjmpCevNvSK2iX", "NZLkaVz14k": "jUZvbzGZZKBMssO2DH6TZ3mVJtYbFO7NZ8slfhbLCZW6L54lln", "p2CwoeWvX5": "6FiFUaNe5PSphsFyTutzlAeczHaMK9yEMU4NteuCDh5Ri2apPU", "GZScYm4GOh": "P8Xg25B278oLfkq52Vw3JYEKaawXYoj1BWaUo26UsXiCu8viNP", "fwVQTwTfZD": "hdtLmUAyS4fnFSMWO1MUnSKDy0l65Fo2omdVbK6tajo1Fx4mkx", "SNzuXS5FDO": "7RcvrJmcQxLQxG48F6M2HqJfCrsTpNaHcdADYSY9z9trCgOYpK", "CVC0rRYAuz": "FUoF9PFjn9BVgSB5xgZdII33s039yJVIqKXaeC3R4DJd3Z5LTy", "dryEXAG1DX": "Ow7x3WbEJRrCLLzIpkdf5c7tK0CdILjgNFsnxUEiRgqLglCWN7", "nCqWeyo1S1": "9HMGvisWCJZgKNBXoUa43qy46zSJ5Ms6lzH81kc1QBQ3mkJH61", "DOqMcE77sM": "xO8OEK2NvmA9tapjrPBjFMVqsxydnjxekrRDTInMnXI1QLMErw", "rpS8Evobly": "TjPQFkIUaCT0TCSjSg9VMKdUapRma9MX3U7F97nAzWDLkrGcvx", "q5wiJACGc4": "i5of3ESapZV4AJc4rf8ygh46TtWFTjMlR9J1RHaOi9ZyZvK4VS", "E8dN1wNKVT": "5UfTIYp906ykaCVWgNJGmw97e7fiVaSYHCJWFEYa9dKkUp4rPv", "givaCDYxha": "cDF90M3JQYOloy8SojpJTG5ZpxUxAqjv2wWusmVwXBVwYkePJl", "7Ko5gSMyCE": "oKQdaVf9zIN7GLdyseh7LkLA0oQFuaNTBc0qEhElJjbmfFkU7q", "LNpW42PUXz": "nZdb4vXbHOYjGoslVCuMt5XQfxi8ZAyY8KK2vOXwcm1kOxvjwl", "oFrroZqTtq": "07KJmSWKQQpLVnWyWhhwjVxEqZzqXgz8o4e54fqCA7EP1EPzch", "9egbxRDsIV": "AJqn5iuB5kKPDMHiPVTF9kLoab6fHYJHMlg2zy4NVqPpZimLmf", "KIedkCPFCQ": "Hv23GLjZcYs1dFraukCNAeSLDr3mKxEjreLEYPBrEZAJ1v84Ds", "xIQsdqjQsa": "UAlbFglZJl7fLW0wIduBuZytgIGP3gMYyFRcUICzEMIYrRzgOT", "jrEysnqTBT": "w2Xp3zTxqpmYqqmgW7Xp2Ms2k3D3ObTxkkZ1HAABhbxTJwgRgy", "S3iUDF7Etr": "8YGXB95jdlf0V2BcqIL82omO6SrSM4Q7iycLWdtKiSlY8lM6sU", "uofOumdHwj": "nikc3yty6xZj8f65PiDXm3YLXTCjpQM26TRlT28PIno9H9FUi0", "tl68D7dGRx": "Q6OPwVxQA9K9SFA6wmD4sMwn5auvrO2jlwRZ9tF86QvhSoohYM", "glLAhPzR5R": "C8e0u9uskIIoq2bNu4v7ZaNqvINCcaGhE3rdzaNwioRAksuLt2", "Vhz06bBi6Y": "QtQ3HSBk9h7Nf3HzOIJ2yVfHgjjAOzBqftgEhXJCw5rOwEb55q", "JdshLjGlHb": "g9fkG0YLwclCISjZRd5fa3KMBmwTM2tf56ldvzlRGqfjwRtXOp", "aNDdoTNovs": "TR5NaOtbyJOhm4AcDJGdC385flXW52OBdCVha3tb9fCgcv7Sl6", "uoY89ZXRPm": "dm1wksAnPeCpBQXIgWlEDsr3c2bQR26xuNt62avwo1PsDTakAN", "UumQYaGO7c": "oTXXZYNsO9emgBt1RJW6bmtfbBLomzD11YTJGZGsmtnNZlMDw8", "C39hd3QmTq": "bayOPAZEBRLHei1NBYCW4mdkSdNlWLAt59tKeYDUpRAOVIFkiA", "YRlCiAWD2T": "ZGHmYsZvKzCMbjdVme5jJ8W8LYRXwhiNAqt4x2AIOBNoV0J6LX", "Ygxo5TtHPI": "GLDGJRasNF7h2Fg0sPe3mC4bkjMwoXhmAi9yh6FzUdaanEPTvh", "kzeRpWLp9d": "h9hbUF9m7NRsg3ZT2KHtPa5PSlhdgGdBjlycXrO7vuZ2VAZLs4", "xtv2XxPC09": "PcD3zK5e8dqGAoEogsQ2uPKNt26hsWKbzaNNoLx9aweKImTp0E", "u48N8QizAu": "lbuHepnCLTSfy7qxNdbtPKjagPqMcZSg6Av0JEwVDapZbiwYM6", "SjCkHCdS9p": "ErdmDGOduomFs3QH3DhBTu1xopxFiukMZlf4YgvgeFukzKh8CE", "RlyvTVrP8Y": "pO45KJP0xRUk5fn7y5uT3xNcMK3U6935GZyQErB6rGImMzxL0Q", "8cU5uDbMh2": "l9DUnQCALxsfNmnv5o06EXIWs6pm2pcCpogie1yri3G6nAoX8U", "0pkctyX8EB": "jqlJw1MUrBHZG7XIRUAbIjSKGh0ZWIX6MTrysrLhSrqRlPpPT0", "sresPUCn5E": "MH2CWYFwBgeugz3khiQBbLn5YCLWSBvQu7Ny0eBIh8phmnEeRM", "YN4gKtrFWO": "4sMKljOHnRECLz23VqvXIYltuWpI5Kxiyl1MyOmUgSHEftcmJa", "ElUXZRPQ9g": "aiI8HSpF1laF0R2MaxTZQRcrT2anKnsKEncFaOQSTe9I25gdO1", "mWnQzbAPoX": "zIXATMvxgRvUSp1U9tn1nBbC4lSV0395iwK0s4mDRGn8aL3zqT", "NEkUFZYJ0c": "ZDBNNNgLMdTXaBaQ7yu2d8mSxYxVDJKq8nLeiA0nLZvl5W3KlY", "sm3MvQzF9B": "k98MwscSxGZmPODlpk3GpbhZs1IghqHetPUPsD48I5MUI9Lygx", "eIpKH5MqIG": "YOY3T85JatBkUwPtLq8Yf3RXZ5cG8BPh2pFBUCgvS4qnB2uVx3", "apdvBlat7C": "chvYYAGrV9pFIJxuo0PNMMUzL1y6SSbBRmDW23ANEfP7dfKImZ", "pHt81wDs1p": "gTkl7ZLTCOIvBKEVw6AzQT5c9pUDN0VTjKPPudh88BMk2vYhlT", "sqRhZ6ZJWH": "5wAA267MHjQ81HFTv7xJ96tHGJ3ZOvUthivimWZPi9Rb7dyzcb", "T7y1gtrkyK": "kI54EWwl5ATnsQrdGoWXC20ww2bNNkn9qCZDhvoq90ynI5wIvF", "00WNpaVHpl": "7XyVCp9C45qpuy2feBpGEEdhckAJXyAa8BEkrgiJBIcyCubDCJ", "ydluGyhjRK": "0Ah98RqGGS6lLxph8TRpbup5mMmOeGe83VheUVB8HQ7i2reFsh", "ulZaRbRG4X": "nR5xW0zwj0mKmGkDZjYaFcLaWHwlfv77qxkVBpCr2YpbyagMvS", "Y1Y6Cp7HHf": "zpybvXe5LhyUxouIrw0i6ojSCQZTcioIXWKEvonTkuM0gOFzRi", "G803qErBWU": "x7esoygCTVR5VxTBcT3FV7PNEPwyhB936O5NDrpmy1AinCuQlI", "mHP5LkcKkV": "hVqXY0j3CCIjksGjjffAv6hhN1mZxVbFZ66nDdxTpQ9T62pASc", "wVg7uKuA5N": "EdnZ4kg5MJQT9RKmez9wtN5bORTtHWAJaCGUnekjafHyX3i9zN", "qrz2yuB3wO": "AVbUtRo4sTBKS83EPHod1gxbnPl4jeZxKJbkVIwu8FfK18VI2h", "8WcEMu8mb5": "PfCLPlZ4z6E9hoQnLGZjLoJ7idrMF8fT7H3jSI90IkZ2O0gae0", "jiET03XgFz": "qKD565TlknwPWqSgMLzThfiEed1Sk3nrBK70yhHvafY1cbtzEA", "p7qIf9WZlL": "SGDqIGaFfYNZEJMKpxiUlIzU5a3pQbpr8kKWFEijYJzmJFYNj5", "AYNLqiyOz6": "5h9y7CE2NU6l4jD9tBj4BQsHMf5hGfIE1wDS5OCcd9iwCzQUST", "8pciHfG6wW": "UYp4rzGqZKuZSl4rJ6zAZQpLFY2X5y7xYBXU8gMjSgHYA3sJ2y", "ohgfDzYYYQ": "JKV7PPzqt6Czpzpp6tbUPavBSAiXiyEmOAPNyRkfotYnIodjCl", "Pf5A1739ZG": "Ra8d1bsBka2ead6jXZu5hX1urW9ZzmLVQ7ALixIQ1GlzgkdDfj", "MJvuc9lsHS": "J10CDpNijNEneIP5ApLFBnvhulyZkubvzH2VPmkk5oS99tCWwj", "uxbBfDMx0s": "qiJ0RzxdDtrFb5lZkc6GG4xtxRw80D5pkDd5YRhMo82iiOJ3Cj", "4DgvFw9VGr": "CeexEZus09Bq2u4DCvINin69LZuHYVnbiWpEPdM7awrkOZ4Lw0", "hkW7c0kUhX": "NOFKoqBF8KOMv5pcrKq7S4CK5860IJeH3GPhbKu6MOyDnLWI9I", "nckzyx8UQS": "PN6cXDd6I020icdTHxwsrJ25yhTWmprEVXCFzvMDPb6c4I9qTh", "lIaAfvBzAP": "oUqHnuxrdIYnuqBrPmrcajDRASwk95qDwviEOZKMYRzKIsMx1d", "UXFcDrthv5": "1bkbs0sC5Lh5aJtLoc12Hes3b6vqI9mwuArQHA0wMYlIK94QM9", "uhsLbTKcd8": "guesuKNF3ZcYpmeZXwzFiW7STVhRoAFHyZO5nucBN62ids31pg", "LztJ4bh2hW": "3xrp53VOFQSrKocq35acDq4QwwfMJM0RPv3pWEWT3osiPHxxvI", "WZ1YWCJg8R": "dI4ZY9tc8tH6AuPvK3Q3g9IMJBMk0CM3gOM7cnDYI6h6XeTcDD", "8fDgVXRJ5x": "9w3thsvMmcJlKKdiayhV4XDjSmZ9ysDWKumzwxnDu1RxvhMyZj", "etRJncef6s": "02ggo1DVXldNElAUqYX0XDhoL3OGkyXe8h1UCOXYRoOYXctvqy", "bjfWOx0vxA": "LR58KmFfcLUGYTqmtwVRpBEzFqUOrwN7nWngKilv5XiBAZyHLC", "KT7O4dqUhK": "Jph3WpEDj8nJxVqCqjtYeB4JPqzs9JI0pKQTv8SvfSMru1Nt41", "klSDMg7gLc": "HGdAu85DKAguhORw03bYTC631M2SV2Gs0ZXjlBjJ3RlIzwKbaL", "ZpQDOvvU3O": "9QtkNpd6vtNWpQpsRcH3PdhGmscodkqgGnpCZzBn6b5npm6IJy", "3qgaOknkoR": "wT5VcKBPmZ54xqV5L91BU0oSNDenhFR9wGCdA9myFKzZCHBlBs", "VAIKRDuw5o": "ITbPt0jVpFBzfSgnsAsXBc6XNRFTb9DJtuJ5A656PAZFQX9mI2", "9b5rIu4Pua": "a0M48BTRFle51qFtPVVvcGIRhriPJa0tkatdeONuFcMN7tfUkJ", "OkLu71Awht": "qYLEBWlva5NKlk3gqbSCbCCK5bVas8KvWQRy0fHHpxKblBTJ6G", "xChfD07fBf": "6YryHwSFECALs2EFGG9bsPrDXDeb5XnesqdkV0acUlBgVDXrjy", "ZyzQVQgsqN": "NmBte0FIQwSRmAk78XqrpjpUyjYau1YEf6NAYPDrNPEX4wrg05", "CUkl5SZtgR": "dW7eVpf9sXje7xOzrv6EWTJmgTsODwvG7fzIwGLrTvzXOOksDn", "LgAMcIjV2F": "0yYaiKNmT3i6wd3PnEVnozP6E396IjqTSWsoKOwTT3WkjFds1p", "L975G32v2b": "Dg8XBc5GdKyIthO8W5PiQsP2GXlI8DAWoDRN1P0guiQGZvXNgR", "v1VqCwSHJA": "WQYKBlt7tNgjifTyr4L1TazdjlFiZpxETP2GVIfd4TTxhaoZ87", "NQqFLNjHji": "uPftxhn0xl30LbiYPA395FY2lg1lD7DdhtEdXzJppqsCbDHb0P", "QDipv7HHe2": "kXvzSIc9OrJ8m89v1pTCV30xm3aq5sX8AdYw83zLEI6a5RXugM", "zWyAajHd3x": "0QjYk6KOqI07TGkxDknQ5gl3bnka0Ab4LPHm9r8lUbxRo43Q35", "05H7HxT5I1": "FGrNuKgdFxfpCoMkwjAC92hWIzyXzPb0Jbol9AsjxTZQrDMQlc", "CJibstraTI": "VozkfUNvprqX1nMr7Nclc8XO0Sth2eu10eeeEwWnzNibAHXOBR", "bkHXawse6L": "LR14tRUlWtA7PCQgPufY0yMhKgjcrZWKvxYRtnikgNEaWhPBP9", "xIKubomws0": "VVJEtcxetlQGupIfezkaZT2leJLcsLF947Q1dmzBpJGyJZ0bka", "vNJ7RcVlxx": "1338P1OgEm0hmRrEQARdmBpcyk5xAODUQDC5or1misHareU2cM", "hdtIIYBTZ6": "fnt6NTDeaqO24xX58uQUHC91oVMhvl9YnmKNUmHQsTFsZCZMsH", "Jr0u3v5hbo": "Qg2JlwntBhFNplmbJW3PawrbTA2wLMRDzUNSDPH4Ql6DkEjisD", "CdU7slKDdl": "8dxFo56aLaOmWBG7kQP8YVI0mo5ukb7GMLiMYoN3r1u0DDpkqG", "HeMZFnFAlH": "Viu1kZ4XDXASXysbHmNUcliohoCY5zWfoKH9PWISimdbD8ODGd", "6cePHIiqkO": "22jyg7V3hQrc11EiBAA1oBoEtBN5nyr7RAQIc2vHgwmwYksYnd", "dOx2AC63Yz": "P0cReAY2CYC8ymGcawMdrlAmFMfN1787dB17UXlGWWwszEFJu6", "VhoNwEazx0": "vEAJv3ydoLOiKpVa2hwWF772iTdHjC1mmPrAXvymeVNicEn4M3", "qB1hAKG0Rw": "YKGBN7pe9HvPIIiT47xHRPkUWnMzy8pwFJrYicthKqH2zzapzb", "DeA1fkAl1V": "aVI1EknlSVYka4XKCvLbZOVBXczkOmoZREIsdwuZkWbum4sFI1", "lRrDNlA6Wf": "4OeCZxcFszIciMzFO86dPh9bRjW1jkzrPyxeOuc42da4spdVsx", "ZHXTowqxnf": "PKKsB9hadrXfdT0b6fqEswtUUn8M6l1nOhlvlFmNEZCOzJtfUf", "qNa1DK0YuL": "aDtg2b8XfyRYdQZPI64EPs9xVEfA8NH72UMZPLvzHTs7H2xaQn", "0fkF3C5KbT": "tFuQhqxnqsIDlKojIu6dtl7vgecoLrE5B5De4CUnPAg4gRwZbZ", "mto1I2rK9V": "pfrvNtIvQPCAwLfEUNjKzGLTS2gk2CvbCpd2lBH1GA55cPxVXH", "2d93Unexqc": "jWFNnzdHNXlog586xZwmmi7HiayRbUwm1FJ4Ufn31xCRHSSTUA", "3xmM0YEDgO": "O26InHC5gcd10R8BAF1htOVQqhLXfBuMtTeJ6dWj5zVbrt13im", "zRNcSytpDo": "FZQaND6O5pFwNkzWqTAagFitpiyzAY5HjTXEcWzC7kscjBE8X5", "GlHSCXmEgm": "QsIP3yR8IbbZO64U6no3pnT1I3s3TjnnSZtcKwDYKaUKtFtCmk", "lJrv4EQYuO": "LQlWcXICZQVCGrlibAT4wF0tzPTDXlzlKQgkHMMzZvjvfvMqqc", "KyS7Kou4gj": "6siJVPStARPlgyyCmabfl9ejlt3pJexFXaC3uRvJiJH3hqwYER", "v8R5YFgUAh": "yGAEGtMijvYew2opx0VF7ZnLgavp50qHZvWzFQjP84LI9G4bld", "2nMA5STe2R": "G1bzRXL2dKQ7q3obrLHTBqmAy2AXDFXWq8WmCyQ7ccpDdngnP7", "EaMV0ucNHJ": "Wi2aPG2jf8a0qeMel3jB9ZXZTRphVRo9fPruKi1Qoep7YKQQ90", "wqi6br1NDL": "vo1XuTuB7khvOL3iA7NogWFOqmS03lOlR4JXgp98uPd1REkIxH", "sAoDaSLl9E": "G5akAP6drW39L0hDbvSSwTGyWfmMILd8RumavzLA7FsXeIzjpw", "63XfKifREE": "an0Y5iDGUU8fpjCO7WmKCgHwLc4CS7vdpAEp7s9erID8SLiha5", "M6RFdZFniT": "AsBtKL1XPy06DcnychGQxbcELX82gI2mqhoIByf9W66aFM27OS", "nyzNLiNqNZ": "y8Fg6Yey58FaUVCIRcPLLKOM3y93CJdjUo1QoketTWNYmrUtB5", "fuSXgTw3sn": "lzLIuG10N5ffRD9RVudXWdmzHu1AVUo2ILpEGRmteuZM2AiOuv", "Ib7v0WyKX2": "JXUyFrRngM6jVVT4nv0uCwhmU8Fn6YQygapIwsAgo7DA3PeLtV", "2FpL2wAOMc": "dok8c5flvMyptv0pImRqp5ZsDHXNfWiKZyj6ENbnhR8HU2Ihnz", "MOoP4gGNIg": "KjYQcw4AU2sF8ez6tpltl83gv4uS5KS1ZpcYnpYtZCTbVS6WYo", "DZOVvlvGbI": "VLiwXOSoS8EatqquorVccCU7Bnd33mIws3W7YdCTLtSOHxOqBf", "znHpx0aSwn": "qnapkgoaq2T8Ze3trdEesRgKfqcN0kplSHJZqgbH7WDeQMXRNV", "Xp444K1bk2": "fw5H59Uq2ZtpkZUbr9n7tcCty3czXeZPbUlKCCI0JkghEx9fk5", "eUxWdzFBkk": "BYCBpzpSVIhio88vZdneG5hXySLX7Ey8hTOZ6cWSSOj6clBAOH", "tZqukSiNIb": "QYPUt5xxtM54T0QuIiIQMAlDduFgPvQNge5K0UkeowIXekgaFu", "xiNVAjV3eR": "yk1L1HAY9EhoaRUR085p0eZD5r8Jy6OV5RUUfeQ67Xw1yBxkyT", "yngbJwbvns": "0ME08ztjPXri39VBTLAMDhpdrW42QcR8wTuTGepywvBoL9R1Or", "LWImWU6r2p": "q71LcQYq6LzeeQtq5sehOXJoXCpCsAqU8Xh2RdX7PrYZvt93kE", "FlGrP2cxor": "Fu5d2QLg1r7RqCAzBMh4htp66K68iAP2HmE52giYCHXZ5UHzJm", "wLQeb413Y0": "dQhcW5X4Y51sWqqdAtRvqQ3auuABedGQMoKoK5Sj0dxI8xDU0W", "aqlM74aakg": "i1zfVjds1PAbs2c4ivK0gtPivKo3T2ZXqfEoUo9x8gW2tR7W2i", "wdGJQr5Wxp": "dAKFlsdbMBkvxG5MvH57lytiJG7sSGtQqReBA8hL696tjN7VQO", "vmAyFeS6nu": "qluWFjrBsM0mLRNo9kqdmNUyevcprsxyI8hZ0LacHYitgvwrXs", "cHCMk7Ltlz": "ZS4KUQ2AfcWfN5k8FTffA5rkzGueAGMy4BRZ7uSSiXESv1Q1rr", "BR2s6R28lH": "U5J02dlJ1Jp1Col5zF8pxW31CTPO9Jl81OJO8Z5s8YL6Z6VaI5", "IL9ag5mUUJ": "SeOjv1nT6qaOyRBTXg3qxNSDGDdpOGICbqgeEJp5vXJHQz3oPA", "4FdaENh3wP": "vYRULWgPt0FFgKfOdMNtk2seQMAJxBPbKimbF4At7RZtX9h0e1", "8RlCJJgjGu": "0JIDx9tXJqQbfapuIv2SgT3aCtgLdPW1uYGP6L89Ijb2CBe0er", "RNRRMYzmlL": "rTUYAlgC9SsX63TDSwQEniCvv0kHSO4qSEAO1W3ByETGSaWtEM", "wT0U14JHBa": "as0QL6kbTyZ1dUdsGnELfieD0dZpmKacL0g1cDpOoJpQAte21y", "fBYDFD3Bo2": "7HawNzUBgcetZ8UhxvTvPEfFjndC1gMBDnokTGqRTZTBiwQJRo", "DCYNxGwZrV": "8xDf1HOTxZeSL7x0lnfVN96g2vvA20yZmEfoDOHS2WNGoPnddp", "Q0TNatr6bU": "KcZoyty74Sd5d4iE5gkl0ssYELoWEgiiY5KqCT9qBThqOZ7C8b", "2BD3zVfk7r": "kCmAZY2jWGthtF1tN9KbOWVbrcnIaD8c0tLZ4OiDPpEWpsMQd2", "06yudlKWGp": "ZvePpKf0XWB2qHs05XmxmeH5DgZclOsi4NpLj2TN67sCN0ygys", "cjUcZZhlCc": "ma38U0w05aTmwkruE0sARAaa1gEcVPYI09Nfz3cT5yMHj5qmZd", "C78DenlpAM": "k8IY7PG89Rva8yxO8ewRHbp830OzeHInTC6jdelynEOl4xWa5O", "1hsl6WprJA": "yOCiNZm6RyR1I81NDtX2P5RrEYEggOuiwKypovk4xohzlidnhQ", "HCqK64vfHu": "zvF9YUGTENjSR8a4reT51BPr9DkFaOyhisq6I2P1FaDIwUYtQy", "M7k2PrerYM": "9IKJkSKVrifwiKzZdLTNCLOrTXTR8GXu5DvmP6ZCiFchftbB39", "f9heKCud7b": "JIwmLASA2NO6m4rmwVVlwzS7j8HM1Tqbqvomxqm7FEsQPvRknX", "onKBg4v3yV": "cBeK1JTqMsVT2tE1tkS8mxlFBlY70qUNQ34BWiDv7adHFI7VzB", "wtOFx5aDpe": "zsr7t3GqEcSNjtDgUOnlDY86iOOFrhubcT9IFP2FTRYV7x52ko", "8ncq9SpcCL": "tznbCHbHiPXK1d9UMAFcXQxkYZxCxAm58mFTV0P398wZOOyf44", "QDFrU6AI1X": "lKmVdDuEYM0PCxJXnDEVvgn0vL0rJDHZdSXMHGsWIzUDfpttiI", "lNmvGa1Fiu": "OGMOMB9wVodnIHPDiswnaKag9e9WtrWVergLCDSY9XOSP2AI4A", "myJ0jnWpCq": "HRtTQCIbKsJ8PYLjOy72UDuCc6NcrcbPCWbbTwikBG3dkzmQp7", "U3xS3blY7i": "Oi8fEPvKaAIhucuTpO16JuU3jBrqDrwsTmnYn8DC7nAGlE42ZG", "An56d7wOft": "ADs2hZVvFwfVXuh6ZZWTsmRZSBhFH7ZzFET0OCyGAYI2Ip7saX", "Uws48qog3e": "chWPAL8yYJ2h6RST4smedaq4IggyKwwgpuDlov3iVFBZkfYTFh", "a57ZZsE1Mm": "cqbhXUlNGyh1eLthoOM5jjNGzdbcSJcASPhS6lfTIsTBmopkFA", "VATOVKq1FX": "lHVEFwmYPhKU5RBAu488DhLhg5xMZtHA82qX5d0peE6OQbA2lY", "YoVcujqHyw": "qCPbMLWADF1BpW2B9RunEU4IXXnC5KxQL5OXG65toVOVTIS0zv", "D1RAv5pNxC": "CLeNqik56OXORDnsybaUVJrkN2G5skNT8yfE1Tc75nOARwwc4g", "ZrNIAQtxXb": "vyCTK3YJRSSPycz2zFDWmRxeKuUau7DW3AsUE1c5wTT4aSUTkx", "7uwOzRZucC": "uRZ3fcyVZlaxwasZstOCDxmYxEWEvrFSiCPiYDztNaPoyTdvDe", "sk6WY0qabA": "zb7PkG9thq12XJM13pRXfJ1CJizVJekduLp5krsexPHnx86ZlK", "4alZtrsAnq": "ZAg233TsmzmmSvz8EjhfHwzwYhG2xjYYVSSkURiBXf78E5edcu", "etqGnywUJZ": "tCySyQQg8wLLIuDmvZML5wJ2dOM3gHAxrbjWX5VZmGrvrmiiYe", "aGeGU7lnM3": "HWyVs3wZwI67UY4qIaOzOl9v2FsFOPOLERB3hsqGZ5EV5N6zq8", "L32732c0cz": "9L0WBjJqj5ETutyqkrvYLeuuvmUUBufjgvUYTZ3ZqWCg9RTlx8", "RhbcxDYA4W": "GQGelkW60ZB67gtzwd0p05xxHOmNLdOWKz1wJPf9Tk7TaXw6K3", "28FTQppKkn": "Yq9t3iV4klfyQEOtIzEuu5GQmCn2i1A1ssssGWXFJMrSwTqtph", "mEIyakipL3": "0GR4AA5fiQ2MBBpHAE7l9lxLqciTUJwv9CgC8Z3J1oe4DpdZdA", "nJKVbOGigN": "9Ed3LmW6IHVDGSdgfpyaTsQ5U2n61pbgv6GI9WSYdFQrlPLw1f", "QwoW7tdEc9": "bbJvHgbBkxGO4qe5qON1OYM4RYateDW3G3xevUC7NH5KOZ6HDf", "8IPPPeCtzJ": "pkCpfjvsTAZmlF24mYysCpxQhHP2YeX9mWNQIurzxMa4mvyUjY", "FGDhqm4fND": "BzUjyHVDCYJ7NqbwZI3iA2e8JeatGPr4rJofuD7QAMtYPe4ob2", "Kv9VibHL7B": "7bFYXGpgYgCUZxcXBpif6yXOf59oLDGIRKiytFnQeE3MwwTgyF", "x1BokkbXuC": "fjIMifzenxdx3voDi3yr4dsYmkc2Ojy5wy9SZwxA7D3yzIfv3F", "cqKFclH9q3": "s1GmT8RG7vULsBDVrMOQwvWpgOKErs3W7VU9Burg5b6Vy4GHdQ", "p9XOt8xyAi": "3rRWCIdCnE11ALRDgiNu6Tijmoj3ssoiJAmWharhZS0GtCkNAp", "5nBJJICc4N": "U2I8vyzE15RZ5mQoaDYtGjiFwwE0Q6ZZuw0OsxHQ48g0zZ7jAX", "mIxYz0O3Pf": "bZJZYgB3oBgFxhFldAv4ReA9Sd4A6MKYAAeGGHzfZLryXKa1tJ", "uxbqT7UhOB": "FqaPyqf09JzUIqg6ow5KtN0OfNMkqod8Ejo8qN9ZstioBZEQKJ", "Hn6DB7Wic0": "oP7q7ceWK3H3M1EltwAqCrIMesotlVuWxHAxB3WWh4CkJdqEti", "phBhUyhILn": "mPmBW4SnmZ2pKtOesMwFmUdrkugIGrO6bNScBtrpsY4mrGE3Y5", "kXopEpNOgB": "KK6LkV3Ueax8fAMvqEEgzkNY21dXon6KEZNFvZXVMYsdB4vCsZ", "1x4K2mYNUa": "nvjxKjwI3fGTpWqRYMVwOMXXUb5yJnj4m5V3EPTICx87eYMEDh", "jlx6Kp436x": "7ob8TuPBgv0nzqSluzA42JThsREY4eZkOk8TutXWMMjy4B32gC", "oRwGFT8NF5": "q6c4q8fLMhyG4Bpi6hysHMFpWzRYcrEssDZH2R61pqUXu70jHr", "AlsxZ5BHA7": "Gd3P3INqHnzEEl3OBFshAdDhQVuqbTD6sfJdIVKWUMZYzscbBF", "b6WiS17x6A": "Fbt1UZwxje6evZTZPSNbeGXTvgklAWZlkBcXGxsAU6nyLtFd6e", "D6orzwPkAk": "caUyr2CUZf5E7UcZnvaH0VmUNyh20SVMKzWBodKCKcLpg5ycWQ", "dO7vzWYNdd": "nlAL3bk6kDPBAri6RvxydDFVTfVfz2fmDFpWvHrKV7QNE02MS8", "uMV18MX3fK": "lCKpbXiONmjMiNir58sW97H4qPAyQQukmZ65CSMRpp6VC51tjU", "cY6SlKbx5F": "xtoPWIHXSLldFeR9XUaaRZ5N8AvUhDlFjUZGvNoag9x00AUEeE", "BTKi4dtjaE": "xWg1b0CGZgB9Z7vyA4E2lmJx32f083FOyZu31a5SU87ZKGy19V", "BYsq7wSuBq": "22wBoGCPiALEuHyPYy65mBCOdIJsO2wYLUXNRFJjqTqvNl5kFJ", "asKxn0TRK3": "c9zFtIqbmxjgroHvYmwrn8rI7URYblpQegUvAxNTVAVkRmtNb2", "quMPqdCm86": "IQYNDfVLAedtNoJVkb1Cpb90WhWgn0z8TbPwVdIvW30xcfIsCe", "UqYwywG7qC": "mFpNLvkIGPSFSswHayOflzC7QZ3Xrd6RYwtpGDgUhXElStRHmW", "e09L0S9yvj": "OKInGU7aGvpGkiZzFkGeVBRVnZMgAPf6TXBDXN6khZujpqowun", "ptypuNWaBU": "dbtJFvipwwa3g6MyaGvC73QozKMyRGhTJ8RFyoiUfxPc0oMEJc", "Mi07OMse1b": "uXePx0w5nM3UBTlU1xyQfbg65cdhbEw9wyheLYWE2R8ByiGM4G", "t61cwnatAt": "AZzRQe7wXGTSwtITPdvcxNoyRq3rgMtXDoYLPjxhxO1m8Q6VQy", "MA4BS1nW77": "x0c1AH3jt05Si8vXUVYoDSKjJx01pcy5mj4bYplIIqvRyYX8Ze", "yVjTDEuDUK": "PTzajAvWUlvG17rTLNss2Ro78zadIIzpfXZxqjndSPXB1zHzUh", "jJXqGoHiAp": "6pSuJHuBBL9vmuZMr2kxF2Wo3M6hyTH30Kiw6L2whFQ2ikc8Pg", "mqLuES7bMb": "3zJmFL4t7GH3TgquoZcLB0bc9GQnQ4q0ixytbBviGkXVMCSXJS", "dVsPNKP0Ao": "7dpejCiyEhAr5oT3HEHHiA936BLTokVWz10SdOayrCGlYCv9kj", "TT4OLryP0J": "5vte5qMwCUsTk2uaMqf8nfSVQfoWEmUD4BWWChEUQQQLU1zVlP", "e0fu95M8sn": "tTTkItCaWqZRy3QZjTzQ5zCQHBR1BuYFPu3jFsLcsP88Se7XqW", "LRkjFSvb5p": "Z9zHk2bAvHjisMmejkvqkpTtyDIf7tfJI5KKTJ2tfY2LibLlc2", "S7sncY6tOo": "CBJDQVuw8j6vddql92OWZJ58VxHGmL5V9jvTjRi6M6PqoDpAm3", "rbo7iNfB7t": "cG4RKrX3012uqgrRFCSQXukZ1G181uesk8VacIDwtKmZrKMZoc", "6yqMy87AeM": "3Usyeke2FvfWplO6OgG3fOBy4XWaHDLoJGpDZ8ILTjRkgWh0ff", "V1TdgDbLIW": "4jekBLeQWd4p6zY6EnQgRMPdcwhZf5ctt1L7sO3RXvhGjEWFrc", "g7ZCLX8VfR": "8dIvHiW0SdmBg4slwYMVM1yEMB5O5RqnbOZ0m95uCXpTi632do", "GDSNqWdunK": "ANSaYAQdU6upgXji2LXsF2FwMxaqIvWezsYbPvzTwWRtSeyNxU", "QWQ1xGiGzQ": "6a2WJRGFzgOca4trcnfuER9zR7kpBPo4aQVLPCjdNLGUIjMwh5", "4uih4jtIge": "7uVDCKsi2ELfvPgbAZSdRzGOomoUMzTwNkuzAUEVkbyww6drmZ", "Y6Th0oRrh1": "fjLUfMrFQ04yN3XjrNFzWVxNBMX2dc63mKhE7kEAg4Gy0XZaEb", "7mbWOnaKoM": "STL32yFmreUqE7gV5WJEFbJLTX50bshTz2r16b7fKRMsgeTogf", "KRaLmhkBhj": "01wDb4DlAUxnoIQuYtHuXv1WHTWW6TTk5VsMQ4ib0yhdMgcFPg", "kEOQnUSUwP": "AyikRWiFzckpUNn7qHN3RKJNXvXcMDS9zOlKmuqc4yxniajTLw", "gajxgQn6MJ": "HY7n58wSmDDmb0aZL1JYwzr3hMpGGJFjamCWjXGtzvQ6QVjIp9", "c4aUh9GQ5N": "idxOxayDCuqOVDChk2293wycosd3gkI24WUj4ctiao13Yyavdv", "R2OVbNq75c": "qKsHdyj772nFfSNYGCvDlib5rJ43pIoe6jOztQzqw1LLxJyH2V", "LsMUL3KxXF": "90llX2dwNUwDiIjvWkTTnrnua1YUgb3r4DPhstSQ3j7H70Npjf", "5Q9yyaIyt5": "qbjuJvcqZcXg6FpXpbR2GtodQGALNc1moyE4e1fllDUyTfsOZh", "BzKzoImbYZ": "PzyTSp7Oy5DdvnV3hrZwxhox26PPW6nmpCiVIHKGLSz6uIjDOw", "uHmJBj2Iyr": "D1AQk0MpHsGxx3HTsO7MDGSEB5OY7KyhY5sffwcnRCmJT2qOtU", "XVPj3Mignp": "K3TsBC8n1NsPud4dwbXFd0EKpI3YemW9WqHqX4XDFojVQdilyn", "h9a5DrD1c6": "E8RIp3RoEdjA6V47iy3hC7D3MNsZa9GIzrL21B9gbIjN5Zq6QK", "a2nG9yH8Pe": "2ErukGvjMxWODPZVAxvmz472r5GeCLyyCczNrQP7wXuSU6dMfr", "rHW7AcyJ8p": "q47isnuOA7JghnTtCbKFKacYDNqyGoPSWmzFptk8td5R6QlXJy", "WKyTZFn0fB": "fzeZaG3keV6aN1KuVnHpMy92l9NE8whhArgJJhJv7u0biCjdgK", "MPLyjWncjW": "AnODK7YizGIndHDMcfxUPh9j3SNChkFtaA8IyRE4RIW7gyH9ae", "uTbFujQEtL": "bPYIVcYLEziR6UD6FgGbK31UgYPM629yK0VeNyA8PdNjsyp4KE", "yC7o6CPrXc": "HJcNXYyL14ASfs8HgSJKrf7jQkgoWjlYMPKGRHArBQPZu3MR0Q", "wQHzwGQ4D7": "68GHLdnYWkzPgqusBHZTeaIL3EFSas7Y3shkfABtlhjnwARSTF", "Oj1E099oCA": "hYj4sIYjyusjobBb9flkxQSM26YVWaUBAWkHrgeenEew6TJHkx", "lfHTX9XUnf": "W66TUfKIPdLydtI0oz7fmgi9DGaeLDtFdOAW3lvc5nQgUw8gKP", "zvRpljglp5": "ITTW3iFVkSgnECboHwGFQdXT3c9iZdv1P7JHJ30mXXSxFiTgLr", "C68NP4WFkW": "ZsdrZxA2TsNJuPke5lX4dxk2RXkIILrozRuO142uV0nCy6GDhe", "CcypJZ40oT": "5em79SF1O29SFhvLAxADEmds4oNY6HQtvemAjeZQc8XapSrwmF", "ZoWXl4Aua8": "YHz5pOn2BgeUfB61u7EHIiHGUp9l4JxewgNBncuVVsNMAVyXOD", "ebYWDwn1wb": "X9Y0YJqQ8uyg7xs0aoTrixFX6wkuzDIabFquLm9jHyqm8iYclZ", "vidw1JzAzV": "bIlNYbyY7B2KULkTjwST59t7purBThamwP1fXHOFOs96hng5R7", "u8RuYyXsQY": "IDbif33AFLVMTt5fk8sO3xYzJ66EFO3RbPZGkW8xpcbbLTUTAj", "n2APq0qSGv": "l0I6BCFf78eEJhBbiVvzFCaR3QdBfuz3mvDAXkYHS2VqdK3Zq3", "kLur210aVq": "Ai2uXxtnRiue3UPdeVHA3XglXk5wMnChZin6umOfTYsAdd4ySq", "uROrM0D4vM": "LHjjZ8e4luh1a5eJh182GB875Z7AEpCQKVss6v1OFosvZPYIJF", "WjyMctIxYw": "58udXEeGSAcMkJhYE3VVOUdV67zldSZvDxvVRssSN6HcLv1uUc", "4AkZT95EMR": "rJF81Xx0MkdMzmS75aH3O70lpSh6kDihfz2bCOackOdNNmVZDo", "YPcwIssthV": "LlyR1gPu7oSsFvHQIcnw98qm3v3frL6NbxbMgNu5X82dt9cp0t", "FgpM0kF11p": "j54WHzsQAP39Kuoq5PjM5L3jDU7ClD8ByLtIrC1KgFzcFgvbx0", "nQOeujGOkr": "hkShVJaz3jXj9eDehiDbn4ou4QEaxxQq1vYuoFNukM8vA3sY60", "rFafzQBajl": "y0kZuNm9x4yxl8IyC38YYWuTa157LabTd8L5K3fZ2ifBo3KsFv", "VrFitrAK4r": "DjK0q6PLKQSGFFoK1BWuZwyk4ktMez8vJZpaOuJObiQC5KHNNy", "PrC5ukD9PZ": "g86ysG7wlgVHByGERFQDrxyayDpkDH2BoRdkD26JURHUHOFYlg", "d3IAqHofxn": "oKuy3I50QQASp30uzXw1YuaYzPGRCTIot62xywN40ZDA2UY8Bm", "OypuFzMtL1": "XZvcz9xTU8d0qmoCiRyobapa0lpJW0FSx8Uq9hktWI78ZD9yJd", "5PXrAYmo9Z": "lzItXyTbBbcNCwuALA1AsxsR6DEPdcTkOxqh682OE9dXQ5GErf", "G3t1ZOREQO": "wugrdHqaFf8rBtoRMZCygpxUJQRx9llkIDlZVEwDNqC8mKZadd", "mT88HNAKBf": "dGVbdLepF72W18CywceHCTR6GWpbUux8sPdXLJftMQjbysa73Y", "gj73zKJ5rX": "CdsW9BfZTbO2JVDzdZPVVO8FjJGbdDHotZuyAFoTVWKINoeQrb", "WS346SrSQA": "EwI9VIGulNQZ7DQOZtSwS8mBmYThxtqyihPPXsNLfdNY2eOcMI", "SHO4cq3bzN": "5HgUkQk4G8CAZQqaLnFsRWUV9JJuzCpP5S8fHi7mYF0KhGyfa4", "V7YXAAnXnl": "pznaoU9F8uroNfoRfC6PxOQ2uaq8AVRPQZk41SuhbCIzLiM1ks", "5C6O8fHpuv": "7Ts8xSvXg2ZaZ7IqHvmfYWqv5pJXpB2imGPnnOcFF5T3pPAZOH", "HJUzm6OoLT": "LnVq1RJKbWFDLQDA7JhXNbhLash7vAuh347GmrwBkqAEUfiuZv", "DjwqKysZEr": "z1BIt5mSIfyuOb8s1Mz8kWO2lnHPaIi1IT70kJF9vlD3Yvav87", "ZIoQtimbui": "fkVcmRpd6pSJ9X5g8KnpfiqYPhxb1ZXBkMQwSsj0gZKUTpZcWB", "r3NCnoJedc": "xEdUTKP6qjnJS2ZlGdDHP9dyDONyTPBFRDMpNkIU4DNDsozypt", "d1Usq5DtZj": "Dbw16ZhhqAoZN6bAtcoyhSUescJu8UE2VZTSIT2g61H3fCpHWp", "G5PhHjVG4S": "lhKN7HMApSUyAaqnlrRjxWbI8Uk29M63Flja4Xd0DUjgdQXpeP", "iKaitpxEak": "nULZZmHKwTcn5CXtZayIQMuVgIb5Pj3ZFjOy0XSRaWfwfBTkii", "FByfJruTDH": "rTWLsTZrLJoEHFRMXSwKyYewJhIJJXe8OJOzgs2TGvKvtgUWRA", "2yPv7IeX0Z": "uValtrZyZUEblIePFjPLAYUlUHbZjVufi6zGXFbKU6Tv2bYxqp", "hZAiY0qECb": "qplmsHN572cSe3Sss7Xsqq7iM9PtJaumaWRFGGXpi5VM1cvTny", "Hk2EM0i1AA": "B1IHGhLumjtSLhDVcduuyudLA17VWAMb6JD6LK3r5F6LNEhyW7", "TygF1Gwyht": "IEJaHXedGMqeaL03KwE3gFWx7a0quz8zr3aQBY6KCYtouFA3XX", "PrvHTQV8Tb": "d4tgNJipHShqfunyNSs05P8u8bKJGx3G2qp7vrxCmsEOHmwTZV", "R1gEBsw0gL": "U3E2K9K6H3CjGmwzzIIzLRLacOjrMtr6Q2ksvLwXnQT9Mi9VEp", "0oBtDYxlQP": "2SNrTQcELV0c7DAxhvxWMn7DQ3QTlGZqqhIC8eoRYz59HHjWEQ", "ig9oe6qMr4": "rCLyayGZesCCrwspfWteRALYdjo46aWfihL9Ry0t7zt2LYEYye", "hdl2DCcPOb": "qGphdqFLJwCm6MTTVQxmvcoBgxeSawJVV5j7az8U9m9Q81RHdu", "Lzyp36S3KB": "KufaIwkPqsYWlSP8aMgHbMLPdS2NJzroONOGGhzOj1Wl8zVISO", "sZoEMBy4ak": "pgjlTNdtHSUeCv15Io1PtQLa7sOu1kCsOlE4lkIQEiRWCuCKzo", "gdjapL6rzM": "xqlV4OZjFUe5he07dDBZadkxA4hKSaLPiHygk49u0OoksY1RT4", "v2IcnMu534": "Tpt8O051IUfPk4QzeQFV3DbDqRePdyfvaGKMyZAzZsMUFOQG5y", "f6n6w8YMeu": "hHBcfIfREAFQAD8cmC92vPkSM3bUcjoOtyc99KNN4H2UBSFGtz", "TAjZqHoup5": "Ho5AD0uwLi6shTdALWcm461FHRbkvbreNZkEJ6PgY5SJYfsnTv", "XRanh0D95e": "mgaYnBWarXSFXjpbOCC0B6M7yeTozePlFgJzAkYrXXPtrtK5Jl", "tTwBibrZaf": "C917W4vT57PfIr7byxEewL1cHLZaUV6YxXlv1FjTtkFiqEo6nN", "hwxmOYyBkH": "tEBZnXt3iTRNmxWDzIiiFgFI8VGqvf2iawTbkAhhRMT8wNBTHI", "m7e20T9Zsz": "PTCQfHNLe5AuXlTgnrw1wSUS30YEBLIUV2gSKZrL7BqkYpSz58", "7Ng6dbUHjR": "rFo5l9zjL9WJoxHOLEkCx1cV9c3VtqoHqMqa58e6dEtycmzOYd", "FWoNJkrKZK": "5MZN089xcPm3aisLPtV1AXcuS3xjrl8tNxJZsL6aRBgoPapmF3", "vyK0tPrRc4": "3NrKuDhCNx1mhBkGTFTfzbHq8szw0WGNMLZl1QoHGvRH40gqrP", "QeLbLstKj5": "9urRZ1eI0pI5JiIgvWKl0jHe2dxBUQ9cGTfXjSPRAKB0gWWNT4", "NAkkKtI8sT": "RU3zUMdXy94a4dCYs62W6WMQGCE6QpccUT7J5HyncVyEHLftDs", "h3pzasqmYs": "eXVS08QVF7MqVbfrMXrhYpfTNMC0iiVtMLLhsrjQHQVpVX1aJL", "ejNh2lYsWt": "YpSI96tW0n30Ogmw0Nwk94k9REdZmET9oLHUEUKVsTyyZUMK7u", "iI6qMVw7sf": "YJ2jXHxBikgPqBvhiFiNg4psJ1cUOOIV2fTZkKICQIJGJuBwMz", "D9ANR3piEM": "BzGmYBrUHAk3jtpWVJsNr7mkFuTjKnucCeuFrN1ZVsLCUovy4c", "YkJKshqM0B": "XCRxEzJ6tx1GIgBbMMpnm9qxUsesCB2K35KtSftTwfLhXDMSou", "PQRc59ETSN": "krzi6iFPvBMdTStBrHLIw3RZrcdVGaE8gfeMxQIF3ssLFCbcZX", "GtxC12rfp5": "XOuRScyZZ3MhqaVKY7E5BLLuSepkZqlDSw2GUSNbRPs96MbQ7J", "dHzIxIUzVx": "RHuJvTmGLHvJ2TvWI0pkuoriyz9Ye4wCAUGP6BwtEcNmvpdrsZ", "cFIaAanHlE": "GELADk2tFqPRuAEnnjPPXgmXQWzkPzbqmacGS4r1gWqC7xQ64Q", "tUZZobePAm": "Pjxap9NyGUvwi9ZCekr3w3cqwaZKGIxK5dnDnxO0CTPCLcb3ee", "wINHDueMr9": "ZTCvyrtG5l339jWFxCeF7UyQ1i9oTAjBeBKTEpSzkcyoWXemGJ", "NrdKOtEMhL": "6LwqY4V8dZphhwLBFSu5q1jpqHFWJzmX7rm6CiaVtIKhFLPCXB", "8AukJjStjo": "wvNrl6QJxuOb4oFDNxzbUxe7YRkXD0pR51RND3do4ucDTpSOru", "Xuirj6XHUK": "pTa9dEr32LLA6x9u0LLmGRSikbgpCe5OdNGclmoPL4FoQ5rY4H", "mLxfOcjXU5": "jMTr1Cd2iiG48fyrZa257Z0qYBzYYW8iIfIdkyfaABmpiVjuRn", "6e08gFP4Zk": "U9lHZBiMnFtlx04Yh4Nh1ZvcWopeZnShBTbWco0ZHxD0yS5BtU", "PV7KXR0r0z": "a2HlCxLndto21QiZMj5RCEwEo1GmunlDhPjbrldiAxC0HaLdXa", "yQGcjmNl3U": "2reoDfDZ65BY8RXQImT0HQVxxs2sJ3W1tkKu71VAUM82CWQZh1", "G0XCUcYo6w": "G3fD3CAdlCw4O4fdQ1Tgl8YUqSqoFQJOdiAhBNE4Y4izcuIyMM", "Jl1IyQIFp2": "KtNuvYcNRP0iQLPBZnmu044U0WI4dthqGPH2Pt4o3Cx9yz3Tw0", "XwlMQvXNAN": "jL0Vjg8YTkKWx8HVUjaSYRra5X8vY3NvBGIVdNHy05NdxhMdsF", "VD6aoFPdQz": "faXEnNTgENJa1vDosonOMVwLBFwAMqbxp8Ahq6K1nWH5bgj9PA", "NbhUBHERX8": "tQtWOfhF66DbgOK6YisdHqGdUB7vvi1eTwA6VKXnbETQ6czi62", "i6cHweBfku": "uh1XIdkeqfgKIqHRX6W7QXu4EVHFtCwa2fH3ikwhS7QQ65BEwt", "ulQMsD03GE": "bw3gokzMSS3AHpH14iXj1qfPOalraCuJEegJTtrba6X8aWTuuV", "a1cqW50Egb": "uxf50lAULly8UiXV4CKqeRU6w3763x1w4HIciVO1YVXkIlmAlv", "9Ztd8v95DV": "OOOKrVp6vpjHG1txJoy6TTW4fY3LVKYk2sLzGNtglprQ1cgNWJ", "q3BGE7tP9d": "9JQEV79XbWaWTkE7mlKFsD0gOYoHo5rUrCowGSJTtpGGHnNpsP", "0I9EGPnb6N": "uOF5WGXNkLaXqvAR5BUuT9I4gaxFezlbZ5Or4MIZF5BGPDFMUA", "6JiZ6FljA7": "TZgjXwgPpVVt0R6gaw7ELoFR6hwzXm0EtN2rJeOFrQuXr92VMp", "yYiXLalV62": "zqWmnIVIZfHhAbq336ZeWTwvFNKvOYeNKWh2LHgb7i8WPY9T6K", "U6W0rZFGTU": "ynBoFCdq2z3y6UQKh6PGHMJl878VZHU62xTxfRKcbdaoZM1TGQ", "q83wnC47vV": "0KYromN4NIbqrrnCZtKtkCdJeECLGvbliLDhTeFC29cgmhnqLN", "SIFMycxPGg": "N606WQmk0NglrEih4EjqV6HmKDVOHIiGEWVRNttHbAX3qLfH1X", "vxZ6LkkcDf": "lZo2OgGE9fgn1sa7MZh33v1AQBMBcBpRcsQ2EeXWmDz0JiwzBj", "YUdvPxZsI3": "xYGVlpYY2sSYbn4vUtCgY64SjQ5NVx0YQlmuBvMTbNGXPBUzoU", "9rS5mjp3Sh": "QR52SSSIHzyJDfNU0kjtU5hOzhGmiVVAKqmSUY1o0pO7xh3of7", "WkAFYN1c9V": "HREvkQCmoU6SQxt07aH0ziUapywKL29Ami5GnoVNbDJ3hr5Y3i", "nOxoAvVsOo": "eCEvayl8Ms52lhU8XOUvM23nvqJMuH4doipwmNzioWEkVa9DHL", "5dnVG3sR4Z": "e9Kq49KMbRiPg8EswiKmuRq10D650cqzvq0QPzrEyYxbIAyBFz", "ZIczjuRgvZ": "Rrfo45voEso26ucDbcYunpTNsPV7K0LCCwpMe42YmT07n5UIUA", "fR3SHySel8": "IfshH9fqblwkJzKCvW0Lj8Gt5UHRvxSnKOwg9K1Rz0EK2reECm", "NN2pc0EvqR": "0VJouYrIhX84YkSQPudvY9clLAa1fAEGfcAoyi9WecLaSdvZGp", "J2raUaaFwc": "VXa93SkZ3883fseejBgB49CvzqXqoyC7VfrqjkeMDctGsvjglh", "258eOhnf4h": "ZArjHtqyxvkEo2YVhutXPE3hYF60tNEIrSjMjIrv0k61KwMla5", "DBf6Tpw8us": "SSzA4H9rfsQRuDD6M2GyyE2eSHxTh28VoYUzTUayO63ryza3pM", "1URjTBUEwI": "O8MYb6kGEa5k2FpanoykQ2PYsjDokul5Kcm6l6G6HHk2aKLZ3h", "rQAld9fbq6": "Ww6FqcGBnoFCfereS4uckYypDK9Ms6XVKBKYc0Mumw4wfwGhBv", "XTPcm1R5Hq": "6F3328fU5LN7Roe1rJ2UM9ZRFlB2Fn6dYqYZ8lIotCr2vaJj7A", "ZJbUc3CiJl": "ExFQSEmRf7zRn8mopRbsisaalJqpNR31KTum7j44FHvbXhCLr9", "k2IIEkgOBr": "KzgpwDkKkLxJwdhFftrj75Ch0EnXszyUM1398r373n4bK7s1wz", "cXh6IN1TDI": "osV9nrxzgLoYiILw5Ej6Zkmgb0uCWbUXCzJwjNER2W5Tf0t8I6", "iUGq4VxvkZ": "hQ2XtW4Ju4lYcCNwaBWRJcWCmGpMIaSweFv8wIu6xRKowBomOe", "e89DjAJi4v": "eF0lTUUE6tpTiOdyUwuzw1o8QlN5YkwxYYo7yeE8z3eRedpIH4", "1z4SFiwZbo": "9EUPyXxx00Lq7dQbMCJt6lAnsGWmpqTlwvFVhgzRCL2qtGMxh2", "OKFm0ongjA": "nZbeQPImZI2MbyQNk8c5s3jbvF9vUILFrY9cp3GU5okDnvTj9h", "0PSwyTEZ9M": "IRSfO0zg7Oqdm1BEcIqXrMDQFipqE7sOSFqLUzoyVU7lMdARoo", "DOThVoNp5B": "xIoXdFNyIGddXzD4HLVc9Qb1GyM5GqAYcCAL7yTHz35mhz5Azp", "8o9aFIXfuN": "xP0IXYKqzvPa7wU3fqsZFNV44Tde25iVrCjy9ZSVBylrYyajOT", "3j2LJ7LsWr": "vFUDncfkKUnnFU8vDeOnLBROQmY9Eo6CzSnyGHttl5xB1oE7WC", "yYo387XISF": "57Y6sQfrXU18ZwhSAKK0BxHp8II4LsINkSw0zHUlUKoV3uUNw9", "G03e6t3Vwb": "OuCKeIvJpXg1sSy5r1YIRW3uASJMW5zIOMsJTbUQ8J66SKfLDl", "hIBFLZkr6k": "vckKMRErqlNMbJoZJSXtcWoYN0g2zgVeeh2NUw9miVnSfxLouW", "gHBgG7B7V4": "8mEUTEzIFQDV6lhGo3X7mrwrBY60mpA6CjzRSSW1GbAh8sIFg3", "AHeP2CS5bo": "xuLknc0rZAvxMkIkkoPRw17bFmvepqTebfBO7ATxpT6X3jqyOg", "2jxjAUkhBg": "tTIb432aOJ1FgCJT7wSBnsy5EI1Orxbr4jKhP1nHP4Twa0Ea4r", "p6OjYqDCBq": "kacfm1qEnpuohzOsGn1BEMJMVbQYfi3U04BkUCqYzIIKSm2ag6", "91q0JbPg9j": "2tdmCzFWiOcSkkSpkBZRaQauDBF7qxTG0sgfrZqdmkfV9SGBHH", "dwzr16WdHh": "MpGtBvGbhPEueteKTsAaqKPneC6m9CwbDMexRU2w8P8W5bHDsJ", "KewlEMa8qt": "JS5Hr6eDuUWGcIvP3x4kgS4X1A0q69LjU6DcspXRyvYzbTsFt2", "VYwo8wnQZe": "fIUhrYLcQFNewbfblM34XgPDt6y2BknAQot9l2RpjL7gCrKQ5z", "lq0c92Yg2W": "xtgwuCQWD2XUtzLYQAsZ0hRw50iuj3OCgcc2ZjL8r2wsoY4yvK", "afl7eRrfuw": "NxcqTp2k3OVrNrIKxJp2oJMUIP8vgXx2cL9xsuCpwddoP4S5eK", "ct2Ps4oHIp": "HDAmfcbeRuPlvVjzrHL1QRBbsS99GDdomxMJxckK47y3MYQhQ1", "ukNQUoirBq": "tuYBmbIsDI7241QQaMVRYaw5OJMzVkokpyIKL2uTTyJf0jPYAQ", "DYZSVaQ7Ua": "zexI8kw1lyMCYoo5eNHq2lRVSWINVRCFhmU8tAu1M6KAWolsXk", "KlGxBQwJ9l": "hFF9UFJrwZkMzz8ASzGUdsb3Fg2cYZQCjjBJY8ifrQRiEUyUCZ", "sZbcVkqshC": "lwePK0z3sD55ZiyKzq5rBiUGvWayEtzXYWb7XJ5QbC305EI6Ir", "vhXgXhYwBa": "EiGRRvfMLsd6tzpIr9P23nayD3dkFXDTkN83p9WhzFtcBUSski", "d1xGTI3fML": "o1qOKnT80h7NkCOpNKq23utiOCwotWRWVPOrO4q74T4luxP9vh", "xA8l2fEBL3": "VrPrpm1vVcVwTv5VoLiFxpquJxc17ybGaongc2gVAWoHToMFYf", "KcGemCcwyI": "wRMcMgbSUUNYwWVcpBFhGb1Rtm5QktLaiXhYIKiH6N3Acue2Xi", "gpZErLb2Pe": "DWu0mZGWxmNfnEjUGHs46HXls0ITIyVSPbCsOXmOZyQqbnzi70", "oJx4cuFLHY": "Smqb8BkHJnVxttlYUmJ8uvDo0BflC8I2dtlIQVRTCBnfbtSgor", "UixZOD7KpN": "EK5HEydg4cViHNFPlHzZc3XvgXVr4RfVm7Q96zKs22AIuBATUs", "VkE3R8PE4E": "cAHM07NP9z5l7uE1UOdV35mfEyvjcM0R8SngYY1DpGLJK9jOrC", "c3aqaXl2nk": "CBJAyioY4W3U4ztiHBgpWV1vqGU8yfrPuApwg2J5VB4xyl0PNv", "QZFjjPIFgx": "qyCcF8xSeUpSIL8Tz7gCi2ePwpyxBYdvY7y5LoIb0lWzfjF7Bs", "QXD4HwoLRV": "4CH8m4RxADsfzzW6GRnXpZleve8s1w2mkYQJrt6bvZQjlY5noZ", "zc7r2GrNeP": "Br4syF4W1jj6tMSmX7aUYhKE67kmRlVnQjCo10fL5HO4c4HDBl", "NR7ylzLwN4": "eX7ihjzSuLBQe2fQff8DcF7waFLPRxT43nRHWr9pnGl2Px5Ltq", "Ij3ZNjJawN": "mrBHkiSOhymZGGpiHAG6RauM0tZ7UjdAASd5xW4A1KmZlgQFSQ", "QsWyXzxQBR": "e8EUDbUiLUuyC7zCjHT4EFxD39xS3y6Rx3dFBq7P0STn6tkdJu", "mUCfm2gd5m": "VlKBpIVQmLUaF4HP5bWIaadzW5HFQNkAXHakR5VTFNfYeeewWY", "MQ6UVpUHJj": "n65wMZNDBaQwlb3BaG7XBldBf9nirPPJpqIF0ZaFglYmXlqqvw", "tq40eveAeL": "f8IbePElRFjRJVgL3uodYBphS7DQ1BOTi8tAEQytsneuxOvMtf", "qTHCKHGJ6r": "UTrzgjxxLFiWe3U61DnFVdAbdC4uxwfXiuXGSPQ4vvTPobPHqO", "2AnQbNI8zN": "E0R4ujm22x9Dm3kGjpjkxX1tGLvWH7aegSCzdqvjyMXoT9Rt2h", "ogEc1WraAt": "yr3RYCN7ThnQVZ8wmLa4GLb2uY6fYz2gdDv20qIKQpcwyI3Zsf", "x0QMjeiEEB": "q5Rr0ZpFVButSfndMHAzrS79ofDxR4NFix8EQdejXuX5czxCFX", "a9YzjVj5be": "p2ON4vkpPrK0BKkveHQjRyh2YBO8HcLz2DDCclmQ9BIAvOwlki", "x7Az8lsu6A": "NtiDGTb0S3jMuY3kMKcX5sn5xYAoOuXg9U8Z5dcQml7OCaMUsc", "wBkapftDXG": "R6CQA8xz55e9k9oMYdWKXkEeCM563iVCTZGS5Ljkd2gXnY0KLB", "p24YoRh6zA": "A02GtHLKFg84ioa4Kdl0tbitwMq0mBq2XdXFWNZJI1BN3blPTJ", "FJHNYinte4": "9PT8v58uyjuXYCMnAVwRszcfWXsb3jf34ulmb9akyqfB9U2wGA", "FBaABXq8aQ": "5Ix89DBAdc1SMtRhSs9V9UcuaIP3ibNfL9Iz0BM5znsx9jU1Ew", "9oNEjwyR7U": "BtsoU90K79Sr3fNCYsawGhleeM2Ys9NYJgDm3CDL7rZY2y4tjJ", "AxYLDQvD3V": "CMLtpFSV7lBFGKffbrcuO2lnZZbr59fqBigQOHozcuoAJ0j1mV", "Jj7TCKEFlW": "IkQqqKItxtgfx7SEGMLhq9YtsN8vZuCw6aj6phlkY3OhPw7kNm", "tsBl6wbUtr": "iKynpHc4dY5f4aEJPrZm3AHWaZSNwfo0jkteBnkH6LW9EXkGSI", "qvMhd8BFCD": "gqF6BM3FXku7ioBydIB8LdoRSIYX8ookNUGdTeUBxI4r0r05iM", "oPgj6wVkHO": "iptVylczsE54we8N6OsgbbTykMRrR43WItj872OhxsCNeRwPFt", "mNPwRMhPle": "Y5fnL7yc2ftIvD2ywxi6AjkyxrKWW4d2CSk7kJFxOsbPF6jRMp", "eOZyhB7wH8": "CMbyl2KLaX6NL0BN6lS6nW5mdyFrDDDvVoTZF9hHnzmArmpE1O", "GEwrtNrC00": "a6xIZSqGUft40qhJhZDdQf370eNBvtuQknANVpxAMVYZImFGs7", "zmU6m9Mg7m": "BZpuyVAwS0zbfJ3yHaPlYwm41d1Yid9DXtoG0LmGEo0unHEv4f", "HA9l9PqQ69": "PBmRfQgeckX5yVoIjs6guvCFOEqbnkXcEBze9dOMdyForTYv04", "GAJjp53Aie": "mn51d1OPJpq4mwdvtTdZXlEkZ4DHZjjHzGkfs55h9Bo4l1EHsv", "seTeCQpVIX": "VGvO7eBmaa48rkqTujqWJbJwfEAk50589FB0BkxSOBY3bQXriO", "12gJ5wFAdf": "OK7GIyIVJ7f0sCyf4Hbz9DMLSTt66y2t694YpTRv8hOAn8A63p", "3wnouMqVcN": "tJXXjKGLOuTxoT64r1oDJdFEAqxjzSKYEoFrMhNLVS1vSi9q8Z", "ZPO8EcTuOW": "WZ1JnfGmz8dZOU03DQ49cKdSKWxnNeWYm0Hpxlb1F5oUpB6aWc", "OiyqiwIN7M": "H9bZEUogSferN3Zr7AxZ8upmtGjUBUZgfLhDP1zTwSDsfQSjI1", "6T0B0GMUfj": "a4cSLi356PGIaPOGymAdHpLI4SsI6eL15vAwqcEspcdudD3qZ8", "lJHzdHQ8tj": "CGG2SmX5PuYmri3OWEqxWVmwbDLyrhnB0zFIcevP3lxhzB2LMh", "2hPdBISLHF": "P8v7cwr2uzPvh6PEWpY229xWzXiZBr6MSpW4TD48zWofHekIwD", "F21Nimxd4D": "8jOQ1OjKKlA8OmSJ4kkkJtKT3LWCTMME0Jm1qCkgJ54StLh6FS", "gYIfva3G18": "P5QuCTugedUdWT95x518vK6hrs7oFiLA3lyQz5Yt4aDzKQu4O8", "GuELt0HdOt": "yVKZ5SPgjYHbW9ZaJ4h6r6dz8sC8P644M139jOcMfy27ilQkdU", "G9BRygNzd9": "dxXeLnSA33XZBvMqL3RdLfBQDNs8OuR6TOD6YLicmfTtqr74xl", "9AX40UQcpp": "GOAsDOiy5ZzSANBy5mNIOxva9EAOzQoEXQ5SlQsStEKh03iCuK", "Blu9e74RiG": "1jtt1uAL8g0V54dsj2FjZQYf9srih9NQb0hKDOcEDEFnSph7aB", "YgQYKEHkV0": "RtQZErlk8SGRmA4EuiP2j7vMSB6yLEtd9ADo8EnFlCKRye3Gtm", "6ucT5mN1Pe": "jjxTtiQPEprTGAvOVJyleP5MJH5Ru5cBYwdHYKoF4dtypML56E", "FJXl0PPZO9": "i7nkld52qfnaORewZchAQvC1HVJ1T13kf9x3SMYlt0zReM3cim", "FfzY46V12x": "CAnkPRH9MI9dM41yI7dWfDtHxoKCONef0zFMUQt7nXZAYBBVhC", "vwiUBwmNL7": "2VjTFhUAJOXnWMYeeKIZG9XwPUQyiQvE5su3l5ymGmwTKz9unn", "vgIfjWnNCC": "nsjMNAJYhWguAe8E2QOFuN4uEVqCbQVDAh7mUdxZfLrSTefTak", "hmZxSSV1T6": "uG6cnuKc9fIyrwpmrR80Pukr3aOWcYmzYLry5qPajvOBzFj0x8", "RoD3oiWKGE": "Yn8eUuX9Uq6MT4N1wI5Z6WpN6IPSwoaHLQjZjjAdR2FMhLY9zk", "BYcIyUtavO": "eVuvDgLEdaZGpQ3vzDxVrBA6kONgO7sJxLzXHx0XhCkoyHDjrM", "daXp9Wxewt": "EilBAPHxLJMrN32pXiroS0nS5IbriPf0n5vnfxzgzqu5Cgdpw5", "KXJ0LHNjyk": "9jGChxO2QkBOzmBMkNwIorMbzLwUeITxfdjY5K3ItTShS4j1Pl", "nFqtgPJGSW": "65ORTcmwfIu13NCxxVVY83pfWoiHUXoBeULba6SOnVgzSW0x2B", "nlZGtdx2l3": "trhJk2OpAv3QDutQwCwqC4zFC7PYTZ7LLkTvUjbtYODQCKi3D1", "Qt6gmf9NTd": "rHap0fUPmpyhwSFaDTFIicS96aVZIRV4vE5GiVACihwalx2rQX", "VWg4eDtMWY": "qDnNcXuV4oEvGtMwjwmLsAIGlLEFwr7Ivt0SLXG2BTxGaXz1b9", "MImbPsPfuj": "tphonuSfLmYCQLy6dN936VWWhtmPDUJ8OjhzjfxpmlvXYfDjBO", "0cKRBgGBvu": "wzqZCn5VF6bwN1qhGrJMtTEIT8slfgDvnRyvKM4XoJQGEhXWPV", "XjCvTRGgxG": "5zEERrACntn5HDJfmKJGvPVvvu1uwKJFzXT5rOHdHn2ZxEKXG9", "kJToI8eI3Q": "csW4vFBTgSmGH9Q8iPEFJeY1Yc5rYL70IYKgZsR9DCllyaJvAC", "UOehRRyq1A": "IXnaCGzoJieQrfusEWk1YbUYvL8mNKiIWXSiRKAG6fPjvAhwou", "qBAhCL1JI1": "fdyacolNluGxq0caLPfEKdq5IH1tzlHoKwQLmogPmZkempKRkA", "Mt0tCIMLid": "F4m1kBlN8eOtmQ5CbJ1xJiRGR9dTI4IPeKwGlPSopb0VFpNztF", "SeWviwhBjq": "1N0aO6kDPQAcu2aBuqkiH0wvUGAlh3JeOR7BGhdv0TUIKk1DCm", "aBbAGdcqm7": "ToneQbuXhZeKb0aXpe85Ppv0JWVptuAtrr5TDTUfPTJSBkLTKv", "6EO4RxcEGp": "DRY2xw7jHxVz264XbFtxxKRiOq3ei8czneKrxCZqL5PsI849zz", "2ICRb7WW68": "z7lTJhFipcVSOq2O07XltU3J26z9Ia4tjd6uwU0ab0tFuEnPZB", "ZZJTHJoqf0": "hRntnngitk9F9H8eNEB8QPwM9GLsjDm0yDqEdkpqW4uCOewg9m", "kGH9crP1Ul": "6ncMxFkG5NMCk9B7xE0btoK28U093ZKp9oZ1lSAwQXm1eDuTCn", "owgrNGB6dg": "IpErdzEsfALH60BZMIAA61znDaks5bvpv6NGKMMIgq0Nk1T6jJ", "4vcmgUTM5T": "TujMzDHDuvsicwY2JsROnMG7JCQZ3uwasZZD9qugYQSVYmLi2J", "nU5HGDCO19": "1TEPthQc56gpjSQmyGCfelNJ3RmA9Tp9p3ui5BZSYUcW2wFn8B", "ijuzB3UP3f": "OpAxcjwDE9iEB6LHmuWilYM8PaIbGdCVuCdQegCViMLGnLSYpT", "tWclfvAXxv": "Qr2Gmc1dbSe9oMeccv58Kh53Voq3LUCu1PoT2urRtLeZx6arm4", "LENCl7thEv": "t28v04SmZXu1QQV89FpuZtmv5Ggu9jT1AcTwdduF82fEe3yrXs", "SLb47jXPqz": "JcthR6OsObN71nTlKPLOkPeeXFe7yklYY6Hr2ccd9wjKtp35Ss", "RmlH7jMNxm": "UaYJwhsS2cDqHIPODxsEf4JjRbD2Zzg2PojD6sm4ChyosI9Ipn", "BQvPXuBJDJ": "9ZnrlFb8kUCKBpGXzYAQIeqietLL57XHJnntAAUlFJcPUJl0dP", "O0ariYTnnp": "jSfflJa0s29MQctN8WGGwOkHRhy2dwS9biLK1gwL0TukxFIgRV", "1UCYEKU6hh": "Ke9HOu8hgAjGIVIoklGY0Lhzv42mBYvxVfgJyyICwN6Ps2iNyq", "GBvUlXbPlU": "GkEJdVt8WwESIfWZh72fCFQmbP28bj0FpygP1HFGW3B33zdeCx", "yE4MaFKaWa": "dhJpgBat1NgJQaMEEBqZDST4ESoloYc25EEmf5E0PU1EanD7qv", "Q8NpC0rBZL": "SIP6JEe28NDqJiHp9DTHC0n7nFD3DmXxSyKRrMaLiJMHxPVwDh", "Qk1C7ad9Wt": "0tOCt6RAwCTEx6eSZSUzfB4eDyBMa2nkHmb64juWvWKJB8qzU4", "ZjMKEIDEzJ": "tIIcfageYwBls4OQIINrlHpqZzpx59q42RZahXy0L6tqP3YsKO", "4iEUFO4Mgl": "dwzVErZHuCZery8jIkB57bTsj60ljjvkn9Gm2x1J32Q4dWdQzu", "XYbCKstgZ8": "rdknuXntYlnBUizBEnKSk6ChJgrVRoD7laWKB9VZ7FE2jjc0xN", "xqvJiyF1VP": "Eic9e0sFJgQOwjvUPUIuDQ7z8AP1lG4urIr3g5sIxNIfoYwLQQ", "CTE5J4PNu0": "Pig3OJxG9A50iebnDVGErPrgRnxMBlmBgkoTwr75EqYarGXq6X", "sggpfnqwZS": "1JfvrOxUvmWx2rC56Q65YI0kXtOZs2lNRhgsDStTSoaAgKzS7l", "D4oxPQd3Na": "4pA7hy4QiKGULPhgzJh9tTcuDzmmlSZ48tqK53CGzUJn6MZkOP", "03EQzwx6u4": "VIjNuLsHS9BEJfnIqqQZ6GWsGBcERRAv3aqHL0CZCgTYYNOCSK", "EaQs0YgM9H": "qb3RytBtKxIcVlgiQcR4R6RaWRKwFcN9Uo6k49UuBpqHnJJqQi", "gBbYKk4OX5": "oMmibywT536abOE3JQ1TXF4UdsaZdWnZ0i9au1Si2s1BnBihpv", "6dHUe4x9mt": "ELTYhDVxoX6rCWFad6JbBraVSD6gE4NqYt2QQj18ZncJBLDLyk", "NDshR459Ia": "HFT2ws7TMKw7xoqs6KIRmTFp7vKnVLdXEq4TXHlSx84hDYCinD", "5vS17Es5QH": "JInJW9wy4SkjVRjbpPNH1n4fuDEJAKxTjhqfBBBq7Nqcj175EE", "VmTrm5Pcer": "9FtajgmjiYbf6rL1srVPl2FHgZo6nEAQTaNnZmOtbEmQ3cKFnS", "nF8XrSdxhd": "7pH2PfWr3qSBGQzQducCmtg62wYE2MnrRWY6C7vWrnj7UyxmVh", "CYNMGx0V4j": "jFs5FDbrteZDuOBlt40NI2cylvhkW03hk8SgvrtS5DhX1qwq7B", "iUXRBc4g7l": "IxJjoakBihY1VSFquiNZXtoqJJWXZEML3HV6mqYsQ719ENKIYw", "iN8akXspwb": "ZvpNxKPC0D9IsrBcDr7gEWrUtGYcWAyYXf4NUvo8MejHRIU65b", "h3sGD2Q1oh": "OvxrT0OF9loNpckMysYWL0RTC6cpCVuo9LT99YJ1ViylK0MI2I", "KtqOvAWxa0": "A0IxZt61rn2LcAM73DFMsVQGwaYoY0bAEDl8MHfO8AVvURAkh7", "kAb8DKKONz": "Tq30H99bUDNXs825WoBOBLxaGYNY8X43zueEHiYkOLJg7Z0Eye", "myWjzOxJcO": "uHaUmKQn6banGCwmFumKkp69vfkdqYzzenYnaz3MEUbyoqL3dy", "MJiiDvQloZ": "yD47SUH4iA5DWxATZ45sbwtkhZqYeD0HaFASHXvIYLeV1gsx52", "hj2VGFzJvc": "puEeyRy18U69CtdYpvb7X0xumLcjgDg0uL1CCX9IhEtYl0VmfQ", "KsbJQGihmS": "Z3truNds88MvvDnAaEYqBq4s3O08irTyKVWlpjJo9m5t75OAwg", "88DcQGvkwN": "NRAeIZcDPqnjuihLCBBhwsqfF173vf9AZAWSVMtsWQKoVRXDwU", "gUtJzDhftY": "pIQoPw3X5I50A9FWt7vvw8y6Wy0eaYv5rcwm5jr8A1Zo6ZUKzQ", "jClPEd9IgK": "bSiQkQvKVWQQAApfAmP1lolszeorbzvsKTXDsDouxINELPgt5u", "g7dRJ56NBc": "amRHImU0uV2plQh7CUmMCOWLaVqh7yXg2nmEYfaxaza2SsqZNJ", "8ebalqa01L": "oZfi0pGagvNQolV2a56TZamgm3k97PDwvWKXrgNyhTJIr69b6w", "prdhMDmQFu": "xvdDoyjLLocu9vtYdcxAPnysk39nAOdoIw6T5y1j3KpfSKPr5q", "TavVmIIdAQ": "dzENXlVbgUh8Wdtkz7LZCNHpUe0NLtAXcdgqE5z6uJFYj8P9A3", "8dPYFRMbFO": "s53FCJGSYPpzHb3ljOv7fV23MZZ0gj67M7giQiTIcNv6w0O5BN", "GM4aaHYaRc": "oIFUmUoRAZRz3u5BxiAWOZVyC00G9mjwh883hafuCu4rjlB4u2", "l1hTQcxsl6": "plNQjWMAXTBcDdCXXoIxwhW7peXeJKfKZgQjqZeEx7pmUFE8gO", "nL8hoZxmke": "sPLURcIFTMFZkbrGiV4XiSQx250yd1VUduoG3x5DI6ZPsZV0wB", "RBPbJLkXfV": "CiFoFZS6hQ36nHf1If3s3ngiNbI4aRfEx5yNhz9MOIT8nmGmND", "UJNSyOc8w7": "7MsdjTKm5RrV3Dhtki7aNciyVbRdA8I6fJFCRoGE9DRjwqH5iC", "43dTF9n9PK": "Xw7I7s0GPEFcMU4hBlfq0kg80kJDJRoo4EwEwJ9tgKgKoZWabA", "FWfki0wTJB": "Dr61obcdBq0FKPSYFEwCHGOJ9ZecHwsDs6fJnjzzzip1dWkhWL", "VNPreOz3AU": "VWZQhWqMtLPJdP883aOvAX9tWrDOBrkooDRGjv2bJWhJeLsP4n", "KbBjSP3KNQ": "HURWjYngbj7FWSzOJR2aa5FDftRBq7jutYZJOru0utqqoDkbaq", "qwTCkP8hxs": "921ht3YU0JoUOLFe3gk4seRMqi3lNTYwmDIdoaj2dhY2eYf606", "b93OHmC4v6": "al6HrRKW1wzKAv7EUhyEm6GjrHYqo6Eohr5lUrktAUFUNj6Wcb", "imeDhsYenG": "qNqpx46966bH4bsT3ChSEoJerDEM3rnTH1RGubFFjmr0sOy9T7", "py6qIz053N": "xqLHyR5WZG5hrl0yViRda5R1Efup7bcdKMn5rftFZIdxzmKF7C", "UYEKHGW7AC": "A1FQyy7CoCkqvgISKxat5bDrK9YcmTxHhmVtp70KtnYlr53WAF", "iYRTAQcHYb": "fpcBpwYhZPR8LTOM8U3RMyHOVUyzWwriQNcuLiOMq3TFbBRxGX", "0AiBXP53Sx": "qFWkpFh8qei9iI98Yn04UC8viNogyTp7z8Qp0hlnTk1wfDdAM4", "BGuvVVZ87m": "9gN4Xf3GjlG0hXJmJGDpSTQRFuDUqGmU2mSTNt29eRYBAyn5dn", "Emcy4Xwcd4": "HBjXSQ99Ob2PQ0dLw9wtePihaRTcQ243YPnl3XnqeXzsFNtrTm", "F8NPfHMZjM": "qc4uBdspVfWWRrtsuZpU1u0Ar5mcnMD8AiipPpDIm2EW5PSteR", "S85Zp4tPiN": "JVAOT6w8PObCJB8werBOsyj0FVkuTFDnsyxoDEEfnwAnG47QCd", "FpIspARB8L": "U5huzomiwyIBWOdcont9LXAb1nxdDayAfrKDQt439QxbQaKDCY", "LCY19w45EI": "oOLChv6OY0u4k39DeJxLZTOpkvGAlPcPnvBbJqrP7o3QSuxlsw", "Tb0Pca2rvy": "RUbDK0Tblp9lJJoVguhgif4xES3juGwbCAaC7QbfEkURlSTKaw", "wgs4Byth20": "6E41JqpxxpBanUF5hraRN5tgVHTS3GmOxDeubA6bXfBKSkh77E", "0U9Kgyc73m": "w72jNnjqBQyTQtGvC8sj5jbuu6N5yB1PFuEQDaJpxzc2MP9d09", "l4u8EMUtMy": "MP95DO9MejlyTQx24EmOqWhpWimy6rvzn6Q4OnPoiuHo6fYy1g", "uPgWEEldfa": "aF4PnBe3WfZVmlgi2WKZ1Q9h9YloNTDZp53MNs94qGMNPedJwe", "dbsqzsW5kz": "eyVEsEnW3jDMEmY19ghi8ws4Ca2XdtC1CmkU5MylSq4EPiD1Zt", "frBu64OrNM": "rIk04EgaM7wBhsplzAbBPW26oBs3giEam6w9FzKy7EvxU1DGF9", "XA1VgPBMc8": "fZHB2RaWdt3BWcwi94TMwkWPuLeB29b4tFFQmDyMc2STjYfrcc", "fWFOfLCLq2": "hSMeD5ftoChCGlQWlBHuCPnQZpn4YMxvjuJXcqbtmLR4a5YYwR", "IG8rBmoCLP": "2rjrIsor7oVhmcJ4ukazX8d5AChMYAmglSWFjN6vVGTqBjAxnn", "SuRs5JbTu3": "DiZrLVe7JqJXf6WtnuBj4IYpTNZSBe7MlItMBJ7U4NsJTwhvrT", "dzxxgYcyxq": "dcwrdGKDigL2AKHycNWWnpsoITXqVBrt91J6p4dkCKKRMqnRfA", "rngI8gLd7P": "Oi7OVtSn2J93pv0ocpiwRXCIII0bs7ntQEY1OcWlPW7wzOAGZd", "jMOSkJ3O7K": "pnc7JHESSlJZFvXMdnCzzORnTGCPMSNEyBMuaqEOZiGG7kuxmP", "Ey31DW15iq": "q7vtANcI6s6UyZHp8Uliox1FIKtHoGxSMWghZHVXWGkc7r5kGw", "WYyGnw8FjI": "nMDpoJ14ZwOE9PKMLCPdlycbfQxHGAr5aCUJY9UoL7jqdjO0xz", "88abywFq3g": "OkS9uo5rIHcRSykZg5formZxmMG98b9yykrXpY1a76kxvl01Tb", "ejlCBtr16F": "3b5yuYQaBhiqpbeOnAGvvIwHzhQ2Tap29Z61R5SLAXLuBOiBU6", "zYHFMRSGpv": "gmx60suBJAMCpZ9waHYlmA0jei1ZfLo9QwdkYp6VRSTRMxy9zI", "Tudwv1e4YD": "HmDx6C2Ztpi3OdH0TsNFhqO2b2iPDDifjjA0GGTIgdG9Xehl4Q", "lXLMxU0kK8": "2EMOajMoUglcMavOMTLsug5nFOa1wUQqQopppbtMLlNGBx046W", "C9tmsuVUFW": "bOFlE0M9slqCSnjTPu4Tj6uXM9tpuXJ6p6sDyQxCv91sWCtnFw", "1TY97rLz5F": "HDciTNZVwVaWt6GGF0d3fFdkYubX5WpOqsfX5On2r2GkKWw56o", "ZvAntipLrt": "kFmVTxtBW088igfpJFXBchttabreHiosPpudJkgxsPO9kBDv2N", "bhbYGXSU6e": "a0Ggh9vyBG4dVKC6RVMRxe06Ia63iiGogJIG8ZGTware0IhV1E", "mEiP4ko4Nd": "z44zp9AUP3fILAqu9jQOuXjWjmoI2SmNPnnjH34umGupjrRjP9", "Vv6ITo99E8": "6u2gHvSvcflntsIr7R554PxmBLmcdAH5UBc3dUa5ELi82a1HrH", "QOE2C0DCaP": "XwlPbkABq74sBjOSER6j8lFpcho6AUZohZx0q5HincaXuC2R1f", "uNTxXAnuRs": "r4x1i4zcPAtUVJaxiUSRMiDwhRBiScZXEIIjAeHQkbO3J1hjQJ", "lDK7spl1ec": "nAVx1P8TdwLUNYkhMUfE9tFoTxRB8zhfJEMsPMm4cSNTAJkEyC", "F2Db2VH573": "qTkHCQYRg9OPzOKDgGwgNC9YWaDZyzAgDrfQgVNW7onXVgGjrH", "3zbLzbAjn3": "97oPocB9rqUCeSNPsp1xRw6zJTxWDuA31WrvSXGdfsKHWmsMcP", "3WiJfOtn4Y": "3hkk4ARReuYPTeKok932Jc4vzqwDxL839Db3scQ1TxeKijzbtZ", "tDVL0JZOHp": "zDAHLxRYmFenbqpeyWuVXPEpv0ugt2YziaPcYD0i3we1PuHcrZ", "pY964Difxg": "dy6NOoINetkU8eAASWARG7biIMAmS3iufVzoY84iA0kADnKe18", "HaNtX9khZ7": "AO6BXDWWgEMw6VMaCGxjWm5BWBLZmdUJdttZE69CMCSKoAwVAX", "QXYhjHxLH7": "e0YC7ZpzBPKUYAFwmTUFOaerzYJlA4N9kVi2LLZOG6bV5vlqOY", "wLTYyJXzmq": "Iz54kxdIe2r9xcAxfbMZZ1umkOWMtcR6tDqkI5jjwboISH4bzh", "aEzIfiRh6W": "yzjKa1kRvRhkvkCWpaflBlf1S1lxMCSlmCeV23GZovLqR6ln6M", "KhDbwTBCMX": "HUV2lPHpRzQcut4AyRHbLWlxYGiwR8InSrCRM1zU6LvKbTZtvf", "Z8ylcNPb7D": "vK28xx8ctW0ynxqIlPByDWxNuBttck3KmErS4aMtLvtoYXHBlL", "J2pbyd26kn": "uIVU2WXqwbYCOzSjp9G28JiiVMFX3YlZQukR7JN18ysAJaeLeT", "tnoE8UMXuf": "4aWvweOXFh0sWAC6kii8Ua6GXbILNrhy8o4FwtjUnNV33uW1Vi", "Z2DJKCTi4b": "3FaeXYxFco3Q49czLHNu0dLHjJxz6maQrtMSvBJrQiH0aP5KoE", "ZZNLVA68kr": "jQ3EGir0IQZv9ESM3vJAbJg2g49tuUEe80oyumdMYTq9CLsKFk", "VdjyL8Y4M6": "MizKVnhwSDiEZJK1PnO0MPdKZJeUv5yEvltWfcVSU29NxwCGPg", "ZzqXszmWVW": "eKiU714hADGd7EH14cBMNwtKkfMMXwmgTfs81CDmd05Y8btZ9p", "kJTPKepNMy": "arZSrfBBBS0MWMEP2MHCgDIMPObJ2lnztoGjVWUHa2RRWLdYqo", "KBTsJsJReO": "j7N4t67NR5EX7BXqGT39MQfZMef75q9GtdX9oZMuIkokOy1dS6", "HbSRlBFEPT": "kSfV2jNKMLc5TmdtzrgXjTMyYe7mpZjjZK12jJm6xZ7Hj13I56", "dQkNkJbvXs": "N5JSw66ImKmJ331xc2hAOzSzc69iAeurfCvXdvF57XwlkbsQyO", "kAc72N8pQ2": "q4rhDJA0PiAQSj8sRhRzuYVLtKuF5DHOGS8cnUf8TMRqctvMgv", "WlIz8w2MEi": "xf7aTGQdQ2QGxUFnFI8wmdRX7jG0EfwRcmbEjVqdbNoFSeHKjW", "faJrI28cHo": "23IKGc9iYwp6vm1qmKxIc1ubnIV6qCPjfDNr6Cq01dSzfzzgD4", "7NSIPYfkqx": "aNG1Njg0pZXCasTmihN6XMvuabOP2wqqXAaqm2GduEpFMUzakv", "T7CuRX0sn3": "3SNBzKxvJ3gUab1R63rD7qQsZAssAGcpMwat5DcJZ8OcR7hLRD", "czwSWcT2qK": "EgVUiAsbXdEKV24NEl5k2PYYuWqm5s8ksaINX19VRLzzGTCsoZ", "T3T7TYZUzb": "vKs8ZuTtx1MMCkKrSm81JNmtYyRDICI7mz0FqqE9xHbTNhK6UK", "0xEwQaIuHu": "PhF2zLesYnYPhFgKjJ6jguz4sDtyL91yFrDi4kCTINLoGkopEX", "Pj1Ns87ZIw": "2WhBkjzp79JLoiIbuhyzZcW1Eqf8DsJcBfX3Mf7muLGCu29aLj", "JisWiiuOuN": "fcipVfGSI0ui82h0iP83Ij6rVpQifgCey6WPEYK9Nzx1OJqaH8", "TmqT6QrO69": "NNDnsnv6LM27cQaTUZIAxgS3jLfBLprST1orYQOruiWBzIVQdd", "v6BnPNaR6X": "RSlNISMir21A36Su4WRyQWEeN1D7pgZRFesv0cjIKX3ft1wBWc", "RMVdVuiYCi": "daf6EeNmQosc5JXuK8Jd6rxi5Wrp99wGStSYXBkqQF8J1o9fme", "vwUDGwNC02": "f81Eu907pgBY2No3DSBw1pVsUQIVF7qowqBTDPi6mT54WrxGC0", "eyUMmjx6z3": "PIp20GF5NMq6hwa16uLOJpYmijILfJ0ITQv8KsS9VQk0WUFxGp", "siJnrcouKa": "xt6NkWYv7X7TQOfO8XrE1pFi3UPdncxpT1VJmxRKXkZoHhSRFg", "KhrgkkF2qn": "uZawMN0NyqDHIzfHF0dbrxVZbtXs8F7L1Q3uRBjAgrqyqbMDAU", "Mjdm7IOPGE": "yOVvcNaofta3G9P9PO7sw2PLtvzXQBjFMHqoEnatOCnwvi89Xp", "cOBAmCeZT1": "dOzyLytrk7nok25eQp6TzPwtnvNFJBmgMyXbRgaXso5J1KQtTu", "RX8zzE1uch": "dfGV451NGXMOocG0IXwaVYiN0UlfQq3l95fdMttgpYeU7F5koC", "P8EbX0AONK": "DgHt0hlRTmNlPau0pXQvNTjpN523cqYxJZmFC0fFrJJCVRwVLx", "ksL3e2rB0a": "m3eNWVMJMvxvFLG7qfdVQyvCKqGSvoAmerKTvaqLvykYoq6exP", "WVqQi6DSBe": "4h3z7fAsnQSXh8c1JNyPWgLxM5q9s4fIYhyxp3tz6LboR8V6qJ", "Ij8xASdp1f": "4lwTew4mLA5GwoOpWbjwLRz7cCTaDxpzZLbgiHziMDLbaYTQAz", "SpzWVcMXMB": "RQMIwP6jNuKTPIyaguWi1wNVxbr7FwYpIBGX3PsUWWD16f56ml", "s4Evsie7m6": "IL2YJ8s60ozzTxWrQaeZpqu3Skao4JvBJw5gbEzt2nBFGtBgPL", "0XSVlS45GO": "FdgkVC0lXDoXwUnvqRJCWObbDeeExbBBaZq7KOqOeOXFwW1Zrp", "XFn1ixGW1g": "h6FP6WoxjaD13FD13qPPXGuNWeaq3Nc2wm07zvhM2HsWJxCbqY", "2jja0AL3Rn": "kbNZjKC32zVvSas7nODpV57AifD9xVcWYvMnoYhhvOe4mfI1ol", "spmhomxSDw": "bb4sFM2LHLogBw96G3trjUYjACepDe66cEEghsD8QFn9AgpAtN"} \ No newline at end of file +{"C72aUUla9W": "GFp2jqKZlBCJpwANZlHZBK4KxtAUQSL22PlnKil17U4DY1OzAP", "fIZ5sGdtDr": "G9rfk7kn7Np7ZzObJr2SWidOE1seJH0KanyGsZSt7x934gnfb0", "CCp7UfWbxS": "dtkYHMLwwyxyVvayZanPM0wOE9GopivwF76XTwA1OHpDbgxNQX", "rFNe0bCaGk": "lUMKCptIzFVdxyPTLYYPEZnnGSE6ZAXxOykKYRV5N6QJejjzpQ", "BYx7YYou1O": "B20YUQemcd6KDxJ8Ro40hvcHT2KXHEjrtE33kv1W66ZtVrco3C", "C3bgGJNfjR": "jkTQ5gBwjVWExA1jJ6LE8BDLcK6TzMJLjYhhT21lS1S6wxrQ5T", "lQD4SCdbah": "e2in1CsSWSU5OMZTVQdzSQdO23t7R8Ryr8FRYOkwsF5EdOijeq", "6OgrE4BG3U": "tFtK2jgIyAkHWoEcL7rIJwN0VYCczXxffOZo3GZ4oCuO8xkOE0", "nXUBzCFTiI": "3RNTzGe1pMghXowbMA4JAstYmWFv1x9brH6INXETkEQytbGZTs", "bJWlwCyjpX": "tAlXIOztQtP98jK10t4oPhHws66rbSHTAng7itXOYcuImeoCRp", "osOVD3sm8D": "44pnbwzFEL09LyCrwvHPouvfm85rXonwct2bGMSCPEXoUeanSt", "s09rKtTtvX": "HVlDMGdNvTNRAIsRC2wHw4dWnpCtKzBEtPPqp1q3bhArhH1wn4", "wOS150cHku": "2uoHFIq2KLXc8cK0V6HzDkWq92nB6IUMLFnuF2e4qluksLIptG", "Q4YqJGk8LM": "MvSWVhvgK8TNIWzh6lMSZj0wqIQ7BT9jnZZ1glqFrVKIeBdnbY", "7uZD6nTOjy": "p1dHRjmG3gPPoZ5UhPEtDnagQW37sfNGVHD4VgPdvqOO0A7NTs", "ir25ceDFTK": "9nQD1q7nf4TUctAQSmulxwXw6DJoZfFkqriPHhASmC4GPN7zl7", "fGEQfvcof8": "an1yNP0uVATJkKpVCmwVUPw6kNYUCLt2y8ldyClWuYpD5sSAOG", "41Ut499l4P": "Sny2W8obXvydkM7jIWMwK7pFSLveQUKWM1PU6qsWYnSSPqFo1H", "bk6eDhQnYy": "XHaLkylJ8Fj6wee9gwA46ykdnMYQm7pnHuMswfmcFGibRr3RND", "dmIdHbFd6m": "lFR3thGRApNO42tOZbXIjE3Q30so0LITLnyWPbO9hr1cPFX3sw", "VJYqnKzvUV": "yfGvypMdYJNi3bGd0buruziJu1S2DWEUlKFU5XinDXsO17SQQo", "oeLSXHZ2Tw": "Q1XqgVSNzNqcGIDgM2dChb83ttL0DWCCCUV0BSgXp3A4QqfX36", "gMDcoQNzNN": "3vSgG8MHQWJfn7QQFXJz78rDl0gQFnRrTvJyS0nBEoTg1FHmGw", "uQfgJqFW4m": "0WxYQ4ixtcJ1ZEzAUxnPastXSjRD3cvlbwBgzZtS53zhQkDRGX", "W6OePnoI9w": "6C3nTKMIo5HVmpm45MhvPaRSiOaEpDoLBA1mEw9IiznZEv2BZB", "m6cGbU8KOj": "75s88xTIlYOjycxYd4PORbXtdDZtqgrxUT18E7PfDdSFq5Xmc2", "FXmXLCG6Uq": "Fp6JFT5OhEEXbzUTGFQBYxKLkWlxZNjrIKzWog712yJEtBVECW", "OHZ1mtyquy": "CgBCXlDB2WJxQJuxn8TIu4DwZCmFDwKJqPsV3pqnDJydO29psm", "a8Zd2CST6R": "zuOwceQQvR0sPTgqs3IDDN6CYBkKwpZVuTZBPucO3KNM6MCGNZ", "IKMnpt2ng2": "Y6NUHPz0gOHQBOHf7AlhnuLKJQblLY71D0IYumlCbmFFsf84zj", "pwKEIRyNTX": "cd1QQ9lrX2t6gdkMZhWpM6NPgwq7vftuacDy0hO8gg83BB8pWc", "ko1HyoFecQ": "GWxOonlL8fCVWaAHl9Zbh3HxUXgG7Ito2b7dQrBICd8OsjHvlD", "JzipnmKEEK": "AgzUp6VIogKpPWp7SP6xjjoyWSmR46wxX9qW5IY8CX4bd11IrP", "CIVidRQNCn": "2HlABNeU4v2UdGAXcFMRztYo6KFHfAAZLiY7v3A0ZsR7mu8NnP", "x3JS6j3Nrm": "02XmqIBExx2L1wF7qS4ufeMlGm2d6idzOyZVjrs71H97RigBwj", "GAlf9lFOYd": "6F6jTBXYccb46UHBkzHXt4iK3zXnJlPj9avaYk4drK3OIsD4Vv", "5ZNdSVqzjH": "eO2x8iNM29tqtGgabpPkmHNk4PJ9jTQGjP9wY3hXISYTfzkwkn", "hXxEKE4nt9": "f8Xo1jNwPbAv3BHDuO4zilAVguju9Jok4Nciqjjwcaf4Ytq86h", "21tKnbwId1": "JrPOmcW4qpffaemiTYCL4G8Jq1yKGfWlsAMTK04hxaFZe6QMyF", "MrxKOIj38U": "fNVTVQySjgnATvhE1VywIaPiVFHuUp49Nwbg49Mv2Gh6qJub54", "KohrGPIXfI": "TIQRfxSYndzrllYlgR7ynG5evnmNWzIBfcJ6qs7a831fYOlTva", "8eVOTkLXO7": "QjymESApOqRbM3Wxf1nCaaHuJNtieK2uWxuneg9DjgC8ctQRTT", "VzXJuW8ysO": "aVGLZG74zHRqGN8Jn2N0kt7Brt571c8lp0VEiwtEoLsZal5o3X", "LhXGmZk8ij": "yRNA7DmhIuQkBY1aEK2FYXp1ILIpgRAdP5ASRvK8tJV7dGEWYH", "wh4GHDMFUL": "LpwRAxd8MLH2kO5UAkYzNZDdxMSAxpEp4R2AZMbMzYC3FKTpqB", "AmMczTt11m": "OSzip2uDhSLHOwtT0dMbGe0RE2UXfAmeeYpzkVw9iRCDkEhxT0", "Z5ck9hDAbR": "uD220S55VhuhDGQ0u5KDuyhg06aLVOMRExDrzbn0WQ6YyGtaxT", "e7q37wroCp": "pjm0JInEEZA47xAvT2MJxj8qjZSFjVZ08daTKfKpClO3l7VObq", "ocRfgkZ0BV": "zfodqtUm16srzVYi95TC4otn2RXP5rihxFKKkq2fvnjL75Yg9y", "yCaI03Eybe": "gyO9jgRZWeHS83Ooihu95k9y88VCqDWYfffFZ0OnJJXeqlbCHF", "ON1gLsGb00": "VURBxIqhR2xTWgavi1N6KvqBPT6yJ2fuge1gs4uCVgp2pavdrl", "pMXdUZz64C": "CgsDcuAFfUOlGHnwbO3495fgw1WmkJeytUWyGBJxBxFJHgBFQv", "9xf9Zs0dD3": "I1GRkjkmEi1sQTYgAVq1aFu9TudqshhiE4Ij3qNUYUxIJ2yODs", "o9EYQBL2bd": "4MiJKVDEh5SQr8o99GwJckOkHVsnL0dwNK1ZG6lHFFNJORqBP1", "DKlf9GirJg": "EKIQZPu7sS3lAPQ7gU9G5f8SAkJrSueTSZKs8DKRxmK9kXCsE5", "ApRt7EOXvE": "QMo4KhubgpLKaWC8rmp5nj2ccgx9bCYZGa6vKCnAl9fZYSiemS", "mOEFLwmwJI": "JfM7uyOhZnb5utP48jZJCSLVVJOfL3pEbRDcgUfpNPguCl2ZIf", "3PelCFOdX9": "qYKU8l9sleAcLsDZ8K4TpCF5nBGjS4FL9AggzpzQTYutfERYVl", "0MwRoKpHr9": "TJqQDFkezPlziUfTeuD6Ufj9LoMAolCHNKl7W2DqEhFtr667HT", "hW3Py64ViM": "f52jLjPspPN39SifxdyMt6PWfZUxmIN5EYKyWd7Ox0gymwBctM", "3v6DsTOqXY": "mQsz62l5vz7IUaltG0H7xcIvLWjjxApeDAvz6t0J6gSbINvVZ7", "sTnBjc3bSz": "qSicqFvEhdLHtTwyQS0i7sPxw8cjJx30RTJTXWt7ALBnWBib3J", "o1may7TMLk": "cSmrbzZuOWTyzR79DlyWhJTvQRkmA9hJmmK1hwGRebcAy8mGKy", "ay2dK4ZwuW": "Jd9F3cqt8siCqcdAKhbyQovMSUHr7tRaIZueG0j4MjoGgEp85q", "L4hDx88kMA": "FHU0jSYVbyq1VkqpujkO3xeLAM0JxoK4fgHz1mnnoFu3KDVQT1", "VzlHaSebTy": "vnjCy4r4JCz3iG8nEVna8UexfaEIQwPuenfqZxfgbbuY3bvMgT", "zHeBaV50K5": "6yu6zCl1u6QPpcNKvimL3fwvzOPyMRb9kYK9gx2NVbYgsvORiT", "k6XZLkA20G": "4Bdz6cYANvDHQbKnJlQUQvAXEIcwE87UcCX07KzZBX51QY0Ozr", "TbGXwaWd8t": "resZEllurht3CCwF4rX04JIvYPDq3ct807oFWkxFSUgxrevsOz", "Y7WIazgkxa": "31NHHBRltzQSAxeLMP5RrP0bKC2BHMef5i2svYfv9SzYTei2gW", "n2d3HoTwQu": "qa5NzzJfVsp527BCF1wMC34oXepaq1eFanR3zghojWJStqAXeG", "0Ij3w63kL6": "ReX1qVsbuupyaGwKHNgEKYpfBqtwRJKOE7f1UuRImTYWdCITGQ", "wMfeA5lm4X": "K2FeYpOfMtNJvFDGksyR65gCd93o5vEyZ9mJAoUE4hcygoRg9r", "WiYE9YffzX": "mnfFEhs4vgug7sCiDn0SL2bqJHzoMF2rWuPlhFsY5sAtAcoDC0", "bg2XUpkzRt": "HTGncqAOpCAEcvjBjULbTMILAcaFcNiVhv0tv9qzPVS46MzosW", "stEXHT8WOB": "yi8E1Q0rQr9FlfoRw01yc3uUkOZdChzYsOolCUQy1hqbOZYpL1", "F9tEbQnUyP": "QadIrxzMPsFQjYbqOjXcMI3HjT4qbO3P7BSuqXmJsfB30e6KD2", "ii2Xkinxlv": "40yh7N3GSUaW6TC4pPBdKL0KigLL2XbF0co0jTeqI1zzI6QKv8", "1zwECHyjeu": "HxbJqDQFntyff931OKmxb2KJjZ9QlDp3Dk5JKr0VcIQ2YS4X0T", "8rPBRYt8PI": "w9dgFKwD0R9avUDasmUQ2Ea7ZYchLR6aZE2b3H5zHHYw3JxIJV", "2EuNVs4JLy": "6fEwJLdSP58my44DEb868SPZZCeddYbpkDmZfhb4KcoEZ0HRic", "B8CzdNkpIP": "CVWerWOXqxZZ63ELE6chIgzvF9K3u5uuHDbJeGcq4DQ2YY5CSM", "r3w2u1ecuC": "Qk9CWfYjxt2j90PddoebKiX62rqdeRx4CtvH2gwQuuUh1mBRrq", "B97uvtFho4": "39wY1CCstM4pxquaN3zjniNNkQBajpql7Sm5VdvwQGUE5BHnXO", "aGTixKv1pc": "ZKW5x8gJNmjEkhZ2RXCsADD1gpNs2N3PvEPMNumGDMfhUwU9rM", "pQu0o1za6m": "6d1isOShZwQv1vGSGty8cUf9IX2KdCxP5xN8iztrHLZVCzUiHg", "fK5rVp3IF9": "tS8lfKMWkK2RzzSCnbRk8wFRmclEZbJunAwrTDMwVPEtPU0zlH", "vsSiGI5c3x": "kWn1ccQjvW4BXBmiX9gOVlvHn06qSOvJSerFD1LSZeZWyniMTT", "FO4iIaBkvQ": "hIxUymXWbszGvVJu7Om6H89QxV89kFYqOC3vK3izwGmx8AeD4f", "RyoJBCVra9": "gz8Y1dzWID4QFJgL848HaB3wz45muqIEjLOnN9iMezKQjXSghs", "j1sdb4hyrI": "OF84jZi5XhYYy2gLzOEAOfOhYPK3zNsKcWuMwhwx01D7wJPCrN", "AQ4dO5Rf9l": "dBqATKQE9a9rt0TAA9JhI1B9fDELjiwEOpIcxF9KWGsOUeUjR6", "yrZtqL97qu": "6Kbz8eBq5DwaVhQ4L40sWiS0D65e96iRxJttV0zFH4hPj9tHQF", "HeFkkHhdfB": "mpvUcXNOUICxc1WKvScqr1R0EvgtdZ8o13rrIxXCej5VCTfSNh", "HkkrhBXbyU": "RE92M0RMBQwHmJuzXksGgP92g7VDhd8wXf8m6Pha84RgLoNQ7X", "BfX8Ejzn02": "99bhU5m62OPYfGbGCmxmOeIPtXIDoeYCZf5y1eFydADhrWsabD", "fjzkNCnsGM": "j2Dlq6Dacouo1B4aZ6Vcc9yokEqukCdor2NH5cy4cZWKKCvirx", "w49wetYHv9": "HbJ9IUMLbDIhKXV4m9sDJ1HTHUYMfWq88Nxqga1xBUQk4Wn9XX", "Z9f0sIcf4S": "zeL1IS01V6fQhTMj0v6eQCTOrfkq1JCOPbyq6MapaVpz9NiEsC", "RgCoQzb38G": "CbeVh0N1H0fglbZx8CLU99GdHuKIDQvMwuryZEONH6Mr9Zjgj3", "6zYOOMghIM": "x2RNiNOZ8cGZwKp3m7GclVh8R6edWqzwrOFyIUy8VR28SEGaGC", "m0LnbokJxn": "VzU9ePSvFTsGo6JtWdaE4YKVR6w3hOvZgJ7GdFixUG14hYKtnY", "Bai5RKdLDc": "WkBZkzfPKiU3Yar7QbtEHYb7cZePEvT0ndX5pHLB8D4Ts82ZCZ", "6WomHwhSab": "JbstDYO76I0HJjuQZhskbc82EB91Cqe5O30f6caPkx2Vl7IVH3", "VPyWjtaCgd": "hsHN4jqIeUOuPbBMK6WQS3mONKBzHpYf0tIqXxHyitCWPuOf7Q", "nSAtwGq3wr": "iq12sXkEehPkO8AMvWGxkdvfgWAObTIc9FHw6lUhqbKDgl8lym", "hF2692Yf2X": "drRsXvzCJCpGK0Q5IQVS3WNeTy6VynlCyrnW20OwDmBhtrdHZi", "aOjXWBeB3O": "s4u8rDy7MUSlLLLP2TJrDuQmPL9Nyu4CvPqoAiWXZRByiQri3s", "or9KLsMrpQ": "N32HkOs1Mt0AWGiKOBMt7Sf0GALF7lAtoQ1x09Cb0qU0O5yKaU", "y1Gg6uigHO": "j923zv3f7MB8cTLpF0v0vDHO73SiZYOOMpFy85ISiGVb9oySNv", "ZELA2m42od": "1y93Plv2OB40VwhtIajdLSkmEBUzzbze2CClP93MnBqbqlvPFJ", "MsIT8xH1xV": "RHUr2fJFqj8d0hVxF3UdCKEdCqr2ZDS6ARl7trVALpvzyorYni", "525aPsJrOj": "AoSYKCmW6RvlNK0JMajpuNkC9DV3xmdeuR94P9cJCyhymGfa6o", "sr1dPOnP3e": "5A5JlgzHTppswwITzLTuK85Uc8XU0X4mEu80JG686XCdQWHIWZ", "q39SaBQ5aA": "jmHtlanUmKgZHXwzbIUJhXfOyjCUFuR3mw2JFW59nYUkc9M1iQ", "N8TvJbPjyQ": "aDBB3rtynidAn4PxU6bxSGGd5FQeMLs0oY8RPyMKN7ywKtFMCS", "Vb1I0K1ZNG": "ktdfumcthHRxbA48YOEmSLKIn3YFfhORn0Vav4VWRW0hNqxl26", "imhs3Xdvyr": "IrR7ydvToFeQ2fNnDyYjRxRijXV7W3SmmXYkl5HgEi8wjjd7lQ", "Zoe8Sbb20H": "J6zhHnDDzH9dmEDYUyeU851YlPdMCWtmuhbGNALpu7qH1esUMz", "LR9PTANQYN": "AHDilSn8Nz5E1WsrUgLHomSCIpcryNI7v4nsVAHnjO7zSV8CVQ", "rESBOCvGo8": "wjLy4FsGzqsNpN6N91ugVLbR7btdqtouCJwZCFeO3JbJSbFO1e", "JCDDuX5NWH": "aYT4D0V2rY1DXxrW7ZeJbODT9tTNMP0mRDsjW61a9SX1M6omkf", "09eFNUNA8X": "6bi3Lmc8AXGZnyZXWSX1ZwkpJLbpKxj0b5vUqolLo6lAAe8ITZ", "v7c1L7Op0k": "IgoiHSlz6SiSIiRaEl6eHtxDEZYPfooeZSNBhF3RZRUVtb3Vyz", "U3VQy856pG": "5h0ZyeOBFQtFynZsbOAqfG4dmoYxKAzKZ0zscxhaIaa2rqRcXT", "CFmBcXy87x": "a7CS0sCNgcc8siR0KxFVYGG5YLzVlyWWmxcycnhpE9nztpl6XP", "gPhKk2INBO": "g6rqKdlpIhaiROIkoTSo3CSVbemkQF73wKgBD96I26jVSyyl8E", "ziySGmJGIc": "OCUtoi4HKcHxsc4cwCZV78RIS1chfSoKc1kpPHiv6ZHunJGQcb", "MBKYRd9i1q": "GEBhJHKusbspmiz8xxOwWPAgyk4yFuShBwa2O9V001q2a51yA9", "DLwpsDC2Np": "hm3J1eSyOEbggyIwW87pTVuU42Nx7wh3FoSY255xprXh9sZ4YA", "r5HYzaAGOL": "nuGFYRhsq6hox9iI2YZeTna0vzaLiK1sg1Rpp4wndAqJPoMEXk", "LqXUoEXnug": "Y3ukD9z8xxqknN0H5BAd3kHb9OQXOsVlDlGmBgOLiblmjX8Qho", "m5v2a43Ipi": "ALjywwC5K2A4sJro09Z8RICfc45SzgsLdZ7e1KRftq0zr8q7nh", "cEhmv6K6eM": "OcDXQsSfNUhNXKhS8UfqfxhFpm9U6mIq8CQMeZ1kl5ae4C04h1", "2Tp7pLjCag": "3hyfYa6NUbFNbGjxvGvDnyvd1VbBhusqw4oX7MaWhLpLkCzozu", "SOZ7oPoc4G": "86OKOYAWGCA3hCHCYY7kwrZIJHQE6xnXDyaR9iLJ89BjKMoq0Y", "n5Bm9um4dJ": "iTlpFmtxkAogllKxaBlt57RswfLiPv97NRKFmYgcd64FoXjt7B", "jaPdYTneyw": "5zHs2NhmMVvBhdG47je3pwR69ZhxIr9Zn5Xgb38biQvYUcZ50G", "sk7WRVyHub": "r25DMHmEO3J47HC0L6mYAhVwr3Z5JcCxNFRx5b4cQOm82WvjRj", "yq4wNYYnBv": "LNi9Ov4pZ1W3jEuQwzkvwgDdZ3BJW27w2qtkAiiUdvN267ApJO", "0xr9ylDowV": "nKL6idWSYEcsavWECx7uwqqQpv2paZe5W3TspamSq3THQCsKP7", "fuBhBXt9cA": "Eg79nvGFz9R0fEUIqTSJ7sFiHLJ3oWC3vN0miRajU8QK5Njlao", "Q7e422OfO6": "lW3BxSR8eycnwZRSnpGpQXc8luR5PqxmcyQasmGlNd3yFZvvZH", "V8rRDIHeYb": "O133MLzxRa6wqTViHYqDCsrWQA38UQtCip10SEqSGfPcKwXCtT", "hvZJtReRnK": "woclFGXqVnblxoxUmQyPgJo37oMWWJzTmITHmSbFOqMIWlb1Oj", "1PXmeHXg0Y": "bQUCisOJ36jqgLT6Ie4wJlsGUvG7qqAVLC6Jj88h6jwiW9zdyV", "vykrCJRX2z": "8NyqZ0JYpFz0UZPCnd2NU60EJ619SCHqXinAT5hAeYBl7frgf1", "E0ifr04Oiz": "XpNXFEioSOkaI8FU5lkh8ftFY3r9s4UgtzBi5ZVqGpaTbLGrZJ", "cU2LZitX8y": "qqyl7ucz4yMi6SKsmO3jjrvIYYxGc99yQjGLpjAVxueXYGYUbk", "S2ORMA9dmH": "BEMYdRjkjttCxfnqw8nWfbH3Yj3278SeXM3MZMmfiYKzOGb5Bg", "AvolcQ01oO": "AShXDo2mNA1yqeXM9aykzdHO9I5mWQosJlPTVm7UIwhcD7DX6y", "A7vRasyCMb": "w0ZjwYqLRJe3MoDHELyq2olOneP88uFOTpj8RMeoGP17rRFPEj", "efsNe97ES0": "HETrTEHxidQ9H6VTEOHYXnD1c6KvcMBHxDWQeCuE40QXOd2yZJ", "vf3cxs1jz9": "fJQjOm7cPhgoPloWkC3sm3CyHyYbPMQZhNzSvCVxrmwBloXm0a", "yVTvV62uja": "hv9UI9xRWR4WIDmRf9WUzaYG122r4MAXfsQDFiyk0KG6eSLXCV", "UiChZbQfTA": "bbWU52vmSPB1jMS1SfNkG6idZ7ZaN1aG2iAbkizpr8reEsrJb9", "MOF9b7xlWr": "JNZCRiiE6YV0e5EpoieC5bhPVqdnJBL3kSS8YV0eyXdFsfyYgk", "vyt31HOq1E": "q86HzbafsN6V9eCi2UxZD5DgHy2ChlGvWmKchHuhNBHhAmXBW9", "HVBpcL8Vm6": "CVi7iLWuEar9eKjjLSIAS2l2GzWT7HgXUV3LZQjArViRxU9vZs", "4yyxLKP8J8": "z91xnk6PuYqKz6wAK8Xb3nrRJwZScVFHCXn1etPJ6BvcFJOO7n", "QNcWwgPx7P": "EaUklX17V4IuOXKabieOLkXcVPnIE0mTvqLf3Tb69RtDU0B4HI", "USOtIFdcFG": "tnegvTE55UcVrrPg1vnjrfw2f0UNOYiM28ryTNhCBNBDZPhw9K", "Dmcs6VxfuI": "tHfwEIJXXFZF4U43NRSXQvaFHhCf2ir6cOkW7TAxeHQKmOHUoy", "HclNrtq5Wv": "soZBfbPzqW16MRYbFW7lLuW5708E0OgV1tcsniGeSQWWiGwgsJ", "LmHfd9ItBG": "EQrSvHEuDh4zEXPTrRirdMCHkMc26Nh7XSQrzIQakYxHjC5ID3", "FBoi0RGMVy": "cdbizX9EJzLkcHLnsWXOJhOT2sSoLji8DOKKc3AB6DsL8wYnNx", "ix7Ls3Is82": "F9PJsrsucPCLXsVhN0jbuORpxx0TZnGJQPV0akiyXTm09gQ8Nm", "F7KzYfPRMo": "8PwukdUWCJZ4Wi6UenHL8bR9g9k30fKQtyP9SY3kxjX3omENcL", "c3gse9xVcM": "oj4otjttmvRfT2hvl1L1dkKr6WuRHpwpil9rjiRzeDlJzlBiXD", "hIY4UiRRE5": "grGHaEpZlT5gyKrUZNdmWN6YxJyonrPQgregOqxTZJ3YPdsKEu", "hMcUYN4HQt": "w4LH6NMNd0cJFqc6myOUtyuDVGJ3xJ9J5Eg0FCx2WkFQDR2JKd", "LorCHjVK18": "83DjNZa0i54wOEOG8vpGdAxorj0eI2CVCvMXOq6WUMts9ZW3JG", "AvR949SEGq": "BbGfTe5mSbELTQpaWg33Jf8r3dN6gw500BINFyG6J29x8FZYrs", "T8it1hOCe0": "DUMSRlcwFts2JMyLforbPxk0pfyf3ZeWwgTCrjSY1XjtrRrDQi", "q2XpdY3GDe": "OLzki4YoerfkSEOUFmdkePCrk2KtGZP2F6ukVEvRHEXk2bQAis", "6mMXVdqARe": "Uo1hM2yf10NuxqvCuVsBkpFN42OJRKglys8TCKYqxB4vhFJCaX", "Ukt9B7TLLg": "QUjKe5CGdzQZX2xBZCWX4KG5njvwy4lccgKohd8BeabmAChNu0", "GNLtBKw22L": "syIfP5VPDt7bBZ2N8BX8L0NWSUfseJKgHZn2DYEKcRrQpJuHyg", "6N3S7FqQk4": "lpt9bGFikhi0cjBd5n4pEY0nE6m7ceMmlByy5JX43qZqhXGzRL", "36n8eS4qpp": "BWtBXn8XJvT1pRqm0v0XxLA3aUpqy1c6YoF6luNdc1u7onx1fd", "I2JwvCeNnK": "hM6rskNdjsJRDuVpAHWn3Z6DluTjpzzqFvdqhGWGIVYpyOexnv", "FC7j4inMdz": "9rUVwEoZfYMRctJbFCNSnO9p3UhAkcKwmzhHMAkVggNk2FMNVI", "nP1HHMxp8C": "T547daYmNBqEHqJQMwxWZVD0i5YEKvMjUEPSo3EjI9Oij5uSwl", "ek53NLTV2V": "jJolXmh8L6ll3sM6uO2LRkgcaNtn2OJp9BlMxjd86DmdCaTcQT", "PlZOZtZ1x3": "Ti33dfMVTKloLi3vFKUZpG4Y8ELfI33fhKXCgZRTztdrsG8q79", "Q5PNnDOKWk": "fpYukoLr3Hkgg8qhy7KHAXndPWPX1dBDzhGKwdNSgycZqmPZQZ", "TclnuC1Ltg": "oC7lcjzPGz8Ql7ZzO2alePzU4VyHSmvEan6wOGX5IH8JwqqfpH", "kalaIDmTLj": "NSLYAaVorVbI7iJKlnGwvKfEprndoZP8wtgMpFSwnWINVbonjV", "ylYjFYQWO2": "6Hzs9f19TfTYLENrWofWexJwwsaSSajW6brdz7lsvbkgzBqeNE", "50jg2tIoj9": "wFkk6yWf2pd4YsHaGK9yjjBD2jxv2u5coKuRxbhwcmm3qc4geR", "TzX7zp4ES8": "uuqEFqkNpXLqGDe3Wg9ljgqLBoTEDWPRqDpqs5vCKJFEo7kVaG", "q70fe0ZHaf": "sRiRQ8gyA7lqXEZLnwz2GuDmIL8VMwKbeG80cWFHx3TzTaFpbM", "oJaePOrF3X": "ugLYPi79ljo72cXBmla6IJKF0gSkCr2cGJKx7XKnGHhLO9GrdZ", "66Dpp37YfP": "ZFf5wlqt4K8dSRMPYF2bskChvNgn6HwYmmFpcJXqPxxohqJ5pl", "17n3GGSzF3": "6Xta8kWjdmIvlpeP73J1WXE9b1VuZXEXRnNBqHzWT64HjjC2IJ", "ZWY2ej82d1": "0dKBwvkET1u5aCAHdhReMieoqmxTdRIVXHO4H7T1PJOvjI5Xms", "AuLzJazRei": "RCSepealBg8L3fQsJouqQNkjc0yn00LNMFy9DaPjJ7zyNo3hPI", "nckBTDCqpp": "sjEcGeyAVOusSZLswImNCYJ77ANqJ5YBg8jfXnzDK1JBlyZ4Fr", "UWrb5dOdKl": "tlIF5IwK4VqCTqInNYfnYxpQGS0928HmU2ebfkYumiHUOUW7vL", "88CZbu2LMM": "DWyWtZZDyKqHiel86HmbFkoHcJtYxFIF7lo2Pc8Yhv3WXax7Hs", "l6VAwa9myI": "P89aK5VMfvJJ8idivUJboz5XQEMMp9st5B4foUq4zC6QBR9vYd", "uOn4tmpJSB": "TPg2FUx1aaOTmWo4DR14UEJu549heWpz3xUAbcNp3EfGvIokcg", "ncFMWXjniv": "kz7mdKNi6QuLVMaevLUQtG3TMijJoKKZJfQPJKiWp3u4IqqNb0", "PwTyG4Xdd6": "0JkU1DAYJsriWSfRZMwOZrKSQuEwLEWjjkhMOBek6208vAboXH", "vDxxGQv42K": "F0Y1L0IwdnYu7kAmklovaXaxa60cSt2CATNJfkgMs9f8rh2TUJ", "q6lHJRsKeu": "q8qpOzapvjYqaLZaetsNM7L1mMscQtmFQZF8ZghO8Rnez1LEgB", "T5QoxAIh97": "s5uwO4SvPMvJscxq18KyK6xp9I8AEosjYQSfQmaS930Ft7L7C0", "zGlnH78tJC": "C2nlefrttCpHkk6gStlgTSRd2T7evwDG999u8eKr9DJ2IAO8fA", "OYkDxfUlE5": "UianZ1zzw7U0c3rePMHH733xATKIaRyvxUg4J5r8hnIhH8IGVR", "NsaSUHoMxt": "dgYCcOb15bAbBGKHHibyPDcgdl9ATHEWbb9pueFM1qeb38wWf5", "M9yfp7AiiG": "zXLfwLT33H8pgpNp5F6Tmq1W3Y4ikmK3DVgYXZ8CF0M0qz3nuC", "K7zQaq3Kzd": "1lEhi48oyX252q3JNdVXLniXLAZsvJjv3grM79jPz2z8SDfrTs", "sUxQ3VC3Lv": "MvMBD9ozMhHXXjTtjk9HipZ0v5GcS76abkKqf3kgDnsZ0RC4yj", "QgFEm4KXXz": "XURMPPoALkhM0ULoTwo1dfsuFe6wPzqLJJzJBKfzgQOmowUE8c", "VNNfuRhKRY": "ZJ0Aq9sNimxwwiAxbAGqws8HBwhavzj4ePQET2lAPEAzl78jI1", "29sSKsuGhj": "yyteb55mN57xEwR9pR3J5NtfDx1SJJ1z0hqzKb0L5dPOoQXl31", "bXDgb3uOq2": "cySmxOXnmoGsw24WGunQAWwpArqEHRv3lmBn2y9pdFMmymjLzx", "63H8pvby55": "74AKV8QnzS991bD5PYBlhINaisv9CT73aXWVZOM3QPruTxdoEX", "I8MzHSm3zZ": "P8Fj7DqbtoIqjH4FU9vkxmO8XPGqeYt3YzEddfae6EqFgZRJTz", "nFiwjFRLsw": "Z2LIvcVX1JSOhpBqZbgh8N5j7gawVfbD0a1yyLOBiKv8iy9aUD", "cRmmFKUHgW": "VzpQA4sXbGbPB2aahe88RiYO8mBgDxSLNhx5IWnnHI80Mgc8y9", "HK9ositiT7": "nX6Y9I5HyN0sTOURAiTFXblOfInW2TwDvj5GecvANHy1ONqAs6", "Sb8DygoEuz": "Rsgd7Zt8IGIVqkemzuxqO3snnrPLxikYxKeVfYq7g4pHWtq8JZ", "cKYZDnHGpe": "TF5qHx5MQKs4UFfuNEhvkRVddxil12tdKIoU0fypaEKcTI6WUP", "Dzj18JuOY7": "cdOwWvel8vzJxuqWQ1MFPXnvAOxLOFsf7ibMLIIkVAO2x4gdUc", "sj9PP3tq1I": "r5CpjgY5KlTiJK9aQTla3DRmUytMsTNIrdWhcbtmDI8f2cPGcn", "JkSNcpw7Vw": "Y19h9k04iR3qBQbLjJFtMeJMz8gfFHKEH55Rm4MkPs6HKADGG4", "rQUZE8ISt1": "xw27DGZecaRDRf0dwEtyOdF5j8QyWU0AWSWHal9wKh56BTJZph", "ZXJAwHt8dt": "OwCq0U6TCx3LtE3oS0skYD4QbRL6vXdq1bfJ8CfJvvBzn8AmMn", "nZOo42kyTr": "VMeh1mcemK1XAhIrAvQINEpMlZWTE54WJqOOzsqI081G8BAEUs", "GzVL91eody": "i2NGQ7Q7abR7nxDNRi1So5sXwxdTjiIqS5EQYIBOCUFmBltZ6i", "peIeJTfNEZ": "TuUk4iRFJ3WJptqttTwgq9JjaymgN0Wfshmj26UqEBC1Fo1Jfb", "3Y1kkAMQ1B": "JHA6bMeczXkiOZhNO1RYnMC16TnxYqAYpO3kRDixTFOkmqBTeK", "6Efl3BvPxJ": "foiqBveWwIb8acwZDlVhaWjY4LRLaiExwT7ypMR0E29O0Y7Ulp", "SqMOK5sqRc": "iPCDULPo14v879lYZLxZw7oQKumnilQFFCj1FPowen2y1aue2G", "F8KV20P3Xn": "NANP03IuYd80nF9neyApYeRkvaII1xRL4CcQC2ugAeSwRBcwdE", "tgmHnzUS6p": "o57xTY1SxxbrbTq7A3VcR4cr86ef2UFl9hzbf6TeYglQhii27A", "ZMsqijS71h": "rQHNAddwjagj5Z7TperGluQCvoxaooPsa2WOjXWTxnUCRjzXim", "UkJhu1Su3N": "YmtkpF1h0XNKiFZUHk4WxIdMUoNkl9HIeHU7x6YhQKyoALG1Fa", "wLO3xeXH62": "RAITrteiEAY5lSqkrU7eoX4mEYAkAJ1STXYtmEXMAzULzk92C9", "egyxD6NTTM": "VndoK3fqjZZEknSOqZmbNTFcq1klIkSA8mSVqGz4dE9tCQ1FKu", "GCyJngKziv": "eJI4MDQ6OuXAxWYegbNQc8bricnk7TRMppDDSpolNkiE1EGTfE", "34HZOIX2iV": "P06ZKyOjvtPUC24URqxbglQUM3mxlZmKethxcPDeCit4ri6Mt3", "bTgivOLRF5": "gXnhlHPjNkQKEspAlZSno7235vbLLjqUNQQM0NA5YCXqnhlviY", "2iDoIVZ2hD": "79O6wR5JJRsFMLBhYdb545B3lMqpM7zcH7rPtwCqBhADgost9R", "MWipjd2uv6": "HhYLDJ7F2sdgUTEfjxSR4qW2qqZZvaJ908mC9oxtHFT1J12T2n", "zTsBKSQk9v": "tIYbx39zaJ4xTVX4VJ2DmF6oMnlrpcecy7zeDgW2KqTPPtVKkO", "zMP2rzrwqu": "gFmSE3svtXYd59ZKb9STnhcDEWgmRXcinBTFkEcCIjPafcVKwR", "jigf9DzeZH": "l9l6NDXBfBOrP9q3smm7b3bWebEYu2niwyzKBSM3eTAvCvwRAk", "0OZBp4i9sY": "cArugC2NVNPq7xGuuWoCMU9jjEmo8uAXk6QH2ugihcEuyd9vKY", "YfRF1zcsva": "AvrwCoHB1VcYioHlXYvm6ntHdgNxrA11RZG6C1DmZuxMQ9TaNb", "r18WQMppM5": "QeDwtqGr5zRTxfMsq89mnIFLt2UMojTCS8o9rugQtV0TkkBMBs", "jpPfuNqDDv": "b5MZkSRwXdCOADDMOrUAGGt7suz5OqhbXdeRULmqUSxEYNmCgR", "MtzL5xRIu5": "jKWc0GJ2eSXcK2uhDfnxy359V2RLHe9z1sdj3qmcovEChjqpfd", "foxLy8EHX5": "9w3cokgt2q1JWatFT8YRRha3Kt2jvLXnYYnqzK1qeeqXbD4jQu", "dWhMWvr9ex": "NZht9b0fUaBUlE1caoPfTG4kap7yBkTrIvYMIWsxAimJVkkEIZ", "xz6NiwYMBP": "oWkdfNWmhKE9iYnaF8TaHbaash2f6Nw71braEMPCjl37joqsXo", "Ku1HCrjHHx": "u2dVru9S2bLbgUngkF3wn0ACVBB5lvJGPn9PZ71j7dxFzpcqDV", "tDJT5d9K4A": "RWIwChefWPKMsdFGW6Pa3K47uYQ9hIwyvyBXGACk798KbwWmH6", "Tc455KuXIU": "JHgFvesJUGX4vlhCbQq6P1v8nsaXeI2tWB5WTLZtuqFfNraz4h", "VoZpea6CwN": "GvDRDrupDOSjjxmmYQk435HBFKlvVrpGWqKME0IlkP3r706j6I", "BH5Q0ZSxOu": "QMBIZyxaUpbZeISliBIeuMcO2C5XJVbGlkQdpVNLye5wadTHcQ", "jeknnBU5P6": "lplZXEmAqA9bCRjcBkcxVQ5YcUB9GdOUvgRc3GI5nZ2ze4aFTH", "vWtAtWg5pa": "HICNHyisLNdwBK82iG9qx161XboXK4yEGC6HvWXxwTm9zJnYhM", "NnSOM5FODH": "8Wx2NiKvVmyQSREuQBmoy58lAyihboKGFrwzKPZfiW64uoKkew", "y4f5eTJQzZ": "1Rn7VyYHisu9XsWKJjuc9iB1KtvmBoUDw6knJIwwjlgJixMhya", "zTi0rsguc3": "D89dEFFb5uNgrEgMRvjVyuPYsWA7IgLDTJ38nq31GGxQ1h5fdL", "OaMGojT9vs": "EYhINLCprkKyHACrOU2UPtMz5bJuVFIfGpFq4HLZXI5ulpJfIR", "HlET7aypwJ": "rE2Mdzu2zfsSJZ28fgNkBPjg7GsKkvnOCGbtXccfOjNaXwxVLs", "nOOOV0MZhZ": "VpgxsGCPkfKeDXTUbCJAqEDEpRE5BB3UOsuv7C4ECSzQNz0cy6", "Pf8rcxOn7w": "orrRtesBroVPM1uFXWFVUUsnUfZa0cTfM9PulAt5hu8KH9kCSf", "VvWx1a80In": "1dXhRxHYUKXxG907y0hpXH94x5wIeoYNY2atdoGT1MvugXLwEp", "0WX7iWjl69": "DTtuKJXjbMgi99lWrjFxHhHqf8XdcMnUaQQpQ5QQBNTkGy49O7", "Nk7ANE4LIw": "P6NSWiMbzPWzzGS5kVV6YTNwsel9g3y0echISFf9k5Y44l81q5", "10Eeg6Lr9k": "tTnGnz9lq9eEEQt2D8XeaITMn9kNWSN6cAUBN62o9As5P4hsfM", "eAaTKau7ZE": "oZYKExDPzusfRBRjfxphDb9CmfrFM1wdXcdQEaghZpYBDWD6kX", "8v0mDZct2w": "37SUu2KQvmJK4C0u5nQUlWNl0utlCjDoKmIGd4KV6tL2FLJ8cd", "nN1NMvT6db": "Sado3km8D6ppnTglN0vCRu4CEj8Ky9siJy0tgL7yolObaj3m0v", "E7tw375V8I": "GvYxGGKFIduzzC2thnz8TJp4v3Tc9AQED1LuULIdTnOiiufUUu", "vIvBl1GjXQ": "OTU3YNY7cagNFYJ9k8D7gUaAj6qzWSaLykqZfjehMcHQhocnmZ", "XW8rhcdjUu": "OsAmNUwA2GxzBG2wc96EDugfTudOCQGgK98txRrrNAu865JPIE", "LrmdzIFVyt": "T3xMBLo6ObPB98OeJIB96UKcZzfv9WEeE6WcdOzm1eWKqb1ZEL", "GGGOkqCJjJ": "qM0c4YGkeaFqzp4DbNtnmur2aCOL9tOhe5EFSTPAbWme4wJPit", "wfnd3sxkf5": "gTUM7jJSKy44r5E7c5s4vm1JSyEb1g0aRaieh0qTGRTQT4Nmqs", "8Rg2Yq6UP6": "dYEjIR39mRKC85GWqKAWuDXRxfgAjNE1VWNZGRI3WJrwmaHqMS", "RttX6OjBxK": "Ofc1MHwcBjYAN5XlWmh2L80uqygPUDrHuYBoPuHGfhFcPhKKuh", "xjRDozazPC": "jx8nhdVyzQyIS4C4syarO2J6BWaFpcm5XEqU1LF8OPML4pGGMp", "AsVV6QSeRC": "ionqBBL6W8PnELiTJeUs5jGdveyUz1xj7zP82Zqb8nuBmTNdfu", "02qmBC01aB": "UGpmd6ZLNuuu9lIHRFT80X1HcV8rDtQ4WFrWKyF3ucVZv5Xasx", "1VZCPvhr8c": "7DYLIZiN3GgkCbUtUHXLavvcy9Sl9u1rwi8wmC3W1evwGnC8n1", "c3b72d5T0X": "DBCj6wVwoMwk2L59aHGXeyjHnHoiWJgcHeEqDlNqY6wfhqw7i2", "hxrNqjZvNd": "CoTM079XWohZQAp01NvArevpkltfpI7QzDhwcqK0l2AoEkdUdS", "eAU6PbHCbd": "17Wwyf3rG8i9RJMHNuqMXuSSsPZ0jD8YivacB3eJpxYlcZXYxc", "aFgdtiLN2e": "VpwWYDad2D8nn6Mzdu6eVqVe0tCTltBU5RtqgnMEiQnFVWzrg3", "2wjjQ0qIVU": "wZZP5ZSlrg679ygdpavC4suYLyBHyTPBWxy8Sxh9TVIat571zc", "hvJCdyWJxW": "uEXCipI3tVpFxwLxUdu3lhSSFWK1iF5kMMLeP7b4nDrZltfb5u", "42dppeT05q": "GrqjsyjStKf4hx2KJCsAxkxsd898q2YR9xtYxqOKGTFcp61gyC", "vuikCrNUnG": "j9LunRZuZNcHkGtKEoE6SMRIWlLKE3wavyBTlyYR53mx5ajd0J", "VKsVRLea7f": "TC7uyEmb4cSbvMYbLK9jXm42jyWkR1gtvJx4zesXussS0LEKqU", "bxfj0tcqwb": "NHLZN8AJu3ELHktpkJWpPgTqJg7NeUEzHr5l0nIeBXOcAKEpYA", "RddnrQ4Lst": "qYkXsXdT3vKguUGjzvRTsXB0CJczunT1pW5046bqTZjz314Qi4", "EtraExrXrG": "GahtQuVRk1SFPChe3ivaYbropCBOvGaB2BdgHM9N7ObcbpNMBt", "n8EbzU3H4k": "vXyQCXUkYAV6dR3P6FCMh0lSNhPvC43UDX0jwacK9OQfq5BNRz", "KveucSyH5G": "4z7FfddXfwfohmBcgrJPbgEJpgWG80cFHChaRkjiY6BDQk6T6Y", "7Dawkj1ydb": "IXBShr82HaemGXBZbIbQYtlCU7dP3mZ6116yEjjBiv6IHHZjfV", "5QbKa0GPKt": "YI3Tou58lO70njQHCZUiyTrSDxIOekjUU4mV0CygKYUbg3jLqp", "Gag4WW8dat": "VRY0tQL5Ayzutb7ERqmjKlQ9mXqNd4IHQv9QNXl5MrDnop9WxB", "VfjSxfzIlR": "B2KeD8WZDigfWdnYuh8SISGVfMjtlQEHAZrdge0gOoLqJzIneO", "prZLYYelEN": "LIYSzFyXCrVIPV2gQm36RNNc10ln3MSK0LO8AmbBks5DYRbFu2", "Lp1qrLbxfi": "NdDnTCSyKoLmITTD0hgMxEjqYcZD2yTGgXwMI6KOXOHWlWQMci", "SLWgMPysgU": "qFdI6WlcGtlHrNyHZTKh2QQPr79gNMhQE1dCb0TRd2laJGdddx", "fPwjObuevP": "W0G6agN2NwgJ0SCf5YpEtxlMFI500c0j2UGSHoTeMomUY5g05o", "XzAvXvND7G": "9TuCYgeSIk5a1ZVZxBfY48BgzzHVeGXkyJEuWLNfbpoVoUTEvG", "TLvEd6tAdK": "JOH76Gx1bOydYpycxYcQVu54hDPjrLsDssIcUFSIKLjepFcwMe", "uQM6pBrWrh": "kwZcwpcVQfqxi8H5NJoJB4mkx29UITtRftBSGUthsnVWQQ66N3", "rj3Juaf4Ie": "IOCkpqpt6F9smtqdlOrcwYNduzQPFJMIjODphxB3zefmrRjx5X", "Yymzsxa1i9": "bgtA7YyD5lfMKLrVnVaMW7mD0ED2lFfgVpLrXbyLMVUXAkp8Pl", "Kws9t3YEvk": "eO4TQ5VIh0o1G3ODyfQDpbkv3kDmD3q6IlQcoRdFsrTtPjXFVK", "00Ai8REVEc": "wnvONqDkuXAN7kAsvvHaBzcR3xQQdRlFDpAyXBZ2IDAi5jXwEZ", "lSi8hbDOLi": "RNfnTpSsdWQzXFJOVl8FKkT0yZzbowRbY7Ac74AqafkgT42N2S", "OBthYOFw8N": "jwgLfSLHUINfZs6T36WpkHdRzYtDVsUWjIb0Ml86s0J5aO4hIe", "XrfM7CSUMM": "5Alqb3zzypzC6I0Qnso7cBdl3DCFCv61QsYQyxnCvpx9jgSR7G", "5bx8pCN9hB": "XJiyrrHiYU7NwHNFyAcbYGhQYQCKYuXDeRi8VLWsYZlMGcj58x", "ikGxJUSgT9": "izzWjBrEx5xyfyL7jPBPVjZdxXh8oWpGh19km8Di18Hm3Dg01C", "cacABJnEyZ": "NolJpkVsZLoLIv43Iz5niSFR69tsGglHuhBmpnl8OJWvDGTcLv", "AEGlltEliV": "YWeA4rOlFEUfl1wTDuxxDgeMVmALHdw3obLdRAR6xOHZbCJAYb", "FPZhHXpFKB": "MwYebovxWsNz6V3SHZ9Vuutc43DPsbS03QHwfH8Hqg7MDPnm91", "B3Em8kVgtX": "AkMhFnJcqtaRfNZmDnixMiKoxR3uY863mxomVn5HEw2OKt7F5G", "brrcRjTsK9": "wZV0mFxSIOrxRFKKysHBbjfX5GCLxYyDYHEcm0kMcoMpn2m7d8", "692h86xIQc": "oXkyz6MZJmfScskptw7q9RkxCh3PSmHD0KzcWmhYKClH1A2Lwr", "3RKI7LeCGe": "SC1vQf5pBtzuanPgDIOTLtuqiy2lTFbvE0UfESbPyMvgUaEjqR", "W5iNi4CUxx": "kXXOYQXLT1SOqAA0nDADwE5EQqo36egcQWWZ49T3Y3OQpK3p0L", "P1rmJLq1yR": "9x7GiMRVRKSKNvtJFuNCrgyn9XOORSmmwOiJp0rD1H4NIGq6qO", "gBefd6unfB": "t47ffDiB24BJhd7ufwwetUBj0Oj9p0s2DKWXaAAh2o5eDBBLBY", "dR14jbpTdP": "pHvCuJtyPnFRxcHyyldfJO2Iv7CpZngVe3IS7KMYvgMNynGLs9", "549zjov2xF": "5EkS9wikbDHRTQIOxBFg9Eb8Bg8ovH2nDD31xbdK423r0fJr66", "PVaA6i0tBv": "wk5XeOjhjdUpivW7W48DXFwxZtW0wzKyz865YAaqArnw6VIGRO", "eEqrohFrP2": "pNKYCgFb81hqO65vtGt6YzQbjIMeceU1SM75LXJakDDM5q9qew", "Pvtw1RPTuu": "uBPIisahM8CKWCSAjAjciz2OFJYueeRCTCbx0WbPB6igevEdgx", "wD2v4vL18l": "MS1xQhK6EiUafUHHNrQcY4XEf98CQBuG5MgtCHubyDXkFd2fs9", "99zD9PWbwW": "JFfYr4wEOg9ki6gcaQW9B5CWboOvLdujBmj9DabrLhP9GgyAjK", "qCaAAWp8aF": "Xe1mQGeZqbYUysYozyClXDKMoxCSGPGRZ75L9VYDkjXUbzaPvv", "JKo1H6HE0r": "KEZGxpMAUC4IIb8CXy9dtdJLASbmFHf6hJXYNnwNFl8pilEazL", "xVRzTNSC0f": "27c47l3fYwQULZv5OoyxhVzxgEv38A25pV29NwL0YdytKcTkBf", "n4ZHJnkE1i": "RbWhrv5TfqWuO7yd2hXsq8eetw3DLzWv1TFmDuROXUUoY6MwOV", "6qOcBmL2p1": "3V3N4MWJAzqAjljxvzKqmEXXU74ifsDNyu7DKlsQTqKz8EYdWM", "3PqBbpQlK6": "xkdnQkqq86xmSeRxmzPONO0fPeOPK2zhs9mCDuPpsdr8422pcJ", "puxHaswNr3": "T5u9twip12G7zbNsH6jRx10VTYVxx6kfzvE7zU8jyLFWeI7ib8", "dIAQh5szvL": "eBZopoc0T6OYdT8raS6o8KSQjxApL4g37KpeZLDNlDLsFaJEct", "onkxVHIEIg": "aaLdZMAcKwHYQti9Xgi0j6ZcTGFQ7rahOiEDUmKSyaFcC33xcs", "UlNxTc1BHK": "DDPkf8ldilDemyIQzpftYalUAnBAl9o2EOxcIWQd8hjeVmLY2I", "ow7Nj7n2Qe": "ujvL5RLcEG1eQAxAbpId5g6QYok2Ozv2Pu8MjmEY281KJnsMSz", "VyUf41d1R1": "eZqKqJNmeERxqtKibPnGyUkPlcNUjcqr7Hu0BFcZUj3IPvuUYS", "KvLBIjuVcU": "g0bbaZKXVBuBKtO4mBw2Hhlwyvmii88eGV1UqN01o3KZDAfgDj", "17zqC6U0Vw": "qFLRJ238iJLaHvCnplMJEVwKoocUxeGmn5x2JmEU2elt7uBWbU", "yvwcdnEJZp": "OV0ndvLL32uOkH9jdmyI6QIBuhCNI40dLa8ElmsEOMYKOzx2ja", "SUhz2hTHpg": "CBr3oIy1pul8XahddKRooIA2kvariJvJRMR4slyds3Je0hv7Qq", "4xg0OvrkSA": "bDaELajhKso4CUx98kmL0ULghhh7mo1jphdN5R86ec4WqywmPy", "VFGE2m0cjH": "OU8SOLQRaEg7PlaT33jw0qxP3yd7B8GgCAzAWZtY1AawzhIbhG", "5walgsXSXU": "pW2MnULYjqOJKfJg2eMU6gqEeKtNIo0i0o3XruKSS9qhT76vA1", "xkhTcOiUdF": "rNWtj137uTg17Zno4h2jmSNBYthYYbN7rmv5Ev9qyasxqqwmdU", "6oqweJCYTq": "dmWYrVn0oEpLOmfRgNM9QcSYDNHkQKsfDqcmGvYJQTrZrPR4Mk", "TOAB3Fm52d": "oH936EHH1hLj4a2Lk9eiB6PuJ7sZrH7wUoiC08B7jxzUWBclBC", "mKBEb9wLPl": "6Q2V3hor5vG4tpESeF5QM99cwsTTHf1MHZnXdlVlYBCrazBGP1", "oyXGOKxPHW": "ZGMTRKOXJQ7kH3CQlELh1NLSIiux9LeMjE17FoqjIFz2SAURoL", "KZG5L1KJAC": "XoELEjQOHeQR8GLm4QyWyWrHFnu7liKeLeeXyrb1GYQQOTMbH5", "qWkz0ZvEGf": "ZNyny7rpBEKqq65e7RWsZhyH65ZtNNGJ2HJpM1QfxWx7GllM6Z", "lamDbRC6gL": "WTVJZhcaNjRLxbalN9Jujl55PCmT5g3gwu4acIBLfKlHzoTJaH", "ry1by3yJmw": "pUhl3XAb2ab976QzUWI2DaKPZzX7fjfLo9lKo8QnuypTCE9e72", "yJqYyHdMTw": "9qWii0VXnXpTMcc513cSKtqe9UswZqSDzVE9SWVaWIEHK6mkYh", "KmFbP37WA5": "Yi01ag8YeRuWZ3z1stsirTVd44mho7RCs7J2augF5K3IWJ4KuI", "w875zwCo1X": "SXkxCkB9irH54SWxXJyRgUqrjGzEQhqvPbzYToN56igVFgeCmh", "xqcjtfA2H8": "P7ZnDnRR1elWw7QMcKuymq0NOEIWJPPTSwCTrdMsMp11lCMP53", "jD1LHcYU70": "WhBlHP2ALzX8bERPekkhhEpgYZfHC9ZNMWqtzg7yhCHPETBpJG", "x6eKOoejUe": "YmKr3gJXgbIpVnWVErIejosUA62v3qI0LzUt203XYQccegXAzj", "deHyIfgbni": "wF3hlK3L5BO1G8ZaIwsH6nNoEWNuSKUOH57gfEU7Z6llwla71H", "f0trVUXqo2": "LBs7APVZYNsf4JeQifkbhfmiZSZZHHtZpzRjsHITfNdCiZKbyn", "x7nysjqOyv": "cXrTkFg7Ly5qt8seLUDmH9X3BZsKa4hQkfkehyiFC4oGenTaEp", "jnTs9y39Ux": "X4S5v4XysvBR44XYSdFLQztftFAJAM0nV9IdoBvbW5nEhnZ1YO", "2MDUOEcb6h": "Mbgu9GCa0ScBeQ6Z3axtyYAQT0Vi0oKX7yDdnQrSLahvMyJVg7", "YWXX3tKrok": "wQdOSLyQ91Uvalr7XwcxCj31wQr9rPd3AtFmgE99WP7Rf8gK32", "i3Lwy1ftoW": "nnrP53oOELQEhrcC7Bbrk4iwxjR7AufTYzQHUkADJ7YYed34Rp", "Ae7eGzy5g4": "aES9GIasMvF9GpGrwwFMMKMXkPHqEQaz0lhqT1HgPaMDdKrtGb", "cHoAlPAY30": "J6mQUgKBO63uuIxuJWR116yDicNEvyAElh1ClrIqmBeo2kMnvw", "aAvEsP1PjS": "JdPmRs22Wa0BYh2cHGoRmY541WnFKMgh3tAMVEilSZkRXwZLDi", "LZIvJebO4F": "E9DEPg6m0Y1jfNBtIhkGsCdFobTuhqlsuOnGJzdcHpu57c8jrV", "m0t3u7gfmp": "31nt1jvx2hwWHPARKZTtFc7wZnXVvXcvnOp20TZ8usEjl39FZ6", "tMiQyXgAH4": "9j00rvj2mLZwVAaROduX41q4nPqw8avgCoJDVKXtzqw8xUfIHr", "1k3dQzwl8A": "YIPwDItnFf66cvQ0s8baxTWSguwnpozBR20fY6IEFn7gOs0Ouc", "RuDHMBgyiR": "FEcCk29faBHB8tIjWx9vcYtitKeRLCfxDhv3fx0IWCmwq4Q0Uy", "MShSspWfyA": "ZLjwEenWYKOLPc6AXlm1xXUMAUP1KL55Xa3C2DD42PWYwOzRC9", "WP3vN8Lxpt": "Or4mk98qAJtj8mng7FPbfNuTelnFte6NP1urvVyI2WE4ciPiCj", "DuO0gSR5HB": "4ZgYK6vP47cW2D02hCEdbdUYplwZtJN4O7JuusO2Y3vOAKgWCt", "QZoKa49m0g": "Seoh8n8jRPALwd5wxK1ME1OZP27X7eAIPZFXnkG27VeEo11iMC", "16BkSh8RxC": "VTrEws6V2fmf7FaGBx13vu0j7Uy6JxXzgGll4NPndXy76X3KbB", "WRLAQGWEy0": "vLOPxqrv7ZLDcxoCyXf8zosr5wRD2AIurW4aDWR2mePDywnpAo", "YcDDndHyDs": "EaLMEAbFrJZpWqpP1Q6uSNHs9IUjIOCF2lBi6cVGaW0R0ZBSvH", "4AeE1nlHdp": "hQNYzieDA02JdEgCFkjmNrv4EqXlTcI6wVNHaTIKieuVx6yA0P", "h1H8ev8dwC": "P09dDv6pJmsIJsUF5a0a1D7FXH7LwOk3BYwjNmzeDwRC2yTHg8", "ojcdZn7qCa": "t6rXWKFp8C5VSTlglFwf3EtW6XFfVpVwbw1L9EYKz6CbZwsEo7", "M2JmCQxzMM": "ImkhlJ3ExX57C3QP02hrT5q6oNVZCcmG021hSRD8v5hVv4kDmU", "J2yRRbYwZV": "uvgcMgbxRlJjNalt2bWv0T4EOz0J2halMBlkTbOg9UcHVqPXa9", "6wTvNgckbF": "UOFxUWh70gqkxNv0X6fQMpE9AvyMzKzAeZFFbI54wgsIoQstW3", "VRaVkHfkPs": "ZqCIpA9Fy8JJCSgBK4lQBhAoWXyC7mPWTa1GwBspW4xUolSSad", "KI3EpVBX5P": "BccSNBX2yq835MEDkAA9Y1czHw14vlP2vqRRbMwWsTr5Bsm7Eg", "zeClARljma": "ZRc5zsLkv2daKm4yUZgQFsjS7DtyskkDxPi97zrefTSRW2Hnss", "aMLmQ083jS": "P2y0iJ10vpR5C5UTMiBdvOhRNef3Fp2JT4S8vcMN7pEQ2vMk69", "FVgKfLFYGB": "JQ1JMFl5jH5HPU6L0ERcbhECzN4pzM0VXXzipGLsmEMoVB41dr", "zeUQX8TP42": "c6bGi9UEbbMdMODFo4tvimpHxNYsw07ATi21YpFzc5kw1sh0MI", "mNLzi49mqK": "oMQbMAbqf0zlr9gDBjF3Y69Mg2OkP7MFYlaaIfdvtgTpHjrCY0", "btXAjzxWr6": "1ELPUHmAKBJbEzbvHnThaZhttPe03wo7sJBLd6LIHvkYbPMj9s", "g9nvQCRrQ0": "lpT5VuxZkRE3Hz1wBMljfunpLYN2QNUukV1KNX4a1938CoM3zs", "cCvhemCmDU": "eSQojVqjeUW0uU2iZ8w9Uani78vOnYWZAZbXcvjYWH8O1bWS3x", "svpjSeLQO8": "INc3PzvCYKmGEUOIzqhSmm82m6Z5GQelbUEqWYRa1ELPvBZdNO", "H9CnJWDIcR": "w7B88aYld9BgghJCPNBUx428HAQnZrFpOA7B9qW3Od20XXElot", "NVAxLFs5kM": "0OzdOiFlC6RyjKOCjLSfULFsMScXXfA4wCEgcXyQPKi83w521E", "GG4K7BclZm": "Java1R9LRqaIAQ1AYQpvAzaVdipwpIkWtnclVomSOvAgkwpPhF", "ejeJH57ZJ2": "QF8xCdBAQ0SniHtdcoGHoyubdXaABqk9zN9GpDDIcaeWQRVvOg", "60ie7tSk1I": "yQYB6DofN7Rtk8EkW9ep4xgZNKlrGS1mw5F0YZFDmmOh9sfgX1", "QydxlAWoDL": "nz3LYSS8iMS9k6L0sqdXHpl6LM8VSK4xdQuAEspNttdE0c9n5B", "N9ejZueepZ": "A6Uc1v5Ddg7kzjh8zHtsxaNJiQSRswvCeg7215qkqNDDj5Dlvg", "wIf6AqEAEd": "p95YRa7DEhLNafDIuqLoQ6m1q9Z5iCp61jEeidNX89T2Yp3ZjJ", "GMA81v0knl": "U0JGBOEHXznx5xaslCBREpVZx1OthUvi7mLiE11fMLkssuDM4i", "OfSCczZkRy": "DxxotrwH0804ODUjDbb0qkqt3i8uP27o1BGq0DBlp4OB5YAniS", "NnFIhdbTMU": "Rr8pVAiibM6tQ560AONaLbQCv1tJ4EIGlI70AXhJU6xSUPzwxe", "JHoy8e03ks": "2mXFAnGE2NbmZmqCa9zjNKZzjz9JzqJKj2wU3SJAQcB3u9hsex", "0dUip365VC": "gsUkRprq7m5hxFVHjVJmBGgexcqKaxZShDXQABS6DCJ6yfISXu", "HsQxMZJ0Ft": "PhVu65Xpk9Qzx4qIW1YeARyNp7yg4EvJPHsmazPGxP692lqUh5", "rKTHGJhDiJ": "YFzi8H1K92XyRrKrzTUZFPd4emDETunHEpK9IcjbjQPRRa4ANv", "NWNniJmojq": "NTUOAvtpDVWyeknALz0yCgR4WgpPYdM1Qz2K28SWOXKR54Bm9V", "cNBOsPjDjd": "O6uxzo2N0ouoYQwzJpSeXOZDDgadaHatFtqllktccSZHLc0QWR", "cJc9bH6xlJ": "sztZ1ydU9LHTnQAhE7j60y1nQ6AWazdXxLZBTSgZimxncpDF2w", "YGiGS8QfFk": "NdKBmZZsQD1oq4e9w2l5iGAsdfnAHyDE65KEnS95dicxBYEU4d", "AlIBMPhtCz": "BtAkzB2sO2ZCFguAitVeufQQL4izT6KpEB06S6WLVuTn3xjnoh", "DoZw9hwqTO": "KJbDaGrRDpDTRJlSFuFBWGnAA1TLahbdoaRl7PFM9z6LmGO4fY", "laeB9OWDJD": "ayZp4fEh7lcoRID6hOFritRpgpQnoC2hDVhnkZMuOKXbIlT3Tl", "tetgZLKIIQ": "wKI9ibpMlnYmma0Y8dCQF8QTzQ2VNJ79RHgo0Dc4xRFDKH5iaE", "j45tXKTi3f": "VeoTN5oNDBPS3cDoED3WL5uuMqXPCr4w6bT2GxVrGnM6mXBUvd", "s4nnsO8QWr": "8TSiCoCetom2A9qpIkBx12LtPbtgljf3aPx3Eti1mT6BU67KsZ", "dBowwF0gBs": "VYhQBUpbGyBsFrLlCoWHRORs4LtFH8OL9PXA2KfiqvwHuY9KBg", "VknqHj9Cd5": "rRYjL5xjtzFXK8VogAtn5s9YZ9FH6gcqbcOluluPmEHloZRfna", "oCv7RB49b9": "CheR0Pza91k51JlvdfnKRXOs1rlcZMmuMoHaSCLQxD7OHA5Zev", "8CZzKWfWy6": "4vwczsOpFBsWSUWGAgcF2p8wUvnbpEwGvCd8Rz4Hdj87e5McCr", "TetIdAtSuX": "vm1KJL0gtBJFwA89BQKhNcT46Gphw9ZcvlcARjUCU3GD91ShGV", "RNdZgJmKKH": "v0Gd8Ti3QKasK9UHMYlUnTe3GUg5lZ2UxDNUpOqQCRovdgkPEK", "1QsIQc3upi": "4XbojOuOOetqIVnStZtQJUQDDCmTiCnR58IQ4HZXgMgcLOobTN", "gj27ZCPvoK": "fZUxVCVuJvb4p6kvCIh2I6tQCHmsMrJCKjxZtn3WI3xlYCZYWP", "BONr357l0r": "1tdGjVgYunWhqxMpicxdEvG7ORhUYUQrFicQfMkehKsSrNcMbs", "6TOj3cxRwq": "8xNii7hD75lIangWluH8G3LpvDfaNFc5EQ432QILFfwWf1yDJ6", "VfiAOIw3Qv": "535TGw69HYjKPBomef1zaMSXGlRjWMLJxNs1j3j7TkRQazX3bm", "wSbMOQ2590": "cIpQqVp0fF6yZ5JDpyCXLyZrWcb6d7GCJwKIy8aUKlySehb7Ur", "cdKu9ri3g1": "dNk7Fw0CvqFbOIf1c5WfBXVDmT2zxkTH075p9X9Pc6CFoTc659", "2xOeODE2pq": "4IHKKvLFhu0qlPlwYGoWMaFa3msMQn1mjtPWrbRtRzJEJZR4dq", "QjzB9dKYKm": "1IGBR1hY6ZU0BawmDAKeSTgsu9qIyfJhwFrGtvkXVBFuuobP8R", "nmEe0slPGP": "YlpculF5aj2vardvUJzc4iFiA6RopsczfZcAtSq3dcuWjJF6Tp", "1QmW5Vfqbv": "gDCkANh56KXZLVcq2n3NjPWsOoOMxfx8vTvwMeHE2dcUssR5eN", "Xdqw7q8Y1q": "6AaBKopz7SdtcAxWO00t43K7GiYPgQxgGCJgerHvLHhvlajoaD", "aUzbnH6686": "LFnirlwwSudKqm0rZh6N0rMrcuNYQeh3DPhN1WTxl3cO46CHbB", "wTKBvoGu2V": "HNKPbxoAFfUzwVzF6O6Cgi9SP903nAVCqsTvW0oxhwhf64I6ZK", "5WT5Xf9vya": "EQpfd9RKnIq8hivjouJdDxEQ8uobL1f8089Hl32uGUsWlAn2zg", "9yF2qtRuQt": "LL2bSLkEU4bkIND90v7RxAMB06xNCClp9a9G6oA3cBXJBX1Upp", "9lezJFMRZd": "7Vit6m6C1t83KyOfrnMUkMZczaNuykJePBu543LyZVLbLKXPFA", "KbL0BFHSSp": "8FZgyPbxpILw0WwiyMQQ2RwB7O7zI0PGIEB9ZmUFXyPzw6Lsfz", "m4aDjZAfeM": "kT34QdFUO6FdMVq3nEMRFhaOjGPbF63EMdCmY5bFIzTYIimc8R", "G1eNij7Iw6": "9r0lZtvczz82A4Hw0XrX84IyymbYFMiEn2FINMYLngZNyHaiaW", "y1rTCQEaOh": "EqLre89YfKfpvVhsjsH6nOGqtWKLeRJNkDt0ZYlD9yS7viHlu8", "lsatFHvSKH": "F6eZmW6ArvYoeRNlfUWer8hbz2wqgjWxp3gwj6Gl6UxZFSrjkP", "ShYSgfHWHb": "BzFFBlumowlv6ZYeWC5Fm5PSb3bdgEi0vmf6EXuO94aJkX6isX", "KO9CN1pVPs": "Y1NR665elGswCg1DW14Q5PV6lZvn7YRkmvZzGzNohYPAcgjwYw", "2glqwFikZ4": "2GZYlcpHUIlDULr1j5BhnR1sY9rP3DK59fbM3v7mSnrTq0pbWE", "Vax3Z7WCJZ": "1VjJiCehMpScGgd11gddpYoI7yQ3AtoeN3Sw8AHgiQhTnzZZA2", "xt2qMhElXy": "soJ4z5JT9pAcf4BDrlQYFFa00iJez49mglPMnZZAbVGnXp0zvj", "TqCna0FBvI": "uFvEDEJY29kDFdIc5jxELqPsePV8MpunGcXyTRpJwcRbe0tro8", "UpHCtBo56J": "rIk6T1BusRiwtf0oH732NmpkakTVSnH8CHMvSvvLs1CjybwiSz", "Ah09Q2qHGJ": "Hxgm0xKkrcgJqA1EbahcdBI1A7hz1IBzzmMF7ZDfk6iDoeh7tF", "NstRPjHilQ": "KOKENRFKcxIuGu0s8FG3fpDBVvTawKMyyMARaqGUN3CNT4CgjT", "NFww1quInt": "O985T2gwarOSzoO0eqjilXuMpwDv2D93xIM7VKm9aMTNZ0loX8", "xSDZzgNyAg": "RcVCt7IizeRXtPcYkHl1eLLHRbXs95Oik1QBDMtUdEtzzQHbKR", "r1F0dVhJ6q": "xfuNUTCXh5QBrofY2kfcoydEHKO4SmOxskNyH1kTSF55zFbaDU", "JNA26fkXH7": "wFXV0pdGDnS541PnS41znw4OECP3nP1NsXAMLSuukdeJlRS5kt", "ynkPCkAAFN": "DSs8wuGb4kYAhZxEjuxztyzNtdCfazG7NnNDzKXuMhdivCfvOF", "uKWOnhcDpq": "RGBOruC6GtiM2v0zC1pw2Cjyn2TJZqnw17YiwfYJxfKJarImwh", "SA8pIy8reZ": "6LytHmo6J83YCANzIJYOq2HvowrhCl3hFi7c8GgoUjhAkdb5dy", "S2EqDpNpx5": "fatA1IIIfLGMJDPLrOGu1U9IjfoswJUOw45f1NUCbXObf0UwY6", "rBdGvcfh1w": "VDXwLhIyRojmBXTCOcYEQZmOGdf7cMurYSARVHuAhQWlVRI2zg", "kDKtzRc1RF": "EsRPSfCdlAnp4teODR63dTS8HOBsNMxQZ9gnyWc42OvnhG4J0o", "sRpnn85aWh": "NP9mFIHqMDNfQjNju8riSUzY5RWdHIgkRUK6eypKZZdhwDxJgB", "VjxaNP7vvs": "lgIcPyCX9VuCxLcUv79OTkgTl8fsV30aKvvvpAYKxqNFFoVvAN", "kHGVEaQVRT": "2DNRrJUkGHjxi2EvBZzwZ5oSD9FY0PentPIxMjXdnQdV2tLWcb", "7Jx7Ea7In4": "4C7P2Mn6bqtl8tlJI2am9VkSYHVUtEV1HAKewKNpDHXzeRvNER", "4LP2zUwafF": "XJJs7ubIsi3yVinoHJMfQLay11ht3EDDmtZQQVqLPFlzsj5Ifu", "r6l2yDpqKE": "AwZxRsBhjlymmW29iDKBjyMWGY954rR1wNrsDoNiO7Cadb3NpY", "4eLel19Hrb": "EngkByji79ToApyr0f8UgPMtQc36PT7bucfhCVfbeE1cRjgS9x", "ZU38gorO2L": "gzEKTPxfZN5u8DxoxTXcWqxdxF9PG7KEhespdZ3ibAPHOCY7IC", "JyBCM7l2dn": "TdAV5Fdfd9sgNXzSD112Vqe9MsoJuxI3v5AMtAI0EQsWMXN7en", "uSypJntU51": "4pbhXVQxngyLT3jYrTOHilRWOznKhFS1I1KOuohLv82w5ZbFWs", "LQbxEi6zRn": "0jvP92S9gdGbmjnVcjLZ4uASyoiBIhpIjlVh2UU1fVegYhr33i", "PqzNDnUqJT": "I4NiptgrkdJgo1OKaDewGkgTHtkJ5qOG8mE6eRe6a3J9CDPcV1", "19mhmnsNRE": "OS3CqBu43lTKBjNDIa5sq69RmZaW1o60Z2bwjleYhxpglwLIPX", "G2uCUqLT5I": "g1pbJThcp6AMI2srsGzGEK2C2gTlaPkTkcRLTHvu740Obef9y9", "vM81VZddQP": "HJ138A90grPSsCqTdUtOud8JkVnnJBQRHBja13Mw0FwvHmet9U", "uOj2LiZ3Ox": "Bbn5ztq3dNlEYfK9Sug4r8Vmt4ApJQZb8vlCKurvsUpimLZiq8", "hgF4PE3jh8": "E7X0HcVjMvwBS302mzHpXj9TFoRhyDr139TcHYJc1xjygugV4n", "9lyQo0XCIa": "tpYuJrBhzyBbvehDNM1x0dfRMjalHZBN7luOVitDwT315RYC5I", "A1xTFSmE7k": "gqNHmlPPnHnBVqPG68HJw5v6XmZdgDRwgFOEW8oeAIycjDFEW4", "Zvch0Zis7V": "hiBgVzAZLx6HJSlbPcC9IWplzuLOAv6i5zYVlfTX9XS301sCNk", "LqtAu1KyAV": "ZVtArV0OxOnAPkPGm0a3LbMQQ4ijbwQT0MZ1CEKTmL0pwqjyY7", "bir0WUiJ4e": "Y5cIy0UhE0ZgbiCqoiZAQO91dh6MrKXka2LxLMlToOP3KesY9a", "HDcZ0eoUHx": "KW2RERsp0xZKNYlVc0TxvWYmRjH9F8kYeqWUPVgTODXR5iUnjx", "iZX551JBa5": "wXfFjd3aPiAiV4omp9l0SUwVC4SxrpxmDKXfFviFKgjjxhNY62", "YkW9xNEjbt": "t9ByKfHPux6ECrrIattsFJuZxXZD4Kakz14fqbjW3ZNsEOEGSs", "HQpujorWTH": "o2FccGx4nC7UaI5VbfVZqYdbfqZa8ZxyBokcH3FSFFTuCO1Q2c", "zGDOiDqRAL": "L30v8wkGVO8ZqtSgjJu3EuQumz28FW9StCFIb6oSlzTSgrzk7L", "iVQdgJkWka": "cPRfVXMYrEFnmzxsxsfspDyiqNoxRxKdT1YwZm5Xl5iqnwTcsj", "JqtjtT88zK": "86ei1Y04BBZD9fytBoGecUfVZuOEw40RVEefe0eHpRYK5ndkfY", "Ln3DBi3lzf": "asMow15ZDNtkc7wW9wBow3ZHccpr47fxvCRzO1mnO2oBW8anzu", "WeTkvwgM1e": "dgMlcbxQm4ds7irc7XyPW05jHatoa8xLCGASb3ycplWWvyXHcw", "C8GPsMwbag": "fgoZcgMGMd6sFF4pPHKiyTX1PRwUUfOCl7io9VI1PU8I402Yub", "WNsLFBL7CB": "BP5roDrERISXqkAp6Okb2q9nkPAJk4S5sj08UnJTzwdEiseRYN", "70sun4m99K": "NmmQIAnZwfeIFrCxk5agJc3FckJ9yX3LH9qKuh9R8fWJ3zRFc3", "bZG2MoLQlS": "9pxO6FzJ8RqbkZUuxnLvyj14qhIitszYgSzAFdKB6f7yVlAtdt", "F2YExtZqYS": "NiLjLPYtUauRVYB3OEIX1JaZOKjEkHHrstgWRaOd9iOxoHCqgh", "J7dJLLNPgJ": "7t0gXRWmtPK7Fh1PcfjT5T3uxRYVLHqSuNnyE0HoPD5t8BaOmP", "JbcDKPYwWF": "7yd4JngOjQoPzpIkrEqhxUxucMqNB7sLCceuMKoGdsLNEEkExR", "RKtYmlbGRf": "o3ZsN8heYrXUfTY2a93VHc64TrxRIWatSTUqom6K5p0S3PQ4O2", "mN3DaTbWdY": "9rc94NOTN3U2Wp83qAF4Cdmn8sfYL0m0mgtAbMNWm47RwWUcUz", "TfQOJoiwbb": "6o8Va9PvKXmUsDbfUXmOZOSsaBVyj9wRZi3CxCBUxLO26HAdNQ", "twYamqBbF9": "OwHXLXqW0roryzITHOhkhDM8tDr4dwrokz5rbXwwchduvtivQW", "jw1oXRMJ2G": "CSOZF2BO8GywAJNfqps80NQ7QUFdZ1TRxWWrq5jjNUXMINnRoh", "iTq96k7ACD": "W8oPOSHtZ06hEWiro2GV3kE2P8bzfKZN40wdYx9iAxv8sdCtvq", "rr6TqLbpJL": "0O7wNPubbxArWTM0JwVgUTNuNrZJKzYE2gjEyjyitr66qMiW49", "GqJTQYVXdV": "lUcf5JsTiqXeDtvRBmaVITjMTHIeMa6ggkjer6fbQ00KudQJNI", "DsrgevZthH": "ioLzRIbbehEuE9MpcyO5dUnoIUkrXg2cQrrmomV8LQcMByXsfx", "1cmahP3HDT": "URCHRkHRIc42GI7gFwP55AMJBYP33JGZmSYwDVbctY0edPCPk7", "mJ2CiLnqgd": "UlYVZHaTOR4zockmVVwwh8XHPMgQmyMLiDDHX2UNNNoYaX6iSF", "XtF3oH866E": "O1xSxjYWywgwjX9dLJFnm6VK4m0hGZVamnd3IqquZpY2yNkX3B", "BnqVcIRzqp": "Hm3iMWZShfzjix86lNq8CddtdA6b2Y8HXu9HvJjSz0ejRiyqF9", "9gdLsQUuBX": "1JBSDIWLxy3L0dKZU9Rnu7Kp8BBfZzQ7rLNWkxAY4pppXjc64g", "h2SLqcwHq1": "VKqTQj1tuD4I6LVvgKYUCltHfz9U7i30hiqrnkkKZ5S0WURmJo", "Wy2BHg6zG4": "lLgngZ78EFPjZAkH2l8Yv4vBb0GrKCcthbZ6be3M8O755uKtsZ", "zaAxRUJfzj": "umjnQse6kZSXQpgkKjLADO2eZs9Dt52CZn3an3k31a6pfEpXMj", "Q1bd0Ytm20": "sUYNQVgEwyanEcl1hh7rWqiZE9bd17pPnI02CNEQNnVHb3dkSR", "dkH5kN10nL": "angc4HYQFsdSwi9A2KJdF734JJRsiwlDzSNYjv4C8CjpcBWLum", "ed5vbfZQff": "ZfL0TQbB6bOzVQSh4hoJUr8eSh829lqYgr9knx9miSfC7D3oli", "X6Oq6Bzoz0": "0zYNYrQkCkOGWzYoyg75mIQC11g4Wco9zQDi20DPP8f8ORh2eS", "MPZFdpyW6p": "9Y8JJkZEmiTkJGUuWIjgEtNhG95JlwaPlgDwcv1npm1QcHzCbG", "fdlxCNkAgQ": "68BOvq9LUPYAvjASfed6FpEoF6vRynzvZMNwFVdqRUVOyzseX9", "LWXQUhO9Rx": "m2aBrXfu3lmyiOevxfcbH8zQsHynIXZyt4JGmIEsfdO7C8YdWr", "2PhOuKveYO": "lXUUAVG6wy0uR053rvK5lAUJWTfpzY3Nc4TZV5aV3fwPyyv06C", "a8zxnk90HY": "7KLmU8n9IIicqYCTQL0Bf9LwM4m9syrFOjCYK6Tb9MR6IhryzC", "I6yQIPZieR": "YhAIqMI0HGvzRU9iw4GbJvaYn8flGmQULLXXj7vRyilToW32DP", "UpuKqcRSkF": "9Pa92qxKGsWRz8amdGzpdiLh7nLFaC3BI7iGXJ2xSdaljCdfzK", "AVv8ZtdNBy": "nK7VrIptHdddQyCWWXhGLM1By2jq9O1xCrnhqXOGI8BKUPqbD3", "LZNP8OXQmJ": "hvBd8ssCUkcWSF1HxPP2tPaGrDoRcLprN3yN3nux20my2gmlTs", "wFxk0Oyyc8": "3TkL6Y6l3yp2GzihmDubVWRn1EfQKK6XUVUg544GjxokntCDet", "bG8TBRJEI5": "J2eeibRGABnJeCjBhQG1SbFX3l2XY3ablJhJW6b381Vbez0tIX", "lXRx3EGVFE": "l2w8zwIA94aMLbLdDYSeQkR4UG7S2IfGFQWNTfo58WSqMG5JCf", "6zRLA1N3Y7": "lBN3HAALxUThF9JinFVBZQYbLuE9fmQrBfSVj2eYAAOKrM6jJo", "BncILNLsjl": "sOCPQgDQUtfrcAyHQyKe5g5HNKrNjdhBDSGW3YYsePiTgHOrKv", "cGCUMCwXMc": "7I8KPPg7ntrfJK3SvZA0QstpVoTHhr4LA25wbZXRG8ivIB4UWK", "6aJGN6Nsjx": "4GGAjOe6wuQziJaOUZB0UHiAqgAHguFPCNjyqbIHwNpcyFrHay", "hxc2U5Sxih": "fhexxdJrhUj4HEMf1jf9G6G6yqkwRGPxCY3z33mh1yhS2pjaff", "cl4WyVYNd3": "52B8J0gQIhzaVazeQ7YuHt2q4ZlMqCOLXPQm8C3i5T37TzS4Rk", "Xe643H2IIk": "mUM4KFXUeed0ic62rDvw7HecOpKPAXz1FVD1rL4UEZPkqC3mXl", "KMKiLXreMQ": "PIwyggHPH0KXCHFV7nIDjJhllNeVEowiNDdWoVXB7rK1UPvZ50", "12qc1tlDd8": "gY8tdmX6PPst0fPttecnD7fYq9KLiRfga4TJ0XUIIqflqOCaDn", "zdWRSy8nNK": "rwtec8x7kq3EWYWklYvfil37UWFDgoF0MG6mPRswvERulFZ1br", "6rBUUsD8E7": "bTFoLmKNfBJLVvrEsHw4Mvhbav0cGmLOXHsnUXYn0qnzplTjmc", "VmcOzHqUVL": "5UlLZTJa4d6s8l9833ifBArKU8zWIvIMd1MXvH3yi3n8QBt3AZ", "SDFR1JopqZ": "PXvANOK56ItWQpvvfdkV2wDC91o18osEpKxVU1L1tDVtSb1jVY", "r4lVDKATxq": "RQUx60uLC7Sal0N4J4pdasFb7qHYcInh080WCq6wq9Ul6yWuKU", "EOxJRxA5ih": "2nK79TgYIESU9h5K5iTWRJa5BFKpO9QJFmkbUkeYFaIdhb7sZs", "wfz3eCVlbt": "urcHDZhDdM2hEaJMvJY8V0O5ChZVg6tli1ZtmqFYGoP9L6uFXp", "iE68UoNXsa": "TNUqZlOF01F05Axt6ni4J9qjbuJ3EUrkr8Z0GHFL16iTeHdgQ3", "ZPQEtjT4ui": "M40chKych3WSDLfeXfqmUygPNSurXCfXvaLXplbPbKs7y19i5d", "DJnYgkha3K": "ZtDKAhxvgcoykyntGKPicrvzDKaJOuGnDQ8t5io5vEMD0QpB2d", "eHZcQNB5Tu": "lFAbFe1VFwWw0iQSCsDKxWOzWb2pRuPPL9etlGsEdhRqLjCLrh", "dLMXKjNyBA": "7leNwmiOH2OFnOU4WHdTJUlF1dU1ANeBsyqmg3U8MP4twhwx2C", "vyE1i9fJQl": "rI551piWMX3hTpBTYCNGIha4NRNKUiozclvNi2DMwneELw79Bf", "a0qTiAnQX5": "9T6WJOLasumX4I671yKcekhXdF8ycZgaj9k9nRPzkiySOdXM0s", "PFlwK09qbn": "zqB15R61nW5cN9Pc9zhiu7iJAuRNjLI2xUMedAAjimGDvxFnwe", "T1rzb4NePB": "Ppt1q6TcGFcZaiwTZrUXrO00F3OvWbdjPevn6ULPNbdXStLJks", "5CxSkU59DK": "DaHMKlIT61IJ4J32BvDYazHL9S4PLnemuS8qJuAzXCCShZXx8Y", "QlXJQsn05l": "Bs99HOBmvB1BmZXZr6ncACHkK8vsa3COIHqgZuHUkICT9JsvoM", "FAt6ooL5QK": "U6wYmbhsKMDu5ekdPTTY1MFCAWzxo7BbPeSHRdNKByhPYBQmA3", "dGw0R5sZUn": "mqvI9cmJB8RV5takwH64khcv57m0eFlhZEpPeZYJa0qQi8iYy7", "nda2C9dCkA": "yZ11GaJeLgKsI8l1ZcmhGyP0DewfVeiqfDDjiuUIQdGwUtLuJK", "yDmbXa1IbX": "hGBgx7G5fLtCWqYZqaTJxhaQbs5rC5t3ocKjmY4nG5j2etIHBO", "9FpUn4XgBy": "ax5HJjvhxeK06fmg7W4RRFr4l1WGgnOgBN0AB4M1S23J2nKTZi", "EqXwIlH0nY": "07JIfnIOHwGiPvN79cQhfJZAE4OOkZIYoytSiffPSgJrylWzF0", "zb89qeAdnW": "CjT5sCpaDFDh4Acq3M24zpYBpwJOoZ4XXsYZS9rIg8060Wbzki", "r14nRtbc27": "wdP7RSpY8YOKUKSob07buhp9BOPnnkW9vHetEOxtNXtruoDlGW", "xs1LeSwoCg": "K5D8HbKQ1TezJLvWDluT1ehirUIvHaO3DOwZnt03hf2yGD3ge4", "QHjcCoh1t8": "YqJeHuNu4WW7bzIJ548Dww8RTPZir8nSsCYYD1SRkOztn11hGG", "kxcX1h0sNi": "95lTRDAqcvUYZ0FJ6PwRuKPhVbtSdLL1cK0gpfkvTCYKW2owkk", "bmpVS4mL9n": "nAO2eRw5NRjbGVcYk7ixVWCuEUNfMxQAzvJg7q59SSE2ObOWa4", "WhntFUArLQ": "cKAMsC783qetNxB9tYxC3tz5A2yOo2stS4umIxnTSO9c6xlOpu", "YyKaLdqcaR": "BHOGOHRCg8hc980yDBgv2lw9U6ypwprMhB9sW4dPK6oJsiseCT", "HN7t9HkCSd": "muhd5mJBRMBuhi5BINRMc8biW9mgsYDWeG94xzjdQmgkNsfMdT", "C5jGnaAi9V": "SUiOK2Val7u0sdbxgI9l5sMtlhEWLQnkLBecitkC1bzIzqPE5p", "WaGxeHLm64": "Zj9K1W6N6st9888c2vde9mDMuXtYP4SbpoC68Kz3SqB2nvqBue", "hVY95Pc6FS": "YTacS1dwJa1HomkLoawEVIiSOO2DHyc0qm40oeCnpy2eMRe9Zk", "FxjCnXNzhU": "Obk6uJ64pqhzyGqtxCiUpfDLo9ULKZMB6cKl6KQGNYrh3HFTsY", "2OYnV65Awr": "eTMJE61bU5ugnMHPKCCwJ5uXCL5mECYOEXKEi4MRzIEySwu8vO", "c1bqmEcAXC": "oFAu8L6qEWt5sii2cdRRA1EW2uuprYmwU3b4LqsiFWpnqWv3WT", "esiXZaG98E": "4aWMSzeLNVFWzw7mhDoaVD0d2VWSCrRFMOY2TfS7W92ModQuGA", "uyECRxF43w": "EKNCVfg7GNC0tyt9FgWtW9AwtxPwDwAejlTSR4aS9tmRmGTsTK", "RzXf1YDZt9": "nuxNx7NUdV6skzD74wpuCC0u9GCJS1MfYaoCGVjwJrWivnKsv5", "nzcMZ7vUq2": "jUUQHXfSDGqlqCdaMu282EZpg91zlXQ9HGACCu2u2qWTlsmMn5", "jfEAbkKUhs": "P8Tdj26oFodufEKFWunw0gjf5oFeaqnv8DsZ2MHVUWWsZ2rH4N", "mJr2rmIm99": "Pss85BbB5OWQxFsgSnfIoyeTlpoPxBoqMUKoxPom4GenxRNcNR", "FD8Sxp5Ho8": "jC1jHWGCL0uNacng2IyB2aey4N9JrIBSCBD1Et4o2cmNJyTCNV", "hJFghxFvUM": "NGe5s1WJnwBzZC5T6CJduZyDs46qUwLcj67ddy2aFSk7Xmi9NA", "I2CERGMdbP": "IaWgOGeu5hYC8we3w2ZXvOFcjfqOo2ABj6zDSK2S1rfrQoefux", "XRbSvRO80O": "IIeQ5mGvoB6fG1psL5ibqd6KgtkdarBi4udbBf34VNJNng0gca", "9P9yQHCpsA": "xbZIZhpiywAdNut0Uhp6eMQSovRGgwuc2OidhtMocD322aFrWV", "S8Q1zXS8mc": "M14kAdptJ0RSmberFnbA7qRmYmRUp7kQk8QJKiZa6naCP9abG6", "QJZ15OavaU": "FKkuzjwi3GK8Efo9C7FB3fOVCXDbI74c7yPhRgDhIHjUvvYhR4", "feUgOML7df": "fUrmnem7UOXr52dRIbjgOynrNeMykLk0Qicl035F9J6nQD8RIo", "57q65gmfeW": "FQZAMecu2RHWJuzn8Zb01wVrXmQ1fqFXOx3kqLQF1lF28QC5Bj", "YQ0A55LsxZ": "cR1qpbUph9G1o8nDqupeErdvwd6NO70lZ5rg0VozyACyx6bIVJ", "sCcNkcvFVH": "VTClKs1XatrfFCn6kT7z0sNBShFgpk9lLcEPkscsvTcG9oT4aa", "42uwYvD0ua": "TabrGJsU0N1vpwXSnRNhZsQOWddHXawvBag8Ji9UuHPFcSnb2Y", "DZzyOIR14h": "ERzikQCoLlqE2OkgLxcIHSMoRrSLDomLX2kz4pITwxv2p1C77z", "JupNKZEDaX": "9bk6MNGZy2tzdZUBonKeVQ6HlbI6Y5OjhDg9rKYBzl7S2ali7P", "3Nyh53op3E": "mil0dVb3fkHta3r8J1rEXubN7oFnzisg1RUPXh6KF6Wtaiuq0k", "sm7AAZvZ5s": "7fpmZSrM523CYC08TlCUVikqNkGO1WVJLJOhvNv7JWWbuKpGDa", "QzULr8oQu5": "uy7iQKMaLaMgHHzGyh7PhPnbuo5YdEeV1ICVcKG6Y7ILnVOaIn", "uDYscNBQtz": "ZCvadgugCNEs4PbIe6Keg43uAxIyecwjQM1QSEcGA5FfduiFYh", "TaPmm1rbe4": "XyOkJjqpISyHFHOLZeqK1PFVORY1wG3MQWx5o9T34UsyrzIAhE", "nGwn1hnxvk": "lGe3ntVX9KLMRrZMyakRKBwbrCCfC5WXSrbIu4tvIZtVysx12d", "Fyz3QvlOS3": "hRdb1jy0OUtgQwDL7WRFZNUMUblD2u59JkTcDF0KszJaXokkVD", "Wps85hLhKI": "OqUk8t60xjKYsnL2hNzgbNAMi3X4thjRP4oGYrx6kdq16au9eU", "nmYLUz6kas": "hJPbGW0KgkBQhkyn5UEYFIgsiB1HJxFHUoweV6UmvmZVAVhtui", "dV1r6zdIgQ": "AiatqxhvyfSGF9zVRa4Ccm2H8sbuBbHYGTr0c5AbaftkHrXdk1", "Red494IwM2": "ramEfGtGuvKbICBtHl2bofYZbVRWMr53LutOe8oNbZsTJNg8EL", "haPEDfEYTy": "wZuFgDsT4uNPprcquSGb7JfaWsXr0hHzzkVhpFcTGYIHI0Iw0x", "vJisJS75Yh": "DGBVsGEbCaDwv4jflfg9M18LKZxWHgYVKaDFXB7vUduuoiFARl", "FdKIaVT05N": "7KQpDsCKgrufXY0o7dcNaJapGhIcWJAINRCH0gNfAAyYuAtZij", "8uJLWXuMNT": "AVqzO6J8Py5Jjk4pnq8pCPZPmX2Hkp1Czb3jnWWxtfgPUTdVya", "NH4mOqyPY0": "Imx61MpyocKmtRD711mbKxHVsDcJbs7w0l9l0NcXaIhUzs1egd", "O5VSpGoGvI": "v9bPj0nq1BAmjaVtwtmDciu5PNsgHvVn8FglvQmBcMDP8vsg2I", "ODFMEo6yeo": "aihLpIzEKaXOpTworN65m0mCd4yyMEXqT0Vz8bZFXFMmdLec6M", "8z56wrLBhW": "Hfmtyr4xwXYxylQYrd7PGJRuR5Y1ksdRFJe20e1Ql8j1khYnNB", "4uCDfzoDo3": "20FGIVJVgPOGEmE48Hc3LufZ9huzUDKbu81aCdoOpAIMNswk2f", "DFbRPDbbvQ": "NunxRSFM4SEE039XhUGZfQ0kcxULpmnFSjpBOeXvI7xKR3EEVv", "H7WUWy4JJ7": "WFZbZehQ685rsjpAIi9iPMwRSHXONANNBwgnXbqqObW6lcQJvo", "VGTOe2RWRX": "n8VM7DAnBSCrkUyQZk2ZFFQbVE0yQzTMTVBOMXd88OPauydXKg", "afMICJjpSW": "WHcSSGJu6diXAcDIVGRu46l0vblb4PTbvfrPBV9WHwRZj9YHq0", "QECKgoIPic": "oYUG7MKpm7i5L9XBOwUxvkIGceNX6nQGuZeldj5QwnFGctGs9S", "LSxpBUT7Cl": "rYBDN6c8IJW9zypmc3jIkGjj1OVslbJkW65FI36wdHJaeHFPP0", "n6PJfVTFMk": "mhACO60lG0isZp891P2Bj36LximbHrja7UgJfJpaTfN4nYsU6u", "5AjqlnLmUb": "J3LgO7XZZSeQ04G2XHvncmdoSJUjlyLe4aslRnP1MH6ZjzHPOm", "WoNV3uFqTw": "99potNbkEdUboBANn72Yx0bejF86MFvfaJzcw1wg9MzuYnwGue", "9p5plZMZWF": "vu4wRNPei0Cn5QpO1gL2p4E3sBWbT36ZkXYP9kmWi7w72dOIob", "D3prGgqd2z": "LgS2uCicGg5axyuQRLyzGBOCsAppcgT1sriR8fMQjhC9pd9RB1", "E1Kjdkpdbh": "NIXHEscbNQ86Q7d2vjjst9NiWWKgi8Qm5JdASvfPTzxM7rZvbd", "ksDf0cJMlf": "eRRpz5x0PIlBA1bo8Y91Wr7mqHGsp2cUBZr7qsV5kFPvfaMVix", "OVZksdJKKq": "n2fyCNP82CIod7EOu0x2BytXVA1nuk91DPuxstfHfH8yL68rnA", "Q9SwXX7lGp": "gb4F0t9f8nrpjprgD5XAuO1rVqo5PZfWrldnIxAGW2dwE1fuUE", "7YQw3Hnkm3": "Xp3a7QP7YZr135U1TOk3IIGNGLd4VYT4bVdpz7XBfzVf474sSn", "UHAjFIaA7y": "enjOfrOwqJNP7dadlZdeqXbQFIDcMCQPhsEW0531YgWRZBpLuq", "GyHWUjiwao": "KY7qtTBWla7ylZ3sR3pUNMCQgTYH2oKRqoQKdFYnEEcpdtL1BR", "96EuzFaIbU": "GsBN15V4cUn3aOb0FQx3iUibBYg3GhKHIqHrqLcN99pqCn209F", "2C35ffPnT2": "3IT2DyVIQspZO3CDOaOkGBSzipn83giYAPEV2UFmrPEnufGaJK", "knCxk5Vnx3": "VPIcO8eaMeCF8nTAlp731nhBPKkPp5oNGiBOTGsFZaCWLJOms4", "fPoWAMLvPI": "Ilu2e6Hdz5nqRdlu7WZ43cSRW1XweQqtqVAobiLGPDSiEYhHsa", "Ita76koxTD": "iqxWIlY15p4DgWi68R4vpwwXRB0LCQ88FaWrHwrE3cWE6EhKo9", "csl8tA5BXv": "vC9zJpxNOHDmPBnfJymrMIzRP5UcHE0auDt5PRmlMwChoeeD9L", "CbxZFYSXil": "PmWDHVI1tjECZQX2u3sHnuG2nPEBN4BYid4bKgsz9HzZv3D2Q3", "ci56ykHJtR": "XodpSDCsPosJnYeIk5Sr5PuHnWDU2Wz2rDJVjir0Hi5MC0VeNd", "rrevagbTbD": "beWKp311KQUWrQOb9AiC0HMHw2lhNi0vndXVEOOPkW6EMQllNw", "Jr68oTIk68": "pX6oXgjUohRRVWaAqDeZwq55Ge3Rhcrxzhn6Knty66dVJr8K8M", "kEjyC4b6gS": "iBnVsDeUM6KKHnGkjEvVbH0JJaJqOoGkUJTu9gbBsaTCgUzQNf", "z47GcB7pcK": "GWEOIfRVw10AAOS9hfutqRz47BNSt0NqMDckn5D2wxbSXtNjfv", "zEsKXz6bLp": "h1cOFENXGKZqremYOVcY7P8YKhexzVNVgxnq2mfoWN0dem38LE", "1Uw6gjQgpD": "5AFrAnxCDd5XPQI1mqftVLh2BurrjhLgWs93e1gSkAlUUYkgsn", "OGff4VQqPY": "KMHq0jvxtR7t0dmPQ9t9IAOoFjDPecga8hpKovx0jUb8zWkGLT", "iO09Hq8UYH": "ViOcIn5SH09WOusQ4dVzKBX1ak4BEo9VNT94Gy97IJMHP11LVn", "NMI3022Kr9": "zXfOKgLU6E10FGFZYBcoObLVmDzv5ik8OgqDRGQYmPv6coxS4Q", "tH4alLlDal": "pUOqGvEWL1G4mvUO0etuHDaU9CNkD6swMxK3jBrxouJ2rLsIzi", "M1nseVN69D": "t1uN1oWcCDlZNql3iKTyFo32Sh29ee0xkiYu70MGrbTkLRuCFw", "cqAUOSQOPX": "GWKpy25c8fujd5gPtbhbNTcOWZLGutfQH7N6xHsePCEIVLqbxI", "gMSigwRiJb": "pWK6cSGP88hgjGB2H77olN9rPvtQprZkMCFZa6cCYQHXMkkBtz", "9nM67yNqM3": "u4dnzIqEdjrNcEDEDmIq8gYK0tTtLVtlJfJE5TBZaa5ULr6Hey", "7ukXp8Ak0z": "WC9ENp2tNEeSlRERMWftupo2f4z5tOFggkJkJGSPlax2KTwTOU", "QjDi6F9VkD": "ELppgFx4kvb1GFKyrpAJCsyKXSC5wlBjuN73S4JwlI6Y9oTrHT", "ofSWVEzdEa": "6mxVItkpP34xH7e81WWpv00VqyQKVzLN3sJpX1jioIUTdC7cHm", "AcGL8DvLc1": "9GnjvUcdRKIFfXNE2l8wI1xHapxHKnb0cXQIm2SOSQ7KT3fWzf", "kSrMqJvmRV": "RFdw0IljeDw0ulB4JsQqAHMna0l27Pm18VBkCovuZgI7IumY8R", "AGV10XIaOx": "KQx75cdqA7BlKSwodllYcKzNnWqMB99Ng3lrCtUPjBrzTh6bxX", "3sqRKOe2U2": "ZxjlydrSacyNvU8qApEASaQBKWNy8152fgKztGEESWSrW4TI0Y", "BQS314QvBV": "86UNC8fwVFeZhJHp6mpyxAW7NR9HTaa2MiEvpLcYC8DpawFdEC", "sD2veWAGhG": "98JNPEItZe586Hk1d1yKQpc37pFmgJfNFzbDfoRueq2B6of0bo", "ipxCn4jvav": "lqF64ti35zXtRg58R11hy5KqcxUHb6JAeUed2tC5gIgrcqJAJS", "Za13pzg9Ez": "9mVZklcYAIf8HTosNwu1Rc61LtY3oUENATWXVhvTkc14hmiDcN", "285OC6PVWt": "p67vuipvYHmt99SPjarQASVQZPuuieMReZxQCKuCBXy7u77y6W", "AqzqUwvZa5": "mMkrV3biBwuWCDYsd9b57jpX9MIbvksLvN78Z2s8C8S2QGEkVF", "jYTsnyi3CC": "fV1pXwlwGAKP3XfA14IoWipEfg9R7MM3izLKlTOljkbOEd57Cd", "Dq1U2tnIvx": "6c51ivYXkVYzuURn56HlQcQ40yJ2Netko5O8VgoFfUEKu9tknj", "fkrAPAZAtq": "KGa1av9y07aFQ6Ya9XR1nugkeQO3VKqNPsu10ZvLMs5cROBxrf", "CmmoxPVv9O": "pgCDmUAzSaGK67sam6OIbaXx3rEtKB2m2Rxa7M8RPH7abV67Nz", "Spcr3VGuyF": "vd8gHOcK32tp4f7fJcW2D2OaqBTvNBSpVFj4vDJPTzP3sxp6Oc", "P2JLrycqwa": "2WjRqeED34ARmeh8Wo3TL2kqR4aNu4gkPuD5N7i0YnwLMYgOsy", "HB8aE6I23h": "XRmAqfUaulyU0kKa6o1vW6MwZVushmcAs5ZvG88UHi6ZurRNfc", "iFQCJpOOQ1": "KRhad9SDt33iHjuJ20oEV5i4Zwj2O7J16RXNVqlgvp1x5H4HYA", "00sqFNdGUh": "3iXsnPLyAzuG8tO1W15PX60mIQOTXtg0Pozon95eR9jWgDN0gY", "FnuILwctzl": "FvAeGNVNbWbVe9iEXs9ySktjWdllceILYWeRDLG7qX4e5wwCEu", "807lzrEcOW": "rpXl5XyxXBNcYly2Zsite4fcW05S1VReq2IUbTUBEAVucqU5qA", "DUdLzP9K3M": "YqlMecrXREnvdqrMXXgInYrE5og5me9fvzGIFk4okkBv1tQ5N5", "WuBcMkbLBF": "Mhihy2df7LidlYWYRQ4yNA3OQVl5TN7abHgtZkTm4busaCxN3z", "bf05TP74Qs": "UVc4AXPJzrUrYN2x7X5s2mdLKNva3PvdPAEZXvfjikqIrhIi9k", "HvrQlOqIP7": "YfUXYB8gKdMo7hp0sfSBuhXjpDBJ4XUbypLGvv3UhMUNsoCOPh", "6mrcHssWkR": "y1Kc9HxlyyTD6BW64QTXRg5j2HtVDL59aYsgLkscsuiLVHqrg8", "BajRTKJyKm": "0vNRqtN5RbayCs1MbviRY5AghTZ1zPBt3OHWsVC9TM1UClYSVy", "wqZLRmlVcM": "P8qH1ItS0GpUAkEXNLeXTFHbIR0ypZ2og8qBI8zo62eGKQbP6j", "4O2KXR8IvX": "ADiXKak7wmYnjlzQmSSP9LEur6CD4OI577ato4zjghiY37ZwSQ", "7Ul5xBT0vZ": "15o9rFn7odtRmRonf3fR2cCzKaLkWX2kXokmFFCg251hM6Jotm", "U4oLc9od7V": "5p5u6tEamMKhkmVb0vNnlBs9Sq8ZEhJMIS7mkUtQFqNn5NFEs2", "3liqkW1VDO": "mncHLdVx6XaMpPPPK2iAuW4uhmHIb8VD5jsolEQ0pkQrQtTQir", "XQYFGx1Zak": "u4QE807q1drPyG13x07GpV9uywVMxcB90ZvIWrcuyjc9MUeE0Z", "YLpbQKr7qU": "u4HSoE02PlSeCEagE1vUQ9H6O1dqUM472fMwhQwEmEMhnr7rJO", "lUCOtOW0CS": "SV5uXWFJYstmL0SSAWEJebIXgfJ2ms1XtnTWMZUecV9h94nPBG", "Pe2gBb7okf": "xussUtvhvRPM9rpYpZSLtS1WkcNSXXHpcot6uJFLnQDKBAZXDI", "hhbikz5xNu": "lAQqwGvOtlXdPU2VWEr6NXUdGUg7I8dyWeTYwcQqgCaqUm8erx", "z7AKUDFIs5": "AcxsQYbpxqPrpjPi5DW7OkcigWKfLHv0cVlHmd4EkYehg7tz19", "9b6RiLqOyu": "h0KTLG5nukf0x2fD3XYU1tU4JBxBb8frgdjgRJ2nQl1ClXAFUe", "gTe7ClqexF": "KzuVxphABOwPMmoRYtSC81zHWplRyBkqMZF4fiAeGOLob7nMYK", "cfpvsEiNdt": "hjfURGfjKzruWLZ0xQGfkfxz9l4AsFauuqHoDkXzgIP0L41Q9v", "cKvnsBvUE5": "2AZ2704LpPaCxroczCLLn4ezJ2BvJsOCN0h0PGAjCu4dziGP0c", "o1fU080dJZ": "f53G7e02aF3ZADhX5R7u2k3n9876lScqNyidLrGk4c0v5UJJhf", "RuJVPAw1zO": "17JbucQ08Q85IWWdy0TftRqdU9BBaAtu54T5bJFhwq04WP7C3O", "F0Z6Q2m8Yg": "NSeJ0Ehdkmj10YvEpuhxQzIdsQZ5czWz18yYCjuvfRIIcWAxQU", "IPBsqBwvPv": "lY8SzORLARVer7av5R21r7xQatTGJl1mg9QG0UVYNoUdMMNLzE", "JQxfx8Vp2V": "8MwklQzET1YPBjkQ2HINilwWdmvqVRiM0OZBdgrAOUPOR6NhHn", "rhnDFaNjPf": "1rybLNbx0I1HgTCp91ngPZIOze8L6IGlZxw00T9939xgk5BMyT", "ktww2XrbIe": "x7H6ucenUKWPCLtklLQbWXTQorUgaDj6oRwZwr36DVBOaPdNvT", "SypyYuAfjC": "pZOSYC7b5Ev9mhSiQO3NyVxYKN0BbtxwMhWvMFXnVq3iwMrQq8", "JCiHAacUv8": "GP2O98xPnUm9FgFYwql5k5EprYGWju9lwesPuf0vrYBze8yjUc", "wE6PrGyvog": "VX2hpkZGaPaSeetrDCc7pq9nYuTUz3ws9kUSENMNku07LRPiPz", "FFBuDPcPuq": "qw4v2mBYYCL1PihgYiRJtSKDlW3vH2C71HVidhw2XeA9NS7Qtx", "qhr1kwOFj7": "dTsRZH4SF1R6JWwDq2uHsB0RRJebCYkmJDCssJVWa6mdtpPWae", "XPgyxh2Aox": "Qihtc5TxJWHtOUU3RKYf1jLV0lbbCaonn6zmkhkjFsOPoNfWhQ", "7SWPySZIQQ": "Xp2yThjkoijH8OcQoTNfqtsYx4tx5lBF38YrLsTRTJCyOJFsvT", "63jmpCvPcO": "crHlRBcl7hneLvoXVVZJT8OHZMPCiEeqTFm0sUupJ6SBTrcEDC", "h9eK2M81KR": "peXVFgHHFnvSBDukN0vhzGyUc3UykMtD8QbPNpWReIMX6kmII2", "CMLXLzmoj6": "2aaNULJZdmpboxEGUBh4CctcrWOKLbTnclkFTXecwfwxt767KS", "PG1MJiqmUK": "SsEBXqMoI3zzwc69AjFktt2hm94MIwUYMdyEODUC8eEx2NBXPw", "frB37OkxPz": "4PxARGJ47sqaKaTgeq4LFbyujR8N9fLeSxzSrcMrRl4djY3Z0A", "lpb2VnzOZT": "a22kJMUtl5FYYeE2dXr5ViqMkyX9HwmoOORFjKUcK3LDpRnW6E", "t1lHz9sLkS": "jQ6hu8THdRiLN09UpbWPjCecNGHpH1vodPxCVEuGhw5FgV87TN", "d5rq51JOmj": "Jh95szSVVwmVNXzfXxVM0uZVoMA8O1GvUXwEtp7YHOwQIdMU1D", "ifkSMt06FZ": "YIv9JUGTZVMCMhRqHG6Q7IHChK5dgPZ0RPTecqfLEPBpqvroTC", "8Xtiz8Nvph": "LmAkI3MJpPnItUSbPXkeI3ms8TOjbpFx99HDKF3xBNx2pKAa1f", "xQ3hBbhfJr": "694grKjxOqfTLHk4FzUn6TOnZSrlmQGwelnHVCaUbxN8ErEqIJ", "oq01nroT52": "VSwvzOoGZt2merFBUAyFdRDSPWUFaVRASN651DOlc2BNzR0U8b", "ZigqmS8EWp": "az7TGY1ssjf24XmbueNb5K1AAewEoJ07Vuh2fHacRnzhN4upSD", "p9IFQDFhqn": "cEh4FDzFJ3UH1eAP8JvCwxAAT1dIf5uwQOHCGxUjC0oAtAbOWB", "tA4c9Kz9Af": "cD1XA7BVlBUtu5We0En1AkKRpYHJjF0du4i4CZhWATN4djwWwy", "AX7r9TtWbk": "jA7RDDDkgiZm9B5fJ0Vcf0IrcEC5pYS4MEzQv4qEmD1YavZ4QO", "QXopfR2kxf": "dhhtjA4gYjwhJzmcAZ9OzElOUWNDfetqmrqoDtTPH44olPqegE", "3Jrn7KvWzo": "tI6h3w3S6gErEeLMtWlhOErUFOZapr2SrRBGtw92pBN720Q7j5", "HVLgb6QQoa": "TGkn8WdiOqMmGDlew9lATq0W5ivBHY9INJyJOzxGLsvGnlNTir", "1xZlrB96hf": "b3vJpvqTGV5XJHKKJuf1ThW6KK0dLWOXfQxRatCrtbDXfsIebn", "7X8WjsQKKI": "pZT2y1bHu8DIvT32qb6r2T4P0LMkAajuCRzvTE9kFf6w4hW4es", "QSzQdmrroG": "ez45rvXdsEVlI79UOCUERKQ2ffqoxVWfBE2egzEcD8dGcJ6kKh", "1hugxQCf23": "q2kzcYEeZDHhXUtV5ZjFqijpcwV08pTYwRKKq083GGSm3hB0YW", "ifejDqncEg": "Y0UQV44ft8AEebPzbdV3wNDrqIQwcXMtUFyYaAApgeQ4hlV4Nj", "ezf3qVUXA1": "7jJvi5n4iTj1dZpMp9dfq5A9R2DkVX5FTL10eZrISJ2lJjpBcW", "eKTiN1T8M6": "NdFlb6AbpPnjwTkQhl0EyOeycGCr4shM7Thr5kmUf7tkmjiqDc", "TWBEgtbS1N": "wkssvkoAqNfYDhU3HbZUubuFsdprxUvQNLYylcyu80ZYXGQoju", "SsRcK9FU0j": "87u6BpYYAlWWJavjRMAUce1BxMcjx6Pc6NLFhgWFa35BQaqH5X", "QKMtOAzIDH": "gpp76Gnzs6Ag93uHUz8sDQh9mbRBI9G9wRBaI1i78mLRLBZSo1", "74YEEvDilr": "EJDJ2j5GqeIJoIEBd60nrLTViGC8FgAboSJeaVDkq8bP83F9pm", "Y6dhk7ZYsH": "O3FT9f9l8BZ9RuHSjBIrQcTDXR9GfF7fYyCAg2T4mQlc9bPJf9", "D8K0IwmdPV": "CFeCOMxHGYrzudY2sXf2vENEOFcvVI78J4q6ibwsNRKE8rTpgN", "iArab4Lg78": "2oUQABvl75YamDZ9DSwG9jaaJrG8xKOzR2AvP1ydQCU3sxg6ED", "Vo5N6vVHUy": "HfiM67Z7gHjBQoTcPEV9uF9B4NeDVccS9NMqTyPhZiTEQF2jNj", "fxJlQtSW1c": "7luOLlZT6lexNugGGgnARPs2GNJ7B0K46hSNsToOZf618hPzxY", "cRrzWABORb": "k87I2JJZ5vjnXQ1adf4gSOrfZX18xSZjw0N2uscJC9N1npE1WV", "xgfjiFLrvd": "c4NSxU5YAsBRL8LBqVJgw1AMhxLmbW02zkdca7S7UgSZTRM3hi", "y8oa5IyoTU": "IIzMRGEn1pO2UiFBgRMjAHl6PTm88ahFfsbGrLSAvcwinUkmeh", "oRXw8S5bao": "tVrrP7BNKTsHHpaLiabOWQr0JddjKKYaRU6kDoXOHnbrBd80c1", "CHbuvg3aqz": "D4UL5D0YnJggrZcD2TaVPPb969j3Wd40TNkWEd23gnM2CTlpaF", "RIV5oI82jm": "N35qtwFvJxgNVIs7HdWR7YrBZ7YyzBmCEpZ4rrh3H4x0OhhoXh", "7TA20gF1ez": "dWifFXTs25OFSeG6NJmJMNuNHCH3LXuvtwuR5LtzcDfNFYfCYu", "kxxYRGM1Xg": "i0CdJTRMloeIRtvUi0hBKjdGb9SWWqYLD1OVqstqlEFgGISlft", "uxUUBex4DF": "LpdzpbosDiwVpbMFN6IzgakHm4gLmBT4aV2gJCpCLnRacrcaEC", "kPwo9qANYN": "kUcnVZMCpNVeX9gw5xzxHV3dZJHmXC0Ey1HEJskVQuebdUQdwG", "DEcWZLYuAG": "1gDAGquRYbDhVAx7OT2HB1w4a5A4DQSUItpuq1Yj3L3OLHpc7i", "iiHt29jd0T": "G1PbklNFZ5GEr0fTdYkJGZSmIqvLYUj63AXc8sBfdcZR3jvArk", "0C2vAiB4dx": "gvMM9vGkXBjIUIZM3ouYjw1X0Aljg6UWimdyfZQTsYG7QZLj4m", "fT4Lgb1Xmr": "a2b6vPfsRgSEMKBXgboYB46AaX1XPHgDRoPwTwpKLivcwbpLnS", "KMCIMuYsEQ": "eU1Yb6BtIYWHatQKaTiOj5yNeAMBr6US4ccMXMsPKW2tjcCcZW", "QedJglhz8b": "2yf4eIVPHDgjctO5XOOiDAoqW9WpNdFZkEtjcTZAQ07AWmGIg1", "bCNweGvPoo": "sQWNgrhtPDoYKhVBh4reGQcZpzZXqHnmknyESTyDwneWVX6767", "so39FU6LWS": "DudrfxdnDuw16LVPJ7yelFawCTYN5fdXahqqk52GT38TAFR3pm", "zzHcPRIj8T": "bQGuFDA0KdDXgyBRzEAo5EGwgNQTpmMZbUzpJ4BV7uZ4xBuG83", "Z4nSSS9Cqn": "5QvWz4KkfSt0xuK56Q6OvPuTEeinQOIFQmdBacplMSQP5PG1M0", "0X0ZPbGzm0": "Y5TSEVG3KnasYm648TvWloO49leQnso91eu7kssNVWu7k6XzB8", "O5fcFpnsbZ": "BmUBm0dAc5Nqf759AAdqC1Ocvlg6thi8fmhDlFFuMquxVJNYqV", "Ft6Iaimvg2": "54Oy83BuKHAP1mG9EdX6d1sxwwOK8OwFgzBHLtMrHse0iPWO84", "xVBwrOv6i5": "UiwMFahNBMXBFp5O8YSFrCb9P7YrZMoqXYZuL7tvLj1ycHlIgz", "TsO18xkMIA": "uixtkkRDnFUGwwx4TgAnezE42Nxdssj7yiYPek2MaaTEBNj4Wb", "icv4UBHZoq": "CfHO2Plakcn9cqLv31IapysNfAHpH5Fiyf2tFAmW1o8iA3larJ", "F1bQa6V2d3": "DX1H8odt7qEes42Msm4OHKtEXY5tfhWItBDhDXn1fkE7JNHxRD", "staoey9d9x": "lYyRVda3YctSm9krmFrQQyxb2Lifl38fmkvlYeRe6Y1PXTuOKN", "cppUYIzftV": "HFeLJZPvfKZG34MitMrQ5i3dyLfxnnVzFanUXmiQWGepBLgk23", "jewxF967yR": "d6tNAl1a01q3qIWSprm86OJ2QpEt5YALktp2njVfJ99O2tbwaB", "bdyUTCKkHS": "eD4F3Nuv6ug73xIimV6FmDswOD6Ixb52wGVVAuNtetIFeeVrjA", "AuLnfizK35": "Kc04yf5xSyBGe8hVxYtMDXg1sx706rQmgzGvq5E1tjRecola3i", "x3ie6x8Atd": "K927NnVVxuYEqupyd176f0aAfJok8ljyiP21XIIgegqUJs58bC", "Usymd0frp9": "YDcHIj2EjZMsBFpZjChFZlgrrfINSdA4AS8toYKepzqqshEPMU", "y8DT0hrpuW": "p9ujZMaXd5opfawgzjXQb4ORFW7Lb1sdFMuC6wgqNagiK1Lhm2", "Fvagg8ik3V": "nP1ynGQ7HWkmZgaj5Ql4l2yprVvAHef5QJ80jlI4FsS978POxg", "qiJtYOqj23": "mtnzo7XQAI4zwsLAKvsGkMAYbPqXghCzKjJBpQCQ9nrK6X53xp", "x5UffXfCe7": "wypA8Hn14W0KaT5dIZCropxAppGOUgSVOKCZdGgsWL7fkbJd9f", "W7BJpiP7HY": "Q9l2nNc4JZU9fntKJP5lI3eUP07TYVOgBg3TewGuPS5qUinhWl", "AkZjN10mag": "QPZlvz9vUcPtQx5ubxNfHXpkuaLf4Fmis8qSp3O6lh1klyKxZw", "1iV2UvxD9z": "nVInbRkU8Fq2j3834N9B0XYGVL3La6HAkK8mP1Bbz3mcnt1J5L", "huZ38Zsn4U": "SFurPG7YQcb1RK4WcJIW1zYrSlA9T4aFI5d0hjCCyqp9EM5LAG", "Nm6BLHIFew": "TDdsswXTxeYUYyAcmU5cS6FchM4tsyc123Kqrv6TxFBPHWGqe2", "cb2kCYouwl": "0jt32Umy30QGQM6a1nKz3odVF0QhPWXPzXhfPYquifUAgc8Wc9", "PhxC5kH9tP": "ZsytZoQu0VcdafgS4zzJrRSqbAlIaUikFGknkjmt30MQ4lVE4Z", "syjPONcxT4": "7AFjSgkk45yU7Dn2wbsnZzrZ32PknwtKBMsP2HbCaFltlbJg27", "LTvs1fKYo1": "wVnWjqPPeIOGaexHhO4zjJW889H1OLdKH5VqUUj0J2DG7P2mPK", "qsQcT7XJm7": "vsgXbGVMRM0TzwfyruLfBCd3eRVLxSq4nFksRNzU8GI8XJXXjJ", "exWjd73uV8": "8u2bVgQ7ZUl0YJd51Xf9cumclDFcnC0PshgN7j8LHVmszaGVtf", "TgEgfJQ3t9": "uCesqV08BHoD2wOH1zFECd00WKRY1AUHBvVk2BpqjTkNVtdSa3", "ylnFPqCUML": "cNTsZkhwjhRPtFQBQSEpSDZ5UHGpsFr3lACpi98ihz9ArJZ64G", "bob79EvfT8": "hCKtogcRXflFAF6Uf3gGE6YhR1zFSj80F9tnyqCpW8SJILSzlD", "ZfNVTSaQMz": "48wcznrBo4C5dCkjIjvyrNC5OCRXGkwaRwBcUoaWTeZ9IPYYtX", "bIvmYp51KG": "aADPUGqx5CoupULfS8at9bLJgoXEsRiqMtKg29vLu4dFJqwJGP", "gEPndSjKYt": "UNN9956fWj2lSLRul79GAX1N4X8J0RP6xFNdyKyqdy5CdAvJM6", "MjFd9EsmPK": "7Fd1WOGOJewSY0A3PDGxcG9koXr7V1Dp01E3s55PNYQIiOyE5Q", "VVkQJsFyO3": "I7yNzTovcOMdXxnkIuDl8dePWGdho0K2WudiMvscLUwlVwx3yi", "uyblqn65Mr": "fnlcQTLXKLQskwz2WINVFCg6VrrdnR01RBO4tquHdmo7EsP54C", "AxzC047Q8w": "yUuaeiPedFeck44FF6ShcOUmi9CB2iMZe4zfuJs433b6fsWzGf", "I4hoVOwvMt": "6DnjBE5Gssj86oLrYEZkb3plaUcWcfNzt2H5rXh6PrOdN8fSSV", "ilcJ64B9vK": "VRZs1BsF9yLCfcM2eZ39f8t6JPyxarpuCTWjG3zubPaeS5fxs0", "lPhPxoldC9": "UDYFaRv4jmdrKJTAEs1qT3De1BXqQdDk5TC6FoNMg3iLkmyfyi", "SIFGtAONGg": "iEyX74arnmpXodDMpTZ7jhG5RGVBaDtR7nfZvCkx0yQMQXrPd1", "jTbNFqF6g4": "47znDNxMyLaneYVi8xbqrljIxQR6uXcVI4CGye4GN1iNL53jII", "EvcC6gtpUL": "YQUW8maJxkcaxBAuKszlTQSokzkxqu7WdpazvuLHAD7qQi36Cw", "T5iZLln6vD": "90YtAuuVEf2paCerJ74Q4WyjHjHTQZ9lXbYKMH3VxzD1iJw1D1", "IvH3rlsAAE": "ltdI5OrdQCkSj0CAcVpBCtAqKdfX9v96ZlIVdjPXjIQwOxOV5t", "fNoty9Lb8j": "48Vh6tWihjNvdF7XyS4N4biO2DyfGgHNZmuPkAVBqgsJR9KoKa", "Js7BTqn1lB": "mYQ8bQJos2fE6oF8OOkFVAgL517Gr638sTTv2a9GM8zHwyVVbg", "67mJOzNSkc": "FjFhaprasU36ZEiFCKXCZFTnPm4VASymoZK7NA3Nw7GEykA3ko", "AawnS3rdIx": "RzQCJCnCMDm6vo3iRqDS3Ona1lgoAtTpQSuLyuolByOhkhVeou", "2TYKiHQnvd": "gDmJI30y6aVIUEHo2Y8iqK9XlSBldK4sjaQ97vihW0TZ1KugiO", "2iXiofde5y": "Ks46Wcm1zMGvHGygn1a32ZBLgBJusDgHuSvFVR6mVWu7iFlAWc", "W9agxxMUUA": "YpYud2ZjVNYMKVSgOzQrjuAhqXcdy0rVe0y5mV0F2pVmIl5MO5", "eb2lqVapn7": "i0EjYqvevIvyViVRcwRJRlf8FuiOpidAjaVsWAV3uMROh21zLc", "TuZrUR1eUh": "oC2gZ1eAU5KD9bNxQ9zYQlXmxgwDThVGfD7FuCvJgBBPozcavQ", "IEwUKKbSVr": "FQXy8Lecf9SGLypSkICLUpIpB3WFDfO8HQ1n3MS8kbV2zoQAMD", "R9qRZpqyTs": "xJpr6zGagLWjHgLsMkRWgkBPY6bJmawz1jwqVz0AUsAjK3e1Ei", "6stOOpD4kt": "XQ97JBhzmQrazqtzD8SVkPFB7hfaDhXOZf0takQiimOcKlD1KZ", "Ne1xibK5hK": "lkbSsJgfV7TnWhhBBazZh9kzImHJfBQrkNhh6Tv4DlcvTQ2me3", "73cR8C7Dfu": "fTbnz69ab2ubPEC7kuixliKB5Eu6z7SDLFQLujBD5rKZrZZgZu", "W6X1yhXlyY": "ONeRnkjJ5sUfG41D9SSbr58owWK7v1rOyufiCtpUsqBxavnLez", "7zDOuRDbNl": "LUxW8o236F9S3RpuyjOyzLxPSTuCO2KDPJZKwg8u7ENG8LHnKM", "M6Iu6yvMS3": "XkjlFq0BMHIsVaZwVJ7K2F8F161IGiLBuPg3BpWZbwpaDPAJUi", "s2EvNz8vcL": "tm0k1Ls2XbMEInQs9fzpHxffpjDPVytJ1ZYvn5JVK4fkBlAj9p", "MSv7uYYn28": "hbRy3PH1bakyr5gxU7pLYT0FOB0DfK820YSid8TjrtKXky8CNh", "w9JlSralSu": "9V1aIXC1DdShhyKbwTbgpAuLPrHHVRQUmOkiWiYHqQVOrWHNuR", "CU7SOPVPv7": "svtcHNfAQyEaovbQviijBNE7ILD2Ig16Ns9m5MlaWpaGIE8F01", "xsImbEPBw9": "EUxzOU8XH6YBcc0bEuU8rwr6xrCjcANtNLzktot31enw8i4vRR", "fbRN63H18h": "6tFP0gNdusCzgF3CU9JzzHXHUzPaKrdW7dA5IiO910LZdzogE0", "LDuz0NeIeP": "LcgKZDL8Xqms3MyJI4Luyccke2qr2wx4hsFcpDEeQJ6dJm8HXQ", "18vQ08Dyao": "sgXubvd5n9TL5R8yDalQFJjP9iplGUTZhP2tc26sIHtOfWWPwj", "FgPaWt36IH": "DIGT26YORPSkBwZZFCi7qPOfBHoEQhwTevz6sbvsgv9BuT8xaM", "Tpd8yavBaR": "Qj1FLpQuqTCaOKSytlSceRO9G1JZseSTbzfAZc641kCBLrL2cc", "HEKsOQGGhA": "FOaJyqdWYG87Os93uiVMVNRHAej9NESyQZxGNnvwsUvjozJpVx", "GWsF1Ein4N": "07ohK6w82Wre6tSKdcPw7zbEB8W4KrZMh31OOG9JammhzF8R8I", "44zU1FkBkF": "5jukhYs2qFZe72LlM5f86pYj8xHmmGWCMZFK1zSnFLCml2eZWN", "wqF3ezxwyC": "hBcYZJf9fm7aLL5OPjyUmRwJsRJ5sNZbdgEHLQA6nh3NJVtSDX", "iLBYZUm5fY": "5NBfdWn22uLYzKJkxoSvWHgay6hHaU36z9DrBMYsG8z7RnSQCc", "xAufy8hCv3": "EvCq1rsZkTYSkLjliCKiJoKLWm7hGYI0iaBcwAZ5EqxSq7advz", "PKGYrmKiiz": "5YQYbnFgnUbYF382l57fVUGRlu47iMtcH35ERjmpCevNvSK2iX", "NZLkaVz14k": "jUZvbzGZZKBMssO2DH6TZ3mVJtYbFO7NZ8slfhbLCZW6L54lln", "p2CwoeWvX5": "6FiFUaNe5PSphsFyTutzlAeczHaMK9yEMU4NteuCDh5Ri2apPU", "GZScYm4GOh": "P8Xg25B278oLfkq52Vw3JYEKaawXYoj1BWaUo26UsXiCu8viNP", "fwVQTwTfZD": "hdtLmUAyS4fnFSMWO1MUnSKDy0l65Fo2omdVbK6tajo1Fx4mkx", "SNzuXS5FDO": "7RcvrJmcQxLQxG48F6M2HqJfCrsTpNaHcdADYSY9z9trCgOYpK", "CVC0rRYAuz": "FUoF9PFjn9BVgSB5xgZdII33s039yJVIqKXaeC3R4DJd3Z5LTy", "dryEXAG1DX": "Ow7x3WbEJRrCLLzIpkdf5c7tK0CdILjgNFsnxUEiRgqLglCWN7", "nCqWeyo1S1": "9HMGvisWCJZgKNBXoUa43qy46zSJ5Ms6lzH81kc1QBQ3mkJH61", "DOqMcE77sM": "xO8OEK2NvmA9tapjrPBjFMVqsxydnjxekrRDTInMnXI1QLMErw", "rpS8Evobly": "TjPQFkIUaCT0TCSjSg9VMKdUapRma9MX3U7F97nAzWDLkrGcvx", "q5wiJACGc4": "i5of3ESapZV4AJc4rf8ygh46TtWFTjMlR9J1RHaOi9ZyZvK4VS", "E8dN1wNKVT": "5UfTIYp906ykaCVWgNJGmw97e7fiVaSYHCJWFEYa9dKkUp4rPv", "givaCDYxha": "cDF90M3JQYOloy8SojpJTG5ZpxUxAqjv2wWusmVwXBVwYkePJl", "7Ko5gSMyCE": "oKQdaVf9zIN7GLdyseh7LkLA0oQFuaNTBc0qEhElJjbmfFkU7q", "LNpW42PUXz": "nZdb4vXbHOYjGoslVCuMt5XQfxi8ZAyY8KK2vOXwcm1kOxvjwl", "oFrroZqTtq": "07KJmSWKQQpLVnWyWhhwjVxEqZzqXgz8o4e54fqCA7EP1EPzch", "9egbxRDsIV": "AJqn5iuB5kKPDMHiPVTF9kLoab6fHYJHMlg2zy4NVqPpZimLmf", "KIedkCPFCQ": "Hv23GLjZcYs1dFraukCNAeSLDr3mKxEjreLEYPBrEZAJ1v84Ds", "xIQsdqjQsa": "UAlbFglZJl7fLW0wIduBuZytgIGP3gMYyFRcUICzEMIYrRzgOT", "jrEysnqTBT": "w2Xp3zTxqpmYqqmgW7Xp2Ms2k3D3ObTxkkZ1HAABhbxTJwgRgy", "S3iUDF7Etr": "8YGXB95jdlf0V2BcqIL82omO6SrSM4Q7iycLWdtKiSlY8lM6sU", "uofOumdHwj": "nikc3yty6xZj8f65PiDXm3YLXTCjpQM26TRlT28PIno9H9FUi0", "tl68D7dGRx": "Q6OPwVxQA9K9SFA6wmD4sMwn5auvrO2jlwRZ9tF86QvhSoohYM", "glLAhPzR5R": "C8e0u9uskIIoq2bNu4v7ZaNqvINCcaGhE3rdzaNwioRAksuLt2", "Vhz06bBi6Y": "QtQ3HSBk9h7Nf3HzOIJ2yVfHgjjAOzBqftgEhXJCw5rOwEb55q", "JdshLjGlHb": "g9fkG0YLwclCISjZRd5fa3KMBmwTM2tf56ldvzlRGqfjwRtXOp", "aNDdoTNovs": "TR5NaOtbyJOhm4AcDJGdC385flXW52OBdCVha3tb9fCgcv7Sl6", "uoY89ZXRPm": "dm1wksAnPeCpBQXIgWlEDsr3c2bQR26xuNt62avwo1PsDTakAN", "UumQYaGO7c": "oTXXZYNsO9emgBt1RJW6bmtfbBLomzD11YTJGZGsmtnNZlMDw8", "C39hd3QmTq": "bayOPAZEBRLHei1NBYCW4mdkSdNlWLAt59tKeYDUpRAOVIFkiA", "YRlCiAWD2T": "ZGHmYsZvKzCMbjdVme5jJ8W8LYRXwhiNAqt4x2AIOBNoV0J6LX", "Ygxo5TtHPI": "GLDGJRasNF7h2Fg0sPe3mC4bkjMwoXhmAi9yh6FzUdaanEPTvh", "kzeRpWLp9d": "h9hbUF9m7NRsg3ZT2KHtPa5PSlhdgGdBjlycXrO7vuZ2VAZLs4", "xtv2XxPC09": "PcD3zK5e8dqGAoEogsQ2uPKNt26hsWKbzaNNoLx9aweKImTp0E", "u48N8QizAu": "lbuHepnCLTSfy7qxNdbtPKjagPqMcZSg6Av0JEwVDapZbiwYM6", "SjCkHCdS9p": "ErdmDGOduomFs3QH3DhBTu1xopxFiukMZlf4YgvgeFukzKh8CE", "RlyvTVrP8Y": "pO45KJP0xRUk5fn7y5uT3xNcMK3U6935GZyQErB6rGImMzxL0Q", "8cU5uDbMh2": "l9DUnQCALxsfNmnv5o06EXIWs6pm2pcCpogie1yri3G6nAoX8U", "0pkctyX8EB": "jqlJw1MUrBHZG7XIRUAbIjSKGh0ZWIX6MTrysrLhSrqRlPpPT0", "sresPUCn5E": "MH2CWYFwBgeugz3khiQBbLn5YCLWSBvQu7Ny0eBIh8phmnEeRM", "YN4gKtrFWO": "4sMKljOHnRECLz23VqvXIYltuWpI5Kxiyl1MyOmUgSHEftcmJa", "ElUXZRPQ9g": "aiI8HSpF1laF0R2MaxTZQRcrT2anKnsKEncFaOQSTe9I25gdO1", "mWnQzbAPoX": "zIXATMvxgRvUSp1U9tn1nBbC4lSV0395iwK0s4mDRGn8aL3zqT", "NEkUFZYJ0c": "ZDBNNNgLMdTXaBaQ7yu2d8mSxYxVDJKq8nLeiA0nLZvl5W3KlY", "sm3MvQzF9B": "k98MwscSxGZmPODlpk3GpbhZs1IghqHetPUPsD48I5MUI9Lygx", "eIpKH5MqIG": "YOY3T85JatBkUwPtLq8Yf3RXZ5cG8BPh2pFBUCgvS4qnB2uVx3", "apdvBlat7C": "chvYYAGrV9pFIJxuo0PNMMUzL1y6SSbBRmDW23ANEfP7dfKImZ", "pHt81wDs1p": "gTkl7ZLTCOIvBKEVw6AzQT5c9pUDN0VTjKPPudh88BMk2vYhlT", "sqRhZ6ZJWH": "5wAA267MHjQ81HFTv7xJ96tHGJ3ZOvUthivimWZPi9Rb7dyzcb", "T7y1gtrkyK": "kI54EWwl5ATnsQrdGoWXC20ww2bNNkn9qCZDhvoq90ynI5wIvF", "00WNpaVHpl": "7XyVCp9C45qpuy2feBpGEEdhckAJXyAa8BEkrgiJBIcyCubDCJ", "ydluGyhjRK": "0Ah98RqGGS6lLxph8TRpbup5mMmOeGe83VheUVB8HQ7i2reFsh", "ulZaRbRG4X": "nR5xW0zwj0mKmGkDZjYaFcLaWHwlfv77qxkVBpCr2YpbyagMvS", "Y1Y6Cp7HHf": "zpybvXe5LhyUxouIrw0i6ojSCQZTcioIXWKEvonTkuM0gOFzRi", "G803qErBWU": "x7esoygCTVR5VxTBcT3FV7PNEPwyhB936O5NDrpmy1AinCuQlI", "mHP5LkcKkV": "hVqXY0j3CCIjksGjjffAv6hhN1mZxVbFZ66nDdxTpQ9T62pASc", "wVg7uKuA5N": "EdnZ4kg5MJQT9RKmez9wtN5bORTtHWAJaCGUnekjafHyX3i9zN", "qrz2yuB3wO": "AVbUtRo4sTBKS83EPHod1gxbnPl4jeZxKJbkVIwu8FfK18VI2h", "8WcEMu8mb5": "PfCLPlZ4z6E9hoQnLGZjLoJ7idrMF8fT7H3jSI90IkZ2O0gae0", "jiET03XgFz": "qKD565TlknwPWqSgMLzThfiEed1Sk3nrBK70yhHvafY1cbtzEA", "p7qIf9WZlL": "SGDqIGaFfYNZEJMKpxiUlIzU5a3pQbpr8kKWFEijYJzmJFYNj5", "AYNLqiyOz6": "5h9y7CE2NU6l4jD9tBj4BQsHMf5hGfIE1wDS5OCcd9iwCzQUST", "8pciHfG6wW": "UYp4rzGqZKuZSl4rJ6zAZQpLFY2X5y7xYBXU8gMjSgHYA3sJ2y", "ohgfDzYYYQ": "JKV7PPzqt6Czpzpp6tbUPavBSAiXiyEmOAPNyRkfotYnIodjCl", "Pf5A1739ZG": "Ra8d1bsBka2ead6jXZu5hX1urW9ZzmLVQ7ALixIQ1GlzgkdDfj", "MJvuc9lsHS": "J10CDpNijNEneIP5ApLFBnvhulyZkubvzH2VPmkk5oS99tCWwj", "uxbBfDMx0s": "qiJ0RzxdDtrFb5lZkc6GG4xtxRw80D5pkDd5YRhMo82iiOJ3Cj", "4DgvFw9VGr": "CeexEZus09Bq2u4DCvINin69LZuHYVnbiWpEPdM7awrkOZ4Lw0", "hkW7c0kUhX": "NOFKoqBF8KOMv5pcrKq7S4CK5860IJeH3GPhbKu6MOyDnLWI9I", "nckzyx8UQS": "PN6cXDd6I020icdTHxwsrJ25yhTWmprEVXCFzvMDPb6c4I9qTh", "lIaAfvBzAP": "oUqHnuxrdIYnuqBrPmrcajDRASwk95qDwviEOZKMYRzKIsMx1d", "UXFcDrthv5": "1bkbs0sC5Lh5aJtLoc12Hes3b6vqI9mwuArQHA0wMYlIK94QM9", "uhsLbTKcd8": "guesuKNF3ZcYpmeZXwzFiW7STVhRoAFHyZO5nucBN62ids31pg", "LztJ4bh2hW": "3xrp53VOFQSrKocq35acDq4QwwfMJM0RPv3pWEWT3osiPHxxvI", "WZ1YWCJg8R": "dI4ZY9tc8tH6AuPvK3Q3g9IMJBMk0CM3gOM7cnDYI6h6XeTcDD", "8fDgVXRJ5x": "9w3thsvMmcJlKKdiayhV4XDjSmZ9ysDWKumzwxnDu1RxvhMyZj", "etRJncef6s": "02ggo1DVXldNElAUqYX0XDhoL3OGkyXe8h1UCOXYRoOYXctvqy", "bjfWOx0vxA": "LR58KmFfcLUGYTqmtwVRpBEzFqUOrwN7nWngKilv5XiBAZyHLC", "KT7O4dqUhK": "Jph3WpEDj8nJxVqCqjtYeB4JPqzs9JI0pKQTv8SvfSMru1Nt41", "klSDMg7gLc": "HGdAu85DKAguhORw03bYTC631M2SV2Gs0ZXjlBjJ3RlIzwKbaL", "ZpQDOvvU3O": "9QtkNpd6vtNWpQpsRcH3PdhGmscodkqgGnpCZzBn6b5npm6IJy", "3qgaOknkoR": "wT5VcKBPmZ54xqV5L91BU0oSNDenhFR9wGCdA9myFKzZCHBlBs", "VAIKRDuw5o": "ITbPt0jVpFBzfSgnsAsXBc6XNRFTb9DJtuJ5A656PAZFQX9mI2", "9b5rIu4Pua": "a0M48BTRFle51qFtPVVvcGIRhriPJa0tkatdeONuFcMN7tfUkJ", "OkLu71Awht": "qYLEBWlva5NKlk3gqbSCbCCK5bVas8KvWQRy0fHHpxKblBTJ6G", "xChfD07fBf": "6YryHwSFECALs2EFGG9bsPrDXDeb5XnesqdkV0acUlBgVDXrjy", "ZyzQVQgsqN": "NmBte0FIQwSRmAk78XqrpjpUyjYau1YEf6NAYPDrNPEX4wrg05", "CUkl5SZtgR": "dW7eVpf9sXje7xOzrv6EWTJmgTsODwvG7fzIwGLrTvzXOOksDn", "LgAMcIjV2F": "0yYaiKNmT3i6wd3PnEVnozP6E396IjqTSWsoKOwTT3WkjFds1p", "L975G32v2b": "Dg8XBc5GdKyIthO8W5PiQsP2GXlI8DAWoDRN1P0guiQGZvXNgR", "v1VqCwSHJA": "WQYKBlt7tNgjifTyr4L1TazdjlFiZpxETP2GVIfd4TTxhaoZ87", "NQqFLNjHji": "uPftxhn0xl30LbiYPA395FY2lg1lD7DdhtEdXzJppqsCbDHb0P", "QDipv7HHe2": "kXvzSIc9OrJ8m89v1pTCV30xm3aq5sX8AdYw83zLEI6a5RXugM", "zWyAajHd3x": "0QjYk6KOqI07TGkxDknQ5gl3bnka0Ab4LPHm9r8lUbxRo43Q35", "05H7HxT5I1": "FGrNuKgdFxfpCoMkwjAC92hWIzyXzPb0Jbol9AsjxTZQrDMQlc", "CJibstraTI": "VozkfUNvprqX1nMr7Nclc8XO0Sth2eu10eeeEwWnzNibAHXOBR", "bkHXawse6L": "LR14tRUlWtA7PCQgPufY0yMhKgjcrZWKvxYRtnikgNEaWhPBP9", "xIKubomws0": "VVJEtcxetlQGupIfezkaZT2leJLcsLF947Q1dmzBpJGyJZ0bka", "vNJ7RcVlxx": "1338P1OgEm0hmRrEQARdmBpcyk5xAODUQDC5or1misHareU2cM", "hdtIIYBTZ6": "fnt6NTDeaqO24xX58uQUHC91oVMhvl9YnmKNUmHQsTFsZCZMsH", "Jr0u3v5hbo": "Qg2JlwntBhFNplmbJW3PawrbTA2wLMRDzUNSDPH4Ql6DkEjisD", "CdU7slKDdl": "8dxFo56aLaOmWBG7kQP8YVI0mo5ukb7GMLiMYoN3r1u0DDpkqG", "HeMZFnFAlH": "Viu1kZ4XDXASXysbHmNUcliohoCY5zWfoKH9PWISimdbD8ODGd", "6cePHIiqkO": "22jyg7V3hQrc11EiBAA1oBoEtBN5nyr7RAQIc2vHgwmwYksYnd", "dOx2AC63Yz": "P0cReAY2CYC8ymGcawMdrlAmFMfN1787dB17UXlGWWwszEFJu6", "VhoNwEazx0": "vEAJv3ydoLOiKpVa2hwWF772iTdHjC1mmPrAXvymeVNicEn4M3", "qB1hAKG0Rw": "YKGBN7pe9HvPIIiT47xHRPkUWnMzy8pwFJrYicthKqH2zzapzb", "DeA1fkAl1V": "aVI1EknlSVYka4XKCvLbZOVBXczkOmoZREIsdwuZkWbum4sFI1", "lRrDNlA6Wf": "4OeCZxcFszIciMzFO86dPh9bRjW1jkzrPyxeOuc42da4spdVsx", "ZHXTowqxnf": "PKKsB9hadrXfdT0b6fqEswtUUn8M6l1nOhlvlFmNEZCOzJtfUf", "qNa1DK0YuL": "aDtg2b8XfyRYdQZPI64EPs9xVEfA8NH72UMZPLvzHTs7H2xaQn", "0fkF3C5KbT": "tFuQhqxnqsIDlKojIu6dtl7vgecoLrE5B5De4CUnPAg4gRwZbZ", "mto1I2rK9V": "pfrvNtIvQPCAwLfEUNjKzGLTS2gk2CvbCpd2lBH1GA55cPxVXH", "2d93Unexqc": "jWFNnzdHNXlog586xZwmmi7HiayRbUwm1FJ4Ufn31xCRHSSTUA", "3xmM0YEDgO": "O26InHC5gcd10R8BAF1htOVQqhLXfBuMtTeJ6dWj5zVbrt13im", "zRNcSytpDo": "FZQaND6O5pFwNkzWqTAagFitpiyzAY5HjTXEcWzC7kscjBE8X5", "GlHSCXmEgm": "QsIP3yR8IbbZO64U6no3pnT1I3s3TjnnSZtcKwDYKaUKtFtCmk", "lJrv4EQYuO": "LQlWcXICZQVCGrlibAT4wF0tzPTDXlzlKQgkHMMzZvjvfvMqqc", "KyS7Kou4gj": "6siJVPStARPlgyyCmabfl9ejlt3pJexFXaC3uRvJiJH3hqwYER", "v8R5YFgUAh": "yGAEGtMijvYew2opx0VF7ZnLgavp50qHZvWzFQjP84LI9G4bld", "2nMA5STe2R": "G1bzRXL2dKQ7q3obrLHTBqmAy2AXDFXWq8WmCyQ7ccpDdngnP7", "EaMV0ucNHJ": "Wi2aPG2jf8a0qeMel3jB9ZXZTRphVRo9fPruKi1Qoep7YKQQ90", "wqi6br1NDL": "vo1XuTuB7khvOL3iA7NogWFOqmS03lOlR4JXgp98uPd1REkIxH", "sAoDaSLl9E": "G5akAP6drW39L0hDbvSSwTGyWfmMILd8RumavzLA7FsXeIzjpw", "63XfKifREE": "an0Y5iDGUU8fpjCO7WmKCgHwLc4CS7vdpAEp7s9erID8SLiha5", "M6RFdZFniT": "AsBtKL1XPy06DcnychGQxbcELX82gI2mqhoIByf9W66aFM27OS", "nyzNLiNqNZ": "y8Fg6Yey58FaUVCIRcPLLKOM3y93CJdjUo1QoketTWNYmrUtB5", "fuSXgTw3sn": "lzLIuG10N5ffRD9RVudXWdmzHu1AVUo2ILpEGRmteuZM2AiOuv", "Ib7v0WyKX2": "JXUyFrRngM6jVVT4nv0uCwhmU8Fn6YQygapIwsAgo7DA3PeLtV", "2FpL2wAOMc": "dok8c5flvMyptv0pImRqp5ZsDHXNfWiKZyj6ENbnhR8HU2Ihnz", "MOoP4gGNIg": "KjYQcw4AU2sF8ez6tpltl83gv4uS5KS1ZpcYnpYtZCTbVS6WYo", "DZOVvlvGbI": "VLiwXOSoS8EatqquorVccCU7Bnd33mIws3W7YdCTLtSOHxOqBf", "znHpx0aSwn": "qnapkgoaq2T8Ze3trdEesRgKfqcN0kplSHJZqgbH7WDeQMXRNV", "Xp444K1bk2": "fw5H59Uq2ZtpkZUbr9n7tcCty3czXeZPbUlKCCI0JkghEx9fk5", "eUxWdzFBkk": "BYCBpzpSVIhio88vZdneG5hXySLX7Ey8hTOZ6cWSSOj6clBAOH", "tZqukSiNIb": "QYPUt5xxtM54T0QuIiIQMAlDduFgPvQNge5K0UkeowIXekgaFu", "xiNVAjV3eR": "yk1L1HAY9EhoaRUR085p0eZD5r8Jy6OV5RUUfeQ67Xw1yBxkyT", "yngbJwbvns": "0ME08ztjPXri39VBTLAMDhpdrW42QcR8wTuTGepywvBoL9R1Or", "LWImWU6r2p": "q71LcQYq6LzeeQtq5sehOXJoXCpCsAqU8Xh2RdX7PrYZvt93kE", "FlGrP2cxor": "Fu5d2QLg1r7RqCAzBMh4htp66K68iAP2HmE52giYCHXZ5UHzJm", "wLQeb413Y0": "dQhcW5X4Y51sWqqdAtRvqQ3auuABedGQMoKoK5Sj0dxI8xDU0W", "aqlM74aakg": "i1zfVjds1PAbs2c4ivK0gtPivKo3T2ZXqfEoUo9x8gW2tR7W2i", "wdGJQr5Wxp": "dAKFlsdbMBkvxG5MvH57lytiJG7sSGtQqReBA8hL696tjN7VQO", "vmAyFeS6nu": "qluWFjrBsM0mLRNo9kqdmNUyevcprsxyI8hZ0LacHYitgvwrXs", "cHCMk7Ltlz": "ZS4KUQ2AfcWfN5k8FTffA5rkzGueAGMy4BRZ7uSSiXESv1Q1rr", "BR2s6R28lH": "U5J02dlJ1Jp1Col5zF8pxW31CTPO9Jl81OJO8Z5s8YL6Z6VaI5", "IL9ag5mUUJ": "SeOjv1nT6qaOyRBTXg3qxNSDGDdpOGICbqgeEJp5vXJHQz3oPA", "4FdaENh3wP": "vYRULWgPt0FFgKfOdMNtk2seQMAJxBPbKimbF4At7RZtX9h0e1", "8RlCJJgjGu": "0JIDx9tXJqQbfapuIv2SgT3aCtgLdPW1uYGP6L89Ijb2CBe0er", "RNRRMYzmlL": "rTUYAlgC9SsX63TDSwQEniCvv0kHSO4qSEAO1W3ByETGSaWtEM", "wT0U14JHBa": "as0QL6kbTyZ1dUdsGnELfieD0dZpmKacL0g1cDpOoJpQAte21y", "fBYDFD3Bo2": "7HawNzUBgcetZ8UhxvTvPEfFjndC1gMBDnokTGqRTZTBiwQJRo", "DCYNxGwZrV": "8xDf1HOTxZeSL7x0lnfVN96g2vvA20yZmEfoDOHS2WNGoPnddp", "Q0TNatr6bU": "KcZoyty74Sd5d4iE5gkl0ssYELoWEgiiY5KqCT9qBThqOZ7C8b", "2BD3zVfk7r": "kCmAZY2jWGthtF1tN9KbOWVbrcnIaD8c0tLZ4OiDPpEWpsMQd2", "06yudlKWGp": "ZvePpKf0XWB2qHs05XmxmeH5DgZclOsi4NpLj2TN67sCN0ygys", "cjUcZZhlCc": "ma38U0w05aTmwkruE0sARAaa1gEcVPYI09Nfz3cT5yMHj5qmZd", "C78DenlpAM": "k8IY7PG89Rva8yxO8ewRHbp830OzeHInTC6jdelynEOl4xWa5O", "1hsl6WprJA": "yOCiNZm6RyR1I81NDtX2P5RrEYEggOuiwKypovk4xohzlidnhQ", "HCqK64vfHu": "zvF9YUGTENjSR8a4reT51BPr9DkFaOyhisq6I2P1FaDIwUYtQy", "M7k2PrerYM": "9IKJkSKVrifwiKzZdLTNCLOrTXTR8GXu5DvmP6ZCiFchftbB39", "f9heKCud7b": "JIwmLASA2NO6m4rmwVVlwzS7j8HM1Tqbqvomxqm7FEsQPvRknX", "onKBg4v3yV": "cBeK1JTqMsVT2tE1tkS8mxlFBlY70qUNQ34BWiDv7adHFI7VzB", "wtOFx5aDpe": "zsr7t3GqEcSNjtDgUOnlDY86iOOFrhubcT9IFP2FTRYV7x52ko", "8ncq9SpcCL": "tznbCHbHiPXK1d9UMAFcXQxkYZxCxAm58mFTV0P398wZOOyf44", "QDFrU6AI1X": "lKmVdDuEYM0PCxJXnDEVvgn0vL0rJDHZdSXMHGsWIzUDfpttiI", "lNmvGa1Fiu": "OGMOMB9wVodnIHPDiswnaKag9e9WtrWVergLCDSY9XOSP2AI4A", "myJ0jnWpCq": "HRtTQCIbKsJ8PYLjOy72UDuCc6NcrcbPCWbbTwikBG3dkzmQp7", "U3xS3blY7i": "Oi8fEPvKaAIhucuTpO16JuU3jBrqDrwsTmnYn8DC7nAGlE42ZG", "An56d7wOft": "ADs2hZVvFwfVXuh6ZZWTsmRZSBhFH7ZzFET0OCyGAYI2Ip7saX", "Uws48qog3e": "chWPAL8yYJ2h6RST4smedaq4IggyKwwgpuDlov3iVFBZkfYTFh", "a57ZZsE1Mm": "cqbhXUlNGyh1eLthoOM5jjNGzdbcSJcASPhS6lfTIsTBmopkFA", "VATOVKq1FX": "lHVEFwmYPhKU5RBAu488DhLhg5xMZtHA82qX5d0peE6OQbA2lY", "YoVcujqHyw": "qCPbMLWADF1BpW2B9RunEU4IXXnC5KxQL5OXG65toVOVTIS0zv", "D1RAv5pNxC": "CLeNqik56OXORDnsybaUVJrkN2G5skNT8yfE1Tc75nOARwwc4g", "ZrNIAQtxXb": "vyCTK3YJRSSPycz2zFDWmRxeKuUau7DW3AsUE1c5wTT4aSUTkx", "7uwOzRZucC": "uRZ3fcyVZlaxwasZstOCDxmYxEWEvrFSiCPiYDztNaPoyTdvDe", "sk6WY0qabA": "zb7PkG9thq12XJM13pRXfJ1CJizVJekduLp5krsexPHnx86ZlK", "4alZtrsAnq": "ZAg233TsmzmmSvz8EjhfHwzwYhG2xjYYVSSkURiBXf78E5edcu", "etqGnywUJZ": "tCySyQQg8wLLIuDmvZML5wJ2dOM3gHAxrbjWX5VZmGrvrmiiYe", "aGeGU7lnM3": "HWyVs3wZwI67UY4qIaOzOl9v2FsFOPOLERB3hsqGZ5EV5N6zq8", "L32732c0cz": "9L0WBjJqj5ETutyqkrvYLeuuvmUUBufjgvUYTZ3ZqWCg9RTlx8", "RhbcxDYA4W": "GQGelkW60ZB67gtzwd0p05xxHOmNLdOWKz1wJPf9Tk7TaXw6K3", "28FTQppKkn": "Yq9t3iV4klfyQEOtIzEuu5GQmCn2i1A1ssssGWXFJMrSwTqtph", "mEIyakipL3": "0GR4AA5fiQ2MBBpHAE7l9lxLqciTUJwv9CgC8Z3J1oe4DpdZdA", "nJKVbOGigN": "9Ed3LmW6IHVDGSdgfpyaTsQ5U2n61pbgv6GI9WSYdFQrlPLw1f", "QwoW7tdEc9": "bbJvHgbBkxGO4qe5qON1OYM4RYateDW3G3xevUC7NH5KOZ6HDf", "8IPPPeCtzJ": "pkCpfjvsTAZmlF24mYysCpxQhHP2YeX9mWNQIurzxMa4mvyUjY", "FGDhqm4fND": "BzUjyHVDCYJ7NqbwZI3iA2e8JeatGPr4rJofuD7QAMtYPe4ob2", "Kv9VibHL7B": "7bFYXGpgYgCUZxcXBpif6yXOf59oLDGIRKiytFnQeE3MwwTgyF", "x1BokkbXuC": "fjIMifzenxdx3voDi3yr4dsYmkc2Ojy5wy9SZwxA7D3yzIfv3F", "cqKFclH9q3": "s1GmT8RG7vULsBDVrMOQwvWpgOKErs3W7VU9Burg5b6Vy4GHdQ", "p9XOt8xyAi": "3rRWCIdCnE11ALRDgiNu6Tijmoj3ssoiJAmWharhZS0GtCkNAp", "5nBJJICc4N": "U2I8vyzE15RZ5mQoaDYtGjiFwwE0Q6ZZuw0OsxHQ48g0zZ7jAX", "mIxYz0O3Pf": "bZJZYgB3oBgFxhFldAv4ReA9Sd4A6MKYAAeGGHzfZLryXKa1tJ", "uxbqT7UhOB": "FqaPyqf09JzUIqg6ow5KtN0OfNMkqod8Ejo8qN9ZstioBZEQKJ", "Hn6DB7Wic0": "oP7q7ceWK3H3M1EltwAqCrIMesotlVuWxHAxB3WWh4CkJdqEti", "phBhUyhILn": "mPmBW4SnmZ2pKtOesMwFmUdrkugIGrO6bNScBtrpsY4mrGE3Y5", "kXopEpNOgB": "KK6LkV3Ueax8fAMvqEEgzkNY21dXon6KEZNFvZXVMYsdB4vCsZ", "1x4K2mYNUa": "nvjxKjwI3fGTpWqRYMVwOMXXUb5yJnj4m5V3EPTICx87eYMEDh", "jlx6Kp436x": "7ob8TuPBgv0nzqSluzA42JThsREY4eZkOk8TutXWMMjy4B32gC", "oRwGFT8NF5": "q6c4q8fLMhyG4Bpi6hysHMFpWzRYcrEssDZH2R61pqUXu70jHr", "AlsxZ5BHA7": "Gd3P3INqHnzEEl3OBFshAdDhQVuqbTD6sfJdIVKWUMZYzscbBF", "b6WiS17x6A": "Fbt1UZwxje6evZTZPSNbeGXTvgklAWZlkBcXGxsAU6nyLtFd6e", "D6orzwPkAk": "caUyr2CUZf5E7UcZnvaH0VmUNyh20SVMKzWBodKCKcLpg5ycWQ", "dO7vzWYNdd": "nlAL3bk6kDPBAri6RvxydDFVTfVfz2fmDFpWvHrKV7QNE02MS8", "uMV18MX3fK": "lCKpbXiONmjMiNir58sW97H4qPAyQQukmZ65CSMRpp6VC51tjU", "cY6SlKbx5F": "xtoPWIHXSLldFeR9XUaaRZ5N8AvUhDlFjUZGvNoag9x00AUEeE", "BTKi4dtjaE": "xWg1b0CGZgB9Z7vyA4E2lmJx32f083FOyZu31a5SU87ZKGy19V", "BYsq7wSuBq": "22wBoGCPiALEuHyPYy65mBCOdIJsO2wYLUXNRFJjqTqvNl5kFJ", "asKxn0TRK3": "c9zFtIqbmxjgroHvYmwrn8rI7URYblpQegUvAxNTVAVkRmtNb2", "quMPqdCm86": "IQYNDfVLAedtNoJVkb1Cpb90WhWgn0z8TbPwVdIvW30xcfIsCe", "UqYwywG7qC": "mFpNLvkIGPSFSswHayOflzC7QZ3Xrd6RYwtpGDgUhXElStRHmW", "e09L0S9yvj": "OKInGU7aGvpGkiZzFkGeVBRVnZMgAPf6TXBDXN6khZujpqowun", "ptypuNWaBU": "dbtJFvipwwa3g6MyaGvC73QozKMyRGhTJ8RFyoiUfxPc0oMEJc", "Mi07OMse1b": "uXePx0w5nM3UBTlU1xyQfbg65cdhbEw9wyheLYWE2R8ByiGM4G", "t61cwnatAt": "AZzRQe7wXGTSwtITPdvcxNoyRq3rgMtXDoYLPjxhxO1m8Q6VQy", "MA4BS1nW77": "x0c1AH3jt05Si8vXUVYoDSKjJx01pcy5mj4bYplIIqvRyYX8Ze", "yVjTDEuDUK": "PTzajAvWUlvG17rTLNss2Ro78zadIIzpfXZxqjndSPXB1zHzUh", "jJXqGoHiAp": "6pSuJHuBBL9vmuZMr2kxF2Wo3M6hyTH30Kiw6L2whFQ2ikc8Pg", "mqLuES7bMb": "3zJmFL4t7GH3TgquoZcLB0bc9GQnQ4q0ixytbBviGkXVMCSXJS", "dVsPNKP0Ao": "7dpejCiyEhAr5oT3HEHHiA936BLTokVWz10SdOayrCGlYCv9kj", "TT4OLryP0J": "5vte5qMwCUsTk2uaMqf8nfSVQfoWEmUD4BWWChEUQQQLU1zVlP", "e0fu95M8sn": "tTTkItCaWqZRy3QZjTzQ5zCQHBR1BuYFPu3jFsLcsP88Se7XqW", "LRkjFSvb5p": "Z9zHk2bAvHjisMmejkvqkpTtyDIf7tfJI5KKTJ2tfY2LibLlc2", "S7sncY6tOo": "CBJDQVuw8j6vddql92OWZJ58VxHGmL5V9jvTjRi6M6PqoDpAm3", "rbo7iNfB7t": "cG4RKrX3012uqgrRFCSQXukZ1G181uesk8VacIDwtKmZrKMZoc", "6yqMy87AeM": "3Usyeke2FvfWplO6OgG3fOBy4XWaHDLoJGpDZ8ILTjRkgWh0ff", "V1TdgDbLIW": "4jekBLeQWd4p6zY6EnQgRMPdcwhZf5ctt1L7sO3RXvhGjEWFrc", "g7ZCLX8VfR": "8dIvHiW0SdmBg4slwYMVM1yEMB5O5RqnbOZ0m95uCXpTi632do", "GDSNqWdunK": "ANSaYAQdU6upgXji2LXsF2FwMxaqIvWezsYbPvzTwWRtSeyNxU", "QWQ1xGiGzQ": "6a2WJRGFzgOca4trcnfuER9zR7kpBPo4aQVLPCjdNLGUIjMwh5", "4uih4jtIge": "7uVDCKsi2ELfvPgbAZSdRzGOomoUMzTwNkuzAUEVkbyww6drmZ", "Y6Th0oRrh1": "fjLUfMrFQ04yN3XjrNFzWVxNBMX2dc63mKhE7kEAg4Gy0XZaEb", "7mbWOnaKoM": "STL32yFmreUqE7gV5WJEFbJLTX50bshTz2r16b7fKRMsgeTogf", "KRaLmhkBhj": "01wDb4DlAUxnoIQuYtHuXv1WHTWW6TTk5VsMQ4ib0yhdMgcFPg", "kEOQnUSUwP": "AyikRWiFzckpUNn7qHN3RKJNXvXcMDS9zOlKmuqc4yxniajTLw", "gajxgQn6MJ": "HY7n58wSmDDmb0aZL1JYwzr3hMpGGJFjamCWjXGtzvQ6QVjIp9", "c4aUh9GQ5N": "idxOxayDCuqOVDChk2293wycosd3gkI24WUj4ctiao13Yyavdv", "R2OVbNq75c": "qKsHdyj772nFfSNYGCvDlib5rJ43pIoe6jOztQzqw1LLxJyH2V", "LsMUL3KxXF": "90llX2dwNUwDiIjvWkTTnrnua1YUgb3r4DPhstSQ3j7H70Npjf", "5Q9yyaIyt5": "qbjuJvcqZcXg6FpXpbR2GtodQGALNc1moyE4e1fllDUyTfsOZh", "BzKzoImbYZ": "PzyTSp7Oy5DdvnV3hrZwxhox26PPW6nmpCiVIHKGLSz6uIjDOw", "uHmJBj2Iyr": "D1AQk0MpHsGxx3HTsO7MDGSEB5OY7KyhY5sffwcnRCmJT2qOtU", "XVPj3Mignp": "K3TsBC8n1NsPud4dwbXFd0EKpI3YemW9WqHqX4XDFojVQdilyn", "h9a5DrD1c6": "E8RIp3RoEdjA6V47iy3hC7D3MNsZa9GIzrL21B9gbIjN5Zq6QK", "a2nG9yH8Pe": "2ErukGvjMxWODPZVAxvmz472r5GeCLyyCczNrQP7wXuSU6dMfr", "rHW7AcyJ8p": "q47isnuOA7JghnTtCbKFKacYDNqyGoPSWmzFptk8td5R6QlXJy", "WKyTZFn0fB": "fzeZaG3keV6aN1KuVnHpMy92l9NE8whhArgJJhJv7u0biCjdgK", "MPLyjWncjW": "AnODK7YizGIndHDMcfxUPh9j3SNChkFtaA8IyRE4RIW7gyH9ae", "uTbFujQEtL": "bPYIVcYLEziR6UD6FgGbK31UgYPM629yK0VeNyA8PdNjsyp4KE", "yC7o6CPrXc": "HJcNXYyL14ASfs8HgSJKrf7jQkgoWjlYMPKGRHArBQPZu3MR0Q", "wQHzwGQ4D7": "68GHLdnYWkzPgqusBHZTeaIL3EFSas7Y3shkfABtlhjnwARSTF", "Oj1E099oCA": "hYj4sIYjyusjobBb9flkxQSM26YVWaUBAWkHrgeenEew6TJHkx", "lfHTX9XUnf": "W66TUfKIPdLydtI0oz7fmgi9DGaeLDtFdOAW3lvc5nQgUw8gKP", "zvRpljglp5": "ITTW3iFVkSgnECboHwGFQdXT3c9iZdv1P7JHJ30mXXSxFiTgLr", "C68NP4WFkW": "ZsdrZxA2TsNJuPke5lX4dxk2RXkIILrozRuO142uV0nCy6GDhe", "CcypJZ40oT": "5em79SF1O29SFhvLAxADEmds4oNY6HQtvemAjeZQc8XapSrwmF", "ZoWXl4Aua8": "YHz5pOn2BgeUfB61u7EHIiHGUp9l4JxewgNBncuVVsNMAVyXOD", "ebYWDwn1wb": "X9Y0YJqQ8uyg7xs0aoTrixFX6wkuzDIabFquLm9jHyqm8iYclZ", "vidw1JzAzV": "bIlNYbyY7B2KULkTjwST59t7purBThamwP1fXHOFOs96hng5R7", "u8RuYyXsQY": "IDbif33AFLVMTt5fk8sO3xYzJ66EFO3RbPZGkW8xpcbbLTUTAj", "n2APq0qSGv": "l0I6BCFf78eEJhBbiVvzFCaR3QdBfuz3mvDAXkYHS2VqdK3Zq3", "kLur210aVq": "Ai2uXxtnRiue3UPdeVHA3XglXk5wMnChZin6umOfTYsAdd4ySq", "uROrM0D4vM": "LHjjZ8e4luh1a5eJh182GB875Z7AEpCQKVss6v1OFosvZPYIJF", "WjyMctIxYw": "58udXEeGSAcMkJhYE3VVOUdV67zldSZvDxvVRssSN6HcLv1uUc", "4AkZT95EMR": "rJF81Xx0MkdMzmS75aH3O70lpSh6kDihfz2bCOackOdNNmVZDo", "YPcwIssthV": "LlyR1gPu7oSsFvHQIcnw98qm3v3frL6NbxbMgNu5X82dt9cp0t", "FgpM0kF11p": "j54WHzsQAP39Kuoq5PjM5L3jDU7ClD8ByLtIrC1KgFzcFgvbx0", "nQOeujGOkr": "hkShVJaz3jXj9eDehiDbn4ou4QEaxxQq1vYuoFNukM8vA3sY60", "rFafzQBajl": "y0kZuNm9x4yxl8IyC38YYWuTa157LabTd8L5K3fZ2ifBo3KsFv", "VrFitrAK4r": "DjK0q6PLKQSGFFoK1BWuZwyk4ktMez8vJZpaOuJObiQC5KHNNy", "PrC5ukD9PZ": "g86ysG7wlgVHByGERFQDrxyayDpkDH2BoRdkD26JURHUHOFYlg", "d3IAqHofxn": "oKuy3I50QQASp30uzXw1YuaYzPGRCTIot62xywN40ZDA2UY8Bm", "OypuFzMtL1": "XZvcz9xTU8d0qmoCiRyobapa0lpJW0FSx8Uq9hktWI78ZD9yJd", "5PXrAYmo9Z": "lzItXyTbBbcNCwuALA1AsxsR6DEPdcTkOxqh682OE9dXQ5GErf", "G3t1ZOREQO": "wugrdHqaFf8rBtoRMZCygpxUJQRx9llkIDlZVEwDNqC8mKZadd", "mT88HNAKBf": "dGVbdLepF72W18CywceHCTR6GWpbUux8sPdXLJftMQjbysa73Y", "gj73zKJ5rX": "CdsW9BfZTbO2JVDzdZPVVO8FjJGbdDHotZuyAFoTVWKINoeQrb", "WS346SrSQA": "EwI9VIGulNQZ7DQOZtSwS8mBmYThxtqyihPPXsNLfdNY2eOcMI", "SHO4cq3bzN": "5HgUkQk4G8CAZQqaLnFsRWUV9JJuzCpP5S8fHi7mYF0KhGyfa4", "V7YXAAnXnl": "pznaoU9F8uroNfoRfC6PxOQ2uaq8AVRPQZk41SuhbCIzLiM1ks", "5C6O8fHpuv": "7Ts8xSvXg2ZaZ7IqHvmfYWqv5pJXpB2imGPnnOcFF5T3pPAZOH", "HJUzm6OoLT": "LnVq1RJKbWFDLQDA7JhXNbhLash7vAuh347GmrwBkqAEUfiuZv", "DjwqKysZEr": "z1BIt5mSIfyuOb8s1Mz8kWO2lnHPaIi1IT70kJF9vlD3Yvav87", "ZIoQtimbui": "fkVcmRpd6pSJ9X5g8KnpfiqYPhxb1ZXBkMQwSsj0gZKUTpZcWB", "r3NCnoJedc": "xEdUTKP6qjnJS2ZlGdDHP9dyDONyTPBFRDMpNkIU4DNDsozypt", "d1Usq5DtZj": "Dbw16ZhhqAoZN6bAtcoyhSUescJu8UE2VZTSIT2g61H3fCpHWp", "G5PhHjVG4S": "lhKN7HMApSUyAaqnlrRjxWbI8Uk29M63Flja4Xd0DUjgdQXpeP", "iKaitpxEak": "nULZZmHKwTcn5CXtZayIQMuVgIb5Pj3ZFjOy0XSRaWfwfBTkii", "FByfJruTDH": "rTWLsTZrLJoEHFRMXSwKyYewJhIJJXe8OJOzgs2TGvKvtgUWRA", "2yPv7IeX0Z": "uValtrZyZUEblIePFjPLAYUlUHbZjVufi6zGXFbKU6Tv2bYxqp", "hZAiY0qECb": "qplmsHN572cSe3Sss7Xsqq7iM9PtJaumaWRFGGXpi5VM1cvTny", "Hk2EM0i1AA": "B1IHGhLumjtSLhDVcduuyudLA17VWAMb6JD6LK3r5F6LNEhyW7", "TygF1Gwyht": "IEJaHXedGMqeaL03KwE3gFWx7a0quz8zr3aQBY6KCYtouFA3XX", "PrvHTQV8Tb": "d4tgNJipHShqfunyNSs05P8u8bKJGx3G2qp7vrxCmsEOHmwTZV", "R1gEBsw0gL": "U3E2K9K6H3CjGmwzzIIzLRLacOjrMtr6Q2ksvLwXnQT9Mi9VEp", "0oBtDYxlQP": "2SNrTQcELV0c7DAxhvxWMn7DQ3QTlGZqqhIC8eoRYz59HHjWEQ", "ig9oe6qMr4": "rCLyayGZesCCrwspfWteRALYdjo46aWfihL9Ry0t7zt2LYEYye", "hdl2DCcPOb": "qGphdqFLJwCm6MTTVQxmvcoBgxeSawJVV5j7az8U9m9Q81RHdu", "Lzyp36S3KB": "KufaIwkPqsYWlSP8aMgHbMLPdS2NJzroONOGGhzOj1Wl8zVISO", "sZoEMBy4ak": "pgjlTNdtHSUeCv15Io1PtQLa7sOu1kCsOlE4lkIQEiRWCuCKzo", "gdjapL6rzM": "xqlV4OZjFUe5he07dDBZadkxA4hKSaLPiHygk49u0OoksY1RT4", "v2IcnMu534": "Tpt8O051IUfPk4QzeQFV3DbDqRePdyfvaGKMyZAzZsMUFOQG5y", "f6n6w8YMeu": "hHBcfIfREAFQAD8cmC92vPkSM3bUcjoOtyc99KNN4H2UBSFGtz", "TAjZqHoup5": "Ho5AD0uwLi6shTdALWcm461FHRbkvbreNZkEJ6PgY5SJYfsnTv", "XRanh0D95e": "mgaYnBWarXSFXjpbOCC0B6M7yeTozePlFgJzAkYrXXPtrtK5Jl", "tTwBibrZaf": "C917W4vT57PfIr7byxEewL1cHLZaUV6YxXlv1FjTtkFiqEo6nN", "hwxmOYyBkH": "tEBZnXt3iTRNmxWDzIiiFgFI8VGqvf2iawTbkAhhRMT8wNBTHI", "m7e20T9Zsz": "PTCQfHNLe5AuXlTgnrw1wSUS30YEBLIUV2gSKZrL7BqkYpSz58", "7Ng6dbUHjR": "rFo5l9zjL9WJoxHOLEkCx1cV9c3VtqoHqMqa58e6dEtycmzOYd", "FWoNJkrKZK": "5MZN089xcPm3aisLPtV1AXcuS3xjrl8tNxJZsL6aRBgoPapmF3", "vyK0tPrRc4": "3NrKuDhCNx1mhBkGTFTfzbHq8szw0WGNMLZl1QoHGvRH40gqrP", "QeLbLstKj5": "9urRZ1eI0pI5JiIgvWKl0jHe2dxBUQ9cGTfXjSPRAKB0gWWNT4", "NAkkKtI8sT": "RU3zUMdXy94a4dCYs62W6WMQGCE6QpccUT7J5HyncVyEHLftDs", "h3pzasqmYs": "eXVS08QVF7MqVbfrMXrhYpfTNMC0iiVtMLLhsrjQHQVpVX1aJL", "ejNh2lYsWt": "YpSI96tW0n30Ogmw0Nwk94k9REdZmET9oLHUEUKVsTyyZUMK7u", "iI6qMVw7sf": "YJ2jXHxBikgPqBvhiFiNg4psJ1cUOOIV2fTZkKICQIJGJuBwMz", "D9ANR3piEM": "BzGmYBrUHAk3jtpWVJsNr7mkFuTjKnucCeuFrN1ZVsLCUovy4c", "YkJKshqM0B": "XCRxEzJ6tx1GIgBbMMpnm9qxUsesCB2K35KtSftTwfLhXDMSou", "PQRc59ETSN": "krzi6iFPvBMdTStBrHLIw3RZrcdVGaE8gfeMxQIF3ssLFCbcZX", "GtxC12rfp5": "XOuRScyZZ3MhqaVKY7E5BLLuSepkZqlDSw2GUSNbRPs96MbQ7J", "dHzIxIUzVx": "RHuJvTmGLHvJ2TvWI0pkuoriyz9Ye4wCAUGP6BwtEcNmvpdrsZ", "cFIaAanHlE": "GELADk2tFqPRuAEnnjPPXgmXQWzkPzbqmacGS4r1gWqC7xQ64Q", "tUZZobePAm": "Pjxap9NyGUvwi9ZCekr3w3cqwaZKGIxK5dnDnxO0CTPCLcb3ee", "wINHDueMr9": "ZTCvyrtG5l339jWFxCeF7UyQ1i9oTAjBeBKTEpSzkcyoWXemGJ", "NrdKOtEMhL": "6LwqY4V8dZphhwLBFSu5q1jpqHFWJzmX7rm6CiaVtIKhFLPCXB", "8AukJjStjo": "wvNrl6QJxuOb4oFDNxzbUxe7YRkXD0pR51RND3do4ucDTpSOru", "Xuirj6XHUK": "pTa9dEr32LLA6x9u0LLmGRSikbgpCe5OdNGclmoPL4FoQ5rY4H", "mLxfOcjXU5": "jMTr1Cd2iiG48fyrZa257Z0qYBzYYW8iIfIdkyfaABmpiVjuRn", "6e08gFP4Zk": "U9lHZBiMnFtlx04Yh4Nh1ZvcWopeZnShBTbWco0ZHxD0yS5BtU", "PV7KXR0r0z": "a2HlCxLndto21QiZMj5RCEwEo1GmunlDhPjbrldiAxC0HaLdXa", "yQGcjmNl3U": "2reoDfDZ65BY8RXQImT0HQVxxs2sJ3W1tkKu71VAUM82CWQZh1", "G0XCUcYo6w": "G3fD3CAdlCw4O4fdQ1Tgl8YUqSqoFQJOdiAhBNE4Y4izcuIyMM", "Jl1IyQIFp2": "KtNuvYcNRP0iQLPBZnmu044U0WI4dthqGPH2Pt4o3Cx9yz3Tw0", "XwlMQvXNAN": "jL0Vjg8YTkKWx8HVUjaSYRra5X8vY3NvBGIVdNHy05NdxhMdsF", "VD6aoFPdQz": "faXEnNTgENJa1vDosonOMVwLBFwAMqbxp8Ahq6K1nWH5bgj9PA", "NbhUBHERX8": "tQtWOfhF66DbgOK6YisdHqGdUB7vvi1eTwA6VKXnbETQ6czi62", "i6cHweBfku": "uh1XIdkeqfgKIqHRX6W7QXu4EVHFtCwa2fH3ikwhS7QQ65BEwt", "ulQMsD03GE": "bw3gokzMSS3AHpH14iXj1qfPOalraCuJEegJTtrba6X8aWTuuV", "a1cqW50Egb": "uxf50lAULly8UiXV4CKqeRU6w3763x1w4HIciVO1YVXkIlmAlv", "9Ztd8v95DV": "OOOKrVp6vpjHG1txJoy6TTW4fY3LVKYk2sLzGNtglprQ1cgNWJ", "q3BGE7tP9d": "9JQEV79XbWaWTkE7mlKFsD0gOYoHo5rUrCowGSJTtpGGHnNpsP", "0I9EGPnb6N": "uOF5WGXNkLaXqvAR5BUuT9I4gaxFezlbZ5Or4MIZF5BGPDFMUA", "6JiZ6FljA7": "TZgjXwgPpVVt0R6gaw7ELoFR6hwzXm0EtN2rJeOFrQuXr92VMp", "yYiXLalV62": "zqWmnIVIZfHhAbq336ZeWTwvFNKvOYeNKWh2LHgb7i8WPY9T6K", "U6W0rZFGTU": "ynBoFCdq2z3y6UQKh6PGHMJl878VZHU62xTxfRKcbdaoZM1TGQ", "q83wnC47vV": "0KYromN4NIbqrrnCZtKtkCdJeECLGvbliLDhTeFC29cgmhnqLN", "SIFMycxPGg": "N606WQmk0NglrEih4EjqV6HmKDVOHIiGEWVRNttHbAX3qLfH1X", "vxZ6LkkcDf": "lZo2OgGE9fgn1sa7MZh33v1AQBMBcBpRcsQ2EeXWmDz0JiwzBj", "YUdvPxZsI3": "xYGVlpYY2sSYbn4vUtCgY64SjQ5NVx0YQlmuBvMTbNGXPBUzoU", "9rS5mjp3Sh": "QR52SSSIHzyJDfNU0kjtU5hOzhGmiVVAKqmSUY1o0pO7xh3of7", "WkAFYN1c9V": "HREvkQCmoU6SQxt07aH0ziUapywKL29Ami5GnoVNbDJ3hr5Y3i", "nOxoAvVsOo": "eCEvayl8Ms52lhU8XOUvM23nvqJMuH4doipwmNzioWEkVa9DHL", "5dnVG3sR4Z": "e9Kq49KMbRiPg8EswiKmuRq10D650cqzvq0QPzrEyYxbIAyBFz", "ZIczjuRgvZ": "Rrfo45voEso26ucDbcYunpTNsPV7K0LCCwpMe42YmT07n5UIUA", "fR3SHySel8": "IfshH9fqblwkJzKCvW0Lj8Gt5UHRvxSnKOwg9K1Rz0EK2reECm", "NN2pc0EvqR": "0VJouYrIhX84YkSQPudvY9clLAa1fAEGfcAoyi9WecLaSdvZGp", "J2raUaaFwc": "VXa93SkZ3883fseejBgB49CvzqXqoyC7VfrqjkeMDctGsvjglh", "258eOhnf4h": "ZArjHtqyxvkEo2YVhutXPE3hYF60tNEIrSjMjIrv0k61KwMla5", "DBf6Tpw8us": "SSzA4H9rfsQRuDD6M2GyyE2eSHxTh28VoYUzTUayO63ryza3pM", "1URjTBUEwI": "O8MYb6kGEa5k2FpanoykQ2PYsjDokul5Kcm6l6G6HHk2aKLZ3h", "rQAld9fbq6": "Ww6FqcGBnoFCfereS4uckYypDK9Ms6XVKBKYc0Mumw4wfwGhBv", "XTPcm1R5Hq": "6F3328fU5LN7Roe1rJ2UM9ZRFlB2Fn6dYqYZ8lIotCr2vaJj7A", "ZJbUc3CiJl": "ExFQSEmRf7zRn8mopRbsisaalJqpNR31KTum7j44FHvbXhCLr9", "k2IIEkgOBr": "KzgpwDkKkLxJwdhFftrj75Ch0EnXszyUM1398r373n4bK7s1wz", "cXh6IN1TDI": "osV9nrxzgLoYiILw5Ej6Zkmgb0uCWbUXCzJwjNER2W5Tf0t8I6", "iUGq4VxvkZ": "hQ2XtW4Ju4lYcCNwaBWRJcWCmGpMIaSweFv8wIu6xRKowBomOe", "e89DjAJi4v": "eF0lTUUE6tpTiOdyUwuzw1o8QlN5YkwxYYo7yeE8z3eRedpIH4", "1z4SFiwZbo": "9EUPyXxx00Lq7dQbMCJt6lAnsGWmpqTlwvFVhgzRCL2qtGMxh2", "OKFm0ongjA": "nZbeQPImZI2MbyQNk8c5s3jbvF9vUILFrY9cp3GU5okDnvTj9h", "0PSwyTEZ9M": "IRSfO0zg7Oqdm1BEcIqXrMDQFipqE7sOSFqLUzoyVU7lMdARoo", "DOThVoNp5B": "xIoXdFNyIGddXzD4HLVc9Qb1GyM5GqAYcCAL7yTHz35mhz5Azp", "8o9aFIXfuN": "xP0IXYKqzvPa7wU3fqsZFNV44Tde25iVrCjy9ZSVBylrYyajOT", "3j2LJ7LsWr": "vFUDncfkKUnnFU8vDeOnLBROQmY9Eo6CzSnyGHttl5xB1oE7WC", "yYo387XISF": "57Y6sQfrXU18ZwhSAKK0BxHp8II4LsINkSw0zHUlUKoV3uUNw9", "G03e6t3Vwb": "OuCKeIvJpXg1sSy5r1YIRW3uASJMW5zIOMsJTbUQ8J66SKfLDl", "hIBFLZkr6k": "vckKMRErqlNMbJoZJSXtcWoYN0g2zgVeeh2NUw9miVnSfxLouW", "gHBgG7B7V4": "8mEUTEzIFQDV6lhGo3X7mrwrBY60mpA6CjzRSSW1GbAh8sIFg3", "AHeP2CS5bo": "xuLknc0rZAvxMkIkkoPRw17bFmvepqTebfBO7ATxpT6X3jqyOg", "2jxjAUkhBg": "tTIb432aOJ1FgCJT7wSBnsy5EI1Orxbr4jKhP1nHP4Twa0Ea4r", "p6OjYqDCBq": "kacfm1qEnpuohzOsGn1BEMJMVbQYfi3U04BkUCqYzIIKSm2ag6", "91q0JbPg9j": "2tdmCzFWiOcSkkSpkBZRaQauDBF7qxTG0sgfrZqdmkfV9SGBHH", "dwzr16WdHh": "MpGtBvGbhPEueteKTsAaqKPneC6m9CwbDMexRU2w8P8W5bHDsJ", "KewlEMa8qt": "JS5Hr6eDuUWGcIvP3x4kgS4X1A0q69LjU6DcspXRyvYzbTsFt2", "VYwo8wnQZe": "fIUhrYLcQFNewbfblM34XgPDt6y2BknAQot9l2RpjL7gCrKQ5z", "lq0c92Yg2W": "xtgwuCQWD2XUtzLYQAsZ0hRw50iuj3OCgcc2ZjL8r2wsoY4yvK", "afl7eRrfuw": "NxcqTp2k3OVrNrIKxJp2oJMUIP8vgXx2cL9xsuCpwddoP4S5eK", "ct2Ps4oHIp": "HDAmfcbeRuPlvVjzrHL1QRBbsS99GDdomxMJxckK47y3MYQhQ1", "ukNQUoirBq": "tuYBmbIsDI7241QQaMVRYaw5OJMzVkokpyIKL2uTTyJf0jPYAQ", "DYZSVaQ7Ua": "zexI8kw1lyMCYoo5eNHq2lRVSWINVRCFhmU8tAu1M6KAWolsXk", "KlGxBQwJ9l": "hFF9UFJrwZkMzz8ASzGUdsb3Fg2cYZQCjjBJY8ifrQRiEUyUCZ", "sZbcVkqshC": "lwePK0z3sD55ZiyKzq5rBiUGvWayEtzXYWb7XJ5QbC305EI6Ir", "vhXgXhYwBa": "EiGRRvfMLsd6tzpIr9P23nayD3dkFXDTkN83p9WhzFtcBUSski", "d1xGTI3fML": "o1qOKnT80h7NkCOpNKq23utiOCwotWRWVPOrO4q74T4luxP9vh", "xA8l2fEBL3": "VrPrpm1vVcVwTv5VoLiFxpquJxc17ybGaongc2gVAWoHToMFYf", "KcGemCcwyI": "wRMcMgbSUUNYwWVcpBFhGb1Rtm5QktLaiXhYIKiH6N3Acue2Xi", "gpZErLb2Pe": "DWu0mZGWxmNfnEjUGHs46HXls0ITIyVSPbCsOXmOZyQqbnzi70", "oJx4cuFLHY": "Smqb8BkHJnVxttlYUmJ8uvDo0BflC8I2dtlIQVRTCBnfbtSgor", "UixZOD7KpN": "EK5HEydg4cViHNFPlHzZc3XvgXVr4RfVm7Q96zKs22AIuBATUs", "VkE3R8PE4E": "cAHM07NP9z5l7uE1UOdV35mfEyvjcM0R8SngYY1DpGLJK9jOrC", "c3aqaXl2nk": "CBJAyioY4W3U4ztiHBgpWV1vqGU8yfrPuApwg2J5VB4xyl0PNv", "QZFjjPIFgx": "qyCcF8xSeUpSIL8Tz7gCi2ePwpyxBYdvY7y5LoIb0lWzfjF7Bs", "QXD4HwoLRV": "4CH8m4RxADsfzzW6GRnXpZleve8s1w2mkYQJrt6bvZQjlY5noZ", "zc7r2GrNeP": "Br4syF4W1jj6tMSmX7aUYhKE67kmRlVnQjCo10fL5HO4c4HDBl", "NR7ylzLwN4": "eX7ihjzSuLBQe2fQff8DcF7waFLPRxT43nRHWr9pnGl2Px5Ltq", "Ij3ZNjJawN": "mrBHkiSOhymZGGpiHAG6RauM0tZ7UjdAASd5xW4A1KmZlgQFSQ", "QsWyXzxQBR": "e8EUDbUiLUuyC7zCjHT4EFxD39xS3y6Rx3dFBq7P0STn6tkdJu", "mUCfm2gd5m": "VlKBpIVQmLUaF4HP5bWIaadzW5HFQNkAXHakR5VTFNfYeeewWY", "MQ6UVpUHJj": "n65wMZNDBaQwlb3BaG7XBldBf9nirPPJpqIF0ZaFglYmXlqqvw", "tq40eveAeL": "f8IbePElRFjRJVgL3uodYBphS7DQ1BOTi8tAEQytsneuxOvMtf", "qTHCKHGJ6r": "UTrzgjxxLFiWe3U61DnFVdAbdC4uxwfXiuXGSPQ4vvTPobPHqO", "2AnQbNI8zN": "E0R4ujm22x9Dm3kGjpjkxX1tGLvWH7aegSCzdqvjyMXoT9Rt2h", "ogEc1WraAt": "yr3RYCN7ThnQVZ8wmLa4GLb2uY6fYz2gdDv20qIKQpcwyI3Zsf", "x0QMjeiEEB": "q5Rr0ZpFVButSfndMHAzrS79ofDxR4NFix8EQdejXuX5czxCFX", "a9YzjVj5be": "p2ON4vkpPrK0BKkveHQjRyh2YBO8HcLz2DDCclmQ9BIAvOwlki", "x7Az8lsu6A": "NtiDGTb0S3jMuY3kMKcX5sn5xYAoOuXg9U8Z5dcQml7OCaMUsc", "wBkapftDXG": "R6CQA8xz55e9k9oMYdWKXkEeCM563iVCTZGS5Ljkd2gXnY0KLB", "p24YoRh6zA": "A02GtHLKFg84ioa4Kdl0tbitwMq0mBq2XdXFWNZJI1BN3blPTJ", "FJHNYinte4": "9PT8v58uyjuXYCMnAVwRszcfWXsb3jf34ulmb9akyqfB9U2wGA", "FBaABXq8aQ": "5Ix89DBAdc1SMtRhSs9V9UcuaIP3ibNfL9Iz0BM5znsx9jU1Ew", "9oNEjwyR7U": "BtsoU90K79Sr3fNCYsawGhleeM2Ys9NYJgDm3CDL7rZY2y4tjJ", "AxYLDQvD3V": "CMLtpFSV7lBFGKffbrcuO2lnZZbr59fqBigQOHozcuoAJ0j1mV", "Jj7TCKEFlW": "IkQqqKItxtgfx7SEGMLhq9YtsN8vZuCw6aj6phlkY3OhPw7kNm", "tsBl6wbUtr": "iKynpHc4dY5f4aEJPrZm3AHWaZSNwfo0jkteBnkH6LW9EXkGSI", "qvMhd8BFCD": "gqF6BM3FXku7ioBydIB8LdoRSIYX8ookNUGdTeUBxI4r0r05iM", "oPgj6wVkHO": "iptVylczsE54we8N6OsgbbTykMRrR43WItj872OhxsCNeRwPFt", "mNPwRMhPle": "Y5fnL7yc2ftIvD2ywxi6AjkyxrKWW4d2CSk7kJFxOsbPF6jRMp", "eOZyhB7wH8": "CMbyl2KLaX6NL0BN6lS6nW5mdyFrDDDvVoTZF9hHnzmArmpE1O", "GEwrtNrC00": "a6xIZSqGUft40qhJhZDdQf370eNBvtuQknANVpxAMVYZImFGs7", "zmU6m9Mg7m": "BZpuyVAwS0zbfJ3yHaPlYwm41d1Yid9DXtoG0LmGEo0unHEv4f", "HA9l9PqQ69": "PBmRfQgeckX5yVoIjs6guvCFOEqbnkXcEBze9dOMdyForTYv04", "GAJjp53Aie": "mn51d1OPJpq4mwdvtTdZXlEkZ4DHZjjHzGkfs55h9Bo4l1EHsv", "seTeCQpVIX": "VGvO7eBmaa48rkqTujqWJbJwfEAk50589FB0BkxSOBY3bQXriO", "12gJ5wFAdf": "OK7GIyIVJ7f0sCyf4Hbz9DMLSTt66y2t694YpTRv8hOAn8A63p", "3wnouMqVcN": "tJXXjKGLOuTxoT64r1oDJdFEAqxjzSKYEoFrMhNLVS1vSi9q8Z", "ZPO8EcTuOW": "WZ1JnfGmz8dZOU03DQ49cKdSKWxnNeWYm0Hpxlb1F5oUpB6aWc", "OiyqiwIN7M": "H9bZEUogSferN3Zr7AxZ8upmtGjUBUZgfLhDP1zTwSDsfQSjI1", "6T0B0GMUfj": "a4cSLi356PGIaPOGymAdHpLI4SsI6eL15vAwqcEspcdudD3qZ8", "lJHzdHQ8tj": "CGG2SmX5PuYmri3OWEqxWVmwbDLyrhnB0zFIcevP3lxhzB2LMh", "2hPdBISLHF": "P8v7cwr2uzPvh6PEWpY229xWzXiZBr6MSpW4TD48zWofHekIwD", "F21Nimxd4D": "8jOQ1OjKKlA8OmSJ4kkkJtKT3LWCTMME0Jm1qCkgJ54StLh6FS", "gYIfva3G18": "P5QuCTugedUdWT95x518vK6hrs7oFiLA3lyQz5Yt4aDzKQu4O8", "GuELt0HdOt": "yVKZ5SPgjYHbW9ZaJ4h6r6dz8sC8P644M139jOcMfy27ilQkdU", "G9BRygNzd9": "dxXeLnSA33XZBvMqL3RdLfBQDNs8OuR6TOD6YLicmfTtqr74xl", "9AX40UQcpp": "GOAsDOiy5ZzSANBy5mNIOxva9EAOzQoEXQ5SlQsStEKh03iCuK", "Blu9e74RiG": "1jtt1uAL8g0V54dsj2FjZQYf9srih9NQb0hKDOcEDEFnSph7aB", "YgQYKEHkV0": "RtQZErlk8SGRmA4EuiP2j7vMSB6yLEtd9ADo8EnFlCKRye3Gtm", "6ucT5mN1Pe": "jjxTtiQPEprTGAvOVJyleP5MJH5Ru5cBYwdHYKoF4dtypML56E", "FJXl0PPZO9": "i7nkld52qfnaORewZchAQvC1HVJ1T13kf9x3SMYlt0zReM3cim", "FfzY46V12x": "CAnkPRH9MI9dM41yI7dWfDtHxoKCONef0zFMUQt7nXZAYBBVhC", "vwiUBwmNL7": "2VjTFhUAJOXnWMYeeKIZG9XwPUQyiQvE5su3l5ymGmwTKz9unn", "vgIfjWnNCC": "nsjMNAJYhWguAe8E2QOFuN4uEVqCbQVDAh7mUdxZfLrSTefTak", "hmZxSSV1T6": "uG6cnuKc9fIyrwpmrR80Pukr3aOWcYmzYLry5qPajvOBzFj0x8", "RoD3oiWKGE": "Yn8eUuX9Uq6MT4N1wI5Z6WpN6IPSwoaHLQjZjjAdR2FMhLY9zk", "BYcIyUtavO": "eVuvDgLEdaZGpQ3vzDxVrBA6kONgO7sJxLzXHx0XhCkoyHDjrM", "daXp9Wxewt": "EilBAPHxLJMrN32pXiroS0nS5IbriPf0n5vnfxzgzqu5Cgdpw5", "KXJ0LHNjyk": "9jGChxO2QkBOzmBMkNwIorMbzLwUeITxfdjY5K3ItTShS4j1Pl", "nFqtgPJGSW": "65ORTcmwfIu13NCxxVVY83pfWoiHUXoBeULba6SOnVgzSW0x2B", "nlZGtdx2l3": "trhJk2OpAv3QDutQwCwqC4zFC7PYTZ7LLkTvUjbtYODQCKi3D1", "Qt6gmf9NTd": "rHap0fUPmpyhwSFaDTFIicS96aVZIRV4vE5GiVACihwalx2rQX", "VWg4eDtMWY": "qDnNcXuV4oEvGtMwjwmLsAIGlLEFwr7Ivt0SLXG2BTxGaXz1b9", "MImbPsPfuj": "tphonuSfLmYCQLy6dN936VWWhtmPDUJ8OjhzjfxpmlvXYfDjBO", "0cKRBgGBvu": "wzqZCn5VF6bwN1qhGrJMtTEIT8slfgDvnRyvKM4XoJQGEhXWPV", "XjCvTRGgxG": "5zEERrACntn5HDJfmKJGvPVvvu1uwKJFzXT5rOHdHn2ZxEKXG9", "kJToI8eI3Q": "csW4vFBTgSmGH9Q8iPEFJeY1Yc5rYL70IYKgZsR9DCllyaJvAC", "UOehRRyq1A": "IXnaCGzoJieQrfusEWk1YbUYvL8mNKiIWXSiRKAG6fPjvAhwou", "qBAhCL1JI1": "fdyacolNluGxq0caLPfEKdq5IH1tzlHoKwQLmogPmZkempKRkA", "Mt0tCIMLid": "F4m1kBlN8eOtmQ5CbJ1xJiRGR9dTI4IPeKwGlPSopb0VFpNztF", "SeWviwhBjq": "1N0aO6kDPQAcu2aBuqkiH0wvUGAlh3JeOR7BGhdv0TUIKk1DCm", "aBbAGdcqm7": "ToneQbuXhZeKb0aXpe85Ppv0JWVptuAtrr5TDTUfPTJSBkLTKv", "6EO4RxcEGp": "DRY2xw7jHxVz264XbFtxxKRiOq3ei8czneKrxCZqL5PsI849zz", "2ICRb7WW68": "z7lTJhFipcVSOq2O07XltU3J26z9Ia4tjd6uwU0ab0tFuEnPZB", "ZZJTHJoqf0": "hRntnngitk9F9H8eNEB8QPwM9GLsjDm0yDqEdkpqW4uCOewg9m", "kGH9crP1Ul": "6ncMxFkG5NMCk9B7xE0btoK28U093ZKp9oZ1lSAwQXm1eDuTCn", "owgrNGB6dg": "IpErdzEsfALH60BZMIAA61znDaks5bvpv6NGKMMIgq0Nk1T6jJ", "4vcmgUTM5T": "TujMzDHDuvsicwY2JsROnMG7JCQZ3uwasZZD9qugYQSVYmLi2J", "nU5HGDCO19": "1TEPthQc56gpjSQmyGCfelNJ3RmA9Tp9p3ui5BZSYUcW2wFn8B", "ijuzB3UP3f": "OpAxcjwDE9iEB6LHmuWilYM8PaIbGdCVuCdQegCViMLGnLSYpT", "tWclfvAXxv": "Qr2Gmc1dbSe9oMeccv58Kh53Voq3LUCu1PoT2urRtLeZx6arm4", "LENCl7thEv": "t28v04SmZXu1QQV89FpuZtmv5Ggu9jT1AcTwdduF82fEe3yrXs", "SLb47jXPqz": "JcthR6OsObN71nTlKPLOkPeeXFe7yklYY6Hr2ccd9wjKtp35Ss", "RmlH7jMNxm": "UaYJwhsS2cDqHIPODxsEf4JjRbD2Zzg2PojD6sm4ChyosI9Ipn", "BQvPXuBJDJ": "9ZnrlFb8kUCKBpGXzYAQIeqietLL57XHJnntAAUlFJcPUJl0dP", "O0ariYTnnp": "jSfflJa0s29MQctN8WGGwOkHRhy2dwS9biLK1gwL0TukxFIgRV", "1UCYEKU6hh": "Ke9HOu8hgAjGIVIoklGY0Lhzv42mBYvxVfgJyyICwN6Ps2iNyq", "GBvUlXbPlU": "GkEJdVt8WwESIfWZh72fCFQmbP28bj0FpygP1HFGW3B33zdeCx", "yE4MaFKaWa": "dhJpgBat1NgJQaMEEBqZDST4ESoloYc25EEmf5E0PU1EanD7qv", "Q8NpC0rBZL": "SIP6JEe28NDqJiHp9DTHC0n7nFD3DmXxSyKRrMaLiJMHxPVwDh", "Qk1C7ad9Wt": "0tOCt6RAwCTEx6eSZSUzfB4eDyBMa2nkHmb64juWvWKJB8qzU4", "ZjMKEIDEzJ": "tIIcfageYwBls4OQIINrlHpqZzpx59q42RZahXy0L6tqP3YsKO", "4iEUFO4Mgl": "dwzVErZHuCZery8jIkB57bTsj60ljjvkn9Gm2x1J32Q4dWdQzu", "XYbCKstgZ8": "rdknuXntYlnBUizBEnKSk6ChJgrVRoD7laWKB9VZ7FE2jjc0xN", "xqvJiyF1VP": "Eic9e0sFJgQOwjvUPUIuDQ7z8AP1lG4urIr3g5sIxNIfoYwLQQ", "CTE5J4PNu0": "Pig3OJxG9A50iebnDVGErPrgRnxMBlmBgkoTwr75EqYarGXq6X", "sggpfnqwZS": "1JfvrOxUvmWx2rC56Q65YI0kXtOZs2lNRhgsDStTSoaAgKzS7l", "D4oxPQd3Na": "4pA7hy4QiKGULPhgzJh9tTcuDzmmlSZ48tqK53CGzUJn6MZkOP", "03EQzwx6u4": "VIjNuLsHS9BEJfnIqqQZ6GWsGBcERRAv3aqHL0CZCgTYYNOCSK", "EaQs0YgM9H": "qb3RytBtKxIcVlgiQcR4R6RaWRKwFcN9Uo6k49UuBpqHnJJqQi", "gBbYKk4OX5": "oMmibywT536abOE3JQ1TXF4UdsaZdWnZ0i9au1Si2s1BnBihpv", "6dHUe4x9mt": "ELTYhDVxoX6rCWFad6JbBraVSD6gE4NqYt2QQj18ZncJBLDLyk", "NDshR459Ia": "HFT2ws7TMKw7xoqs6KIRmTFp7vKnVLdXEq4TXHlSx84hDYCinD", "5vS17Es5QH": "JInJW9wy4SkjVRjbpPNH1n4fuDEJAKxTjhqfBBBq7Nqcj175EE", "VmTrm5Pcer": "9FtajgmjiYbf6rL1srVPl2FHgZo6nEAQTaNnZmOtbEmQ3cKFnS", "nF8XrSdxhd": "7pH2PfWr3qSBGQzQducCmtg62wYE2MnrRWY6C7vWrnj7UyxmVh", "CYNMGx0V4j": "jFs5FDbrteZDuOBlt40NI2cylvhkW03hk8SgvrtS5DhX1qwq7B", "iUXRBc4g7l": "IxJjoakBihY1VSFquiNZXtoqJJWXZEML3HV6mqYsQ719ENKIYw", "iN8akXspwb": "ZvpNxKPC0D9IsrBcDr7gEWrUtGYcWAyYXf4NUvo8MejHRIU65b", "h3sGD2Q1oh": "OvxrT0OF9loNpckMysYWL0RTC6cpCVuo9LT99YJ1ViylK0MI2I", "KtqOvAWxa0": "A0IxZt61rn2LcAM73DFMsVQGwaYoY0bAEDl8MHfO8AVvURAkh7", "kAb8DKKONz": "Tq30H99bUDNXs825WoBOBLxaGYNY8X43zueEHiYkOLJg7Z0Eye", "myWjzOxJcO": "uHaUmKQn6banGCwmFumKkp69vfkdqYzzenYnaz3MEUbyoqL3dy", "MJiiDvQloZ": "yD47SUH4iA5DWxATZ45sbwtkhZqYeD0HaFASHXvIYLeV1gsx52", "hj2VGFzJvc": "puEeyRy18U69CtdYpvb7X0xumLcjgDg0uL1CCX9IhEtYl0VmfQ", "KsbJQGihmS": "Z3truNds88MvvDnAaEYqBq4s3O08irTyKVWlpjJo9m5t75OAwg", "88DcQGvkwN": "NRAeIZcDPqnjuihLCBBhwsqfF173vf9AZAWSVMtsWQKoVRXDwU", "gUtJzDhftY": "pIQoPw3X5I50A9FWt7vvw8y6Wy0eaYv5rcwm5jr8A1Zo6ZUKzQ", "jClPEd9IgK": "bSiQkQvKVWQQAApfAmP1lolszeorbzvsKTXDsDouxINELPgt5u", "g7dRJ56NBc": "amRHImU0uV2plQh7CUmMCOWLaVqh7yXg2nmEYfaxaza2SsqZNJ", "8ebalqa01L": "oZfi0pGagvNQolV2a56TZamgm3k97PDwvWKXrgNyhTJIr69b6w", "prdhMDmQFu": "xvdDoyjLLocu9vtYdcxAPnysk39nAOdoIw6T5y1j3KpfSKPr5q", "TavVmIIdAQ": "dzENXlVbgUh8Wdtkz7LZCNHpUe0NLtAXcdgqE5z6uJFYj8P9A3", "8dPYFRMbFO": "s53FCJGSYPpzHb3ljOv7fV23MZZ0gj67M7giQiTIcNv6w0O5BN", "GM4aaHYaRc": "oIFUmUoRAZRz3u5BxiAWOZVyC00G9mjwh883hafuCu4rjlB4u2", "l1hTQcxsl6": "plNQjWMAXTBcDdCXXoIxwhW7peXeJKfKZgQjqZeEx7pmUFE8gO", "nL8hoZxmke": "sPLURcIFTMFZkbrGiV4XiSQx250yd1VUduoG3x5DI6ZPsZV0wB", "RBPbJLkXfV": "CiFoFZS6hQ36nHf1If3s3ngiNbI4aRfEx5yNhz9MOIT8nmGmND", "UJNSyOc8w7": "7MsdjTKm5RrV3Dhtki7aNciyVbRdA8I6fJFCRoGE9DRjwqH5iC", "43dTF9n9PK": "Xw7I7s0GPEFcMU4hBlfq0kg80kJDJRoo4EwEwJ9tgKgKoZWabA", "FWfki0wTJB": "Dr61obcdBq0FKPSYFEwCHGOJ9ZecHwsDs6fJnjzzzip1dWkhWL", "VNPreOz3AU": "VWZQhWqMtLPJdP883aOvAX9tWrDOBrkooDRGjv2bJWhJeLsP4n", "KbBjSP3KNQ": "HURWjYngbj7FWSzOJR2aa5FDftRBq7jutYZJOru0utqqoDkbaq", "qwTCkP8hxs": "921ht3YU0JoUOLFe3gk4seRMqi3lNTYwmDIdoaj2dhY2eYf606", "b93OHmC4v6": "al6HrRKW1wzKAv7EUhyEm6GjrHYqo6Eohr5lUrktAUFUNj6Wcb", "imeDhsYenG": "qNqpx46966bH4bsT3ChSEoJerDEM3rnTH1RGubFFjmr0sOy9T7", "py6qIz053N": "xqLHyR5WZG5hrl0yViRda5R1Efup7bcdKMn5rftFZIdxzmKF7C", "UYEKHGW7AC": "A1FQyy7CoCkqvgISKxat5bDrK9YcmTxHhmVtp70KtnYlr53WAF", "iYRTAQcHYb": "fpcBpwYhZPR8LTOM8U3RMyHOVUyzWwriQNcuLiOMq3TFbBRxGX", "0AiBXP53Sx": "qFWkpFh8qei9iI98Yn04UC8viNogyTp7z8Qp0hlnTk1wfDdAM4", "BGuvVVZ87m": "9gN4Xf3GjlG0hXJmJGDpSTQRFuDUqGmU2mSTNt29eRYBAyn5dn", "Emcy4Xwcd4": "HBjXSQ99Ob2PQ0dLw9wtePihaRTcQ243YPnl3XnqeXzsFNtrTm", "F8NPfHMZjM": "qc4uBdspVfWWRrtsuZpU1u0Ar5mcnMD8AiipPpDIm2EW5PSteR", "S85Zp4tPiN": "JVAOT6w8PObCJB8werBOsyj0FVkuTFDnsyxoDEEfnwAnG47QCd", "FpIspARB8L": "U5huzomiwyIBWOdcont9LXAb1nxdDayAfrKDQt439QxbQaKDCY", "LCY19w45EI": "oOLChv6OY0u4k39DeJxLZTOpkvGAlPcPnvBbJqrP7o3QSuxlsw", "Tb0Pca2rvy": "RUbDK0Tblp9lJJoVguhgif4xES3juGwbCAaC7QbfEkURlSTKaw", "wgs4Byth20": "6E41JqpxxpBanUF5hraRN5tgVHTS3GmOxDeubA6bXfBKSkh77E", "0U9Kgyc73m": "w72jNnjqBQyTQtGvC8sj5jbuu6N5yB1PFuEQDaJpxzc2MP9d09", "l4u8EMUtMy": "MP95DO9MejlyTQx24EmOqWhpWimy6rvzn6Q4OnPoiuHo6fYy1g", "uPgWEEldfa": "aF4PnBe3WfZVmlgi2WKZ1Q9h9YloNTDZp53MNs94qGMNPedJwe", "dbsqzsW5kz": "eyVEsEnW3jDMEmY19ghi8ws4Ca2XdtC1CmkU5MylSq4EPiD1Zt", "frBu64OrNM": "rIk04EgaM7wBhsplzAbBPW26oBs3giEam6w9FzKy7EvxU1DGF9", "XA1VgPBMc8": "fZHB2RaWdt3BWcwi94TMwkWPuLeB29b4tFFQmDyMc2STjYfrcc", "fWFOfLCLq2": "hSMeD5ftoChCGlQWlBHuCPnQZpn4YMxvjuJXcqbtmLR4a5YYwR", "IG8rBmoCLP": "2rjrIsor7oVhmcJ4ukazX8d5AChMYAmglSWFjN6vVGTqBjAxnn", "SuRs5JbTu3": "DiZrLVe7JqJXf6WtnuBj4IYpTNZSBe7MlItMBJ7U4NsJTwhvrT", "dzxxgYcyxq": "dcwrdGKDigL2AKHycNWWnpsoITXqVBrt91J6p4dkCKKRMqnRfA", "rngI8gLd7P": "Oi7OVtSn2J93pv0ocpiwRXCIII0bs7ntQEY1OcWlPW7wzOAGZd", "jMOSkJ3O7K": "pnc7JHESSlJZFvXMdnCzzORnTGCPMSNEyBMuaqEOZiGG7kuxmP", "Ey31DW15iq": "q7vtANcI6s6UyZHp8Uliox1FIKtHoGxSMWghZHVXWGkc7r5kGw", "WYyGnw8FjI": "nMDpoJ14ZwOE9PKMLCPdlycbfQxHGAr5aCUJY9UoL7jqdjO0xz", "88abywFq3g": "OkS9uo5rIHcRSykZg5formZxmMG98b9yykrXpY1a76kxvl01Tb", "ejlCBtr16F": "3b5yuYQaBhiqpbeOnAGvvIwHzhQ2Tap29Z61R5SLAXLuBOiBU6", "zYHFMRSGpv": "gmx60suBJAMCpZ9waHYlmA0jei1ZfLo9QwdkYp6VRSTRMxy9zI", "Tudwv1e4YD": "HmDx6C2Ztpi3OdH0TsNFhqO2b2iPDDifjjA0GGTIgdG9Xehl4Q", "lXLMxU0kK8": "2EMOajMoUglcMavOMTLsug5nFOa1wUQqQopppbtMLlNGBx046W", "C9tmsuVUFW": "bOFlE0M9slqCSnjTPu4Tj6uXM9tpuXJ6p6sDyQxCv91sWCtnFw", "1TY97rLz5F": "HDciTNZVwVaWt6GGF0d3fFdkYubX5WpOqsfX5On2r2GkKWw56o", "ZvAntipLrt": "kFmVTxtBW088igfpJFXBchttabreHiosPpudJkgxsPO9kBDv2N", "bhbYGXSU6e": "a0Ggh9vyBG4dVKC6RVMRxe06Ia63iiGogJIG8ZGTware0IhV1E", "mEiP4ko4Nd": "z44zp9AUP3fILAqu9jQOuXjWjmoI2SmNPnnjH34umGupjrRjP9", "Vv6ITo99E8": "6u2gHvSvcflntsIr7R554PxmBLmcdAH5UBc3dUa5ELi82a1HrH", "QOE2C0DCaP": "XwlPbkABq74sBjOSER6j8lFpcho6AUZohZx0q5HincaXuC2R1f", "uNTxXAnuRs": "r4x1i4zcPAtUVJaxiUSRMiDwhRBiScZXEIIjAeHQkbO3J1hjQJ", "lDK7spl1ec": "nAVx1P8TdwLUNYkhMUfE9tFoTxRB8zhfJEMsPMm4cSNTAJkEyC", "F2Db2VH573": "qTkHCQYRg9OPzOKDgGwgNC9YWaDZyzAgDrfQgVNW7onXVgGjrH", "3zbLzbAjn3": "97oPocB9rqUCeSNPsp1xRw6zJTxWDuA31WrvSXGdfsKHWmsMcP", "3WiJfOtn4Y": "3hkk4ARReuYPTeKok932Jc4vzqwDxL839Db3scQ1TxeKijzbtZ", "tDVL0JZOHp": "zDAHLxRYmFenbqpeyWuVXPEpv0ugt2YziaPcYD0i3we1PuHcrZ", "pY964Difxg": "dy6NOoINetkU8eAASWARG7biIMAmS3iufVzoY84iA0kADnKe18", "HaNtX9khZ7": "AO6BXDWWgEMw6VMaCGxjWm5BWBLZmdUJdttZE69CMCSKoAwVAX", "QXYhjHxLH7": "e0YC7ZpzBPKUYAFwmTUFOaerzYJlA4N9kVi2LLZOG6bV5vlqOY", "wLTYyJXzmq": "Iz54kxdIe2r9xcAxfbMZZ1umkOWMtcR6tDqkI5jjwboISH4bzh", "aEzIfiRh6W": "yzjKa1kRvRhkvkCWpaflBlf1S1lxMCSlmCeV23GZovLqR6ln6M", "KhDbwTBCMX": "HUV2lPHpRzQcut4AyRHbLWlxYGiwR8InSrCRM1zU6LvKbTZtvf", "Z8ylcNPb7D": "vK28xx8ctW0ynxqIlPByDWxNuBttck3KmErS4aMtLvtoYXHBlL", "J2pbyd26kn": "uIVU2WXqwbYCOzSjp9G28JiiVMFX3YlZQukR7JN18ysAJaeLeT", "tnoE8UMXuf": "4aWvweOXFh0sWAC6kii8Ua6GXbILNrhy8o4FwtjUnNV33uW1Vi", "Z2DJKCTi4b": "3FaeXYxFco3Q49czLHNu0dLHjJxz6maQrtMSvBJrQiH0aP5KoE", "ZZNLVA68kr": "jQ3EGir0IQZv9ESM3vJAbJg2g49tuUEe80oyumdMYTq9CLsKFk", "VdjyL8Y4M6": "MizKVnhwSDiEZJK1PnO0MPdKZJeUv5yEvltWfcVSU29NxwCGPg", "ZzqXszmWVW": "eKiU714hADGd7EH14cBMNwtKkfMMXwmgTfs81CDmd05Y8btZ9p", "kJTPKepNMy": "arZSrfBBBS0MWMEP2MHCgDIMPObJ2lnztoGjVWUHa2RRWLdYqo", "KBTsJsJReO": "j7N4t67NR5EX7BXqGT39MQfZMef75q9GtdX9oZMuIkokOy1dS6", "HbSRlBFEPT": "kSfV2jNKMLc5TmdtzrgXjTMyYe7mpZjjZK12jJm6xZ7Hj13I56", "dQkNkJbvXs": "N5JSw66ImKmJ331xc2hAOzSzc69iAeurfCvXdvF57XwlkbsQyO", "kAc72N8pQ2": "q4rhDJA0PiAQSj8sRhRzuYVLtKuF5DHOGS8cnUf8TMRqctvMgv", "WlIz8w2MEi": "xf7aTGQdQ2QGxUFnFI8wmdRX7jG0EfwRcmbEjVqdbNoFSeHKjW", "faJrI28cHo": "23IKGc9iYwp6vm1qmKxIc1ubnIV6qCPjfDNr6Cq01dSzfzzgD4", "7NSIPYfkqx": "aNG1Njg0pZXCasTmihN6XMvuabOP2wqqXAaqm2GduEpFMUzakv", "T7CuRX0sn3": "3SNBzKxvJ3gUab1R63rD7qQsZAssAGcpMwat5DcJZ8OcR7hLRD", "czwSWcT2qK": "EgVUiAsbXdEKV24NEl5k2PYYuWqm5s8ksaINX19VRLzzGTCsoZ", "T3T7TYZUzb": "vKs8ZuTtx1MMCkKrSm81JNmtYyRDICI7mz0FqqE9xHbTNhK6UK", "0xEwQaIuHu": "PhF2zLesYnYPhFgKjJ6jguz4sDtyL91yFrDi4kCTINLoGkopEX", "Pj1Ns87ZIw": "2WhBkjzp79JLoiIbuhyzZcW1Eqf8DsJcBfX3Mf7muLGCu29aLj", "JisWiiuOuN": "fcipVfGSI0ui82h0iP83Ij6rVpQifgCey6WPEYK9Nzx1OJqaH8", "TmqT6QrO69": "NNDnsnv6LM27cQaTUZIAxgS3jLfBLprST1orYQOruiWBzIVQdd", "v6BnPNaR6X": "RSlNISMir21A36Su4WRyQWEeN1D7pgZRFesv0cjIKX3ft1wBWc", "RMVdVuiYCi": "daf6EeNmQosc5JXuK8Jd6rxi5Wrp99wGStSYXBkqQF8J1o9fme", "vwUDGwNC02": "f81Eu907pgBY2No3DSBw1pVsUQIVF7qowqBTDPi6mT54WrxGC0", "eyUMmjx6z3": "PIp20GF5NMq6hwa16uLOJpYmijILfJ0ITQv8KsS9VQk0WUFxGp", "siJnrcouKa": "xt6NkWYv7X7TQOfO8XrE1pFi3UPdncxpT1VJmxRKXkZoHhSRFg", "KhrgkkF2qn": "uZawMN0NyqDHIzfHF0dbrxVZbtXs8F7L1Q3uRBjAgrqyqbMDAU", "Mjdm7IOPGE": "yOVvcNaofta3G9P9PO7sw2PLtvzXQBjFMHqoEnatOCnwvi89Xp", "cOBAmCeZT1": "dOzyLytrk7nok25eQp6TzPwtnvNFJBmgMyXbRgaXso5J1KQtTu", "RX8zzE1uch": "dfGV451NGXMOocG0IXwaVYiN0UlfQq3l95fdMttgpYeU7F5koC", "P8EbX0AONK": "DgHt0hlRTmNlPau0pXQvNTjpN523cqYxJZmFC0fFrJJCVRwVLx", "ksL3e2rB0a": "m3eNWVMJMvxvFLG7qfdVQyvCKqGSvoAmerKTvaqLvykYoq6exP", "WVqQi6DSBe": "4h3z7fAsnQSXh8c1JNyPWgLxM5q9s4fIYhyxp3tz6LboR8V6qJ", "Ij8xASdp1f": "4lwTew4mLA5GwoOpWbjwLRz7cCTaDxpzZLbgiHziMDLbaYTQAz", "SpzWVcMXMB": "RQMIwP6jNuKTPIyaguWi1wNVxbr7FwYpIBGX3PsUWWD16f56ml", "s4Evsie7m6": "IL2YJ8s60ozzTxWrQaeZpqu3Skao4JvBJw5gbEzt2nBFGtBgPL", "0XSVlS45GO": "FdgkVC0lXDoXwUnvqRJCWObbDeeExbBBaZq7KOqOeOXFwW1Zrp", "XFn1ixGW1g": "h6FP6WoxjaD13FD13qPPXGuNWeaq3Nc2wm07zvhM2HsWJxCbqY", "2jja0AL3Rn": "kbNZjKC32zVvSas7nODpV57AifD9xVcWYvMnoYhhvOe4mfI1ol", "spmhomxSDw": "bb4sFM2LHLogBw96G3trjUYjACepDe66cEEghsD8QFn9AgpAtN"} diff --git a/tests/data/files/test-txt.txt b/tests/data/files/test-txt.txt index aa0b0d3d..cd5a1592 100644 --- a/tests/data/files/test-txt.txt +++ b/tests/data/files/test-txt.txt @@ -1,3 +1,3 @@ Hello, world! This is a test file. -My secret is: FIRST TXT SECRET PHRASE \ No newline at end of file +My secret is: FIRST TXT SECRET PHRASE diff --git a/tests/data/tools/ExampleTool1.py b/tests/data/tools/ExampleTool1.py index 91b9898f..56f87c46 100644 --- a/tests/data/tools/ExampleTool1.py +++ b/tests/data/tools/ExampleTool1.py @@ -1,11 +1,14 @@ -from agency_swarm.tools import BaseTool from pydantic import Field +from agency_swarm.tools import BaseTool + class ExampleTool1(BaseTool): """Enter your tool description here. It should be informative for the Agent.""" + content: str = Field( - ..., description="Enter parameter descriptions using pydantic for the model here." + ..., + description="Enter parameter descriptions using pydantic for the model here.", ) def run(self): @@ -13,4 +16,4 @@ def run(self): # do_something(self.content) - return "Tool output" \ No newline at end of file + return "Tool output" diff --git a/tests/demos/demo_gradio.py b/tests/demos/demo_gradio.py index cb8d3afd..3c31fe51 100644 --- a/tests/demos/demo_gradio.py +++ b/tests/demos/demo_gradio.py @@ -1,49 +1,58 @@ import sys -import gradio as gr - -sys.path.insert(0, './agency-swarm') - -from agency_swarm import set_openai_key, Agent +sys.path.insert(0, "./agency-swarm") +from agency_swarm import Agent from agency_swarm.agency.agency import Agency -from agency_swarm.tools.oai import FileSearch, CodeInterpreter from agency_swarm.tools.BaseTool import BaseTool +from agency_swarm.tools.oai import CodeInterpreter, FileSearch + class PrintTool(BaseTool): def run(self, **kwargs): print("This is a test tool from BaseTool.") return "Printed successfully." + class AnotherPrintTool(BaseTool): def run(self, **kwargs): print("This is another test tool from BaseTool.") return "Another print successful." -ceo = Agent(name="CEO", - description="Responsible for client communication, task planning and management.", - instructions="Analyze uploaded files with myfiles_browser tool.", # can be a file like ./instructions.md - tools=[FileSearch, CodeInterpreter, PrintTool, AnotherPrintTool], - file_search={'max_num_results': 50}) +ceo = Agent( + name="CEO", + description="Responsible for client communication, task planning and management.", + instructions="Analyze uploaded files with myfiles_browser tool.", # can be a file like ./instructions.md + tools=[FileSearch, CodeInterpreter, PrintTool, AnotherPrintTool], + file_search={"max_num_results": 50}, +) -test_agent = Agent(name="Test Agent1", - description="Responsible for testing.", - instructions="Read files with myfiles_browser tool.", # can be a file like ./instructions.md - tools=[FileSearch]) -test_agent2 = Agent(name="Test Agent2", - description="Responsible for testing.", - instructions="Read files with myfiles_browser tool.", # can be a file like ./instructions.md - tools=[FileSearch]) +test_agent = Agent( + name="Test Agent1", + description="Responsible for testing.", + instructions="Read files with myfiles_browser tool.", # can be a file like ./instructions.md + tools=[FileSearch], +) +test_agent2 = Agent( + name="Test Agent2", + description="Responsible for testing.", + instructions="Read files with myfiles_browser tool.", # can be a file like ./instructions.md + tools=[FileSearch], +) -agency = Agency([ - ceo, [ceo, test_agent, test_agent2], -], shared_instructions="", settings_path="./test_settings.json") +agency = Agency( + [ + ceo, + [ceo, test_agent, test_agent2], + ], + shared_instructions="", + settings_path="./test_settings.json", +) # agency.demo_gradio() print(agency.get_completion("Use 2 print tools", yield_messages=False)) - diff --git a/tests/demos/streaming_demo.py b/tests/demos/streaming_demo.py index b5798a5d..2e235960 100644 --- a/tests/demos/streaming_demo.py +++ b/tests/demos/streaming_demo.py @@ -5,7 +5,7 @@ from agency_swarm import Agent, BaseTool from agency_swarm.agency.agency import Agency -sys.path.insert(0, '../agency-swarm') +sys.path.insert(0, "../agency-swarm") class StreamingTest(unittest.TestCase): @@ -13,23 +13,30 @@ def setUp(self): class TestTool(BaseTool): def run(self): time.sleep(10) - print('done') + print("done") return "Test Successful" - self.ceo = Agent(name="ceo", instructions="You are a CEO of an agency made for testing purposes.", - model='gpt-3.5-turbo') - self.test_agent1 = Agent(name="test_agent1", tools=[TestTool], model='gpt-3.5-turbo') - self.test_agent2 = Agent(name="test_agent2", model='gpt-3.5-turbo') - - self.agency = Agency([ - self.ceo, - [self.ceo, self.test_agent1, self.test_agent2], - [self.ceo, self.test_agent2] - ]) + self.ceo = Agent( + name="ceo", + instructions="You are a CEO of an agency made for testing purposes.", + model="gpt-3.5-turbo", + ) + self.test_agent1 = Agent( + name="test_agent1", tools=[TestTool], model="gpt-3.5-turbo" + ) + self.test_agent2 = Agent(name="test_agent2", model="gpt-3.5-turbo") + + self.agency = Agency( + [ + self.ceo, + [self.ceo, self.test_agent1, self.test_agent2], + [self.ceo, self.test_agent2], + ] + ) def test_demo(self): self.agency.demo_gradio() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/demos/term_demo.py b/tests/demos/term_demo.py index 550aafa8..bf985903 100644 --- a/tests/demos/term_demo.py +++ b/tests/demos/term_demo.py @@ -5,10 +5,11 @@ from agency_swarm.agency.agency import Agency from agency_swarm.threads import Thread from tests.ceo.ceo import Ceo + from .test_agent.test_agent import TestAgent from .test_agent2.test_agent2 import TestAgent2 -sys.path.insert(0, '../agency-swarm') +sys.path.insert(0, "../agency-swarm") import json @@ -18,19 +19,28 @@ def setUp(self): self.test_agent2 = TestAgent2() self.ceo = Ceo() - self.agency = Agency([ - self.ceo, - [self.ceo, self.test_agent1, self.test_agent2], - [self.ceo, self.test_agent2] - ]) + self.agency = Agency( + [ + self.ceo, + [self.ceo, self.test_agent1, self.test_agent2], + [self.ceo, self.test_agent2], + ] + ) def custom_serializer(obj): if isinstance(obj, Thread): - return {"agent": obj.agent.name, "recipient_agent": obj.recipient_agent.name} + return { + "agent": obj.agent.name, + "recipient_agent": obj.recipient_agent.name, + } # You can add more types here if needed raise TypeError(f"Type {type(obj)} not serializable") - print(json.dumps(self.agency.agents_and_threads, indent=4, default=custom_serializer)) + print( + json.dumps( + self.agency.agents_and_threads, indent=4, default=custom_serializer + ) + ) print("Ceo Tools: ", self.agency.ceo.tools) @@ -38,5 +48,5 @@ def test_demo(self): self.agency.run_demo() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_agency.py b/tests/test_agency.py index 88177118..c9ab25f9 100644 --- a/tests/test_agency.py +++ b/tests/test_agency.py @@ -4,26 +4,31 @@ import shutil import sys import time -from typing import ClassVar import unittest from openai.types.beta.threads import Text from openai.types.beta.threads.runs import ToolCall -from agency_swarm.tools import CodeInterpreter, FileSearch +from agency_swarm.tools import FileSearch -sys.path.insert(0, '../agency-swarm') -from agency_swarm.tools.send_message import SendMessageAsyncThreading -from agency_swarm.util import create_agent_template +sys.path.insert(0, "../agency-swarm") -from agency_swarm import set_openai_key, Agent, Agency, AgencyEventHandler, get_openai_client +from pydantic import BaseModel from typing_extensions import override -from agency_swarm.tools import BaseTool, ToolFactory -from pydantic import BaseModel +from agency_swarm import ( + Agency, + AgencyEventHandler, + Agent, + get_openai_client, +) +from agency_swarm.tools import BaseTool, ToolFactory +from agency_swarm.tools.send_message import SendMessageAsyncThreading +from agency_swarm.util import create_agent_template os.environ["DEBUG_MODE"] = "True" + class AgencyTest(unittest.TestCase): TestTool = None agency = None @@ -81,37 +86,55 @@ def save_thread_callback(agents_and_thread_ids): f.write("") # create agent templates in test_agents - create_agent_template("CEO", "CEO Test Agent", path="./test_agents", - instructions="Your task is to tell TestAgent1 to say test to another test agent. If the " - "agent, does not respond or something goes wrong please say 'error' and " - "nothing else. Otherwise say 'success' and nothing else.", - include_example_tool=True) - create_agent_template("TestAgent1", "Test Agent 1", path="./test_agents", - instructions="Your task is to say test to another test agent using SendMessage tool. " - "If the agent, does not " - "respond or something goes wrong please say 'error' and nothing else. " - "Otherwise say 'success' and nothing else.", code_interpreter=True, - include_example_tool=False) - create_agent_template("TestAgent2", "Test Agent 2", path="./test_agents", - instructions="After using TestTool, please respond to the user that test was a success in JSON format. You can use the following format: {'test': 'success'}.", - include_example_tool=False) - - sys.path.insert(0, './test_agents') + create_agent_template( + "CEO", + "CEO Test Agent", + path="./test_agents", + instructions="Your task is to tell TestAgent1 to say test to another test agent. If the " + "agent, does not respond or something goes wrong please say 'error' and " + "nothing else. Otherwise say 'success' and nothing else.", + include_example_tool=True, + ) + create_agent_template( + "TestAgent1", + "Test Agent 1", + path="./test_agents", + instructions="Your task is to say test to another test agent using SendMessage tool. " + "If the agent, does not " + "respond or something goes wrong please say 'error' and nothing else. " + "Otherwise say 'success' and nothing else.", + code_interpreter=True, + include_example_tool=False, + ) + create_agent_template( + "TestAgent2", + "Test Agent 2", + path="./test_agents", + instructions="After using TestTool, please respond to the user that test was a success in JSON format. You can use the following format: {'test': 'success'}.", + include_example_tool=False, + ) + + sys.path.insert(0, "./test_agents") # copy files from data/files to test_agents/TestAgent1/files for file in os.listdir("./data/files"): - shutil.copyfile("./data/files/" + file, "./test_agents/TestAgent1/files/" + file) + shutil.copyfile( + "./data/files/" + file, "./test_agents/TestAgent1/files/" + file + ) cls.num_files += 1 # copy schemas from data/schemas to test_agents/TestAgent2/schemas for file in os.listdir("./data/schemas"): - shutil.copyfile("./data/schemas/" + file, "./test_agents/TestAgent2/schemas/" + file) + shutil.copyfile( + "./data/schemas/" + file, "./test_agents/TestAgent2/schemas/" + file + ) cls.num_schemas += 1 class TestTool(BaseTool): """ A simple test tool that returns "Test Successful" to demonstrate the functionality of a custom tool within the Agency Swarm framework. """ + class ToolConfig: strict = True @@ -128,13 +151,11 @@ def run(self): from test_agents.CEO import CEO from test_agents.TestAgent1 import TestAgent1 from test_agents.TestAgent2 import TestAgent2 + cls.agent1 = TestAgent1() cls.agent1.add_tool(FileSearch) - cls.agent1.truncation_strategy = { - "type": "last_messages", - "last_messages": 10 - } - cls.agent1.file_search = {'max_num_results': 49} + cls.agent1.truncation_strategy = {"type": "last_messages", "last_messages": 10} + cls.agent1.file_search = {"max_num_results": 49} cls.agent2 = TestAgent2() cls.agent2.add_tool(cls.TestTool) @@ -143,28 +164,27 @@ def run(self): "type": "json_object", } - cls.agent2.model="gpt-4o-2024-08-06" + cls.agent2.model = "gpt-4o-2024-08-06" cls.ceo = CEO() cls.ceo.examples = [ - { - "role": "user", - "content": "Hi!" - }, + {"role": "user", "content": "Hi!"}, { "role": "assistant", - "content": "Hi! I am the CEO. I am here to help you with your testing. Please tell me who to send message to." - } + "content": "Hi! I am the CEO. I am here to help you with your testing. Please tell me who to send message to.", + }, ] cls.ceo.max_completion_tokens = 100 def test_1_init_agency(self): """it should initialize agency with agents""" - self.__class__.agency = Agency([ - self.__class__.ceo, - [self.__class__.ceo, self.__class__.agent1], - [self.__class__.agent1, self.__class__.agent2]], + self.__class__.agency = Agency( + [ + self.__class__.ceo, + [self.__class__.ceo, self.__class__.agent1], + [self.__class__.agent1, self.__class__.agent2], + ], shared_instructions="This is a shared instruction", settings_callbacks=self.__class__.settings_callbacks, threads_callbacks=self.__class__.threads_callbacks, @@ -178,6 +198,7 @@ def test_1_init_agency(self): def test_2_load_agent(self): """it should load existing assistant from settings""" from test_agents.TestAgent1 import TestAgent1 + agent3 = TestAgent1() agent3.add_shared_instructions(self.__class__.agency.shared_instructions) agent3.tools = self.__class__.agent1.tools @@ -191,13 +212,16 @@ def test_2_load_agent(self): self.assertTrue(self.__class__.agent1.id == agent3.id) # check that assistant settings match - self.assertTrue(agent3._check_parameters(self.__class__.agent1.assistant.model_dump())) + self.assertTrue( + agent3._check_parameters(self.__class__.agent1.assistant.model_dump()) + ) self.check_agent_settings(agent3) def test_3_load_agent_id(self): """it should load existing assistant from id""" from test_agents import TestAgent1 + agent3 = Agent(id=self.__class__.agent1.id) agent3.tools = self.__class__.agent1.tools agent3.file_search = self.__class__.agent1.file_search @@ -209,7 +233,9 @@ def test_3_load_agent_id(self): self.assertTrue(self.__class__.agent1.id == agent3.id) # check that assistant settings match - self.assertTrue(agent3._check_parameters(self.__class__.agent1.assistant.model_dump())) + self.assertTrue( + agent3._check_parameters(self.__class__.agent1.assistant.model_dump()) + ) self.check_agent_settings(agent3) @@ -217,23 +243,39 @@ def test_4_agent_communication(self): """it should communicate between agents""" print("TestAgent1 tools", self.__class__.agent1.tools) self.__class__.agent1.parallel_tool_calls = False - message = self.__class__.agency.get_completion("Please tell TestAgent1 to say test to TestAgent2.", - tool_choice={"type": "function", "function": {"name": "SendMessage"}}) + message = self.__class__.agency.get_completion( + "Please tell TestAgent1 to say test to TestAgent2.", + tool_choice={"type": "function", "function": {"name": "SendMessage"}}, + ) - self.assertFalse('error' in message.lower(), f"Error found in message: {message}. Thread url: {self.__class__.agency.main_thread.thread_url}") + self.assertFalse( + "error" in message.lower(), + f"Error found in message: {message}. Thread url: {self.__class__.agency.main_thread.thread_url}", + ) - self.assertTrue(self.__class__.agency.agents_and_threads['main_thread'].id) - self.assertTrue(self.__class__.agency.agents_and_threads['CEO']['TestAgent1'].id) - self.assertTrue(self.__class__.agency.agents_and_threads['TestAgent1']['TestAgent2'].id) + self.assertTrue(self.__class__.agency.agents_and_threads["main_thread"].id) + self.assertTrue( + self.__class__.agency.agents_and_threads["CEO"]["TestAgent1"].id + ) + self.assertTrue( + self.__class__.agency.agents_and_threads["TestAgent1"]["TestAgent2"].id + ) for agent in self.__class__.agency.agents: - self.assertTrue(agent.id in [settings['id'] for settings in self.__class__.loaded_agents_settings]) + self.assertTrue( + agent.id + in [ + settings["id"] for settings in self.__class__.loaded_agents_settings + ] + ) # assistants v2 checks main_thread = self.__class__.agency.main_thread main_thread_id = main_thread.id - thread_messages = self.__class__.client.beta.threads.messages.list(main_thread_id, limit=100, order="asc") + thread_messages = self.__class__.client.beta.threads.messages.list( + main_thread_id, limit=100, order="asc" + ) self.assertTrue(len(thread_messages.data) == 4) @@ -241,14 +283,20 @@ def test_4_agent_communication(self): run = main_thread._run self.assertTrue(run.max_prompt_tokens == self.__class__.ceo.max_prompt_tokens) - self.assertTrue(run.max_completion_tokens == self.__class__.ceo.max_completion_tokens) + self.assertTrue( + run.max_completion_tokens == self.__class__.ceo.max_completion_tokens + ) self.assertTrue(run.tool_choice.type == "function") - agent1_thread = self.__class__.agency.agents_and_threads[self.__class__.ceo.name][self.__class__.agent1.name] + agent1_thread = self.__class__.agency.agents_and_threads[ + self.__class__.ceo.name + ][self.__class__.agent1.name] agent1_thread_id = agent1_thread.id - agent1_thread_messages = self.__class__.client.beta.threads.messages.list(agent1_thread_id, limit=100) + agent1_thread_messages = self.__class__.client.beta.threads.messages.list( + agent1_thread_id, limit=100 + ) self.assertTrue(len(agent1_thread_messages.data) == 2) @@ -258,7 +306,9 @@ def test_4_agent_communication(self): self.assertTrue(agent1_run.truncation_strategy.last_messages == 10) self.assertFalse(agent1_run.parallel_tool_calls) - agent2_thread = self.__class__.agency.agents_and_threads[self.__class__.agent1.name][self.__class__.agent2.name] + agent2_thread = self.__class__.agency.agents_and_threads[ + self.__class__.agent1.name + ][self.__class__.agent2.name] agent2_message = agent2_thread._get_last_message_text() @@ -298,8 +348,9 @@ def on_all_streams_end(cls): "Please tell TestAgent1 to tell TestAgent2 to use TestTool.", event_handler=EventHandler, additional_instructions="\n\n**Your message to TestAgent1 should be exactly as follows:** " - "'Please tell TestAgent2 to use TestTool.'", - tool_choice={"type": "function", "function": {"name": "SendMessage"}}) + "'Please tell TestAgent2 to use TestTool.'", + tool_choice={"type": "function", "function": {"name": "SendMessage"}}, + ) # self.assertFalse('error' in message.lower()) @@ -309,15 +360,26 @@ def on_all_streams_end(cls): self.assertTrue(self.__class__.TestTool._shared_state.get("test_tool_used")) - agent1_thread = self.__class__.agency.agents_and_threads[self.__class__.ceo.name][self.__class__.agent1.name] + agent1_thread = self.__class__.agency.agents_and_threads[ + self.__class__.ceo.name + ][self.__class__.agent1.name] self.assertFalse(agent1_thread._run.parallel_tool_calls) self.assertTrue(self.__class__.agency.main_thread.id) - self.assertTrue(self.__class__.agency.agents_and_threads['CEO']['TestAgent1'].id) - self.assertTrue(self.__class__.agency.agents_and_threads['TestAgent1']['TestAgent2'].id) + self.assertTrue( + self.__class__.agency.agents_and_threads["CEO"]["TestAgent1"].id + ) + self.assertTrue( + self.__class__.agency.agents_and_threads["TestAgent1"]["TestAgent2"].id + ) for agent in self.__class__.agency.agents: - self.assertTrue(agent.id in [settings['id'] for settings in self.__class__.loaded_agents_settings]) + self.assertTrue( + agent.id + in [ + settings["id"] for settings in self.__class__.loaded_agents_settings + ] + ) def test_6_load_from_db(self): """it should load agents from db""" @@ -329,15 +391,13 @@ def test_6_load_from_db(self): from test_agents.CEO import CEO from test_agents.TestAgent1 import TestAgent1 from test_agents.TestAgent2 import TestAgent2 + agent1 = TestAgent1() agent1.add_tool(FileSearch) - agent1.truncation_strategy = { - "type": "last_messages", - "last_messages": 10 - } + agent1.truncation_strategy = {"type": "last_messages", "last_messages": 10} - agent1.file_search = {'max_num_results': 49} + agent1.file_search = {"max_num_results": 49} agent2 = TestAgent2() agent2.add_tool(self.__class__.TestTool) @@ -349,10 +409,8 @@ def test_6_load_from_db(self): ceo = CEO() # check that agents are loaded - agency = Agency([ - ceo, - [ceo, agent1], - [agent1, agent2]], + agency = Agency( + [ceo, [ceo, agent1], [agent1, agent2]], shared_instructions="This is a shared instruction", settings_path="./settings2.json", settings_callbacks=self.__class__.settings_callbacks, @@ -378,14 +436,34 @@ def test_6_load_from_db(self): continue for other_agent, thread in threads.items(): print(f"Thread ID between {agent} and {other_agent}: {thread.id}") - self.assertTrue(self.__class__.agency.agents_and_threads['main_thread'].id == previous_loaded_thread_ids['main_thread'] == self.__class__.loaded_thread_ids['main_thread']) - self.assertTrue(self.__class__.agency.agents_and_threads['CEO']['TestAgent1'].id == previous_loaded_thread_ids['CEO']['TestAgent1'] == self.__class__.loaded_thread_ids['CEO']['TestAgent1']) - self.assertTrue(self.__class__.agency.agents_and_threads['TestAgent1']['TestAgent2'].id == previous_loaded_thread_ids['TestAgent1']['TestAgent2'] == self.__class__.loaded_thread_ids['TestAgent1']['TestAgent2']) + self.assertTrue( + self.__class__.agency.agents_and_threads["main_thread"].id + == previous_loaded_thread_ids["main_thread"] + == self.__class__.loaded_thread_ids["main_thread"] + ) + self.assertTrue( + self.__class__.agency.agents_and_threads["CEO"]["TestAgent1"].id + == previous_loaded_thread_ids["CEO"]["TestAgent1"] + == self.__class__.loaded_thread_ids["CEO"]["TestAgent1"] + ) + self.assertTrue( + self.__class__.agency.agents_and_threads["TestAgent1"]["TestAgent2"].id + == previous_loaded_thread_ids["TestAgent1"]["TestAgent2"] + == self.__class__.loaded_thread_ids["TestAgent1"]["TestAgent2"] + ) # check that agents are the same for agent in agency.agents: - self.assertTrue(agent.id in [settings['id'] for settings in self.__class__.loaded_agents_settings]) - self.assertTrue(agent.id in [settings['id'] for settings in previous_loaded_agents_settings]) + self.assertTrue( + agent.id + in [ + settings["id"] for settings in self.__class__.loaded_agents_settings + ] + ) + self.assertTrue( + agent.id + in [settings["id"] for settings in previous_loaded_agents_settings] + ) def test_7_init_async_agency(self): """it should initialize async agency with agents""" @@ -397,12 +475,14 @@ def test_7_init_async_agency(self): self.__class__.agent1.id = None self.__class__.agent2.id = None - self.__class__.agent1.file_search = {'max_num_results': 49} + self.__class__.agent1.file_search = {"max_num_results": 49} - self.__class__.agency = Agency([ - self.__class__.ceo, - [self.__class__.ceo, self.__class__.agent1], - [self.__class__.agent1, self.__class__.agent2]], + self.__class__.agency = Agency( + [ + self.__class__.ceo, + [self.__class__.ceo, self.__class__.agent1], + [self.__class__.agent1, self.__class__.agent2], + ], shared_instructions="", settings_callbacks=self.__class__.settings_callbacks, threads_callbacks=self.__class__.threads_callbacks, @@ -414,9 +494,11 @@ def test_7_init_async_agency(self): def test_8_async_agent_communication(self): """it should communicate between agents asynchronously""" - self.__class__.agency.get_completion("Please tell TestAgent2 hello.", - tool_choice={"type": "function", "function": {"name": "SendMessage"}}, - recipient_agent=self.__class__.agent1) + self.__class__.agency.get_completion( + "Please tell TestAgent2 hello.", + tool_choice={"type": "function", "function": {"name": "SendMessage"}}, + recipient_agent=self.__class__.agent1, + ) time.sleep(10) @@ -445,7 +527,8 @@ def on_all_streams_end(cls): "Please check response. If output includes `TestAgent2's Response`, say 'success'. If the function output does not include `TestAgent2's Response`, or if you get a System Notification, or an error instead, say 'error'.", tool_choice={"type": "function", "function": {"name": "GetResponse"}}, recipient_agent=self.__class__.agent1, - event_handler=EventHandler) + event_handler=EventHandler, + ) self.assertTrue(num_on_all_streams_end_calls == 1) @@ -454,20 +537,31 @@ def on_all_streams_end(cls): self.assertTrue(EventHandler.agent_name == "User") self.assertTrue(EventHandler.recipient_agent_name == "TestAgent1") - if 'error' in message.lower(): - self.assertFalse('error' in message.lower(), self.__class__.agency.main_thread.thread_url) + if "error" in message.lower(): + self.assertFalse( + "error" in message.lower(), self.__class__.agency.main_thread.thread_url + ) self.assertTrue(self.__class__.agency.main_thread.id) - self.assertTrue(self.__class__.agency.agents_and_threads['TestAgent1']['TestAgent2'].id) + self.assertTrue( + self.__class__.agency.agents_and_threads["TestAgent1"]["TestAgent2"].id + ) for agent in self.__class__.agency.agents: - self.assertTrue(agent.id in [settings['id'] for settings in self.__class__.loaded_agents_settings]) + self.assertTrue( + agent.id + in [ + settings["id"] for settings in self.__class__.loaded_agents_settings + ] + ) def test_9_async_tool_calls(self): """it should execute tools asynchronously""" + class PrintTool(BaseTool): class ToolConfig: async_mode = "threading" + def run(self, **kwargs): time.sleep(2) # Simulate a delay return "Printed successfully." @@ -479,15 +573,15 @@ class ToolConfig: def run(self, **kwargs): time.sleep(2) # Simulate a delay return "Another print successful." - + ceo = Agent(name="CEO", tools=[PrintTool, AnotherPrintTool]) - agency = Agency( - [ceo], - temperature=0 - ) + agency = Agency([ceo], temperature=0) - result = agency.get_completion("Use 2 print tools together at the same time and output the results exectly as they are. ", yield_messages=False) + result = agency.get_completion( + "Use 2 print tools together at the same time and output the results exectly as they are. ", + yield_messages=False, + ) self.assertIn("success", result.lower(), agency.main_thread.thread_url) self.assertIn("success", result.lower(), agency.main_thread.thread_url) @@ -498,13 +592,21 @@ def test_10_concurrent_API_calls(self): with open("./data/schemas/get-headers-params.json", "r") as f: tools = ToolFactory.from_openapi_schema(f.read(), {}) - ceo = Agent(name="CEO", tools=tools, instructions="You are an agent that tests concurrent API calls. You must say 'success' if the output contains headers, and 'error' if it does not and **nothing else**.") + ceo = Agent( + name="CEO", + tools=tools, + instructions="You are an agent that tests concurrent API calls. You must say 'success' if the output contains headers, and 'error' if it does not and **nothing else**.", + ) agency = Agency([ceo], temperature=0) - result = agency.get_completion("Please call PrintHeaders tool TWICE at the same time in a single message. If any of the function outputs do not contains headers, please say 'error'.") + result = agency.get_completion( + "Please call PrintHeaders tool TWICE at the same time in a single message. If any of the function outputs do not contains headers, please say 'error'." + ) - self.assertTrue(result.lower().count('error') == 0, agency.main_thread.thread_url) + self.assertTrue( + result.lower().count("error") == 0, agency.main_thread.thread_url + ) def test_11_structured_outputs(self): class MathReasoning(BaseModel): @@ -515,13 +617,17 @@ class Step(BaseModel): steps: list[Step] final_answer: str - math_tutor_prompt = ''' + math_tutor_prompt = """ You are a helpful math tutor. You will be provided with a math problem, and your goal will be to output a step by step solution, along with a final answer. For each step, just provide the output as an equation use the explanation field to detail the reasoning. - ''' + """ - agent = Agent(name="MathTutor", response_format=MathReasoning, instructions=math_tutor_prompt) + agent = Agent( + name="MathTutor", + response_format=MathReasoning, + instructions=math_tutor_prompt, + ) agency = Agency([agent], temperature=0) @@ -530,7 +636,9 @@ class Step(BaseModel): # check if result is a MathReasoning object self.assertTrue(MathReasoning.model_validate_json(result)) - result = agency.get_completion_parse("how can I solve 3x + 2 = 14", response_format=MathReasoning) + result = agency.get_completion_parse( + "how can I solve 3x + 2 = 14", response_format=MathReasoning + ) # check if result is a MathReasoning object self.assertTrue(isinstance(result, MathReasoning)) @@ -544,11 +652,13 @@ def check_agent_settings(self, agent, async_mode=False): try: settings_path = agent.get_settings_path() self.assertTrue(os.path.exists(settings_path)) - with open(settings_path, 'r') as f: + with open(settings_path, "r") as f: settings = json.load(f) for assistant_settings in settings: - if assistant_settings['id'] == agent.id: - self.assertTrue(agent._check_parameters(assistant_settings, debug=True)) + if assistant_settings["id"] == agent.id: + self.assertTrue( + agent._check_parameters(assistant_settings, debug=True) + ) assistant = agent.assistant self.assertTrue(assistant) @@ -556,10 +666,26 @@ def check_agent_settings(self, agent, async_mode=False): if agent.name == "TestAgent1": num_tools = 3 if not async_mode else 4 - self.assertTrue(len(assistant.tool_resources.model_dump()['code_interpreter']['file_ids']) == 3) - self.assertTrue(len(assistant.tool_resources.model_dump()['file_search']['vector_store_ids']) == 1) + self.assertTrue( + len( + assistant.tool_resources.model_dump()["code_interpreter"][ + "file_ids" + ] + ) + == 3 + ) + self.assertTrue( + len( + assistant.tool_resources.model_dump()["file_search"][ + "vector_store_ids" + ] + ) + == 1 + ) - vector_store_id = assistant.tool_resources.model_dump()['file_search']['vector_store_ids'][0] + vector_store_id = assistant.tool_resources.model_dump()["file_search"][ + "vector_store_ids" + ][0] vector_store_files = agent.client.beta.vector_stores.files.list( vector_store_id=vector_store_id ) @@ -573,7 +699,9 @@ def check_agent_settings(self, agent, async_mode=False): self.assertTrue(assistant.tools[0].type == "code_interpreter") self.assertTrue(assistant.tools[1].type == "file_search") if not async_mode: - self.assertTrue(assistant.tools[1].file_search.max_num_results == 49) # Updated line + self.assertTrue( + assistant.tools[1].file_search.max_num_results == 49 + ) # Updated line self.assertTrue(assistant.tools[2].type == "function") self.assertTrue(assistant.tools[2].function.name == "SendMessage") self.assertFalse(assistant.tools[2].function.strict) @@ -581,13 +709,22 @@ def check_agent_settings(self, agent, async_mode=False): self.assertTrue(assistant.tools[3].type == "function") self.assertTrue(assistant.tools[3].function.name == "GetResponse") self.assertFalse(assistant.tools[3].function.strict) - + elif agent.name == "TestAgent2": self.assertTrue(len(assistant.tools) == self.__class__.num_schemas + 1) for tool in assistant.tools: self.assertTrue(tool.type == "function") - self.assertTrue(tool.function.name in [tool.__name__ for tool in agent.tools]) - test_tool = next((tool for tool in assistant.tools if tool.function.name == "TestTool"), None) + self.assertTrue( + tool.function.name in [tool.__name__ for tool in agent.tools] + ) + test_tool = next( + ( + tool + for tool in assistant.tools + if tool.function.name == "TestTool" + ), + None, + ) self.assertTrue(test_tool.function.strict, test_tool) elif agent.name == "CEO": num_tools = 1 if not async_mode else 2 @@ -613,5 +750,5 @@ def tearDownClass(cls): cls.agency.delete() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_communication.py b/tests/test_communication.py index 216efd29..205ef8c2 100644 --- a/tests/test_communication.py +++ b/tests/test_communication.py @@ -1,64 +1,89 @@ import unittest -from agency_swarm import Agent, Agency -from agency_swarm.tools.send_message import SendMessageSwarm -from agency_swarm.tools import BaseTool + from pydantic import Field +from agency_swarm import Agency, Agent +from agency_swarm.tools import BaseTool +from agency_swarm.tools.send_message import SendMessageSwarm + + class TestSendMessage(unittest.TestCase): def setUp(self): class PrintTool(BaseTool): """ A simple tool that prints a message. """ + message: str = Field(..., description="The message to print.") def run(self): print(self.message) return f"Printed: {self.message}" - + self.ceo = Agent( name="CEO", description="Responsible for client communication, task planning and management.", instructions="Your role is to route messages to other agents within your agency.", - tools=[PrintTool] + tools=[PrintTool], ) self.customer_support = Agent( name="Customer Support", description="Responsible for customer support.", instructions="You are a Customer Support agent. Answer customer questions and help with issues.", - tools=[] + tools=[], + ) + + self.agency = Agency( + [ + self.ceo, + [self.ceo, self.customer_support], + [self.customer_support, self.ceo], + ], + temperature=0, + send_message_tool_class=SendMessageSwarm, ) - - self.agency = Agency([self.ceo, [self.ceo, self.customer_support], [self.customer_support, self.ceo]], - temperature=0, send_message_tool_class=SendMessageSwarm) def test_send_message_swarm(self): - response = self.agency.get_completion("Hello, can you send me to customer support? If tool responds says that you have NOT been rerouted, or if there is another error, please say 'error'") - self.assertFalse("error" in response.lower(), self.agency.main_thread.thread_url) + response = self.agency.get_completion( + "Hello, can you send me to customer support? If tool responds says that you have NOT been rerouted, or if there is another error, please say 'error'" + ) + self.assertFalse( + "error" in response.lower(), self.agency.main_thread.thread_url + ) response = self.agency.get_completion("Who are you?") - self.assertTrue("customer support" in response.lower(), self.agency.main_thread.thread_url) + self.assertTrue( + "customer support" in response.lower(), self.agency.main_thread.thread_url + ) main_thread = self.agency.main_thread # check if recipient agent is correct self.assertEqual(main_thread.recipient_agent, self.customer_support) - #check if all messages in the same thread (this is how Swarm works) - self.assertTrue(len(main_thread.get_messages()) >= 4) # sometimes run does not cancel immediately, so there might be 5 messages + # check if all messages in the same thread (this is how Swarm works) + self.assertTrue( + len(main_thread.get_messages()) >= 4 + ) # sometimes run does not cancel immediately, so there might be 5 messages def test_send_message_double_recepient_error(self): - ceo = Agent(name="CEO", + ceo = Agent( + name="CEO", description="Responsible for client communication, task planning and management.", instructions="You are an agent for testing. Route request AT THE SAME TIME as instructed. If there is an error in a single request, please say 'error'. If there are errors in both requests, please say 'fatal'. do not output anything else.", ) - test_agent = Agent(name="Test Agent1", - description="Responsible for testing.", - instructions="Test agent for testing.") + test_agent = Agent( + name="Test Agent1", + description="Responsible for testing.", + instructions="Test agent for testing.", + ) agency = Agency([ceo, [ceo, test_agent]], temperature=0) - response = agency.get_completion("Please route me to customer support TWICE at the same time. I am testing something.") + response = agency.get_completion( + "Please route me to customer support TWICE at the same time. I am testing something." + ) self.assertTrue("error" in response.lower(), agency.main_thread.thread_url) self.assertTrue("fatal" not in response.lower(), agency.main_thread.thread_url) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_tool_factory.py b/tests/test_tool_factory.py index bb0a1815..a8bd5f33 100644 --- a/tests/test_tool_factory.py +++ b/tests/test_tool_factory.py @@ -1,18 +1,17 @@ import asyncio -from enum import Enum import json import os import sys import unittest +from enum import Enum from typing import List, Optional -from pydantic import Field, BaseModel +from pydantic import BaseModel, Field -sys.path.insert(0, '../agency-swarm') -from agency_swarm.tools import ToolFactory, BaseTool -from agency_swarm.util.schema import dereference_schema, reference_schema +sys.path.insert(0, "../agency-swarm") from langchain.tools import MoveFileTool, YouTubeSearchTool +from agency_swarm.tools import BaseTool, ToolFactory from agency_swarm.util import get_openai_client @@ -25,8 +24,10 @@ def test_move_file_tool(self): print(json.dumps(tool.openai_schema, indent=4)) print(tool) - tool = tool(destination_path="Move a file from one folder to another", - source_path="Move a file from one folder to another") + tool = tool( + destination_path="Move a file from one folder to another", + source_path="Move a file from one folder to another", + ) print(tool.model_dump()) @@ -35,19 +36,27 @@ def test_move_file_tool(self): def test_complex_schema(self): class FriendDetail(BaseModel): "test 123" + id: int = Field(..., description="Unique identifier for each friend.") name: str = Field(..., description="Name of the friend.") age: Optional[int] = Field(25, description="Age of the friend.") - email: Optional[str] = Field(None, description="Email address of the friend.") - is_active: Optional[bool] = Field(None, description="Indicates if the friend is currently active.") + email: Optional[str] = Field( + None, description="Email address of the friend." + ) + is_active: Optional[bool] = Field( + None, description="Indicates if the friend is currently active." + ) class UserDetail(BaseModel): """Hey this is a test?""" + id: int = Field(..., description="Unique identifier for each user.") age: int name: str - friends: List[FriendDetail] = Field(..., - description="List of friends, each represented by a FriendDetail model.") + friends: List[FriendDetail] = Field( + ..., + description="List of friends, each represented by a FriendDetail model.", + ) class RelationshipType(Enum): FAMILY = "family" @@ -56,34 +65,39 @@ class RelationshipType(Enum): class UserRelationships(BaseTool): """Hey this is a test?""" - users: List[UserDetail] = Field(..., - description="Collection of users, correctly capturing the relationships among them.", title="Users") - relationship_type: RelationshipType = Field(..., description="Type of relationship among users.", title="Relationship Type") + + users: List[UserDetail] = Field( + ..., + description="Collection of users, correctly capturing the relationships among them.", + title="Users", + ) + relationship_type: RelationshipType = Field( + ..., + description="Type of relationship among users.", + title="Relationship Type", + ) print("schema", json.dumps(UserRelationships.openai_schema, indent=4)) # print("ref", json.dumps(reference_schema(deref_schema), indent=4)) - tool = ToolFactory.from_openai_schema(UserRelationships.openai_schema, lambda x: x) + tool = ToolFactory.from_openai_schema( + UserRelationships.openai_schema, lambda x: x + ) print(json.dumps(tool.openai_schema, indent=4)) user_detail_instance = { "id": 1, "age": 20, "name": "John Doe", - "friends": [ - { - "id": 1, - "name": "Jane Doe" - } - ] + "friends": [{"id": 1, "name": "Jane Doe"}], } user_relationships_instance = { "users": [user_detail_instance], - "relationship_type": "family" + "relationship_type": "family", } - - #print user detail instance + + # print user detail instance tool = tool(**user_relationships_instance) user_relationships_schema = UserRelationships.openai_schema @@ -94,7 +108,9 @@ def remove_empty_fields(d): """ if not isinstance(d, dict): return d - return {k: remove_empty_fields(v) for k, v in d.items() if v not in [{}, [], '']} + return { + k: remove_empty_fields(v) for k, v in d.items() if v not in [{}, [], ""] + } cleaned_schema = remove_empty_fields(user_relationships_schema) @@ -124,12 +140,12 @@ def test_custom_tool(self): }, "required": ["query"], }, - "strict": False + "strict": False, } tool = ToolFactory.from_openai_schema(schema, lambda x: x) - schema['strict'] = True + schema["strict"] = True tool2 = ToolFactory.from_openai_schema(schema, lambda x: x) @@ -155,30 +171,32 @@ def test_get_weather_openapi(self): def test_relevance_openapi_schema(self): with open("./data/schemas/relevance.json", "r") as f: - tools = ToolFactory.from_openapi_schema(f.read(), { - "Authorization": os.environ.get("TEST_SCHEMA_API_KEY") - }) + tools = ToolFactory.from_openapi_schema( + f.read(), {"Authorization": os.environ.get("TEST_SCHEMA_API_KEY")} + ) print(json.dumps(tools[0].openai_schema, indent=4)) async def gather_output(): - output = await tools[0](requestBody={"text": 'test'}).run() + output = await tools[0](requestBody={"text": "test"}).run() return output output = asyncio.run(gather_output()) print(output) - assert output['output']['transformed']['data'] == 'test complete.' + assert output["output"]["transformed"]["data"] == "test complete." def test_get_headers_openapi_schema(self): with open("./data/schemas/get-headers-params.json", "r") as f: - tools = ToolFactory.from_openapi_schema(f.read(),{ - "Bearer": os.environ.get("GET_HEADERS_SCHEMA_API_KEY") - }) + tools = ToolFactory.from_openapi_schema( + f.read(), {"Bearer": os.environ.get("GET_HEADERS_SCHEMA_API_KEY")} + ) async def gather_output(): - output = await tools[0](parameters={"domain": "print-headers", "query": "test"}).run() + output = await tools[0]( + parameters={"domain": "print-headers", "query": "test"} + ).run() return output output = asyncio.run(gather_output()) @@ -200,7 +218,7 @@ def test_import_from_file(self): self.assertTrue(tool.__name__ == "ExampleTool1") - self.assertTrue(tool(content='test').run() == "Tool output") + self.assertTrue(tool(content="test").run() == "Tool output") # def test_openapi_schema(self): # with open("./data/schemas/get-headers-params.json", "r") as f: @@ -211,7 +229,5 @@ def test_import_from_file(self): # self.assertTrue(schema) - - -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()