From e7026c34c6f077ec908a2a269ad67802df0e2ee9 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Tue, 11 Jul 2023 18:04:02 +0530 Subject: [PATCH 01/77] first commit --- config/blacklist.txt | 0 config/comments.txt | 0 config/followed.txt | 0 config/friends.txt | 0 config/log/instabot_139732300466736.log | 15 +++ config/log/instabot_139888606519360.log | 15 +++ config/log/instabot_139900443239328.log | 7 ++ config/log/instabot_140334293153200.log | 2 + config/log/instabot_140362199539776.log | 7 ++ config/skipped.txt | 0 config/unfollowed.txt | 0 config/whitelist.txt | 0 docker-compose.yaml | 2 +- superagi/tools/instagram_tool/F35.jpeg | Bin 0 -> 4879 bytes superagi/tools/instagram_tool/__init__.py | 0 superagi/tools/instagram_tool/instagram.py | 87 ++++++++++++++++++ .../tools/instagram_tool/instagram_toolkit.py | 18 ++++ superagi/tools/instagram_tool/test.txt | 1 + 18 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 config/blacklist.txt create mode 100644 config/comments.txt create mode 100644 config/followed.txt create mode 100644 config/friends.txt create mode 100644 config/log/instabot_139732300466736.log create mode 100644 config/log/instabot_139888606519360.log create mode 100644 config/log/instabot_139900443239328.log create mode 100644 config/log/instabot_140334293153200.log create mode 100644 config/log/instabot_140362199539776.log create mode 100644 config/skipped.txt create mode 100644 config/unfollowed.txt create mode 100644 config/whitelist.txt create mode 100644 superagi/tools/instagram_tool/F35.jpeg create mode 100644 superagi/tools/instagram_tool/__init__.py create mode 100644 superagi/tools/instagram_tool/instagram.py create mode 100644 superagi/tools/instagram_tool/instagram_toolkit.py create mode 100644 superagi/tools/instagram_tool/test.txt diff --git a/config/blacklist.txt b/config/blacklist.txt new file mode 100644 index 000000000..e69de29bb diff --git a/config/comments.txt b/config/comments.txt new file mode 100644 index 000000000..e69de29bb diff --git a/config/followed.txt b/config/followed.txt new file mode 100644 index 000000000..e69de29bb diff --git a/config/friends.txt b/config/friends.txt new file mode 100644 index 000000000..e69de29bb diff --git a/config/log/instabot_139732300466736.log b/config/log/instabot_139732300466736.log new file mode 100644 index 000000000..0ddea0a14 --- /dev/null +++ b/config/log/instabot_139732300466736.log @@ -0,0 +1,15 @@ +2023-07-10 04:48:27,462 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started +2023-07-10 04:48:27,462 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py +2023-07-10 04:48:27,470 - instabot version: 0.117.0 (api_login) - INFO - Not yet logged in starting: PRE-LOGIN FLOW! +2023-07-10 04:48:28,122 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: +2023-07-10 04:48:28,122 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' +2023-07-10 04:48:28,122 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! +2023-07-10 04:48:28,123 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 5 minutes. +2023-07-10 04:53:28,667 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: +2023-07-10 04:53:28,668 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' +2023-07-10 04:53:28,668 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! +2023-07-10 04:53:28,669 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 10 minutes. +2023-07-10 05:03:29,119 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: +2023-07-10 05:03:29,119 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' +2023-07-10 05:03:29,119 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! +2023-07-10 05:03:29,120 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 15 minutes. diff --git a/config/log/instabot_139888606519360.log b/config/log/instabot_139888606519360.log new file mode 100644 index 000000000..a636e71af --- /dev/null +++ b/config/log/instabot_139888606519360.log @@ -0,0 +1,15 @@ +2023-07-10 05:16:23,290 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started +2023-07-10 05:16:23,291 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py +2023-07-10 05:16:23,295 - instabot version: 0.117.0 (api_login) - INFO - Not yet logged in starting: PRE-LOGIN FLOW! +2023-07-10 05:16:23,704 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: +2023-07-10 05:16:23,704 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' +2023-07-10 05:16:23,704 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! +2023-07-10 05:16:23,705 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 5 minutes. +2023-07-10 05:21:24,139 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: +2023-07-10 05:21:24,139 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' +2023-07-10 05:21:24,139 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! +2023-07-10 05:21:24,140 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 10 minutes. +2023-07-10 05:31:25,010 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: +2023-07-10 05:31:25,011 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' +2023-07-10 05:31:25,011 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! +2023-07-10 05:31:25,011 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 15 minutes. diff --git a/config/log/instabot_139900443239328.log b/config/log/instabot_139900443239328.log new file mode 100644 index 000000000..a11a48fe0 --- /dev/null +++ b/config/log/instabot_139900443239328.log @@ -0,0 +1,7 @@ +2023-07-07 13:02:07,385 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started +2023-07-07 13:02:07,385 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py +2023-07-07 13:02:07,391 - instabot version: 0.117.0 (api_login) - INFO - Not yet logged in starting: PRE-LOGIN FLOW! +2023-07-07 13:02:07,860 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: +2023-07-07 13:02:07,860 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' +2023-07-07 13:02:07,860 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! +2023-07-07 13:02:07,860 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 5 minutes. diff --git a/config/log/instabot_140334293153200.log b/config/log/instabot_140334293153200.log new file mode 100644 index 000000000..3aed232ec --- /dev/null +++ b/config/log/instabot_140334293153200.log @@ -0,0 +1,2 @@ +2023-07-07 12:52:28,385 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started +2023-07-07 12:52:28,385 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py diff --git a/config/log/instabot_140362199539776.log b/config/log/instabot_140362199539776.log new file mode 100644 index 000000000..eb0b71872 --- /dev/null +++ b/config/log/instabot_140362199539776.log @@ -0,0 +1,7 @@ +2023-07-07 12:56:51,299 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started +2023-07-07 12:56:51,300 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py +2023-07-07 12:56:51,304 - instabot version: 0.117.0 (api_login) - INFO - Not yet logged in starting: PRE-LOGIN FLOW! +2023-07-07 12:56:51,738 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: +2023-07-07 12:56:51,738 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' +2023-07-07 12:56:51,738 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! +2023-07-07 12:56:51,738 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 5 minutes. diff --git a/config/skipped.txt b/config/skipped.txt new file mode 100644 index 000000000..e69de29bb diff --git a/config/unfollowed.txt b/config/unfollowed.txt new file mode 100644 index 000000000..e69de29bb diff --git a/config/whitelist.txt b/config/whitelist.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docker-compose.yaml b/docker-compose.yaml index 6dabb981d..30b6795a7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,4 @@ -version: '3.8' +version: '3.3' services: backend: volumes: diff --git a/superagi/tools/instagram_tool/F35.jpeg b/superagi/tools/instagram_tool/F35.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0eabaa6747a9c61abfc19a1122604ca826c0fbd0 GIT binary patch literal 4879 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 Rg8nWNi6GRWLl|Do{0E~`>&O5A literal 0 HcmV?d00001 diff --git a/superagi/tools/instagram_tool/__init__.py b/superagi/tools/instagram_tool/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py new file mode 100644 index 000000000..5dd5af942 --- /dev/null +++ b/superagi/tools/instagram_tool/instagram.py @@ -0,0 +1,87 @@ +import json +from typing import Type, Optional + +from pydantic import BaseModel, Field +from superagi.helper.token_counter import TokenCounter +from superagi.llms.base_llm import BaseLlm +from superagi.tools.base_tool import BaseTool +from instabot import Bot +import os +import glob +import requests + +class InstagramSchema(BaseModel): + caption: str = Field( + ..., + description="Caption for the photo", + ) + +class InstagramTool(BaseTool): + """ + Instagram tool + + Attributes: + name : The name. + description : The description. + args_schema : The args schema. + """ + llm: Optional[BaseLlm] = None + name = "Instagram tool" + description = ( + "A tool performing posting jpeg on Instagram" + ) + args_schema: Type[InstagramSchema] = InstagramSchema + + class Config: + arbitrary_types_allowed = True + + #17841460418095214 + #100599793108367P + #EAASZB6VduVlMBAAQCjVahF13FTlTtcvtsiQlSyXQ0H48tSazdGZBZAZC2lENNbHXWMZANRIZC9l78sHMMbMEGXcaqjFdZBdFhMSnZAPw7syF50KwyB94BnUWErM9puJqKWhODBIAQGDcbgMmITL2qAUDzR4S28jDmq7OlLXE80qXDt6rZCfkhbtgXuaBYIcFPXEoapCw4rraC23HQ2ba3o84L + def _execute(self, caption: str) -> str: + """ + Execute the Instagram tool. + + Args: + query : The query to search for. + + Returns: + Search result summary along with related links + """ + + # Set your access token and photo URL + print("***********************INSTA TOOL CALLED************************") + user_access_token = self.get_tool_config("USER_ACCESS_TOKEN") + insta_bussiness_account_id=self.get_tool_config("INSTAGRAM_BUSINESS_ACCOUNT_ID") + + if user_access_token is None: + return "Error: Missing user access token." + + if insta_bussiness_account_id is None: + return "Error: Missing insta bussiness account id." + ##################### + + #functionality to get URL of the image generated from prompt + + ##################### + image_url="https://wallpaperaccess.com/full/1198080.jpg" + response = requests.post( + f"https://graph.facebook.com/v17.0/{insta_bussiness_account_id}/media?image_url={image_url}&access_token={user_access_token}" + ) + + if response.status_code != 200: + return f"Non-200 response: {str(response.text)}" + + data = response.json() + container_ID=data["id"] + response = requests.post( + f"https://graph.facebook.com/v17.0/{insta_bussiness_account_id}/media_publish?creation_id={container_ID}&access_token={user_access_token}", + ) + + if response.status_code != 200: + return f"Non-200 response: {str(response.text)}" + + print('************************Photo posted successfully!') + + return "Photo posted successfully!" + diff --git a/superagi/tools/instagram_tool/instagram_toolkit.py b/superagi/tools/instagram_tool/instagram_toolkit.py new file mode 100644 index 000000000..9456cf5e6 --- /dev/null +++ b/superagi/tools/instagram_tool/instagram_toolkit.py @@ -0,0 +1,18 @@ +from abc import ABC +from typing import List +from superagi.tools.base_tool import BaseTool, BaseToolkit +from superagi.tools.instagram_tool.instagram import InstagramTool + +class InstagramToolkit(BaseToolkit, ABC): + name: str = "Instagram Toolkit" + description: str = "Toolkit containing tools for performing posting photos on Instagram" + + def get_tools(self) -> List[BaseTool]: + return [InstagramTool()] + + def get_env_keys(self) -> List[str]: + return [ + "META_USER_ACCESS_TOKEN", + "INSTAGRAM_BUSINESS_ACCOUNT_ID" + # Add more config keys specific to your project + ] \ No newline at end of file diff --git a/superagi/tools/instagram_tool/test.txt b/superagi/tools/instagram_tool/test.txt new file mode 100644 index 000000000..359b9da12 --- /dev/null +++ b/superagi/tools/instagram_tool/test.txt @@ -0,0 +1 @@ +********************TEST************************************ \ No newline at end of file From 898c75ff15024fe348f9a2988e17cba8ebbb05e4 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Thu, 13 Jul 2023 14:48:16 +0530 Subject: [PATCH 02/77] instagram APIs implemented and AI generated caption utility added --- superagi/tools/instagram_tool/instagram.py | 82 +++++++++++++------ .../tools/instagram_tool/instagram_toolkit.py | 2 +- superagi/tools/instagram_tool/test.txt | 1 - 3 files changed, 59 insertions(+), 26 deletions(-) delete mode 100644 superagi/tools/instagram_tool/test.txt diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index 5dd5af942..ed58f8216 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -1,19 +1,18 @@ import json +import urllib from typing import Type, Optional from pydantic import BaseModel, Field from superagi.helper.token_counter import TokenCounter from superagi.llms.base_llm import BaseLlm from superagi.tools.base_tool import BaseTool -from instabot import Bot import os -import glob import requests class InstagramSchema(BaseModel): - caption: str = Field( + photo_description: str = Field( ..., - description="Caption for the photo", + description="description of the photo", ) class InstagramTool(BaseTool): @@ -28,60 +27,95 @@ class InstagramTool(BaseTool): llm: Optional[BaseLlm] = None name = "Instagram tool" description = ( - "A tool performing posting jpeg on Instagram" + "A tool performing posting AI generated photos on Instagram" ) args_schema: Type[InstagramSchema] = InstagramSchema class Config: arbitrary_types_allowed = True - #17841460418095214 - #100599793108367P - #EAASZB6VduVlMBAAQCjVahF13FTlTtcvtsiQlSyXQ0H48tSazdGZBZAZC2lENNbHXWMZANRIZC9l78sHMMbMEGXcaqjFdZBdFhMSnZAPw7syF50KwyB94BnUWErM9puJqKWhODBIAQGDcbgMmITL2qAUDzR4S28jDmq7OlLXE80qXDt6rZCfkhbtgXuaBYIcFPXEoapCw4rraC23HQ2ba3o84L - def _execute(self, caption: str) -> str: + def _execute(self, photo_description: str) -> str: """ Execute the Instagram tool. Args: - query : The query to search for. + photo_description : description of the photo to be posted Returns: - Search result summary along with related links + Image posted successfully message if image has been posted on instagram or error message. """ # Set your access token and photo URL print("***********************INSTA TOOL CALLED************************") - user_access_token = self.get_tool_config("USER_ACCESS_TOKEN") - insta_bussiness_account_id=self.get_tool_config("INSTAGRAM_BUSINESS_ACCOUNT_ID") - if user_access_token is None: - return "Error: Missing user access token." + meta_user_access_token = self.get_tool_config("META_USER_ACCESS_TOKEN") + facebook_page_id=self.get_tool_config("FACEBOOK_PAGE_ID") - if insta_bussiness_account_id is None: - return "Error: Missing insta bussiness account id." + if meta_user_access_token is None: + return "Error: Missing meta user access token." + + if facebook_page_id is None: + return "Error: Missing facebook page id." + + #create caption for the instagram + caption=self.create_caption(photo_description) + ##################### - #functionality to get URL of the image generated from prompt + #functionality to get URL of the image generated by stable diffusion ##################### - image_url="https://wallpaperaccess.com/full/1198080.jpg" + + response=requests.get( + f"https://graph.facebook.com/v17.0/{facebook_page_id}?fields=instagram_business_account&access_token={meta_user_access_token}" + ) + + if response.status_code != 200: + return f"Non-200 response: {str(response.text)}" + print("business_id found****************************") + data = response.json() + insta_bussiness_account_id=data["instagram_business_account"]["id"] + + image_url="https://images-cdn.ubuy.co.in/6353b235df14022c3858e456-petfon-pet-gps-tracker-no-monthly-fee.jpg" + encoded_caption=urllib. parse. quote(caption[1:-1]) + container_gen_url="https://graph.facebook.com/v17.0/"+insta_bussiness_account_id+"/media?image_url="+image_url+"&caption="+encoded_caption+"&access_token="+meta_user_access_token + response = requests.post( - f"https://graph.facebook.com/v17.0/{insta_bussiness_account_id}/media?image_url={image_url}&access_token={user_access_token}" + container_gen_url ) if response.status_code != 200: return f"Non-200 response: {str(response.text)}" - + print("container_ID generated****************************") + data = response.json() container_ID=data["id"] response = requests.post( - f"https://graph.facebook.com/v17.0/{insta_bussiness_account_id}/media_publish?creation_id={container_ID}&access_token={user_access_token}", + f"https://graph.facebook.com/v17.0/{insta_bussiness_account_id}/media_publish?creation_id={container_ID}&access_token={meta_user_access_token}", ) if response.status_code != 200: return f"Non-200 response: {str(response.text)}" - - print('************************Photo posted successfully!') return "Photo posted successfully!" + + def create_caption(self, photo_description: str) -> str: + """ + Create a caption for the instagram post based on the photo description + + Args: + photo_description : Description of the photo to be posted + + Returns: + Description of the photo to be posted + """ + caption_prompt ="""Generate an instagram post caption for the following text `{photo_description}` + Write a concise as necessary and attempt to make it relevant to the description as best as possible. Add emojis if needed.""" + + caption_prompt = caption_prompt.replace("{photo_description}", str(photo_description)) + + messages = [{"role": "system", "content": caption_prompt}] + result = self.llm.chat_completion(messages, max_tokens=self.max_token_limit) + caption=result["content"] + return caption \ No newline at end of file diff --git a/superagi/tools/instagram_tool/instagram_toolkit.py b/superagi/tools/instagram_tool/instagram_toolkit.py index 9456cf5e6..23449d016 100644 --- a/superagi/tools/instagram_tool/instagram_toolkit.py +++ b/superagi/tools/instagram_tool/instagram_toolkit.py @@ -13,6 +13,6 @@ def get_tools(self) -> List[BaseTool]: def get_env_keys(self) -> List[str]: return [ "META_USER_ACCESS_TOKEN", - "INSTAGRAM_BUSINESS_ACCOUNT_ID" + "FACEBOOK_PAGE_ID" # Add more config keys specific to your project ] \ No newline at end of file diff --git a/superagi/tools/instagram_tool/test.txt b/superagi/tools/instagram_tool/test.txt deleted file mode 100644 index 359b9da12..000000000 --- a/superagi/tools/instagram_tool/test.txt +++ /dev/null @@ -1 +0,0 @@ -********************TEST************************************ \ No newline at end of file From fce5808c5696ed9f879880eae1d932abdd857350 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Fri, 14 Jul 2023 16:07:01 +0530 Subject: [PATCH 03/77] Image upload to S3 and URL generation added --- superagi/tools/instagram_tool/instagram.py | 37 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index ed58f8216..71d5bb86f 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -1,7 +1,10 @@ import json import urllib +import boto3 +import os +from superagi.config.config import get_config +from superagi.helper.resource_helper import ResourceHelper from typing import Type, Optional - from pydantic import BaseModel, Field from superagi.helper.token_counter import TokenCounter from superagi.llms.base_llm import BaseLlm @@ -72,13 +75,34 @@ def _execute(self, photo_description: str) -> str: if response.status_code != 200: return f"Non-200 response: {str(response.text)}" - print("business_id found****************************") + data = response.json() - insta_bussiness_account_id=data["instagram_business_account"]["id"] + insta_business_account_id=data["instagram_business_account"]["id"] + + import boto3 + s3 = boto3.client( + 's3', + aws_access_key_id=get_config("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=get_config("AWS_SECRET_ACCESS_KEY"), + ) + bucket_name = get_config("INSTAGRAM_TOOL_BUCKET_NAME") + object_key="image/F35.jpeg" + input_root_dir = ResourceHelper.get_root_input_dir() + + image_path=input_root_dir+"F35.jpeg" + # response=s3.upload_file(image_path, bucket_name, object_key) + + response = s3.list_objects_v2( + Bucket=bucket_name, + ) + + for content in response.get('Contents', []): + print(content['Key']) + + image_url = f"https://{bucket_name}.s3.amazonaws.com/{object_key}" - image_url="https://images-cdn.ubuy.co.in/6353b235df14022c3858e456-petfon-pet-gps-tracker-no-monthly-fee.jpg" encoded_caption=urllib. parse. quote(caption[1:-1]) - container_gen_url="https://graph.facebook.com/v17.0/"+insta_bussiness_account_id+"/media?image_url="+image_url+"&caption="+encoded_caption+"&access_token="+meta_user_access_token + container_gen_url=f"https://graph.facebook.com/v17.0/{insta_business_account_id}/media?image_url={image_url}&caption={encoded_caption}&access_token={meta_user_access_token}" response = requests.post( container_gen_url @@ -86,12 +110,11 @@ def _execute(self, photo_description: str) -> str: if response.status_code != 200: return f"Non-200 response: {str(response.text)}" - print("container_ID generated****************************") data = response.json() container_ID=data["id"] response = requests.post( - f"https://graph.facebook.com/v17.0/{insta_bussiness_account_id}/media_publish?creation_id={container_ID}&access_token={meta_user_access_token}", + f"https://graph.facebook.com/v17.0/{insta_business_account_id}/media_publish?creation_id={container_ID}&access_token={meta_user_access_token}", ) if response.status_code != 200: From ac457a53ef6bb477dedb2c0f78ecdb4cde951109 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Tue, 18 Jul 2023 15:24:00 +0530 Subject: [PATCH 04/77] added upload to s3 instagram bucket --- .../stable_diffusion_image_gen.py | 9 +++- superagi/tools/instagram_tool/instagram.py | 51 +++++++++++-------- workspace/input/testing.txt | 1 - 3 files changed, 38 insertions(+), 23 deletions(-) delete mode 100644 workspace/input/testing.txt diff --git a/superagi/tools/image_generation/stable_diffusion_image_gen.py b/superagi/tools/image_generation/stable_diffusion_image_gen.py index 983d7ec5e..0e772b05b 100644 --- a/superagi/tools/image_generation/stable_diffusion_image_gen.py +++ b/superagi/tools/image_generation/stable_diffusion_image_gen.py @@ -5,7 +5,7 @@ import requests from PIL import Image from pydantic import BaseModel, Field - +from superagi.helper.resource_helper import ResourceHelper from superagi.resource_manager.file_manager import FileManager from superagi.tools.base_tool import BaseTool @@ -69,7 +69,12 @@ def _execute(self, prompt: str, image_names: list, width: int = 512, height: int self.resource_manager.write_binary_file(image_names[i], img_byte_arr.getvalue()) - return "Images downloaded and saved successfully" + image_paths=[] + + for image in image_names: + image_paths.append(ResourceHelper.get_agent_resource_path(image,self.agent_id)) + + return f"Images downloaded and saved successfully at the following locations: {image_paths}" def call_stable_diffusion(self, api_key, width, height, num, prompt, steps): engine_id = self.get_tool_config("ENGINE_ID") diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index 71d5bb86f..13082c337 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -11,6 +11,7 @@ from superagi.tools.base_tool import BaseTool import os import requests +from superagi.tools.tool_response_query_manager import ToolResponseQueryManager class InstagramSchema(BaseModel): photo_description: str = Field( @@ -33,6 +34,7 @@ class InstagramTool(BaseTool): "A tool performing posting AI generated photos on Instagram" ) args_schema: Type[InstagramSchema] = InstagramSchema + tool_response_manager: Optional[ToolResponseQueryManager] = None class Config: arbitrary_types_allowed = True @@ -50,6 +52,23 @@ def _execute(self, photo_description: str) -> str: # Set your access token and photo URL print("***********************INSTA TOOL CALLED************************") + import boto3 + s3 = boto3.client( + 's3', + aws_access_key_id=get_config("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=get_config("AWS_SECRET_ACCESS_KEY"), + ) + last_tool_response = self.tool_response_manager.get_last_response() + print('last_tool_response******************',last_tool_response) + + file_path="resources"+last_tool_response.partition("['")[2].partition("']")[0] + print("**********file path********",file_path) + + response = s3.get_object(Bucket=get_config("BUCKET_NAME"), Key='output/car_dealership') + content = response["Body"] + + print("***********content************",content) + return "image posted successfully" meta_user_access_token = self.get_tool_config("META_USER_ACCESS_TOKEN") facebook_page_id=self.get_tool_config("FACEBOOK_PAGE_ID") @@ -62,13 +81,7 @@ def _execute(self, photo_description: str) -> str: #create caption for the instagram caption=self.create_caption(photo_description) - - ##################### - - #functionality to get URL of the image generated by stable diffusion - - ##################### - + response=requests.get( f"https://graph.facebook.com/v17.0/{facebook_page_id}?fields=instagram_business_account&access_token={meta_user_access_token}" ) @@ -79,25 +92,23 @@ def _execute(self, photo_description: str) -> str: data = response.json() insta_business_account_id=data["instagram_business_account"]["id"] - import boto3 - s3 = boto3.client( - 's3', - aws_access_key_id=get_config("AWS_ACCESS_KEY_ID"), - aws_secret_access_key=get_config("AWS_SECRET_ACCESS_KEY"), - ) + bucket_name = get_config("INSTAGRAM_TOOL_BUCKET_NAME") - object_key="image/F35.jpeg" - input_root_dir = ResourceHelper.get_root_input_dir() + output_root_dir = ResourceHelper.get_root_output_dir() + + for i in os.listdir(output_root_dir): + # List files with .py + if i.endswith(".jpeg") | i.endswith(".png"): + object_key=i + print("********object key",object_key) - image_path=input_root_dir+"F35.jpeg" - # response=s3.upload_file(image_path, bucket_name, object_key) + + image_path=output_root_dir+object_key + response=s3.upload_file(image_path, bucket_name, object_key) response = s3.list_objects_v2( Bucket=bucket_name, ) - - for content in response.get('Contents', []): - print(content['Key']) image_url = f"https://{bucket_name}.s3.amazonaws.com/{object_key}" diff --git a/workspace/input/testing.txt b/workspace/input/testing.txt deleted file mode 100644 index fe857725e..000000000 --- a/workspace/input/testing.txt +++ /dev/null @@ -1 +0,0 @@ -"Hello world" From 59a95edb694deb144e337fd7f6155c568fb713ab Mon Sep 17 00:00:00 2001 From: TransformerOptimus Date: Tue, 18 Jul 2023 19:15:02 +0530 Subject: [PATCH 05/77] fixing the send email attachement issue --- superagi/tools/email/send_email_attachment.py | 9 ++++++--- superagi/tools/file/read_file.py | 9 ++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/superagi/tools/email/send_email_attachment.py b/superagi/tools/email/send_email_attachment.py index 3de093e5c..9e623b586 100644 --- a/superagi/tools/email/send_email_attachment.py +++ b/superagi/tools/email/send_email_attachment.py @@ -50,9 +50,12 @@ def _execute(self, to: str, subject: str, body: str, filename: str) -> str: success or failure message """ final_path = ResourceHelper.get_agent_read_resource_path(file_name=filename, - agent=Agent.get_agent_from_id(self.toolkit_config - .session, - self.agent_id)) + agent=Agent.get_agent_from_id(self.toolkit_config.session, + self.agent_id), + agent_execution=AgentExecution.get_agent_execution_from_id( + session=self.toolkit_config.session, + agent_execution_id=self.agent_execution_id) + ) if final_path is None or not os.path.exists(final_path): raise FileNotFoundError(f"File '{filename}' not found.") attachment = os.path.basename(final_path) diff --git a/superagi/tools/file/read_file.py b/superagi/tools/file/read_file.py index 1d5a0072d..b5e92b6b7 100644 --- a/superagi/tools/file/read_file.py +++ b/superagi/tools/file/read_file.py @@ -42,11 +42,10 @@ def _execute(self, file_name: str): The file content and the file name """ final_path = ResourceHelper.get_agent_read_resource_path(file_name, agent=Agent.get_agent_from_id( - session=self.toolkit_config.session, agent_id=self.agent_id), agent_execution=AgentExecution - .get_agent_execution_from_id(session=self - .toolkit_config.session, - agent_execution_id=self - .agent_execution_id)) + session=self.toolkit_config.session, agent_id=self.agent_id), + agent_execution=AgentExecution.get_agent_execution_from_id( + session=self.toolkit_config.session, + agent_execution_id=self.agent_execution_id)) if final_path is None or not os.path.exists(final_path): raise FileNotFoundError(f"File '{file_name}' not found.") From 35b5bc46e3604edb39358f0a80bd43e2a2d6a80c Mon Sep 17 00:00:00 2001 From: TransformerOptimus Date: Tue, 18 Jul 2023 20:16:20 +0530 Subject: [PATCH 06/77] fixing schedule agent run --- superagi/helper/time_helper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superagi/helper/time_helper.py b/superagi/helper/time_helper.py index bc1b9f51d..eddee29c7 100644 --- a/superagi/helper/time_helper.py +++ b/superagi/helper/time_helper.py @@ -35,7 +35,9 @@ def get_time_difference(timestamp1, timestamp2): def parse_interval_to_seconds(interval: str) -> int: units = {"Minutes": 60, "Hours": 3600, "Days": 86400, "Weeks": 604800, "Months": 2592000} + interval = ' '.join(interval.split()) value, unit = interval.split(" ") + return int(value) * units[unit] From 2551e8fc1da6ca035a020e1d9350d036ce7abadb Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Wed, 19 Jul 2023 12:47:37 +0530 Subject: [PATCH 07/77] Refactored code --- .../stable_diffusion_image_gen.py | 5 +- superagi/tools/instagram_tool/instagram.py | 142 +++++++++++------- .../tools/instagram_tool/instagram_toolkit.py | 5 +- 3 files changed, 96 insertions(+), 56 deletions(-) diff --git a/superagi/tools/image_generation/stable_diffusion_image_gen.py b/superagi/tools/image_generation/stable_diffusion_image_gen.py index 0e772b05b..946086b80 100644 --- a/superagi/tools/image_generation/stable_diffusion_image_gen.py +++ b/superagi/tools/image_generation/stable_diffusion_image_gen.py @@ -59,6 +59,7 @@ def _execute(self, prompt: str, image_names: list, width: int = 512, height: int for artifact in artifacts: base64_strings.append(artifact['base64']) + image_paths=[] for i in range(num): image_base64 = base64_strings[i] img_data = base64.b64decode(image_base64) @@ -69,10 +70,8 @@ def _execute(self, prompt: str, image_names: list, width: int = 512, height: int self.resource_manager.write_binary_file(image_names[i], img_byte_arr.getvalue()) - image_paths=[] - for image in image_names: - image_paths.append(ResourceHelper.get_agent_resource_path(image,self.agent_id)) + image_paths.append(ResourceHelper.get_resource_path(image)) return f"Images downloaded and saved successfully at the following locations: {image_paths}" diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index 13082c337..91cd4e366 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -35,7 +35,7 @@ class InstagramTool(BaseTool): ) args_schema: Type[InstagramSchema] = InstagramSchema tool_response_manager: Optional[ToolResponseQueryManager] = None - + agent_id:int =None class Config: arbitrary_types_allowed = True @@ -49,27 +49,7 @@ def _execute(self, photo_description: str) -> str: Returns: Image posted successfully message if image has been posted on instagram or error message. """ - - # Set your access token and photo URL - print("***********************INSTA TOOL CALLED************************") - import boto3 - s3 = boto3.client( - 's3', - aws_access_key_id=get_config("AWS_ACCESS_KEY_ID"), - aws_secret_access_key=get_config("AWS_SECRET_ACCESS_KEY"), - ) - last_tool_response = self.tool_response_manager.get_last_response() - print('last_tool_response******************',last_tool_response) - - file_path="resources"+last_tool_response.partition("['")[2].partition("']")[0] - print("**********file path********",file_path) - - response = s3.get_object(Bucket=get_config("BUCKET_NAME"), Key='output/car_dealership') - content = response["Body"] - - print("***********content************",content) - return "image posted successfully" - + #get meta user access token and facebook page ID meta_user_access_token = self.get_tool_config("META_USER_ACCESS_TOKEN") facebook_page_id=self.get_tool_config("FACEBOOK_PAGE_ID") @@ -81,53 +61,45 @@ def _execute(self, photo_description: str) -> str: #create caption for the instagram caption=self.create_caption(photo_description) - + + #get request for fetching the instagram_business_account_id response=requests.get( f"https://graph.facebook.com/v17.0/{facebook_page_id}?fields=instagram_business_account&access_token={meta_user_access_token}" ) - if response.status_code != 200: return f"Non-200 response: {str(response.text)}" data = response.json() insta_business_account_id=data["instagram_business_account"]["id"] - - bucket_name = get_config("INSTAGRAM_TOOL_BUCKET_NAME") - output_root_dir = ResourceHelper.get_root_output_dir() + #creating an s3 client + s3 = self.create_s3_client() - for i in os.listdir(output_root_dir): - # List files with .py - if i.endswith(".jpeg") | i.endswith(".png"): - object_key=i - print("********object key",object_key) - - - image_path=output_root_dir+object_key - response=s3.upload_file(image_path, bucket_name, object_key) - - response = s3.list_objects_v2( - Bucket=bucket_name, - ) - - image_url = f"https://{bucket_name}.s3.amazonaws.com/{object_key}" + #getting the file path of the image generated by image generation tool + file_path=self.get_file_path_from_image_generation_tool() - encoded_caption=urllib. parse. quote(caption[1:-1]) - container_gen_url=f"https://graph.facebook.com/v17.0/{insta_business_account_id}/media?image_url={image_url}&caption={encoded_caption}&access_token={meta_user_access_token}" + #fetching the image from the s3 using the file_path + content = self.get_image_from_s3(s3,file_path) - response = requests.post( - container_gen_url + #storing the image in a public bucket and getting the image url + image_url = self.get_img_public_url(s3,file_path,content) + #encoding the caption with possible emojis and hashtags and removing the starting and ending double quotes + encoded_caption=urllib. parse. quote(caption[1:-1]) + + #post request for getting the media container ID + response = requests.post( + f"https://graph.facebook.com/v17.0/{insta_business_account_id}/media?image_url={image_url}&caption={encoded_caption}&access_token={meta_user_access_token}" ) - if response.status_code != 200: return f"Non-200 response: {str(response.text)}" data = response.json() container_ID=data["id"] + + #post request to post the media container on instagram account response = requests.post( - f"https://graph.facebook.com/v17.0/{insta_business_account_id}/media_publish?creation_id={container_ID}&access_token={meta_user_access_token}", + f"https://graph.facebook.com/v17.0/{insta_business_account_id}/media_publish?creation_id={container_ID}&access_token={meta_user_access_token}" ) - if response.status_code != 200: return f"Non-200 response: {str(response.text)}" @@ -144,7 +116,7 @@ def create_caption(self, photo_description: str) -> str: Description of the photo to be posted """ caption_prompt ="""Generate an instagram post caption for the following text `{photo_description}` - Write a concise as necessary and attempt to make it relevant to the description as best as possible. Add emojis if needed.""" + Attempt to make it as relevant as possible to the description. Add relevant emojis and hashtags.""" caption_prompt = caption_prompt.replace("{photo_description}", str(photo_description)) @@ -152,4 +124,72 @@ def create_caption(self, photo_description: str) -> str: result = self.llm.chat_completion(messages, max_tokens=self.max_token_limit) caption=result["content"] - return caption \ No newline at end of file + return caption + + def get_image_from_s3(self,s3,file_path): + """ + Gets the image from the s3 bucket + + Args: + s3: S3 client + file_path: path of the image file in s3 + + Returns + The image file from s3 + """ + + response = s3.get_object(Bucket=get_config("BUCKET_NAME"), Key=file_path) + content = response["Body"].read() + + return content + + def get_file_path_from_image_generation_tool(self): + """ + Parses the output of the previous tool (Stable diffusion) and returns the path of the image file + + Args: + + Returns: + The path of the image file generated by the image generation toolkit + """ + + last_tool_response = self.tool_response_manager.get_last_response() + file_path="resources"+last_tool_response.partition("['")[2].partition("']")[0] + return file_path + + def create_s3_client(self): + """ + Creates an s3 client + + Args: + + Returns: + The s3 client + """ + + s3 = boto3.client( + 's3', + aws_access_key_id=get_config("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=get_config("AWS_SECRET_ACCESS_KEY"), + ) + + return s3 + + def get_img_public_url(self,s3,file_path,content): + """ + Puts the image generated by image generation tool in the s3 bucket and returns the public url of the same + Args: + s3 : S3 bucket + file_path: Path of the image file in s3 + content: Image file + + Returns: + The public url of the image put in s3 bucket + """ + + bucket_name = get_config("INSTAGRAM_TOOL_BUCKET_NAME") + object_key=file_path.split('/')[-1] + s3.put_object(Bucket=bucket_name, Key=object_key, Body=content) + + image_url = f"https://{bucket_name}.s3.amazonaws.com/{object_key}" + return image_url \ No newline at end of file diff --git a/superagi/tools/instagram_tool/instagram_toolkit.py b/superagi/tools/instagram_tool/instagram_toolkit.py index 23449d016..abfedaabb 100644 --- a/superagi/tools/instagram_tool/instagram_toolkit.py +++ b/superagi/tools/instagram_tool/instagram_toolkit.py @@ -2,13 +2,14 @@ from typing import List from superagi.tools.base_tool import BaseTool, BaseToolkit from superagi.tools.instagram_tool.instagram import InstagramTool +from superagi.tools.image_generation.stable_diffusion_image_gen import StableDiffusionImageGenTool class InstagramToolkit(BaseToolkit, ABC): name: str = "Instagram Toolkit" - description: str = "Toolkit containing tools for performing posting photos on Instagram" + description: str = "Toolkit containing tools for posting AI generated photos on Instagram" def get_tools(self) -> List[BaseTool]: - return [InstagramTool()] + return [InstagramTool(),StableDiffusionImageGenTool()] def get_env_keys(self) -> List[str]: return [ From 281753fdcf3f3b2533fd3f6614cc12d9e6baa00b Mon Sep 17 00:00:00 2001 From: luciferlinx <129729795+luciferlinx101@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:53:12 +0530 Subject: [PATCH 08/77] Resource duplicate fix (#803) --- superagi/helper/resource_helper.py | 40 +++++++++++++++---- superagi/resource_manager/file_manager.py | 15 +++---- .../unit_tests/helper/test_resource_helper.py | 23 +++++++++-- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/superagi/helper/resource_helper.py b/superagi/helper/resource_helper.py index cd39eeb14..ec606e40e 100644 --- a/superagi/helper/resource_helper.py +++ b/superagi/helper/resource_helper.py @@ -10,7 +10,7 @@ class ResourceHelper: @classmethod - def make_written_file_resource(cls, file_name: str, agent: Agent, agent_execution: AgentExecution): + def make_written_file_resource(cls, file_name: str, agent: Agent, agent_execution: AgentExecution, session): """ Function to create a Resource object for a written file. @@ -18,6 +18,7 @@ def make_written_file_resource(cls, file_name: str, agent: Agent, agent_executio file_name (str): The name of the file. agent (Agent): Agent related to resource. agent_execution(AgentExecution): Agent Execution related to a resource + session (Session): The database session. Returns: Resource: The Resource object. @@ -46,13 +47,36 @@ def make_written_file_resource(cls, file_name: str, agent: Agent, agent_executio logger.info("make_written_file_resource:", final_path) if StorageType.get_storage_type(get_config("STORAGE_TYPE", StorageType.FILE.value)) == StorageType.S3: file_path = "resources" + file_path - resource = Resource(name=file_name, path=file_path, storage_type=storage_type.value, - size=file_size, - type=file_type, - channel="OUTPUT", - agent_id=agent.id, - agent_execution_id=agent_execution.id) - return resource + existing_resource = session.query(Resource).filter_by( + name=file_name, + path=file_path, + storage_type=storage_type.value, + type=file_type, + channel="OUTPUT", + agent_id=agent.id, + agent_execution_id=agent_execution.id + ).first() + + if existing_resource: + # Update the existing resource attributes + existing_resource.size = file_size + session.commit() + session.flush() + return existing_resource + else: + resource = Resource( + name=file_name, + path=file_path, + storage_type=storage_type.value, + size=file_size, + type=file_type, + channel="OUTPUT", + agent_id=agent.id, + agent_execution_id=agent_execution.id + ) + session.add(resource) + session.commit() + return resource @classmethod def get_formatted_agent_level_path(cls, agent: Agent, path) -> object: diff --git a/superagi/resource_manager/file_manager.py b/superagi/resource_manager/file_manager.py index c93c64c6d..6d681b5f5 100644 --- a/superagi/resource_manager/file_manager.py +++ b/superagi/resource_manager/file_manager.py @@ -26,7 +26,6 @@ def write_binary_file(self, file_name: str, data): self.agent_execution_id)) else: final_path = ResourceHelper.get_resource_path(file_name) - try: with open(final_path, mode="wb") as img: img.write(data) @@ -44,14 +43,11 @@ def write_to_s3(self, file_name, final_path): self.agent_id), agent_execution=AgentExecution .get_agent_execution_from_id(self.session, - self.agent_execution_id)) - if resource is not None: - self.session.add(resource) - self.session.commit() - self.session.flush() - if resource.storage_type == StorageType.S3.value: - s3_helper = S3Helper() - s3_helper.upload_file(img, path=resource.path) + self.agent_execution_id), + session=self.session) + if resource.storage_type == StorageType.S3.value: + s3_helper = S3Helper() + s3_helper.upload_file(img, path=resource.path) def write_file(self, file_name: str, content): if self.agent_id is not None: @@ -63,7 +59,6 @@ def write_file(self, file_name: str, content): self.agent_execution_id)) else: final_path = ResourceHelper.get_resource_path(file_name) - try: with open(final_path, mode="w") as file: file.write(content) diff --git a/tests/unit_tests/helper/test_resource_helper.py b/tests/unit_tests/helper/test_resource_helper.py index 4cc9a5f65..d1e9eb42a 100644 --- a/tests/unit_tests/helper/test_resource_helper.py +++ b/tests/unit_tests/helper/test_resource_helper.py @@ -1,8 +1,9 @@ -from unittest.mock import patch +from unittest.mock import patch, MagicMock from superagi.helper.resource_helper import ResourceHelper from superagi.models.agent import Agent from superagi.models.agent_execution import AgentExecution +from superagi.models.resource import Resource def test_make_written_file_resource(mocker): @@ -10,12 +11,28 @@ def test_make_written_file_resource(mocker): mocker.patch('os.makedirs', return_value=None) mocker.patch('os.path.getsize', return_value=1000) mocker.patch('os.path.splitext', return_value=("", ".txt")) - mocker.patch('superagi.helper.resource_helper.get_config', side_effect=['FILE','/','/','FILE']) + mocker.patch('superagi.helper.resource_helper.get_config', side_effect=['FILE', '/', '/', 'FILE']) mock_agent = Agent(id=1, name='TestAgent') mock_agent_execution = AgentExecution(id=1, name='TestExecution') + session = MagicMock() with patch('superagi.helper.resource_helper.logger') as logger_mock: - result = ResourceHelper.make_written_file_resource('test.txt', mock_agent, mock_agent_execution) + session.query.return_value.filter_by.return_value.first.return_value = None + # Create a Resource object + resource = Resource( + name='test.txt', + path='/test.txt', + storage_type='FILE', + size=1000, + type='application/txt', + channel='OUTPUT', + agent_id=1, + agent_execution_id=1 + ) + + # Mock the session.add() method to return the created Resource object + session.add.return_value = resource + result = ResourceHelper.make_written_file_resource('test.txt', mock_agent, mock_agent_execution, session) assert result.name == 'test.txt' assert result.path == '/test.txt' From 6f3deb4f2c5fe88171f165be86e03ab092c21a11 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Wed, 19 Jul 2023 13:58:31 +0530 Subject: [PATCH 09/77] removed jpeg files used for testing --- superagi/tools/instagram_tool/F35.jpeg | Bin 4879 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 superagi/tools/instagram_tool/F35.jpeg diff --git a/superagi/tools/instagram_tool/F35.jpeg b/superagi/tools/instagram_tool/F35.jpeg deleted file mode 100644 index 0eabaa6747a9c61abfc19a1122604ca826c0fbd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4879 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 Rg8nWNi6GRWLl|Do{0E~`>&O5A From c697937088255eb18f6b2a39310960ec49da1f83 Mon Sep 17 00:00:00 2001 From: Taran <97586318+Tarraann@users.noreply.github.com> Date: Wed, 19 Jul 2023 16:31:50 +0530 Subject: [PATCH 10/77] Dalle fix (#812) Co-authored-by: Tarraann --- superagi/image_llms/openai_dalle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superagi/image_llms/openai_dalle.py b/superagi/image_llms/openai_dalle.py index 15bd9e0f1..b91c279ad 100644 --- a/superagi/image_llms/openai_dalle.py +++ b/superagi/image_llms/openai_dalle.py @@ -1,10 +1,10 @@ import openai from superagi.config.config import get_config -from superagi.llms.base_llm import BaseLlm +from superagi.image_llms.base_image_llm import BaseImageLlm -class OpenAiDalle(BaseLlm): +class OpenAiDalle(BaseImageLlm): def __init__(self, api_key, image_model=None, number_of_results=1): """ Args: From 2889187ca6b4e3e1ecb4a4bf28be4ed6aab38858 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Thu, 20 Jul 2023 12:47:17 +0530 Subject: [PATCH 11/77] fixed recurring run issues --- .../image_generation/stable_diffusion_image_gen.py | 5 ++--- superagi/tools/instagram_tool/instagram.py | 11 +++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/superagi/tools/image_generation/stable_diffusion_image_gen.py b/superagi/tools/image_generation/stable_diffusion_image_gen.py index 946086b80..33d1280a0 100644 --- a/superagi/tools/image_generation/stable_diffusion_image_gen.py +++ b/superagi/tools/image_generation/stable_diffusion_image_gen.py @@ -14,7 +14,7 @@ class StableDiffusionImageGenInput(BaseModel): prompt: str = Field(..., description="Prompt for Image Generation to be used by Stable Diffusion.") height: int = Field(..., description="Height of the image to be Generated. default height is 512") width: int = Field(..., description="Width of the image to be Generated. default width is 512") - num: int = Field(..., description="Number of Images to be generated. default num is 2") + num: int = Field(..., description="Number of Images to be generated. default num is 1") steps: int = Field(..., description="Number of diffusion steps to run. default steps are 50") image_names: list = Field(..., description="Image Names for the generated images, example 'image_1.png'. Only include the image name. Don't include path.") @@ -40,13 +40,12 @@ class StableDiffusionImageGenTool(BaseTool): class Config: arbitrary_types_allowed = True - def _execute(self, prompt: str, image_names: list, width: int = 512, height: int = 512, num: int = 2, + def _execute(self, prompt: str, image_names: list, width: int = 512, height: int = 512, num: int = 1, steps: int = 50): api_key = self.get_tool_config("STABILITY_API_KEY") if api_key is None: return "Error: Missing Stability API key." - response = self.call_stable_diffusion(api_key, width, height, num, prompt, steps) if response.status_code != 200: diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index 91cd4e366..dbf003320 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -12,6 +12,7 @@ import os import requests from superagi.tools.tool_response_query_manager import ToolResponseQueryManager +import random class InstagramSchema(BaseModel): photo_description: str = Field( @@ -84,7 +85,7 @@ def _execute(self, photo_description: str) -> str: #storing the image in a public bucket and getting the image url image_url = self.get_img_public_url(s3,file_path,content) #encoding the caption with possible emojis and hashtags and removing the starting and ending double quotes - encoded_caption=urllib. parse. quote(caption[1:-1]) + encoded_caption=self.create_caption(photo_description) #post request for getting the media container ID response = requests.post( @@ -116,15 +117,17 @@ def create_caption(self, photo_description: str) -> str: Description of the photo to be posted """ caption_prompt ="""Generate an instagram post caption for the following text `{photo_description}` - Attempt to make it as relevant as possible to the description. Add relevant emojis and hashtags.""" + Attempt to make it as relevant as possible to the description and should be different and unique everytime. Add relevant emojis and hashtags.""" caption_prompt = caption_prompt.replace("{photo_description}", str(photo_description)) messages = [{"role": "system", "content": caption_prompt}] result = self.llm.chat_completion(messages, max_tokens=self.max_token_limit) caption=result["content"] + + encoded_caption=urllib. parse. quote(caption) - return caption + return encoded_caption def get_image_from_s3(self,s3,file_path): """ @@ -188,7 +191,7 @@ def get_img_public_url(self,s3,file_path,content): """ bucket_name = get_config("INSTAGRAM_TOOL_BUCKET_NAME") - object_key=file_path.split('/')[-1] + object_key=f"instagram_upload_images/{file_path.split('/')[-1]}{random.randint(0, 1000)}" s3.put_object(Bucket=bucket_name, Key=object_key, Body=content) image_url = f"https://{bucket_name}.s3.amazonaws.com/{object_key}" From 9f0a78df6a38e7f336ce807270218d66f9703d07 Mon Sep 17 00:00:00 2001 From: Autocop-Agent <129729746+Autocop-Agent@users.noreply.github.com> Date: Thu, 20 Jul 2023 13:18:10 +0530 Subject: [PATCH 12/77] Supercoder Improve tool addition (#755) --- superagi/resource_manager/file_manager.py | 41 ++++++-- superagi/tools/code/coding_toolkit.py | 3 +- superagi/tools/code/improve_code.py | 99 +++++++++++++++++++ superagi/tools/code/prompts/improve_code.txt | 26 +++++ superagi/tools/code/prompts/write_code.txt | 2 +- superagi/tools/code/write_code.py | 4 +- .../tools/code/test_improve_code.py | 52 ++++++++++ 7 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 superagi/tools/code/improve_code.py create mode 100644 superagi/tools/code/prompts/improve_code.txt create mode 100644 tests/unit_tests/tools/code/test_improve_code.py diff --git a/superagi/resource_manager/file_manager.py b/superagi/resource_manager/file_manager.py index 6d681b5f5..e9059ac20 100644 --- a/superagi/resource_manager/file_manager.py +++ b/superagi/resource_manager/file_manager.py @@ -1,21 +1,17 @@ import csv - from sqlalchemy.orm import Session - +import os from superagi.helper.resource_helper import ResourceHelper from superagi.helper.s3_helper import S3Helper from superagi.lib.logger import logger from superagi.models.agent import Agent from superagi.models.agent_execution import AgentExecution from superagi.types.storage_types import StorageType - - class FileManager: def __init__(self, session: Session, agent_id: int = None, agent_execution_id: int = None): self.session = session self.agent_id = agent_id self.agent_execution_id = agent_execution_id - def write_binary_file(self, file_name: str, data): if self.agent_id is not None: final_path = ResourceHelper.get_agent_write_resource_path(file_name, @@ -35,7 +31,6 @@ def write_binary_file(self, file_name: str, data): return f"Binary {file_name} saved successfully" except Exception as err: return f"Error write_binary_file: {err}" - def write_to_s3(self, file_name, final_path): with open(final_path, 'rb') as img: resource = ResourceHelper.make_written_file_resource(file_name=file_name, @@ -68,7 +63,6 @@ def write_file(self, file_name: str, content): return f"{file_name} - File written successfully" except Exception as err: return f"Error write_file: {err}" - def write_csv_file(self, file_name: str, csv_data): if self.agent_id is not None: final_path = ResourceHelper.get_agent_write_resource_path(file_name, @@ -79,7 +73,6 @@ def write_csv_file(self, file_name: str, csv_data): self.agent_execution_id)) else: final_path = ResourceHelper.get_resource_path(file_name) - try: with open(final_path, mode="w") as file: writer = csv.writer(file, lineterminator="\n") @@ -90,6 +83,7 @@ def write_csv_file(self, file_name: str, csv_data): return f"{file_name} - File written successfully" except Exception as err: return f"Error write_csv_file: {err}" + def get_agent_resource_path(self, file_name: str): return ResourceHelper.get_agent_write_resource_path(file_name, agent=Agent.get_agent_from_id(self.session, @@ -97,3 +91,34 @@ def get_agent_resource_path(self, file_name: str): agent_execution=AgentExecution .get_agent_execution_from_id(self.session, self.agent_execution_id)) + def read_file(self, file_name: str): + if self.agent_id is not None: + final_path = self.get_agent_resource_path(file_name) + else: + final_path = ResourceHelper.get_resource_path(file_name) + + try: + with open(final_path, mode="r") as file: + content = file.read() + logger.info(f"{file_name} - File read successfully") + return content + except Exception as err: + return f"Error while reading file {file_name}: {err}" + def get_files(self): + """ + Gets all file names generated by the CodingTool. + Returns: + A list of file names. + """ + + if self.agent_id is not None: + final_path = self.get_agent_resource_path("") + else: + final_path = ResourceHelper.get_resource_path("") + try: + # List all files in the directory + files = os.listdir(final_path) + except Exception as err: + logger.error(f"Error while accessing files in {final_path}: {err}") + files = [] + return files diff --git a/superagi/tools/code/coding_toolkit.py b/superagi/tools/code/coding_toolkit.py index f96f9e800..248af2536 100644 --- a/superagi/tools/code/coding_toolkit.py +++ b/superagi/tools/code/coding_toolkit.py @@ -5,6 +5,7 @@ from superagi.tools.code.write_code import CodingTool from superagi.tools.code.write_spec import WriteSpecTool from superagi.tools.code.write_test import WriteTestTool +from superagi.tools.code.improve_code import ImproveCodeTool class CodingToolkit(BaseToolkit, ABC): @@ -12,7 +13,7 @@ class CodingToolkit(BaseToolkit, ABC): description: str = "Coding Tool kit contains all tools related to coding tasks" def get_tools(self) -> List[BaseTool]: - return [CodingTool(), WriteSpecTool(), WriteTestTool()] + return [CodingTool(), WriteSpecTool(), WriteTestTool(), ImproveCodeTool()] def get_env_keys(self) -> List[str]: return [] diff --git a/superagi/tools/code/improve_code.py b/superagi/tools/code/improve_code.py new file mode 100644 index 000000000..c54856811 --- /dev/null +++ b/superagi/tools/code/improve_code.py @@ -0,0 +1,99 @@ +import re +from typing import Type, Optional, List + +from pydantic import BaseModel, Field + +from superagi.agent.agent_prompt_builder import AgentPromptBuilder +from superagi.helper.prompt_reader import PromptReader +from superagi.helper.token_counter import TokenCounter +from superagi.lib.logger import logger +from superagi.llms.base_llm import BaseLlm +from superagi.resource_manager.file_manager import FileManager +from superagi.tools.base_tool import BaseTool +from superagi.tools.tool_response_query_manager import ToolResponseQueryManager + + +class ImproveCodeSchema(BaseModel): + pass + + +class ImproveCodeTool(BaseTool): + """ + Used to improve the already generated code by reading the code from the files + + Attributes: + llm: LLM used for code generation. + name : The name of the tool. + description : The description of the tool. + resource_manager: Manages the file resources. + """ + llm: Optional[BaseLlm] = None + agent_id: int = None + name = "ImproveCodeTool" + description = ( + "This tool improves the generated code." + ) + args_schema: Type[ImproveCodeSchema] = ImproveCodeSchema + resource_manager: Optional[FileManager] = None + tool_response_manager: Optional[ToolResponseQueryManager] = None + goals: List[str] = [] + + class Config: + arbitrary_types_allowed = True + + def _execute(self) -> str: + """ + Execute the improve code tool. + + Returns: + Improved code or error message. + """ + # Get all file names that the CodingTool has written + file_names = self.resource_manager.get_files() + logger.info(file_names) + # Loop through each file + for file_name in file_names: + if '.txt' not in file_name and '.sh' not in file_name and '.json' not in file_name: + # Read the file content + content = self.resource_manager.read_file(file_name) + + # Generate a prompt from improve_code.txt + prompt = PromptReader.read_tools_prompt(__file__, "improve_code.txt") + + # Combine the hint from the file, goals, and content + prompt = prompt.replace("{goals}", AgentPromptBuilder.add_list_items_to_string(self.goals)) + prompt = prompt.replace("{content}", content) + + # Add the file content to the chat completion prompt + prompt = prompt + "\nOriginal Code:\n```\n" + content + "\n```" + + + + # Use LLM to generate improved code + result = self.llm.chat_completion([{'role': 'system', 'content': prompt}]) + + # Extract the response first + response = result.get('response') + if not response: + logger.info("RESPONSE NOT AVAILABLE") + + # Now extract the choices from response + choices = response.get('choices') + if not choices: + logger.info("CHOICES NOT AVAILABLE") + + # Now you can safely extract the message content + improved_content = choices[0]["message"]["content"] + # improved_content = result["messages"][0]["content"] + parsed_content = re.findall("```(?:\w*\n)?(.*?)```", improved_content, re.DOTALL) + parsed_content_code = "\n".join(parsed_content) + + # Rewrite the file with the improved content + save_result = self.resource_manager.write_file(file_name, parsed_content_code) + + if save_result.startswith("Error"): + return save_result + else: + continue + + return f"All codes improved and saved successfully in: " + " ".join(file_names) \ No newline at end of file diff --git a/superagi/tools/code/prompts/improve_code.txt b/superagi/tools/code/prompts/improve_code.txt new file mode 100644 index 000000000..973525210 --- /dev/null +++ b/superagi/tools/code/prompts/improve_code.txt @@ -0,0 +1,26 @@ +You are a super smart developer. You have been tasked with fixing and filling the function and classes where only the description of code is written without the actual code . There might be placeholders in the code you have to fill in. +You provide fully functioning, well formatted code with few comments, that works and has no bugs. +If the code is already correct and doesn't need change, just return the same code +However, make sure that you only return the improved code, without any additional content. + + +Please structure the improved code as follows: + +``` +CODE +``` + +Please return the full new code in same format as the original code +Don't write any explanation or description in your response other than the actual code + +Your high-level goal is: +{goals} + +The content of the file you need to improve is: +{content} + +Only return the code and not any other line + +To start, first analyze the existing code. Check for any function with missing logic inside it and fill the function. +Make sure, that not a single function is empty or contains just comments, there should be function logic inside it +Return fully completed functions by filling the placeholders \ No newline at end of file diff --git a/superagi/tools/code/prompts/write_code.txt b/superagi/tools/code/prompts/write_code.txt index 17f8c37ae..67d65a621 100644 --- a/superagi/tools/code/prompts/write_code.txt +++ b/superagi/tools/code/prompts/write_code.txt @@ -1,4 +1,5 @@ You are a super smart developer who practices good Development for writing code according to a specification. +Please note that the code should be fully functional. There should be no placeholder in functions or classes in any file. Your high-level goal is: {goals} @@ -23,7 +24,6 @@ Each file must strictly follow a markdown code block format, where the following ``` You will start with the "entrypoint" file, then go to the ones that are imported by that file, and so on. -Please note that the code should be fully functional. No placeholders. Follow a language and framework appropriate best practice file naming convention. Make sure that files contain all imports, types etc. Make sure that code in different files are compatible with each other. diff --git a/superagi/tools/code/write_code.py b/superagi/tools/code/write_code.py index 9c1a001a9..1eed98c36 100644 --- a/superagi/tools/code/write_code.py +++ b/superagi/tools/code/write_code.py @@ -40,7 +40,7 @@ class CodingTool(BaseTool): "Make sure that every detail of the architecture is, in the end, implemented as code. " "Think step by step and reason yourself to the right decisions to make sure we get it right. " "You will first lay out the names of the core classes, functions, methods that will be necessary, " - "as well as a quick comment on their purpose. Then you will output the content of each file including ALL code." + "as well as a quick comment on their purpose. Then you will output the content of each file including each function and class and ALL code." ) args_schema: Type[CodingSchema] = CodingSchema goals: List[str] = [] @@ -84,6 +84,8 @@ def _execute(self, code_description: str) -> str: for match in matches: # Get the filename file_name = re.sub(r'[<>"|?*]', "", match.group(1)) + if not file_name[0].isalnum(): + file_name = file_name[1:-1] # Get the code code = match.group(2) diff --git a/tests/unit_tests/tools/code/test_improve_code.py b/tests/unit_tests/tools/code/test_improve_code.py new file mode 100644 index 000000000..8deae28c5 --- /dev/null +++ b/tests/unit_tests/tools/code/test_improve_code.py @@ -0,0 +1,52 @@ +import pytest +from unittest.mock import Mock, MagicMock +from superagi.tools.code.improve_code import ImproveCodeTool + +@pytest.fixture +def mock_improve_code_tool(): + improve_code_tool = ImproveCodeTool() + improve_code_tool.resource_manager = Mock() + improve_code_tool.llm = Mock() + return improve_code_tool + +def test_execute(mock_improve_code_tool): + mock_improve_code_tool.resource_manager.get_files.return_value = ['test1', 'test2'] + mock_improve_code_tool.resource_manager.read_file.return_value = "test file content" + mock_improve_code_tool.llm.chat_completion.return_value = { + "response": + { + "choices": + [ + { + "message": + { + "content": "```\nimproved code\n```" + } + } + ] + } + } + mock_improve_code_tool.resource_manager.write_file.return_value = "file saved successfully" + + assert mock_improve_code_tool._execute() == "All codes improved and saved successfully in: test1 test2" + +def test_execute_with_error(mock_improve_code_tool): + mock_improve_code_tool.resource_manager.get_files.return_value = ['test1'] + mock_improve_code_tool.resource_manager.read_file.return_value = "test file content" + mock_improve_code_tool.llm.chat_completion.return_value = { + "response": + { + "choices": + [ + { + "message": + { + "content": "```\nimproved code\n```" + } + } + ] + } + } + mock_improve_code_tool.resource_manager.write_file.return_value = "Error: Could not save file" + + assert mock_improve_code_tool._execute() == "Error: Could not save file" From e37014e341104e6a17939b76f7cfaf69650879d6 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Thu, 20 Jul 2023 15:11:49 +0530 Subject: [PATCH 13/77] fixed recurring issues --- .../tools/image_generation/stable_diffusion_image_gen.py | 2 +- superagi/tools/instagram_tool/instagram.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/superagi/tools/image_generation/stable_diffusion_image_gen.py b/superagi/tools/image_generation/stable_diffusion_image_gen.py index 33d1280a0..7f4912b49 100644 --- a/superagi/tools/image_generation/stable_diffusion_image_gen.py +++ b/superagi/tools/image_generation/stable_diffusion_image_gen.py @@ -11,7 +11,7 @@ class StableDiffusionImageGenInput(BaseModel): - prompt: str = Field(..., description="Prompt for Image Generation to be used by Stable Diffusion.") + prompt: str = Field(..., description="Prompt for Image Generation to be used by Stable Diffusion. The prompt should be as descriptive as possible and mention all the details of the image to be generated") height: int = Field(..., description="Height of the image to be Generated. default height is 512") width: int = Field(..., description="Width of the image to be Generated. default width is 512") num: int = Field(..., description="Number of Images to be generated. default num is 1") diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index dbf003320..097b5c3ab 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -158,6 +158,11 @@ def get_file_path_from_image_generation_tool(self): last_tool_response = self.tool_response_manager.get_last_response() file_path="resources"+last_tool_response.partition("['")[2].partition("']")[0] + if ',' in file_path: + # Split the string based on the comma and get the first element (substring before the comma) + file_path = file_path.split(',')[0].strip() + file_path=file_path[:-1] + return file_path def create_s3_client(self): From 0937ba528d38e7a5bb7b7e085ce12670fd8740a8 Mon Sep 17 00:00:00 2001 From: Anisha Gupta <60440541+anisha1607@users.noreply.github.com> Date: Thu, 20 Jul 2023 16:24:22 +0530 Subject: [PATCH 14/77] Edit agent templates (#793) * Added Controller Function * updated frontend * made minor changes in frontend * added unit test * made minor changes in frontend and backend * added another unit test * added minor change in frontend --------- Co-authored-by: anisha-contlo --- gui/pages/Content/Agents/AgentCreate.js | 101 +++++++++++--- gui/pages/api/DashboardService.js | 4 + superagi/controllers/agent_template.py | 54 ++++++++ .../controllers/test_agent_template.py | 127 ++++++++++++++++++ 4 files changed, 264 insertions(+), 22 deletions(-) create mode 100644 tests/unit_tests/controllers/test_agent_template.py diff --git a/gui/pages/Content/Agents/AgentCreate.js b/gui/pages/Content/Agents/AgentCreate.js index c0c8afc1f..0de46038a 100644 --- a/gui/pages/Content/Agents/AgentCreate.js +++ b/gui/pages/Content/Agents/AgentCreate.js @@ -5,6 +5,7 @@ import 'react-toastify/dist/ReactToastify.css'; import styles from './Agents.module.css'; import { createAgent, + editAgentTemplate, fetchAgentTemplateConfigLocal, getOrganisationConfig, updateExecution, @@ -21,17 +22,11 @@ import {EventBus} from "@/utils/eventBus"; import 'moment-timezone'; import AgentSchedule from "@/pages/Content/Agents/AgentSchedule"; -export default function AgentCreate({ - sendAgentData, - selectedProjectId, - fetchAgents, - toolkits, - organisationId, - template, - internalId - }) { +export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents,toolkits,organisationId,template,internalId}) { + const [advancedOptions, setAdvancedOptions] = useState(false); const [agentName, setAgentName] = useState(""); + const [agentTemplateId, setAgentTemplateId] = useState(null); const [agentDescription, setAgentDescription] = useState(""); const [longTermMemory, setLongTermMemory] = useState(true); const [addResources, setAddResources] = useState(true); @@ -42,6 +37,7 @@ export default function AgentCreate({ const [maxIterations, setIterations] = useState(25); const [toolkitList, setToolkitList] = useState(toolkits) const [searchValue, setSearchValue] = useState(''); + const [showButton, setShowButton] = useState(false); const constraintsArray = [ "If you are unsure how you previously did something or want to recall past events, thinking about similar events will help you remember.", @@ -137,6 +133,7 @@ export default function AgentCreate({ setLocalStorageValue("agent_name_" + String(internalId), template.name, setAgentName); setLocalStorageValue("agent_description_" + String(internalId), template.description, setAgentDescription); setLocalStorageValue("advanced_options_" + String(internalId), true, setAdvancedOptions); + setLocalStorageValue("agent_template_id_" + String(internalId), template.id, setAgentTemplateId); fetchAgentTemplateConfigLocal(template.id) .then((response) => { @@ -151,6 +148,8 @@ export default function AgentCreate({ setLocalStorageValue("agent_database_" + String(internalId), data.LTM_DB, setDatabase); setLocalStorageValue("agent_model_" + String(internalId), data.model, setModel); setLocalStorageArray("tool_names_" + String(internalId), data.tools, setToolNames); + setLocalStorageValue("is_agent_template_" + String(internalId), true, setShowButton); + setShowButton(true); }) .catch((error) => { console.error('Error fetching template details:', error); @@ -362,33 +361,35 @@ export default function AgentCreate({ } }, [scheduleData]); - const handleAddAgent = () => { - if (!hasAPIkey) { + const validateAgentData = (isNewAgent) => { + if (isNewAgent && !hasAPIkey) { toast.error("Your OpenAI/Palm API key is empty!", {autoClose: 1800}); openNewTab(-3, "Settings", "Settings", false); - return + return false; } - - if (agentName.replace(/\s/g, '') === '') { + + if (agentName?.replace(/\s/g, '') === '') { toast.error("Agent name can't be blank", {autoClose: 1800}); - return + return false; } - - if (agentDescription.replace(/\s/g, '') === '') { + if (agentDescription?.replace(/\s/g, '') === '') { toast.error("Agent description can't be blank", {autoClose: 1800}); - return + return false; } - const isEmptyGoal = goals.some((goal) => goal.replace(/\s/g, '') === ''); if (isEmptyGoal) { toast.error("Goal can't be empty", {autoClose: 1800}); - return; + return false; } - if (selectedTools.length <= 0) { toast.error("Add atleast one tool", {autoClose: 1800}); - return + return false; } + return true; + } + + const handleAddAgent = () => { + if (!validateAgentData(true)) return; setCreateClickable(false); @@ -531,6 +532,46 @@ export default function AgentCreate({ event.preventDefault(); }; + function updateTemplate() { + + if (!validateAgentData(false)) return; + + let permission_type = permission; + if (permission.includes("RESTRICTED")) { + permission_type = "RESTRICTED"; + } + + const agentTemplateConfigData = { + "goal": goals, + "instruction": instructions, + "agent_type": agentType, + "constraints": constraints, + "tools": toolNames, + "exit": exitCriterion, + "iteration_interval": stepTime, + "model": model, + "max_iterations": maxIterations, + "permission_type": permission_type, + "LTM_DB": longTermMemory ? database : null, + } + const editTemplateData = { + "name": agentName, + "description": agentDescription, + "agent_configs": agentTemplateConfigData + } + + editAgentTemplate(agentTemplateId, editTemplateData) + .then((response) => { + if (response.status === 200) { + toast.success('Agent template has been updated successfully!', {autoClose: 1800}); + } + }) + .catch((error) => { + toast.error("Error updating agent template") + console.error('Error updating agent template:', error); + }); + }; + function setFileData(files) { if (files.length > 0) { const fileData = { @@ -579,11 +620,21 @@ export default function AgentCreate({ setAdvancedOptions(JSON.parse(advanced_options)); } + const is_agent_template = localStorage.getItem("is_agent_template_" + String(internalId)); + if (is_agent_template) { + setShowButton(true); + } + const agent_name = localStorage.getItem("agent_name_" + String(internalId)); if (agent_name) { setAgentName(agent_name); } + const agent_template_id = localStorage.getItem("agent_template_id_"+ String(internalId)); + if(agent_template_id){ + setAgentTemplateId(agent_template_id) + } + const agent_description = localStorage.getItem("agent_description_" + String(internalId)); if (agent_description) { setAgentDescription(agent_description); @@ -986,6 +1037,12 @@ export default function AgentCreate({ + {showButton && ( + + )}
{createDropdown && (
{ return api.put(`/agentexecutions/update/${executionId}`, executionData); }; +export const editAgentTemplate = (agentTemplateId, agentTemplateData) => { + return api.put(`/agent_templates/update_agent_template/${agentTemplateId}`, agentTemplateData) +} + export const addExecution = (executionData) => { return api.post(`/agentexecutions/add`, executionData); }; diff --git a/superagi/controllers/agent_template.py b/superagi/controllers/agent_template.py index 140622aee..100bee8d0 100644 --- a/superagi/controllers/agent_template.py +++ b/superagi/controllers/agent_template.py @@ -13,6 +13,7 @@ from superagi.models.agent_template_config import AgentTemplateConfig from superagi.models.agent_workflow import AgentWorkflow from superagi.models.tool import Tool +import json # from superagi.types.db import AgentTemplateIn, AgentTemplateOut router = APIRouter() @@ -144,6 +145,59 @@ def update_agent_template(agent_template_id: int, return db_agent_template +@router.put("/update_agent_template/{agent_template_id}", status_code=200) +def edit_agent_template(agent_template_id: int, + updated_agent_configs: dict, + organisation=Depends(get_user_organisation)): + + """ + Update the details of an agent template. + + Args: + agent_template_id (int): The ID of the agent template to update. + edited_agent_configs (dict): The updated agent configurations. + organisation (Depends): Dependency to get the user organisation. + + Returns: + HTTPException (status_code=200): If the agent gets successfully edited. + + Raises: + HTTPException (status_code=404): If the agent template is not found. + """ + + db_agent_template = db.session.query(AgentTemplate).filter(AgentTemplate.organisation_id == organisation.id, + AgentTemplate.id == agent_template_id).first() + if db_agent_template is None: + raise HTTPException(status_code=404, detail="Agent Template not found") + + db_agent_template.name = updated_agent_configs["name"] + db_agent_template.description = updated_agent_configs["description"] + + db.session.commit() + + agent_config_values = updated_agent_configs.get('agent_configs', {}) + + for key, value in agent_config_values.items(): + if isinstance(value, (list, dict)): + value = json.dumps(value) + config = db.session.query(AgentTemplateConfig).filter( + AgentTemplateConfig.agent_template_id == agent_template_id, + AgentTemplateConfig.key == key + ).first() + + if config is not None: + config.value = value + else: + new_config = AgentTemplateConfig( + agent_template_id=agent_template_id, + key=key, + value= value + ) + db.session.add(new_config) + + db.session.commit() + db.session.flush() + @router.post("/save_agent_as_template/{agent_id}") def save_agent_as_template(agent_id: str, diff --git a/tests/unit_tests/controllers/test_agent_template.py b/tests/unit_tests/controllers/test_agent_template.py new file mode 100644 index 000000000..050e9e216 --- /dev/null +++ b/tests/unit_tests/controllers/test_agent_template.py @@ -0,0 +1,127 @@ +from unittest.mock import patch, MagicMock +from superagi.models.agent_template import AgentTemplate +from superagi.models.agent_template_config import AgentTemplateConfig +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +@patch('superagi.controllers.agent_template.db') +@patch('superagi.helper.auth.db') +@patch('superagi.helper.auth.get_user_organisation') +def test_edit_agent_template_success(mock_get_user_org, mock_auth_db, mock_db): + # Create a mock agent template + mock_agent_template = AgentTemplate(id=1, name="Test Agent Template", description="Test Description") + # mock_agent_goals = AgentTemplateConfig() + + # Create a mock edited agent configuration + mock_updated_agent_configs = { + "name": "Updated Agent Template", + "description": "Updated Description", + "agent_configs": { + "goal": ["Create a simple pacman game for me.", "Write all files properly."], + "instruction": ["write spec","write code","improve the code","write test"], + "agent_type": "Don't Maintain Task Queue", + "constraints": ["If you are unsure how you previously did something or want to recall past events, thinking about similar events will help you remember.","Ensure the tool and args are as per current plan and reasoning","Exclusively use the tools listed under \"TOOLS\"","REMEMBER to format your response as JSON, using double quotes (\"\") around keys and string values, and commas (,) to separate items in arrays and objects. IMPORTANTLY, to use a JSON object as a string in another JSON object, you need to escape the double quotes."], + "tools": ["Read Email", "Send Email", "Write File"], + "exit": "No exit criterion", + "iteration_interval": 500, + "model": "gpt-4", + "max_iterations": 25, + "permission_type": "God Mode", + "LTM_DB": "Pinecone" + } + } + + # Mocking the user organisation + mock_get_user_org.return_value = MagicMock(id=1) + + # Create a session mock + session_mock = MagicMock() + mock_db.session = session_mock + mock_db.session.query.return_value.filter.return_value.first.return_value = mock_agent_template + mock_db.session.commit.return_value = None + mock_db.session.add.return_value = None + mock_db.session.flush.return_value = None + + mock_agent_template_config = AgentTemplateConfig(agent_template_id = 1, key="goal", value=["Create a simple pacman game for me.", "Write all files properly."]) + + + # Call the endpoint + response = client.put("agent_templates/update_agent_template/1", json=mock_updated_agent_configs) + + assert response.status_code == 200 + + # Verify changes in the mock agent template + assert mock_agent_template.name == "Updated Agent Template" + assert mock_agent_template.description == "Updated Description" + assert mock_agent_template_config.key == "goal" + assert mock_agent_template_config.value == ["Create a simple pacman game for me.", "Write all files properly."] + + + session_mock.commit.assert_called() + session_mock.flush.assert_called() + + +@patch('superagi.controllers.agent_template.db') +@patch('superagi.helper.auth.db') +@patch('superagi.helper.auth.get_user_organisation') +def test_edit_agent_template_failure(mock_get_user_org, mock_auth_db, mock_db): + # Setup: The user organisation exists, but the agent template does not exist. + mock_get_user_org.return_value = MagicMock(id=1) + + # Create a session mock + session_mock = MagicMock() + mock_db.session = session_mock + mock_db.session.query.return_value.filter.return_value.first.return_value = None + + # Call the endpoint + response = client.put("agent_templates/update_agent_template/1", json={}) + + # Verify: The response status code should be 404, indicating that the agent template was not found. + assert response.status_code == 404 + assert response.json() == {"detail": "Agent Template not found"} + + # Verify: The database commit method should not have been called because the agent template was not found. + session_mock.commit.assert_not_called() + session_mock.flush.assert_not_called() + + +@patch('superagi.controllers.agent_template.db') +@patch('superagi.helper.auth.db') +@patch('superagi.helper.auth.get_user_organisation') +def test_edit_agent_template_with_new_config_success(mock_get_user_org, mock_auth_db, mock_db): + # Create a mock agent template + mock_agent_template = AgentTemplate(id=1, name="Test Agent Template", description="Test Description") + + # Create a mock edited agent configuration + mock_updated_agent_configs = { + "name": "Updated Agent Template", + "description": "Updated Description", + "agent_configs": { + "new_config_key": "New config value" # This is a new config + } + } + + # Mocking the user organisation + mock_get_user_org.return_value = MagicMock(id=1) + + # Create a session mock + session_mock = MagicMock() + mock_db.session = session_mock + mock_db.session.query.return_value.filter.return_value.first.return_value = mock_agent_template + mock_db.session.commit.return_value = None + mock_db.session.add.return_value = None + mock_db.session.flush.return_value = None + + # Call the endpoint + response = client.put("agent_templates/update_agent_template/1", json=mock_updated_agent_configs) + + assert response.status_code == 200 + + # Verify changes in the mock agent template + assert mock_agent_template.name == "Updated Agent Template" + assert mock_agent_template.description == "Updated Description" + + session_mock.commit.assert_called() + session_mock.flush.assert_called() \ No newline at end of file From ded659c3725b095d95237501a289c5dee92578e4 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Thu, 20 Jul 2023 16:34:14 +0530 Subject: [PATCH 15/77] Readme added --- superagi/tools/instagram_tool/README.MD | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 superagi/tools/instagram_tool/README.MD diff --git a/superagi/tools/instagram_tool/README.MD b/superagi/tools/instagram_tool/README.MD new file mode 100644 index 000000000..aa9b79823 --- /dev/null +++ b/superagi/tools/instagram_tool/README.MD @@ -0,0 +1,38 @@ +

+ +

+ +# SuperAGI Instagram Tool + +The SuperAGI Instagram Tool works with the stable diffusion tool, generates an image & caption based on the goals defined by the user and posts it on their instagram business account. + +## ⚙️ Installation + +### 🛠 **Setting Up of SuperAGI** +Set up the SuperAGI by following the instructions given (https://github.com/TransformerOptimus/SuperAGI/blob/main/README.MD) + +If you've put the correct Google API key and Custom Search Engine ID, you'll be able to use the Google Search Tool as well. + +### 🔧 **Instagram tool requirements** + +Since the tool uses the official instagram graph API's to post media on user accounts, There are a few requirements: + +You will need access to the following: + + 1. An Instagram Business Account or Instagram Creator Account + 2. A Facebook Page connected to that account + 3. A Facebook Developer account that can perform Tasks on that Page + 4. A registered Facebook App with Basic settings configured + +Once everything is set up, add the meta user access token (to be generated from facebook developer account), Facebook page ID (can be found on the facebook page connected to the instagram account under 'Page transparency' in 'About' section of the page ) and the stability API key to the correspponding toolkits. + +Follow the steps given in the link to set up meta requirements: (https://developers.facebook.com/docs/instagram-api/getting-started) +Follow the link to generate stability API key: (https://dreamstudio.com/api/) + +### 🔧 **Configuring in SuperAGI Dashboard:** + +-You can add your meta user access token and facebook ID to the Instagram Toolkit Page and stability API key to the Image Generation Toolkit Page + +## Running SuperAGI Instagram Tool + +Once everything has been set up just run/schedule an agent with the goal explaining the media to be published and add instagram tool (which will automatically add stable diffusion tool) \ No newline at end of file From 3708a3054dfc1f32d05f9da751def9215710e874 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Thu, 20 Jul 2023 16:35:45 +0530 Subject: [PATCH 16/77] instabot config folder deleted --- config/blacklist.txt | 0 config/comments.txt | 0 config/followed.txt | 0 config/friends.txt | 0 config/log/instabot_139732300466736.log | 15 --------------- config/log/instabot_139888606519360.log | 15 --------------- config/log/instabot_139900443239328.log | 7 ------- config/log/instabot_140334293153200.log | 2 -- config/log/instabot_140362199539776.log | 7 ------- config/skipped.txt | 0 config/unfollowed.txt | 0 config/whitelist.txt | 0 12 files changed, 46 deletions(-) delete mode 100644 config/blacklist.txt delete mode 100644 config/comments.txt delete mode 100644 config/followed.txt delete mode 100644 config/friends.txt delete mode 100644 config/log/instabot_139732300466736.log delete mode 100644 config/log/instabot_139888606519360.log delete mode 100644 config/log/instabot_139900443239328.log delete mode 100644 config/log/instabot_140334293153200.log delete mode 100644 config/log/instabot_140362199539776.log delete mode 100644 config/skipped.txt delete mode 100644 config/unfollowed.txt delete mode 100644 config/whitelist.txt diff --git a/config/blacklist.txt b/config/blacklist.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/config/comments.txt b/config/comments.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/config/followed.txt b/config/followed.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/config/friends.txt b/config/friends.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/config/log/instabot_139732300466736.log b/config/log/instabot_139732300466736.log deleted file mode 100644 index 0ddea0a14..000000000 --- a/config/log/instabot_139732300466736.log +++ /dev/null @@ -1,15 +0,0 @@ -2023-07-10 04:48:27,462 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started -2023-07-10 04:48:27,462 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py -2023-07-10 04:48:27,470 - instabot version: 0.117.0 (api_login) - INFO - Not yet logged in starting: PRE-LOGIN FLOW! -2023-07-10 04:48:28,122 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: -2023-07-10 04:48:28,122 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' -2023-07-10 04:48:28,122 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! -2023-07-10 04:48:28,123 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 5 minutes. -2023-07-10 04:53:28,667 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: -2023-07-10 04:53:28,668 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' -2023-07-10 04:53:28,668 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! -2023-07-10 04:53:28,669 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 10 minutes. -2023-07-10 05:03:29,119 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: -2023-07-10 05:03:29,119 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' -2023-07-10 05:03:29,119 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! -2023-07-10 05:03:29,120 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 15 minutes. diff --git a/config/log/instabot_139888606519360.log b/config/log/instabot_139888606519360.log deleted file mode 100644 index a636e71af..000000000 --- a/config/log/instabot_139888606519360.log +++ /dev/null @@ -1,15 +0,0 @@ -2023-07-10 05:16:23,290 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started -2023-07-10 05:16:23,291 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py -2023-07-10 05:16:23,295 - instabot version: 0.117.0 (api_login) - INFO - Not yet logged in starting: PRE-LOGIN FLOW! -2023-07-10 05:16:23,704 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: -2023-07-10 05:16:23,704 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' -2023-07-10 05:16:23,704 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! -2023-07-10 05:16:23,705 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 5 minutes. -2023-07-10 05:21:24,139 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: -2023-07-10 05:21:24,139 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' -2023-07-10 05:21:24,139 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! -2023-07-10 05:21:24,140 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 10 minutes. -2023-07-10 05:31:25,010 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: -2023-07-10 05:31:25,011 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' -2023-07-10 05:31:25,011 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! -2023-07-10 05:31:25,011 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 15 minutes. diff --git a/config/log/instabot_139900443239328.log b/config/log/instabot_139900443239328.log deleted file mode 100644 index a11a48fe0..000000000 --- a/config/log/instabot_139900443239328.log +++ /dev/null @@ -1,7 +0,0 @@ -2023-07-07 13:02:07,385 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started -2023-07-07 13:02:07,385 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py -2023-07-07 13:02:07,391 - instabot version: 0.117.0 (api_login) - INFO - Not yet logged in starting: PRE-LOGIN FLOW! -2023-07-07 13:02:07,860 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: -2023-07-07 13:02:07,860 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' -2023-07-07 13:02:07,860 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! -2023-07-07 13:02:07,860 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 5 minutes. diff --git a/config/log/instabot_140334293153200.log b/config/log/instabot_140334293153200.log deleted file mode 100644 index 3aed232ec..000000000 --- a/config/log/instabot_140334293153200.log +++ /dev/null @@ -1,2 +0,0 @@ -2023-07-07 12:52:28,385 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started -2023-07-07 12:52:28,385 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py diff --git a/config/log/instabot_140362199539776.log b/config/log/instabot_140362199539776.log deleted file mode 100644 index eb0b71872..000000000 --- a/config/log/instabot_140362199539776.log +++ /dev/null @@ -1,7 +0,0 @@ -2023-07-07 12:56:51,299 - instabot version: 0.117.0 (bot) - INFO - Instabot version: 0.117.0 Started -2023-07-07 12:56:51,300 - instabot version: 0.117.0 (bot) - DEBUG - Bot imported from /usr/local/lib/python3.9/site-packages/instabot/bot/bot.py -2023-07-07 12:56:51,304 - instabot version: 0.117.0 (api_login) - INFO - Not yet logged in starting: PRE-LOGIN FLOW! -2023-07-07 12:56:51,738 - instabot version: 0.117.0 (api) - DEBUG - POST to endpoint: accounts/get_prefill_candidates/ returned response: -2023-07-07 12:56:51,738 - instabot version: 0.117.0 (api) - DEBUG - Responsecode indicates error; response content: b'{"message":"Please wait a few minutes before you try again.","status":"fail"}' -2023-07-07 12:56:51,738 - instabot version: 0.117.0 (api) - ERROR - Request returns 429 error! -2023-07-07 12:56:51,738 - instabot version: 0.117.0 (api) - WARNING - That means 'too many requests'. I'll go to sleep for 5 minutes. diff --git a/config/skipped.txt b/config/skipped.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/config/unfollowed.txt b/config/unfollowed.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/config/whitelist.txt b/config/whitelist.txt deleted file mode 100644 index e69de29bb..000000000 From cdf34958fe30e77949bf9c1768430ee115c8f19f Mon Sep 17 00:00:00 2001 From: luciferlinx <129729795+luciferlinx101@users.noreply.github.com> Date: Thu, 20 Jul 2023 17:50:37 +0530 Subject: [PATCH 17/77] Read file s3 fix (#823) * Updated * Read from s3 works * Read from s3 works * Minor Updates * Added Unit Tests * Updated Tests * Updated Tests * Refactored * Updated Test * Updated Test * Updated Test --- superagi/helper/resource_helper.py | 20 ++- superagi/helper/s3_helper.py | 27 +++- superagi/resource_manager/file_manager.py | 1 + superagi/tools/file/read_file.py | 16 ++- tests/unit_tests/helper/test_s3_helper.py | 129 ++++++++++++++++++ ...source_manager.py => test_file_manager.py} | 0 6 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 tests/unit_tests/helper/test_s3_helper.py rename tests/unit_tests/resource_manager/{test_resource_manager.py => test_file_manager.py} (100%) diff --git a/superagi/helper/resource_helper.py b/superagi/helper/resource_helper.py index ec606e40e..961cbd998 100644 --- a/superagi/helper/resource_helper.py +++ b/superagi/helper/resource_helper.py @@ -1,10 +1,11 @@ +import os + from superagi.config.config import get_config +from superagi.helper.s3_helper import S3Helper +from superagi.lib.logger import logger from superagi.models.agent import Agent from superagi.models.agent_execution import AgentExecution from superagi.models.resource import Resource -import os -import datetime -from superagi.lib.logger import logger from superagi.types.storage_types import StorageType @@ -142,6 +143,15 @@ def get_agent_write_resource_path(cls, file_name: str, agent: Agent, agent_execu final_path = root_dir + file_name return final_path + @staticmethod + def __check_file_path_exists(path): + return (StorageType.get_storage_type(get_config("STORAGE_TYPE", + StorageType.FILE.value)) is StorageType.S3 and + not S3Helper().check_file_exists_in_s3(path)) or ( + StorageType.get_storage_type( + get_config("STORAGE_TYPE", StorageType.FILE.value)) is StorageType.FILE + and not os.path.exists(path)) + @classmethod def get_agent_read_resource_path(cls, file_name, agent: Agent, agent_execution: AgentExecution): """Get agent resource path to read files i.e. both input and output directory @@ -152,13 +162,13 @@ def get_agent_read_resource_path(cls, file_name, agent: Agent, agent_execution: agent (Agent): The agent corresponding to resource. agent_execution (AgentExecution): The agent execution corresponding to the resource. """ - output_root_dir = ResourceHelper.get_root_output_dir() final_path = ResourceHelper.get_root_input_dir() + file_name if "{agent_id}" in final_path: final_path = ResourceHelper.get_formatted_agent_level_path( agent=agent, path=final_path) - if final_path is None or not os.path.exists(final_path): + output_root_dir = ResourceHelper.get_root_output_dir() + if final_path is None or cls.__check_file_path_exists(final_path): if output_root_dir is not None: final_path = ResourceHelper.get_root_output_dir() + file_name if "{agent_id}" in final_path: diff --git a/superagi/helper/s3_helper.py b/superagi/helper/s3_helper.py index 2dee0a84e..d8aeb2e6c 100644 --- a/superagi/helper/s3_helper.py +++ b/superagi/helper/s3_helper.py @@ -3,18 +3,29 @@ from fastapi import HTTPException from superagi.lib.logger import logger + class S3Helper: def __init__(self): """ Initialize the S3Helper class. Using the AWS credentials from the configuration file, create a boto3 client. """ - self.s3 = boto3.client( + self.s3 = S3Helper.__get_s3_client() + self.bucket_name = get_config("BUCKET_NAME") + + @classmethod + def __get_s3_client(cls): + """ + Get an S3 client. + + Returns: + s3 (S3Helper): The S3Helper object. + """ + return boto3.client( 's3', aws_access_key_id=get_config("AWS_ACCESS_KEY_ID"), aws_secret_access_key=get_config("AWS_SECRET_ACCESS_KEY"), ) - self.bucket_name = get_config("BUCKET_NAME") def upload_file(self, file, path): """ @@ -35,3 +46,15 @@ def upload_file(self, file, path): logger.info("File uploaded to S3 successfully!") except: raise HTTPException(status_code=500, detail="AWS credentials not found. Check your configuration.") + + def check_file_exists_in_s3(self, file_path): + response = self.s3.list_objects_v2(Bucket=get_config("BUCKET_NAME"), Prefix="resources" + file_path) + return 'Contents' in response + + def read_from_s3(self, file_path): + file_path = "resources" + file_path + logger.info(f"Reading file from s3: {file_path}") + response = self.s3.get_object(Bucket=get_config("BUCKET_NAME"), Key=file_path) + if response['ResponseMetadata']['HTTPStatusCode'] == 200: + return response['Body'].read().decode('utf-8') + raise Exception(f"Error read_from_s3: {response}") diff --git a/superagi/resource_manager/file_manager.py b/superagi/resource_manager/file_manager.py index e9059ac20..7379f7585 100644 --- a/superagi/resource_manager/file_manager.py +++ b/superagi/resource_manager/file_manager.py @@ -1,5 +1,6 @@ import csv from sqlalchemy.orm import Session +from superagi.config.config import get_config import os from superagi.helper.resource_helper import ResourceHelper from superagi.helper.s3_helper import S3Helper diff --git a/superagi/tools/file/read_file.py b/superagi/tools/file/read_file.py index b5e92b6b7..0f9d6c730 100644 --- a/superagi/tools/file/read_file.py +++ b/superagi/tools/file/read_file.py @@ -4,10 +4,13 @@ from pydantic import BaseModel, Field from superagi.helper.resource_helper import ResourceHelper +from superagi.helper.s3_helper import S3Helper from superagi.models.agent_execution import AgentExecution from superagi.resource_manager.file_manager import FileManager from superagi.tools.base_tool import BaseTool from superagi.models.agent import Agent +from superagi.types.storage_types import StorageType +from superagi.config.config import get_config class ReadFileSchema(BaseModel): @@ -42,13 +45,16 @@ def _execute(self, file_name: str): The file content and the file name """ final_path = ResourceHelper.get_agent_read_resource_path(file_name, agent=Agent.get_agent_from_id( - session=self.toolkit_config.session, agent_id=self.agent_id), - agent_execution=AgentExecution.get_agent_execution_from_id( - session=self.toolkit_config.session, - agent_execution_id=self.agent_execution_id)) + session=self.toolkit_config.session, agent_id=self.agent_id), agent_execution=AgentExecution + .get_agent_execution_from_id(session=self + .toolkit_config.session, + agent_execution_id=self + .agent_execution_id)) + if StorageType.get_storage_type(get_config("STORAGE_TYPE", StorageType.FILE.value)) == StorageType.S3: + return S3Helper().read_from_s3(final_path) + if final_path is None or not os.path.exists(final_path): raise FileNotFoundError(f"File '{file_name}' not found.") - directory = os.path.dirname(final_path) os.makedirs(directory, exist_ok=True) diff --git a/tests/unit_tests/helper/test_s3_helper.py b/tests/unit_tests/helper/test_s3_helper.py new file mode 100644 index 000000000..5333c9f6a --- /dev/null +++ b/tests/unit_tests/helper/test_s3_helper.py @@ -0,0 +1,129 @@ +from unittest.mock import MagicMock, patch + +import pytest +from botocore.exceptions import BotoCoreError + +from superagi.config.config import get_config +from superagi.helper.s3_helper import S3Helper + + +@pytest.fixture +def mock_boto3_client(): + with patch('superagi.helper.s3_helper.boto3.client') as mock: + yield mock + + +@pytest.fixture +def s3_helper(): + s3_helper = S3Helper() + s3_helper.s3 = MagicMock() + return s3_helper + + +def test_upload_file_success(mock_boto3_client): + # Mock S3 client and file + mock_s3_client = MagicMock() + mock_boto3_client.return_value = mock_s3_client + mock_file = MagicMock() + + # Create an instance of the S3Helper class + s3_helper = S3Helper() + + # Call the method under test + path = "example/file.txt" + s3_helper.upload_file(mock_file, path) + + # Assert the expected behavior + mock_boto3_client.assert_called_with( + 's3', + aws_access_key_id=get_config("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=get_config("AWS_SECRET_ACCESS_KEY") + ) + mock_s3_client.upload_fileobj.assert_called_with(mock_file, get_config("BUCKET_NAME"), path) + + +def test_upload_file_exception(mock_boto3_client): + # Mock S3 client and file + mock_s3_client = MagicMock() + mock_boto3_client.return_value = mock_s3_client + mock_file = MagicMock() + + # Set up the exception scenario + mock_s3_client.upload_fileobj.side_effect = BotoCoreError + + # Create an instance of the S3Helper class + s3_helper = S3Helper() + + # Call the method under test and assert the exception + path = "example/file.txt" + with pytest.raises(Exception): + s3_helper.upload_file(mock_file, path) + + mock_boto3_client.assert_called_with( + 's3', + aws_access_key_id=get_config("AWS_ACCESS_KEY_ID"), + aws_secret_access_key=get_config("AWS_SECRET_ACCESS_KEY") + ) + mock_s3_client.upload_fileobj.assert_called_with(mock_file, get_config("BUCKET_NAME"), path) + + +def test_upload_file(s3_helper): + # Mock file object and call the upload_file method + file_mock = MagicMock() + s3_helper.upload_file(file_mock, "test_path") + + # Assert that the upload_fileobj method was called with the correct arguments + s3_helper.s3.upload_fileobj.assert_called_once_with(file_mock, s3_helper.bucket_name, "test_path") + + +def test_check_file_exists_in_s3(s3_helper): + # Mock the response from S3 + response_mock = { + 'Contents': [{'Key': 'resources/test_file.txt'}] + } + s3_helper.s3.list_objects_v2.return_value = response_mock + + # Call the check_file_exists_in_s3 method + result = s3_helper.check_file_exists_in_s3("/test_file.txt") + + # Assert that the method returns True when the file exists + assert result is True + + +def test_check_file_does_not_exist_in_s3(s3_helper): + # Mock the response from S3 where the file does not exist + response_mock = {} + s3_helper.s3.list_objects_v2.return_value = response_mock + + # Call the check_file_exists_in_s3 method + result = s3_helper.check_file_exists_in_s3("/non_existing_file.txt") + + # Assert that the method returns False when the file does not exist + assert result is False + + +def test_read_from_s3(s3_helper): + # Mock the response from S3 + response_mock = { + 'ResponseMetadata': {'HTTPStatusCode': 200}, + 'Body': MagicMock(read=MagicMock(return_value=b'File content')), # Use bytes for the mock response + } + s3_helper.s3.get_object.return_value = response_mock + + # Call the read_from_s3 method + result = s3_helper.read_from_s3("/test_file.txt") + + # Assert that the method returns the correct file content + assert result == 'File content' + + +def test_read_from_s3_failure(s3_helper): + # Mock the response from S3 where reading fails + response_mock = { + 'ResponseMetadata': {'HTTPStatusCode': 500}, + } + s3_helper.s3.get_object.return_value = response_mock + + # Call the read_from_s3 method and expect it to raise an Exception + with pytest.raises(Exception): + s3_helper.read_from_s3("/test_file.txt") diff --git a/tests/unit_tests/resource_manager/test_resource_manager.py b/tests/unit_tests/resource_manager/test_file_manager.py similarity index 100% rename from tests/unit_tests/resource_manager/test_resource_manager.py rename to tests/unit_tests/resource_manager/test_file_manager.py From 0bb600055a7e8d66f1934f8960c96fd9d8ca2329 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Thu, 20 Jul 2023 18:49:59 +0530 Subject: [PATCH 18/77] refactored code --- superagi/tools/instagram_tool/instagram.py | 78 ++++++++++++++-------- 1 file changed, 50 insertions(+), 28 deletions(-) diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index 097b5c3ab..01606a87c 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -50,7 +50,6 @@ def _execute(self, photo_description: str) -> str: Returns: Image posted successfully message if image has been posted on instagram or error message. """ - #get meta user access token and facebook page ID meta_user_access_token = self.get_tool_config("META_USER_ACCESS_TOKEN") facebook_page_id=self.get_tool_config("FACEBOOK_PAGE_ID") @@ -59,53 +58,35 @@ def _execute(self, photo_description: str) -> str: if facebook_page_id is None: return "Error: Missing facebook page id." - #create caption for the instagram caption=self.create_caption(photo_description) #get request for fetching the instagram_business_account_id - response=requests.get( - f"https://graph.facebook.com/v17.0/{facebook_page_id}?fields=instagram_business_account&access_token={meta_user_access_token}" - ) + root_api_url="https://graph.facebook.com/v17.0/" + response=self.get_req_insta_id(root_api_url,facebook_page_id,meta_user_access_token) + if response.status_code != 200: return f"Non-200 response: {str(response.text)}" data = response.json() insta_business_account_id=data["instagram_business_account"]["id"] - #creating an s3 client - s3 = self.create_s3_client() - - #getting the file path of the image generated by image generation tool - file_path=self.get_file_path_from_image_generation_tool() - - #fetching the image from the s3 using the file_path - content = self.get_image_from_s3(s3,file_path) - - #storing the image in a public bucket and getting the image url - image_url = self.get_img_public_url(s3,file_path,content) - #encoding the caption with possible emojis and hashtags and removing the starting and ending double quotes - encoded_caption=self.create_caption(photo_description) - + image_url,encoded_caption=self.get_img_url_and_encoded_caption(photo_description) #post request for getting the media container ID - response = requests.post( - f"https://graph.facebook.com/v17.0/{insta_business_account_id}/media?image_url={image_url}&caption={encoded_caption}&access_token={meta_user_access_token}" - ) + response=self.post_media_container_id(root_api_url,insta_business_account_id,image_url,encoded_caption,meta_user_access_token) + if response.status_code != 200: return f"Non-200 response: {str(response.text)}" data = response.json() container_ID=data["id"] - #post request to post the media container on instagram account - response = requests.post( - f"https://graph.facebook.com/v17.0/{insta_business_account_id}/media_publish?creation_id={container_ID}&access_token={meta_user_access_token}" - ) + response=self.post_media(root_api_url,insta_business_account_id,container_ID,meta_user_access_token) if response.status_code != 200: return f"Non-200 response: {str(response.text)}" return "Photo posted successfully!" - + def create_caption(self, photo_description: str) -> str: """ Create a caption for the instagram post based on the photo description @@ -200,4 +181,45 @@ def get_img_public_url(self,s3,file_path,content): s3.put_object(Bucket=bucket_name, Key=object_key, Body=content) image_url = f"https://{bucket_name}.s3.amazonaws.com/{object_key}" - return image_url \ No newline at end of file + return image_url + + def get_img_url_and_encoded_caption(self,photo_description): + #creating an s3 client + s3 = self.create_s3_client() + + #getting the file path of the image generated by image generation tool + file_path=self.get_file_path_from_image_generation_tool() + + #fetching the image from the s3 using the file_path + content = self.get_image_from_s3(s3,file_path) + + #storing the image in a public bucket and getting the image url + image_url = self.get_img_public_url(s3,file_path,content) + #encoding the caption with possible emojis and hashtags and removing the starting and ending double quotes + encoded_caption=self.create_caption(photo_description) + + return image_url,encoded_caption + + def get_req_insta_id(self,root_api_url,facebook_page_id,meta_user_access_token): + url_to_get_acc_id=f"{root_api_url}{facebook_page_id}?fields=instagram_business_account&access_token={meta_user_access_token}" + response=requests.get( + url_to_get_acc_id + ) + + return response + + def post_media_container_id(self,root_api_url,insta_business_account_id,image_url,encoded_caption,meta_user_access_token): + url_to_create_media_container=f"{root_api_url}{insta_business_account_id}/media?image_url={image_url}&caption={encoded_caption}&access_token={meta_user_access_token}" + response = requests.post( + url_to_create_media_container + ) + + return response + + def post_media(self,root_api_url,insta_business_account_id,container_ID,meta_user_access_token): + url_to_post_media_container=f"{root_api_url}{insta_business_account_id}/media_publish?creation_id={container_ID}&access_token={meta_user_access_token}" + response = requests.post( + url_to_post_media_container + ) + + return response From 1218fd681ebe0ef76f16f7a2744a8b10d25f93b2 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Fri, 21 Jul 2023 11:03:29 +0530 Subject: [PATCH 19/77] Handled the case where stable diffusion generated multiple photos --- .../stable_diffusion_image_gen.py | 4 ++-- superagi/tools/instagram_tool/README.MD | 6 +++++- superagi/tools/instagram_tool/instagram.py | 16 ++++++++-------- .../tools/instagram_tool/instagram_toolkit.py | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/superagi/tools/image_generation/stable_diffusion_image_gen.py b/superagi/tools/image_generation/stable_diffusion_image_gen.py index 7f4912b49..b329b3ebf 100644 --- a/superagi/tools/image_generation/stable_diffusion_image_gen.py +++ b/superagi/tools/image_generation/stable_diffusion_image_gen.py @@ -14,7 +14,7 @@ class StableDiffusionImageGenInput(BaseModel): prompt: str = Field(..., description="Prompt for Image Generation to be used by Stable Diffusion. The prompt should be as descriptive as possible and mention all the details of the image to be generated") height: int = Field(..., description="Height of the image to be Generated. default height is 512") width: int = Field(..., description="Width of the image to be Generated. default width is 512") - num: int = Field(..., description="Number of Images to be generated. default num is 1") + num: int = Field(..., description="Number of Images to be generated. default num is 2") steps: int = Field(..., description="Number of diffusion steps to run. default steps are 50") image_names: list = Field(..., description="Image Names for the generated images, example 'image_1.png'. Only include the image name. Don't include path.") @@ -40,7 +40,7 @@ class StableDiffusionImageGenTool(BaseTool): class Config: arbitrary_types_allowed = True - def _execute(self, prompt: str, image_names: list, width: int = 512, height: int = 512, num: int = 1, + def _execute(self, prompt: str, image_names: list, width: int = 512, height: int = 512, num: int = 2, steps: int = 50): api_key = self.get_tool_config("STABILITY_API_KEY") diff --git a/superagi/tools/instagram_tool/README.MD b/superagi/tools/instagram_tool/README.MD index aa9b79823..9d6f0f50d 100644 --- a/superagi/tools/instagram_tool/README.MD +++ b/superagi/tools/instagram_tool/README.MD @@ -35,4 +35,8 @@ Follow the link to generate stability API key: (https://dreamstudio.com/api/) ## Running SuperAGI Instagram Tool -Once everything has been set up just run/schedule an agent with the goal explaining the media to be published and add instagram tool (which will automatically add stable diffusion tool) \ No newline at end of file +Once everything has been set up just run/schedule an agent with the goal explaining the media to be published and add instagram tool (which will automatically add stable diffusion tool) + +## Warning + +It is advised to run the instagram tool in restricted mode since it allows you to validate the photos generated. You can schedule agent runs (recurring runs are supported as well). \ No newline at end of file diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index 01606a87c..e744f0c20 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -32,7 +32,7 @@ class InstagramTool(BaseTool): llm: Optional[BaseLlm] = None name = "Instagram tool" description = ( - "A tool performing posting AI generated photos on Instagram" + "A tool for posting an AI generated photo on Instagram" ) args_schema: Type[InstagramSchema] = InstagramSchema tool_response_manager: Optional[ToolResponseQueryManager] = None @@ -70,8 +70,11 @@ def _execute(self, photo_description: str) -> str: data = response.json() insta_business_account_id=data["instagram_business_account"]["id"] - - image_url,encoded_caption=self.get_img_url_and_encoded_caption(photo_description) + file_path=self.get_file_path_from_image_generation_tool() + if(file_path=="resources"): + return "A photo has already been posted on your instagram account" + + image_url,encoded_caption=self.get_img_url_and_encoded_caption(photo_description,file_path) #post request for getting the media container ID response=self.post_media_container_id(root_api_url,insta_business_account_id,image_url,encoded_caption,meta_user_access_token) @@ -84,7 +87,6 @@ def _execute(self, photo_description: str) -> str: response=self.post_media(root_api_url,insta_business_account_id,container_ID,meta_user_access_token) if response.status_code != 200: return f"Non-200 response: {str(response.text)}" - return "Photo posted successfully!" def create_caption(self, photo_description: str) -> str: @@ -139,6 +141,7 @@ def get_file_path_from_image_generation_tool(self): last_tool_response = self.tool_response_manager.get_last_response() file_path="resources"+last_tool_response.partition("['")[2].partition("']")[0] + if ',' in file_path: # Split the string based on the comma and get the first element (substring before the comma) file_path = file_path.split(',')[0].strip() @@ -183,13 +186,10 @@ def get_img_public_url(self,s3,file_path,content): image_url = f"https://{bucket_name}.s3.amazonaws.com/{object_key}" return image_url - def get_img_url_and_encoded_caption(self,photo_description): + def get_img_url_and_encoded_caption(self,photo_description,file_path): #creating an s3 client s3 = self.create_s3_client() - #getting the file path of the image generated by image generation tool - file_path=self.get_file_path_from_image_generation_tool() - #fetching the image from the s3 using the file_path content = self.get_image_from_s3(s3,file_path) diff --git a/superagi/tools/instagram_tool/instagram_toolkit.py b/superagi/tools/instagram_tool/instagram_toolkit.py index abfedaabb..513699a6d 100644 --- a/superagi/tools/instagram_tool/instagram_toolkit.py +++ b/superagi/tools/instagram_tool/instagram_toolkit.py @@ -6,7 +6,7 @@ class InstagramToolkit(BaseToolkit, ABC): name: str = "Instagram Toolkit" - description: str = "Toolkit containing tools for posting AI generated photos on Instagram" + description: str = "Toolkit containing tools for posting AI generated photo on Instagram. Posts only one photo in a run " def get_tools(self) -> List[BaseTool]: return [InstagramTool(),StableDiffusionImageGenTool()] From 37bab8dab9e928da6399a11bec831c6e8c213f14 Mon Sep 17 00:00:00 2001 From: Kalki Date: Fri, 21 Jul 2023 11:22:42 +0530 Subject: [PATCH 20/77] ui bug bash fixes --- gui/pages/Content/APM/ApmDashboard.js | 42 +++++++-------------------- gui/pages/_app.css | 9 ++++-- gui/utils/utils.js | 2 +- superagi/apm/tools_handler.py | 17 +++++++++-- 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/gui/pages/Content/APM/ApmDashboard.js b/gui/pages/Content/APM/ApmDashboard.js index 510c2ee43..5c0bd8dde 100644 --- a/gui/pages/Content/APM/ApmDashboard.js +++ b/gui/pages/Content/APM/ApmDashboard.js @@ -3,7 +3,7 @@ import Image from "next/image"; import style from "./Apm.module.css"; import 'react-toastify/dist/ReactToastify.css'; import {getActiveRuns, getAgentRuns, getAllAgents, getToolsUsage, getMetrics} from "@/pages/api/DashboardService"; -import {formatNumber, formatTime} from "@/utils/utils"; +import {formatNumber, formatTime, returnToolkitIcon} from "@/utils/utils"; import {BarGraph} from "./BarGraph.js"; import {WidthProvider, Responsive} from 'react-grid-layout'; import 'react-grid-layout/css/styles.css'; @@ -221,7 +221,10 @@ export default function ApmDashboard() { {toolsUsed.map((tool, index) => ( - {tool.tool_name} + + tool-icon + {tool.tool_name} + {tool.unique_agents} {tool.total_usage} @@ -245,37 +248,14 @@ export default function ApmDashboard() { Agent Name - Model arrow_down - - Tokens Consumed arrow_down - - Runs arrow_down - + Model arrow_down + Tokens Consumed arrow_down + Runs arrow_down Avg tokens per run arrow_down - Tools arrow_down - - Calls arrow_down - - Avg Run Time arrow_down - + Tools arrow_down + Calls arrow_down + Avg Run Time arrow_down diff --git a/gui/pages/_app.css b/gui/pages/_app.css index 7cb54b15c..e02ca3fb5 100644 --- a/gui/pages/_app.css +++ b/gui/pages/_app.css @@ -399,7 +399,7 @@ input[type="range"]::-moz-range-track { align-self: stretch; margin-top: 2px; max-height: 200px; - overflow-y: scroll; + overflow-y: auto; box-shadow: 0 2px 7px rgba(0,0,0,.4), 0 0 2px rgba(0,0,0,.22); } @@ -983,6 +983,11 @@ tr{ } .tools_used_tooltip{ - position: relative; + position: absolute; cursor: pointer; +} + +.image_class{ + background: #FFFFFF80; + border-radius: 20px; } \ No newline at end of file diff --git a/gui/utils/utils.js b/gui/utils/utils.js index 2375d56cc..d04d959cc 100644 --- a/gui/utils/utils.js +++ b/gui/utils/utils.js @@ -345,7 +345,7 @@ export const returnToolkitIcon = (toolkitName) => { ]; const toolkit = toolkitData.find((tool) => tool.name === toolkitName); - return toolkit ? toolkit.imageSrc : '/images/custom_tool.svg'; + return toolkit ? toolkit.imageSrc : '/images/app-logo-light.png'; } export const returnResourceIcon = (file) => { diff --git a/superagi/apm/tools_handler.py b/superagi/apm/tools_handler.py index 71b0937ce..72048d529 100644 --- a/superagi/apm/tools_handler.py +++ b/superagi/apm/tools_handler.py @@ -4,14 +4,22 @@ from sqlalchemy.orm import Session from superagi.models.events import Event +from superagi.models.tool import Tool +from superagi.models.toolkit import Toolkit class ToolsHandler: - def __init__(self, session: Session, organisation_id: int): self.session = session self.organisation_id = organisation_id + def get_tool_and_toolkit(self): + tools_and_toolkits = self.session.query( + Tool.name.label('tool_name'), Toolkit.name.label('toolkit_name')).join( + Toolkit, Tool.toolkit_id == Toolkit.id).all() + + return {item.tool_name: item.toolkit_name for item in tools_and_toolkits} + def calculate_tool_usage(self) -> List[Dict[str, int]]: tool_usage = [] tool_used_subquery = self.session.query( @@ -32,15 +40,18 @@ def calculate_tool_usage(self) -> List[Dict[str, int]]: query = self.session.query( agent_count.c.tool_name, agent_count.c.unique_agents, - total_usage.c.total_usage + total_usage.c.total_usage, ).join(total_usage, total_usage.c.tool_name == agent_count.c.tool_name) + tool_and_toolkit = self.get_tool_and_toolkit() + result = query.all() tool_usage = [{ 'tool_name': row.tool_name, 'unique_agents': row.unique_agents, - 'total_usage': row.total_usage + 'total_usage': row.total_usage, + 'toolkit': tool_and_toolkit.get(row.tool_name, None) } for row in result] return tool_usage \ No newline at end of file From 2607db35426b5b00ad25c03a3447af9a447354a9 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Fri, 21 Jul 2023 11:24:44 +0530 Subject: [PATCH 21/77] docker compose.yaml version reverted to original --- docker-compose.yaml | 2 +- superagi/tools/instagram_tool/instagram.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 30b6795a7..6dabb981d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,4 @@ -version: '3.3' +version: '3.8' services: backend: volumes: diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index e744f0c20..a16d2793c 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -71,6 +71,7 @@ def _execute(self, photo_description: str) -> str: data = response.json() insta_business_account_id=data["instagram_business_account"]["id"] file_path=self.get_file_path_from_image_generation_tool() + #handling case where image generation generates multiple images if(file_path=="resources"): return "A photo has already been posted on your instagram account" From 9f6c0456d1668b5caf2c977481c1bcb58046ecab Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Fri, 21 Jul 2023 11:50:48 +0530 Subject: [PATCH 22/77] removed the utility to add stable diffusion automatically --- superagi/tools/instagram_tool/instagram_toolkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superagi/tools/instagram_tool/instagram_toolkit.py b/superagi/tools/instagram_tool/instagram_toolkit.py index 513699a6d..506211ead 100644 --- a/superagi/tools/instagram_tool/instagram_toolkit.py +++ b/superagi/tools/instagram_tool/instagram_toolkit.py @@ -9,7 +9,7 @@ class InstagramToolkit(BaseToolkit, ABC): description: str = "Toolkit containing tools for posting AI generated photo on Instagram. Posts only one photo in a run " def get_tools(self) -> List[BaseTool]: - return [InstagramTool(),StableDiffusionImageGenTool()] + return [InstagramTool()] def get_env_keys(self) -> List[str]: return [ From e2dbae1f732a6ef1b9c233aea22c860dd74bafa3 Mon Sep 17 00:00:00 2001 From: Kalki Date: Fri, 21 Jul 2023 12:16:48 +0530 Subject: [PATCH 23/77] ui bug bash fixes --- gui/utils/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/utils/utils.js b/gui/utils/utils.js index d04d959cc..2375d56cc 100644 --- a/gui/utils/utils.js +++ b/gui/utils/utils.js @@ -345,7 +345,7 @@ export const returnToolkitIcon = (toolkitName) => { ]; const toolkit = toolkitData.find((tool) => tool.name === toolkitName); - return toolkit ? toolkit.imageSrc : '/images/app-logo-light.png'; + return toolkit ? toolkit.imageSrc : '/images/custom_tool.svg'; } export const returnResourceIcon = (file) => { From 873a04922b792cbf32008f911164018ce6b93530 Mon Sep 17 00:00:00 2001 From: Kalki Date: Fri, 21 Jul 2023 12:22:38 +0530 Subject: [PATCH 24/77] ui bug bash fixes --- gui/utils/utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/utils/utils.js b/gui/utils/utils.js index 2375d56cc..2cb43a846 100644 --- a/gui/utils/utils.js +++ b/gui/utils/utils.js @@ -340,6 +340,7 @@ export const returnToolkitIcon = (toolkitName) => { {name: 'Google SERP Toolkit', imageSrc: '/images/google_serp_icon.svg'}, {name: 'File Toolkit', imageSrc: '/images/filemanager_icon.svg'}, {name: 'CodingToolkit', imageSrc: '/images/app-logo-light.png'}, + {name: 'Thinking Toolkit', imageSrc: '/images/app-logo-light.png'}, {name: 'Image Generation Toolkit', imageSrc: '/images/app-logo-light.png'}, {name: 'DuckDuckGo Search Toolkit', imageSrc: '/images/duckduckgo_icon.png'}, ]; From 167c4bf2b819913887c746a9aa2c3c7e0ea70768 Mon Sep 17 00:00:00 2001 From: Anisha Gupta <60440541+anisha1607@users.noreply.github.com> Date: Fri, 21 Jul 2023 13:33:55 +0530 Subject: [PATCH 25/77] Edit agent templates fix (#838) --- gui/utils/utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/utils/utils.js b/gui/utils/utils.js index 2375d56cc..00affe4d5 100644 --- a/gui/utils/utils.js +++ b/gui/utils/utils.js @@ -257,6 +257,8 @@ const removeAgentInternalId = (internalId) => { localStorage.removeItem("agent_time_unit_" + String(internalId)); localStorage.removeItem("agent_time_value_" + String(internalId)); localStorage.removeItem("agent_is_recurring_" + String(internalId)); + localStorage.removeItem("is_agent_template_" + String(internalId)); + localStorage.removeItem("agent_template_id_" + String(internalId)); } } From c76c453469eca0527c2af31f634a05a062bffd62 Mon Sep 17 00:00:00 2001 From: Kalki Date: Fri, 21 Jul 2023 14:01:47 +0530 Subject: [PATCH 26/77] ui bug bash fixes --- gui/pages/Content/Agents/ResourceManager.js | 37 +++++++++++---------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/gui/pages/Content/Agents/ResourceManager.js b/gui/pages/Content/Agents/ResourceManager.js index f15e4e4c2..d710dca49 100644 --- a/gui/pages/Content/Agents/ResourceManager.js +++ b/gui/pages/Content/Agents/ResourceManager.js @@ -14,19 +14,28 @@ export default function ResourceManager({agentId, runs}) { const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); - const handleFileInputChange = (event) => { - const files = event.target.files; + function handleFile(files) { if (files.length > 0) { - const fileData = { - "file": files[0], - "name": files[0].name, - "size": files[0].size, - "type": files[0].type, - }; - uploadResource(fileData); + const sizeInMB = files[0].size / (1024*1024); + if (sizeInMB > 5) { + toast.error('File size should not exceed 5MB', {autoClose: 1800}); + } else { + const fileData = { + "file": files[0], + "name": files[0].name, + "size": files[0].size, + "type": files[0].type, + }; + uploadResource(fileData); + } } }; + const handleFileInputChange = (event) => { + const files = event.target.files; + handleFile(files); + }; + const handleDropAreaClick = () => { fileInputRef.current.click(); }; @@ -48,15 +57,7 @@ export default function ResourceManager({agentId, runs}) { event.preventDefault(); setIsDragging(false); const files = event.dataTransfer.files; - if (files.length > 0) { - const fileData = { - "file": files[0], - "name": files[0].name, - "size": files[0].size, - "type": files[0].type, - }; - uploadResource(fileData); - } + handleFile(files); }; useEffect(() => { From b6587bf5c8cecb13dfefa5251ec3a3d53450b5ed Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Tue, 25 Jul 2023 11:35:39 +0530 Subject: [PATCH 27/77] added test cases and modified readme --- superagi/tools/instagram_tool/README.MD | 2 +- superagi/tools/instagram_tool/instagram.py | 2 +- .../tools/instagram_tool/instagram_toolkit.py | 1 - .../test_stable_diffusion_image_gen.py | 3 +- .../tools/instagram_tool/__init__.py | 0 .../instagram_tool/test_instagram_tool.py | 97 +++++++++++++++++++ .../instagram_tool/test_instagram_toolkit.py | 40 ++++++++ 7 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 tests/unit_tests/tools/instagram_tool/__init__.py create mode 100644 tests/unit_tests/tools/instagram_tool/test_instagram_tool.py create mode 100644 tests/unit_tests/tools/instagram_tool/test_instagram_toolkit.py diff --git a/superagi/tools/instagram_tool/README.MD b/superagi/tools/instagram_tool/README.MD index 9d6f0f50d..fcaa034ac 100644 --- a/superagi/tools/instagram_tool/README.MD +++ b/superagi/tools/instagram_tool/README.MD @@ -39,4 +39,4 @@ Once everything has been set up just run/schedule an agent with the goal explain ## Warning -It is advised to run the instagram tool in restricted mode since it allows you to validate the photos generated. You can schedule agent runs (recurring runs are supported as well). \ No newline at end of file +It is advised to run the instagram tool in restricted mode since it allows you to validate the photos generated. You can schedule agent runs (recurring runs are supported as well). Also, only one photo will be posted to your account in a run. To post multiple photos use recurring runs. \ No newline at end of file diff --git a/superagi/tools/instagram_tool/instagram.py b/superagi/tools/instagram_tool/instagram.py index a16d2793c..d6a464a9a 100644 --- a/superagi/tools/instagram_tool/instagram.py +++ b/superagi/tools/instagram_tool/instagram.py @@ -73,7 +73,7 @@ def _execute(self, photo_description: str) -> str: file_path=self.get_file_path_from_image_generation_tool() #handling case where image generation generates multiple images if(file_path=="resources"): - return "A photo has already been posted on your instagram account" + return "A photo has already been posted on your instagram account. To post multiple photos use recurring runs." image_url,encoded_caption=self.get_img_url_and_encoded_caption(photo_description,file_path) #post request for getting the media container ID diff --git a/superagi/tools/instagram_tool/instagram_toolkit.py b/superagi/tools/instagram_tool/instagram_toolkit.py index 506211ead..a0b66cefb 100644 --- a/superagi/tools/instagram_tool/instagram_toolkit.py +++ b/superagi/tools/instagram_tool/instagram_toolkit.py @@ -2,7 +2,6 @@ from typing import List from superagi.tools.base_tool import BaseTool, BaseToolkit from superagi.tools.instagram_tool.instagram import InstagramTool -from superagi.tools.image_generation.stable_diffusion_image_gen import StableDiffusionImageGenTool class InstagramToolkit(BaseToolkit, ABC): name: str = "Instagram Toolkit" diff --git a/tests/unit_tests/tools/image_generation/test_stable_diffusion_image_gen.py b/tests/unit_tests/tools/image_generation/test_stable_diffusion_image_gen.py index aa48b70b2..dafd60b27 100644 --- a/tests/unit_tests/tools/image_generation/test_stable_diffusion_image_gen.py +++ b/tests/unit_tests/tools/image_generation/test_stable_diffusion_image_gen.py @@ -48,8 +48,7 @@ def test_execute(stable_diffusion_tool): result = tool._execute('prompt', ['img1.png', 'img2.png']) - - assert result == 'Images downloaded and saved successfully' + assert result.startswith('Images downloaded and saved successfully') tool.resource_manager.write_binary_file.assert_called() def test_call_stable_diffusion(stable_diffusion_tool): diff --git a/tests/unit_tests/tools/instagram_tool/__init__.py b/tests/unit_tests/tools/instagram_tool/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/tools/instagram_tool/test_instagram_tool.py b/tests/unit_tests/tools/instagram_tool/test_instagram_tool.py new file mode 100644 index 000000000..cfeeacfc9 --- /dev/null +++ b/tests/unit_tests/tools/instagram_tool/test_instagram_tool.py @@ -0,0 +1,97 @@ +import pytest +from superagi.tools.instagram_tool.instagram import InstagramTool +from unittest.mock import MagicMock, patch + +# Create a fixture for the InstagramTool instance +@pytest.fixture +def instagram_tool(): + return InstagramTool() + +def test_execute_missing_meta_user_access_token(instagram_tool): + # Test for the case when META_USER_ACCESS_TOKEN is missing + + # Mock the get_tool_config method to return None for META_USER_ACCESS_TOKEN + instagram_tool.toolkit_config.get_tool_config = MagicMock(return_value=None) + + # Call the _execute method + result = instagram_tool._execute("A beautiful sunset") + + # Verify the output + assert result == "Error: Missing meta user access token." + +def test_execute_missing_facebook_page_id(instagram_tool): + # Test for the case when FACEBOOK_PAGE_ID is missing + + # Mock the get_tool_config method to return None for FACEBOOK_PAGE_ID + instagram_tool.toolkit_config.get_tool_config = MagicMock(side_effect=lambda key: "your_meta_user_access_token" if key == "META_USER_ACCESS_TOKEN" else None) + + # Call the _execute method + result = instagram_tool._execute("A beautiful sunset") + + # Verify the output + assert result == "Error: Missing facebook page id." + +def test_get_file_path_from_image_generation_tool(instagram_tool): + # Test for the get_file_path_from_image_generation_tool method + # Mock the tool_response_manager to return a response + instagram_tool.tool_response_manager = MagicMock() + instagram_tool.tool_response_manager.get_last_response.return_value = "['/path/to/image.jpg']" + file_path = instagram_tool.get_file_path_from_image_generation_tool() + assert file_path == "resources/path/to/image.jpg" + + +def test_get_img_public_url(instagram_tool): + # Test for the get_img_public_url method + # Mock the S3 client and its put_object method + s3_client_mock = MagicMock() + s3_client_mock.get_object.return_value = {"Body": MagicMock(read=lambda: b"image_content")} + with patch.object(InstagramTool, 'create_s3_client', return_value=s3_client_mock): + file_path = "path/to/image.jpg" + content = b"image_content" + image_url = instagram_tool.get_img_public_url(s3_client_mock, file_path, content) + assert image_url.startswith("https://") + assert file_path.split("/")[-1] in image_url + +def test_get_req_insta_id(instagram_tool): + # Test for the get_req_insta_id method + # Mock the requests.get method + response_mock = MagicMock() + response_mock.status_code = 200 + response_mock.json.return_value = {"instagram_business_account": {"id": "account_id"}} + with patch("requests.get", return_value=response_mock): + root_api_url = "https://graph.facebook.com/v17.0/" + facebook_page_id = "page_id" + meta_user_access_token = "access_token" + response = instagram_tool.get_req_insta_id(root_api_url, facebook_page_id, meta_user_access_token) + assert response.status_code == 200 + assert response.json()["instagram_business_account"]["id"] == "account_id" + +def test_post_media_container_id(instagram_tool): + # Test for the post_media_container_id method + # Mock the requests.post method + response_mock = MagicMock() + response_mock.status_code = 200 + response_mock.json.return_value = {"id": "container_id"} + with patch("requests.post", return_value=response_mock): + root_api_url = "https://graph.facebook.com/v17.0/" + insta_business_account_id = "account_id" + image_url = "https://example.com/image.jpg" + encoded_caption = "encoded_caption" + meta_user_access_token = "access_token" + response = instagram_tool.post_media_container_id(root_api_url, insta_business_account_id, image_url, encoded_caption, meta_user_access_token) + assert response.status_code == 200 + assert response.json()["id"] == "container_id" + +def test_post_media(instagram_tool): + # Test for the post_media method + # Mock the requests.post method + response_mock = MagicMock() + response_mock.status_code = 200 + with patch("requests.post", return_value=response_mock): + root_api_url = "https://graph.facebook.com/v17.0/" + insta_business_account_id = "account_id" + container_ID = "container_id" + meta_user_access_token = "access_token" + response = instagram_tool.post_media(root_api_url, insta_business_account_id, container_ID, meta_user_access_token) + assert response.status_code == 200 + diff --git a/tests/unit_tests/tools/instagram_tool/test_instagram_toolkit.py b/tests/unit_tests/tools/instagram_tool/test_instagram_toolkit.py new file mode 100644 index 000000000..18f73f32c --- /dev/null +++ b/tests/unit_tests/tools/instagram_tool/test_instagram_toolkit.py @@ -0,0 +1,40 @@ +import pytest +from superagi.tools.instagram_tool.instagram import InstagramTool +from superagi.tools.instagram_tool.instagram_toolkit import InstagramToolkit + +class TestInstagramToolKit: + def setup_method(self): + """ + Set up the test fixture. + + This method is called before each test method is executed to prepare the test environment. + + Returns: + None + """ + self.toolkit = InstagramToolkit() + + def test_get_tools(self): + """ + Test the `get_tools` method of the `DuckDuckGoToolkit` class. + + It should return a list of tools, containing one instance of `DuckDuckGoSearchTool`. + + Returns: + None + """ + tools = self.toolkit.get_tools() + assert len(tools) == 1 + assert isinstance(tools[0], InstagramTool) + + def test_get_env_keys(self): + """ + Test the `get_env_keys` method of the `DuckDuckGoToolkit` class. + + It should return an empty list of environment keys. + + Returns: + None + """ + env_keys = self.toolkit.get_env_keys() + assert len(env_keys) == 2 From 6595b1a5692be280f2212c71078c46016ff71b0c Mon Sep 17 00:00:00 2001 From: Nishant Borthakur <101320057+nborthy@users.noreply.github.com> Date: Tue, 25 Jul 2023 13:53:50 +0530 Subject: [PATCH 28/77] Frontend bugs (#849) * asyn function in content.js, utils refactored * minor fixes * prevent default centralisation * dropdown issue fix --------- Co-authored-by: NishantBorthakur --- gui/pages/Content/Agents/AgentCreate.js | 28 ++++-- gui/pages/Content/Agents/AgentSchedule.js | 6 +- gui/pages/Content/Agents/AgentWorkspace.js | 38 ++++---- gui/pages/Dashboard/Content.js | 102 +++++++++++++-------- gui/pages/Dashboard/SideBar.js | 6 +- gui/utils/utils.js | 102 +++++++++++---------- 6 files changed, 158 insertions(+), 124 deletions(-) diff --git a/gui/pages/Content/Agents/AgentCreate.js b/gui/pages/Content/Agents/AgentCreate.js index 0de46038a..1220751eb 100644 --- a/gui/pages/Content/Agents/AgentCreate.js +++ b/gui/pages/Content/Agents/AgentCreate.js @@ -16,13 +16,21 @@ import { openNewTab, removeTab, setLocalStorageValue, - setLocalStorageArray, returnResourceIcon, getUserTimezone, createInternalId, + setLocalStorageArray, returnResourceIcon, getUserTimezone, createInternalId, preventDefault } from "@/utils/utils"; import {EventBus} from "@/utils/eventBus"; import 'moment-timezone'; import AgentSchedule from "@/pages/Content/Agents/AgentSchedule"; -export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents,toolkits,organisationId,template,internalId}) { +export default function AgentCreate({ + sendAgentData, + selectedProjectId, + fetchAgents, + toolkits, + organisationId, + template, + internalId + }) { const [advancedOptions, setAdvancedOptions] = useState(false); const [agentName, setAgentName] = useState(""); @@ -319,13 +327,11 @@ export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents const handleDescriptionChange = (event) => { setLocalStorageValue("agent_description_" + String(internalId), event.target.value, setAgentDescription); }; + const closeCreateModal = () => { setCreateModal(false); setCreateDropdown(false); }; - const preventDefault = (e) => { - e.stopPropagation(); - }; function uploadResource(agentId, fileData) { const formData = new FormData(); @@ -367,7 +373,7 @@ export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents openNewTab(-3, "Settings", "Settings", false); return false; } - + if (agentName?.replace(/\s/g, '') === '') { toast.error("Agent name can't be blank", {autoClose: 1800}); return false; @@ -630,8 +636,8 @@ export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents setAgentName(agent_name); } - const agent_template_id = localStorage.getItem("agent_template_id_"+ String(internalId)); - if(agent_template_id){ + const agent_template_id = localStorage.getItem("agent_template_id_" + String(internalId)); + if (agent_template_id) { setAgentTemplateId(agent_template_id) } @@ -1038,8 +1044,10 @@ export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents onClick={() => removeTab(-1, "new agent", "Create_Agent", internalId)}>Cancel {showButton && ( - )} diff --git a/gui/pages/Content/Agents/AgentSchedule.js b/gui/pages/Content/Agents/AgentSchedule.js index 0272c2b85..b78b2b168 100644 --- a/gui/pages/Content/Agents/AgentSchedule.js +++ b/gui/pages/Content/Agents/AgentSchedule.js @@ -1,5 +1,5 @@ import React, {useState, useEffect, useRef} from 'react'; -import {setLocalStorageValue, convertToGMT} from "@/utils/utils"; +import {setLocalStorageValue, convertToGMT, preventDefault} from "@/utils/utils"; import styles from "@/pages/Content/Agents/Agents.module.css"; import styles1 from "@/pages/Content/Agents/react-datetime.css"; import Image from "next/image"; @@ -136,10 +136,6 @@ export default function AgentSchedule({ setLocalStorageValue("agent_expiry_runs_" + String(internalId), event.target.value, setExpiryRuns); }; - const preventDefault = (e) => { - e.stopPropagation(); - }; - const addScheduledAgent = () => { if ((startTime === '' || (isRecurring === true && (timeValue == null || (expiryType === "After certain number of runs" && (parseInt(expiryRuns, 10) < 1)) || (expiryType === "Specific date" && expiryDate == null))))) { toast.error('Please input correct details', {autoClose: 1800}); diff --git a/gui/pages/Content/Agents/AgentWorkspace.js b/gui/pages/Content/Agents/AgentWorkspace.js index fcd68cd19..b980ad645 100644 --- a/gui/pages/Content/Agents/AgentWorkspace.js +++ b/gui/pages/Content/Agents/AgentWorkspace.js @@ -9,6 +9,7 @@ import RunHistory from "./RunHistory"; import ActionConsole from "./ActionConsole"; import Details from "./Details"; import ResourceManager from "./ResourceManager"; +import {preventDefault} from "@/utils/utils"; import { getAgentDetails, getAgentExecutions, @@ -57,11 +58,13 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents const handleEditScheduleClick = () => { setCreateEditModal(true); + setDropdown(false); }; const handleStopScheduleClick = () => { setCreateStopModal(true); setCreateModal(false); + setDropdown(false); }; function fetchStopSchedule() {//Stop Schedule @@ -157,16 +160,22 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents setDeleteModal(false); if (response.status === 200) { EventBus.emit('reFetchAgents', {}); - EventBus.emit('removeTab',{element: {id: agentId, name: agentName, contentType: "Agents", internalId: internalId}}) + EventBus.emit('removeTab', { + element: { + id: agentId, + name: agentName, + contentType: "Agents", + internalId: internalId + } + }) toast.success("Agent Deleted Successfully", {autoClose: 1800}); - } - else{ - toast.error("Agent Could not be Deleted", { autoClose: 1800 }); + } else { + toast.error("Agent Could not be Deleted", {autoClose: 1800}); } }) .catch((error) => { setDeleteModal(false); - toast.error("Agent Could not be Deleted", { autoClose: 1800 }); + toast.error("Agent Could not be Deleted", {autoClose: 1800}); console.error("Agent could not be deleted: ", error); }); } @@ -199,10 +208,6 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents setDropdown(false); }; - const preventDefault = (e) => { - e.stopPropagation(); - }; - useEffect(() => { fetchAgentDetails(agentId); fetchExecutions(agentId); @@ -367,9 +372,9 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents
  • Stop Schedule
  • ) : (
    {agent && !agent?.is_running && !agent?.is_scheduled && -
  • setCreateModal(true)}>Schedule Run
  • } +
  • {setDropdown(false);setCreateModal(true)}}>Schedule Run
  • }
    )} -
  • setDeleteModal(true)}>Delete Agent
  • +
  • {setDropdown(false);setDeleteModal(true)}}>Delete Agent
  • } @@ -467,7 +472,6 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents - {runModal && (
    Run agent name
    @@ -532,15 +536,16 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents
    )} {deleteModal && (
    -
    +
    - +
    -
    -
    )} -
    diff --git a/gui/pages/Dashboard/Content.js b/gui/pages/Dashboard/Content.js index fd02188f5..8d9270712 100644 --- a/gui/pages/Dashboard/Content.js +++ b/gui/pages/Dashboard/Content.js @@ -10,10 +10,10 @@ import Image from "next/image"; import {EventBus} from "@/utils/eventBus"; import { getAgents, - getToolKit, getLastActiveAgent, - sendTwitterCreds, - sendGoogleCreds + getToolKit, + sendGoogleCreds, + sendTwitterCreds } from "@/pages/api/DashboardService"; import Market from "../Content/Marketplace/Market"; import AgentTemplatesList from '../Content/Agents/AgentTemplatesList'; @@ -21,7 +21,7 @@ import {useRouter} from 'next/router'; import querystring from 'querystring'; import styles1 from '../Content/Agents/Agents.module.css'; import AddTool from "@/pages/Content/Toolkits/AddTool"; -import {createInternalId, resetLocalStorage} from "@/utils/utils"; +import {createInternalId, resetLocalStorage, preventDefault} from "@/utils/utils"; export default function Content({env, selectedView, selectedProjectId, organisationId}) { const [tabs, setTabs] = useState([]); @@ -34,41 +34,55 @@ export default function Content({env, selectedView, selectedProjectId, organisat const router = useRouter(); const multipleTabContentTypes = ['Create_Agent', 'Add_Toolkit']; - function fetchAgents() { - getAgents(selectedProjectId) - .then((response) => { - const data = response.data || []; - const updatedData = data.map(item => { - return {...item, contentType: "Agents"}; - }); - setAgents(updatedData); + async function fetchAgents() { + try { + const response = await getAgents(selectedProjectId); + const data = response.data || []; + const updatedData = data.map(item => { + return { ...item, contentType: "Agents" }; + }); + setAgents(updatedData); + } catch (error) { + console.error('Error fetching agents:', error); + } + } + + function getAgentList() { + fetchAgents() + .then(() => { + console.log('Agents fetched successfully!'); }) .catch((error) => { console.error('Error fetching agents:', error); }); } - function fetchToolkits() { - getToolKit() - .then((response) => { - const data = response.data || []; - const updatedData = data.map(item => { - return {...item, contentType: "Toolkits", isOpen: false, internalId: createInternalId()}; - }); - setToolkits(updatedData); + async function fetchToolkits() { + try { + const response = await getToolKit(); + const data = response.data || []; + const updatedData = data.map(item => { + return { ...item, contentType: "Toolkits", isOpen: false, internalId: createInternalId() }; + }); + setToolkits(updatedData); + } catch (error) { + console.error('Error fetching toolkits:', error); + } + } + + function getToolkitList() { + fetchToolkits() + .then(() => { + console.log('Toolkits fetched successfully!'); }) .catch((error) => { console.error('Error fetching toolkits:', error); }); } - const preventDefault = (e) => { - e.stopPropagation(); - }; - useEffect(() => { - fetchAgents(); - fetchToolkits(); + getAgentList(); + getToolkitList(); }, [selectedProjectId]) const cancelTab = (index, contentType, internalId) => { @@ -142,10 +156,11 @@ export default function Content({env, selectedView, selectedProjectId, organisat const queryParams = router.asPath.split('?')[1]; const parsedParams = querystring.parse(queryParams); parsedParams["toolkit_id"] = toolkitDetails.toolkit_id; + if (window.location.href.indexOf("twitter_creds") > -1) { - const toolkit_id = localStorage.getItem("twitter_toolkit_id") || null; - parsedParams["toolkit_id"] = toolkit_id; + parsedParams["toolkit_id"] = localStorage.getItem("twitter_toolkit_id") || null; const params = JSON.stringify(parsedParams) + sendTwitterCreds(params) .then((response) => { console.log("Authentication completed successfully"); @@ -154,11 +169,12 @@ export default function Content({env, selectedView, selectedProjectId, organisat console.error("Error fetching data: ", error); }) } - ; + if (window.location.href.indexOf("google_calendar_creds") > -1) { const toolkit_id = localStorage.getItem("google_calendar_toolkit_id") || null; - var data = Object.keys(parsedParams)[0]; - var params = JSON.parse(data) + let data = Object.keys(parsedParams)[0]; + let params = JSON.parse(data); + sendGoogleCreds(params, toolkit_id) .then((response) => { console.log("Authentication completed successfully"); @@ -167,7 +183,6 @@ export default function Content({env, selectedView, selectedProjectId, organisat console.error("Error fetching data: ", error); }) } - ; }, [selectedTab]); useEffect(() => { @@ -195,21 +210,30 @@ export default function Content({env, selectedView, selectedProjectId, organisat }; EventBus.on('openNewTab', openNewTab); - EventBus.on('reFetchAgents', fetchAgents); + EventBus.on('reFetchAgents', getAgentList); EventBus.on('removeTab', removeTab); EventBus.on('openToolkitTab', openToolkitTab); return () => { EventBus.off('openNewTab', openNewTab); - EventBus.off('reFetchAgents', fetchAgents); + EventBus.off('reFetchAgents', getAgentList); EventBus.off('removeTab', removeTab); }; }); + async function fetchLastActive() { + try { + const response = await getLastActiveAgent(selectedProjectId); + addTab(response.data); + } catch (error) { + console.error('Error fetching last active agent:', error); + } + } + function getLastActive() { - getLastActiveAgent(selectedProjectId) - .then((response) => { - addTab(response.data); + fetchLastActive() + .then(() => { + console.log('Last active agent fetched successfully!'); }) .catch((error) => { console.error('Error fetching last active agent:', error); @@ -338,7 +362,7 @@ export default function Content({env, selectedView, selectedProjectId, organisat {selectedTab === index &&
    {tab.contentType === 'Agents' && } + agents={agents} fetchAgents={getAgentList}/>} {tab.contentType === 'Toolkits' && } {tab.contentType === 'Settings' && } @@ -347,7 +371,7 @@ export default function Content({env, selectedView, selectedProjectId, organisat {tab.contentType === 'Create_Agent' && } + fetchAgents={getAgentList} toolkits={toolkits}/>} {tab.contentType === 'APM' && }
    }
    diff --git a/gui/pages/Dashboard/SideBar.js b/gui/pages/Dashboard/SideBar.js index f30912757..3d330ccb0 100644 --- a/gui/pages/Dashboard/SideBar.js +++ b/gui/pages/Dashboard/SideBar.js @@ -1,7 +1,7 @@ import React, {useState} from 'react'; import Image from 'next/image'; import styles from './Dashboard.module.css'; -import {refreshUrl, openNewTab} from "@/utils/utils"; +import {openNewTab} from "@/utils/utils"; export default function SideBar({onSelectEvent}) { const [sectionSelected, setSelection] = useState(''); @@ -10,7 +10,7 @@ export default function SideBar({onSelectEvent}) { setSelection(value); onSelectEvent(value); if (value === 'apm') { - openNewTab(-9, "APM", "APM"); + openNewTab(-9, "APM", "APM", false); } }; @@ -36,7 +36,7 @@ export default function SideBar({onSelectEvent}) {
    handleClick(sectionSelected !== 'apm' ? 'apm' : '')} - className={`${styles.section} ${sectionSelected === 'apm' ? styles.selected : ''}`}> + className={styles.section}>
    apm-icon
    APM
    diff --git a/gui/utils/utils.js b/gui/utils/utils.js index 14798f353..957cbeccc 100644 --- a/gui/utils/utils.js +++ b/gui/utils/utils.js @@ -5,6 +5,24 @@ import {EventBus} from "@/utils/eventBus"; import JSZip from "jszip"; import moment from 'moment'; +const toolkitData = { + 'Jira Toolkit': '/images/jira_icon.svg', + 'Email Toolkit': '/images/gmail_icon.svg', + 'Google Calendar Toolkit': '/images/google_calender_icon.svg', + 'GitHub Toolkit': '/images/github_icon.svg', + 'Google Search Toolkit': '/images/google_search_icon.svg', + 'Searx Toolkit': '/images/searx_icon.svg', + 'Slack Toolkit': '/images/slack_icon.svg', + 'Web Scrapper Toolkit': '/images/webscraper_icon.svg', + 'Twitter Toolkit': '/images/twitter_icon.svg', + 'Google SERP Toolkit': '/images/google_serp_icon.svg', + 'File Toolkit': '/images/filemanager_icon.svg', + 'CodingToolkit': '/images/app-logo-light.png', + 'Thinking Toolkit': '/images/app-logo-light.png', + 'Image Generation Toolkit': '/images/app-logo-light.png', + 'DuckDuckGo Search Toolkit': '/images/duckduckgo_icon.png', +}; + export const getUserTimezone = () => { return Intl.DateTimeFormat().resolvedOptions().timeZone; } @@ -84,7 +102,7 @@ export const formatBytes = (bytes, decimals = 2) => { const formattedValue = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)); return `${formattedValue} ${sizes[i]}`; -} +}; export const downloadFile = (fileId, fileName = null) => { const authToken = localStorage.getItem('accessToken'); @@ -179,7 +197,8 @@ export const refreshUrl = () => { return; } - const urlWithoutToken = window.location.origin + window.location.pathname; + const { origin, pathname } = window.location; + const urlWithoutToken = origin + pathname; window.history.replaceState({}, document.title, urlWithoutToken); }; @@ -193,35 +212,35 @@ export const loadingTextEffect = (loadingText, setLoadingText, timer) => { }, timer); return () => clearInterval(interval) -} +}; -export const openNewTab = (id, name, contentType, hasInternalId) => { +export const openNewTab = (id, name, contentType, hasInternalId = false) => { EventBus.emit('openNewTab', { element: {id: id, name: name, contentType: contentType, internalId: hasInternalId ? createInternalId() : 0} }); -} +}; export const removeTab = (id, name, contentType, internalId) => { EventBus.emit('removeTab', { element: {id: id, name: name, contentType: contentType, internalId: internalId} }); -} +}; export const setLocalStorageValue = (key, value, stateFunction) => { stateFunction(value); localStorage.setItem(key, value); -} +}; export const setLocalStorageArray = (key, value, stateFunction) => { stateFunction(value); const arrayString = JSON.stringify(value); localStorage.setItem(key, arrayString); -} +}; const getInternalIds = () => { const internal_ids = localStorage.getItem("agi_internal_ids"); return internal_ids ? JSON.parse(internal_ids) : []; -} +}; const removeAgentInternalId = (internalId) => { let idsArray = getInternalIds(); @@ -260,7 +279,7 @@ const removeAgentInternalId = (internalId) => { localStorage.removeItem("is_agent_template_" + String(internalId)); localStorage.removeItem("agent_template_id_" + String(internalId)); } -} +}; const removeAddToolkitInternalId = (internalId) => { let idsArray = getInternalIds(); @@ -271,7 +290,7 @@ const removeAddToolkitInternalId = (internalId) => { localStorage.setItem('agi_internal_ids', JSON.stringify(idsArray)); localStorage.removeItem('tool_github_' + String(internalId)); } -} +}; const removeToolkitsInternalId = (internalId) => { let idsArray = getInternalIds(); @@ -283,7 +302,7 @@ const removeToolkitsInternalId = (internalId) => { localStorage.removeItem('toolkit_tab_' + String(internalId)); localStorage.removeItem('api_configs_' + String(internalId)); } -} +}; export const resetLocalStorage = (contentType, internalId) => { switch (contentType) { @@ -305,7 +324,7 @@ export const resetLocalStorage = (contentType, internalId) => { default: break; } -} +}; export const createInternalId = () => { let newId = 1; @@ -326,54 +345,37 @@ export const createInternalId = () => { } return newId; -} +}; export const returnToolkitIcon = (toolkitName) => { - const toolkitData = [ - {name: 'Jira Toolkit', imageSrc: '/images/jira_icon.svg'}, - {name: 'Email Toolkit', imageSrc: '/images/gmail_icon.svg'}, - {name: 'Google Calendar Toolkit', imageSrc: '/images/google_calender_icon.svg'}, - {name: 'GitHub Toolkit', imageSrc: '/images/github_icon.svg'}, - {name: 'Google Search Toolkit', imageSrc: '/images/google_search_icon.svg'}, - {name: 'Searx Toolkit', imageSrc: '/images/searx_icon.svg'}, - {name: 'Slack Toolkit', imageSrc: '/images/slack_icon.svg'}, - {name: 'Web Scrapper Toolkit', imageSrc: '/images/webscraper_icon.svg'}, - {name: 'Twitter Toolkit', imageSrc: '/images/twitter_icon.svg'}, - {name: 'Google SERP Toolkit', imageSrc: '/images/google_serp_icon.svg'}, - {name: 'File Toolkit', imageSrc: '/images/filemanager_icon.svg'}, - {name: 'CodingToolkit', imageSrc: '/images/app-logo-light.png'}, - {name: 'Thinking Toolkit', imageSrc: '/images/app-logo-light.png'}, - {name: 'Image Generation Toolkit', imageSrc: '/images/app-logo-light.png'}, - {name: 'DuckDuckGo Search Toolkit', imageSrc: '/images/duckduckgo_icon.png'}, - ]; - - const toolkit = toolkitData.find((tool) => tool.name === toolkitName); - return toolkit ? toolkit.imageSrc : '/images/custom_tool.svg'; -} + return toolkitData[toolkitName] || '/images/custom_tool.svg'; +}; export const returnResourceIcon = (file) => { - let fileIcon; - const fileTypeIcons = { - 'application/pdf': '/images/pdf_file.svg', - 'application/txt': '/images/txt_file.svg', - 'text/plain': '/images/txt_file.svg', - }; - - if (file.type.includes('image')) { - fileIcon = '/images/img_file.svg'; - } else { - fileIcon = fileTypeIcons[file.type] || '/images/default_file.svg'; + const fileType = file.type; + + switch (true) { + case fileType.includes('image'): + return '/images/img_file.svg'; + case fileType === 'application/pdf': + return '/images/pdf_file.svg'; + case fileType === 'application/txt' || fileType === 'text/plain': + return '/images/txt_file.svg'; + default: + return '/images/default_file.svg'; } - - return fileIcon; }; export const convertToTitleCase = (str) => { - if (str === null || str === '') { + if (!str) { return ''; } const words = str.toLowerCase().split('_'); const capitalizedWords = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)); return capitalizedWords.join(' '); -} \ No newline at end of file +}; + +export const preventDefault = (e) => { + e.stopPropagation(); +}; \ No newline at end of file From 58355d3d525231df3f300f499a10e348fef85965 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Tue, 25 Jul 2023 15:53:01 +0530 Subject: [PATCH 29/77] Added instagram tool bucket entry in config_template.yaml --- config_template.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/config_template.yaml b/config_template.yaml index f048df782..970ba6c85 100644 --- a/config_template.yaml +++ b/config_template.yaml @@ -38,6 +38,7 @@ RESOURCES_OUTPUT_ROOT_DIR: workspace/output/{agent_id}/{agent_execution_id} # Fo #S3 RELATED DETAILS ONLY WHEN STORAGE_TYPE IS "S3" BUCKET_NAME: +INSTAGRAM_TOOL_BUCKET_NAME: AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: From bb0304fe4ac888629fa659cc2c6ea393c72421d5 Mon Sep 17 00:00:00 2001 From: Maverick-F35 Date: Tue, 25 Jul 2023 15:55:45 +0530 Subject: [PATCH 30/77] added instagram tool bucket entry in config template --- config_template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_template.yaml b/config_template.yaml index 970ba6c85..331553776 100644 --- a/config_template.yaml +++ b/config_template.yaml @@ -38,7 +38,7 @@ RESOURCES_OUTPUT_ROOT_DIR: workspace/output/{agent_id}/{agent_execution_id} # Fo #S3 RELATED DETAILS ONLY WHEN STORAGE_TYPE IS "S3" BUCKET_NAME: -INSTAGRAM_TOOL_BUCKET_NAME: +INSTAGRAM_TOOL_BUCKET_NAME: #Public read bucket, Images generated by stable diffusion are put in this bucket and the public url of the same is generated. AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: From e2c01bced158eb6a16ea7540229a472a8a7ef24b Mon Sep 17 00:00:00 2001 From: Kalki Date: Tue, 25 Jul 2023 16:37:56 +0530 Subject: [PATCH 31/77] ui bug fixes --- gui/package-lock.json | 10 +++++ gui/package.json | 1 + gui/pages/Content/Agents/ActivityFeed.js | 49 ++++++++++++---------- gui/pages/Content/Agents/AgentWorkspace.js | 2 +- gui/pages/_app.css | 3 +- 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/gui/package-lock.json b/gui/package-lock.json index b0a186e98..ebb65645d 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -27,6 +27,7 @@ "react-draggable": "^4.4.5", "react-grid-layout": "^1.3.4", "react-markdown": "^8.0.7", + "react-spinners": "^0.13.8", "react-toastify": "^9.1.3" } }, @@ -3683,6 +3684,15 @@ "react": ">= 16.3" } }, + "node_modules/react-spinners": { + "version": "0.13.8", + "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.13.8.tgz", + "integrity": "sha512-3e+k56lUkPj0vb5NDXPVFAOkPC//XyhKPJjvcGjyMNPWsBKpplfeyialP74G7H7+It7KzhtET+MvGqbKgAqpZA==", + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-toastify": { "version": "9.1.3", "license": "MIT", diff --git a/gui/package.json b/gui/package.json index b9b588838..f788aee8d 100644 --- a/gui/package.json +++ b/gui/package.json @@ -29,6 +29,7 @@ "react-draggable": "^4.4.5", "react-grid-layout": "^1.3.4", "react-markdown": "^8.0.7", + "react-spinners": "^0.13.8", "react-toastify": "^9.1.3" } } diff --git a/gui/pages/Content/Agents/ActivityFeed.js b/gui/pages/Content/Agents/ActivityFeed.js index 45a326459..8e592af8b 100644 --- a/gui/pages/Content/Agents/ActivityFeed.js +++ b/gui/pages/Content/Agents/ActivityFeed.js @@ -4,6 +4,7 @@ import {getExecutionFeeds, getDateTime} from "@/pages/api/DashboardService"; import Image from "next/image"; import {loadingTextEffect, formatTimeDifference} from "@/utils/utils"; import {EventBus} from "@/utils/eventBus"; +import { ClipLoader } from 'react-spinners'; export default function ActivityFeed({selectedRunId, selectedView, setFetchedData, agent}) { const [loadingText, setLoadingText] = useState("Thinking"); @@ -13,6 +14,7 @@ export default function ActivityFeed({selectedRunId, selectedView, setFetchedDat const [prevFeedsLength, setPrevFeedsLength] = useState(0); const [scheduleDate, setScheduleDate] = useState(null); const [scheduleTime, setScheduleTime] = useState(null); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { const interval = window.setInterval(function () { @@ -72,17 +74,22 @@ export default function ActivityFeed({selectedRunId, selectedView, setFetchedDat }, [runStatus]) function fetchFeeds() { + console.log("In") + setIsLoading(true); + console.log(isLoading) getExecutionFeeds(selectedRunId) - .then((response) => { - const data = response.data; - setFeeds(data.feeds); - setRunStatus(data.status); - setFetchedData(data.permissions); - EventBus.emit('resetRunStatus', {executionId: selectedRunId, status: data.status}); - }) - .catch((error) => { - console.error('Error fetching execution feeds:', error); - }); + .then((response) => { + const data = response.data; + setFeeds(data.feeds); + setRunStatus(data.status); + setFetchedData(data.permissions); + EventBus.emit('resetRunStatus', {executionId: selectedRunId, status: data.status}); + setIsLoading(false); //add this line + }) + .catch((error) => { + console.error('Error fetching execution feeds:', error); + setIsLoading(false); // and this line + }); } useEffect(() => { @@ -114,8 +121,7 @@ export default function ActivityFeed({selectedRunId, selectedView, setFetchedDat This agent is scheduled to start on {scheduleDate}, at {scheduleTime}
    :
    - {feeds && feeds.map((f, index) => (
    + {feeds && feeds.map((f, index) => (
    {f.role === 'user' &&
    💁
    } {f.role === 'system' &&
    🛠️
    } @@ -158,17 +164,14 @@ export default function ActivityFeed({selectedRunId, selectedView, setFetchedDat
    }
    } - {!agent?.is_scheduled && !agent?.is_running && feeds.length < 1 && -
    - The Agent is not scheduled -
    + {feeds.length < 1 && !agent?.is_running && !agent?.is_scheduled ? + (isLoading ? +
    + +
    + :
    + The Agent is not scheduled +
    ): null }
    {feedContainerRef.current && feedContainerRef.current.scrollTop >= 1200 && diff --git a/gui/pages/Content/Agents/AgentWorkspace.js b/gui/pages/Content/Agents/AgentWorkspace.js index fcd68cd19..d5905e664 100644 --- a/gui/pages/Content/Agents/AgentWorkspace.js +++ b/gui/pages/Content/Agents/AgentWorkspace.js @@ -40,7 +40,7 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents const [fetchedData, setFetchedData] = useState(null); const [instructions, setInstructions] = useState(['']); const [currentInstructions, setCurrentInstructions] = useState(['']); - const [pendingPermission, setPendingPermissions] = useState(0) + const [pendingPermission, setPendingPermissions] = useState(0); const agent = agents.find(agent => agent.id === agentId); diff --git a/gui/pages/_app.css b/gui/pages/_app.css index e02ca3fb5..217eaf6c3 100644 --- a/gui/pages/_app.css +++ b/gui/pages/_app.css @@ -983,8 +983,9 @@ tr{ } .tools_used_tooltip{ - position: absolute; + position: relative; cursor: pointer; + z-index: 100; } .image_class{ From e57b8bc01423fa4c3d9c46dbf78872b1ab1bb3b4 Mon Sep 17 00:00:00 2001 From: Kalki Date: Tue, 25 Jul 2023 17:45:38 +0530 Subject: [PATCH 32/77] ui bug fixes --- gui/pages/Content/Agents/AgentCreate.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/gui/pages/Content/Agents/AgentCreate.js b/gui/pages/Content/Agents/AgentCreate.js index 0de46038a..1f39cb399 100644 --- a/gui/pages/Content/Agents/AgentCreate.js +++ b/gui/pages/Content/Agents/AgentCreate.js @@ -38,6 +38,7 @@ export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents const [toolkitList, setToolkitList] = useState(toolkits) const [searchValue, setSearchValue] = useState(''); const [showButton, setShowButton] = useState(false); + const [showPlaceholder, setShowPlaceholder] = useState(true); const constraintsArray = [ "If you are unsure how you previously did something or want to recall past events, thinking about similar events will help you remember.", @@ -794,17 +795,19 @@ export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents
    setToolkitDropdown(!toolkitDropdown)} style={{width: '100%', alignItems: 'flex-start'}}> - {toolNames && toolNames.length > 0 ?
    - {toolNames.map((tool, index) => ( -
    -
    {tool}
    -
    close-icon removeTool(index)}/>
    -
    ))} - setSearchValue(e.target.value)} onFocus={() => setToolkitDropdown(true)} - onClick={(e) => e.stopPropagation()}/> -
    :
    Select Tools
    } +
    + {toolNames && toolNames.length > 0 && toolNames.map((tool, index) => ( +
    +
    {tool}
    +
    close-icon removeTool(index)}/>
    +
    + ))} + setSearchValue(e.target.value)} + onFocus={() => {setToolkitDropdown(true);setShowPlaceholder(false);}} onBlur={() => {setShowPlaceholder(true);}} + onClick={(e) => e.stopPropagation()}/> + {toolNames && toolNames.length === 0 && showPlaceholder &&
    Select Tools
    } +
    clearTools(e)} src='/images/clear_input.svg' alt="clear-input"/> From 802eeb1100379f143085695f8a687a867f4cfcfa Mon Sep 17 00:00:00 2001 From: Kalki Date: Tue, 25 Jul 2023 17:49:42 +0530 Subject: [PATCH 33/77] ui bug fixes --- gui/pages/Content/Agents/AgentCreate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/pages/Content/Agents/AgentCreate.js b/gui/pages/Content/Agents/AgentCreate.js index 1f39cb399..1ce0a5470 100644 --- a/gui/pages/Content/Agents/AgentCreate.js +++ b/gui/pages/Content/Agents/AgentCreate.js @@ -806,7 +806,7 @@ export default function AgentCreate({sendAgentData,selectedProjectId,fetchAgents setSearchValue(e.target.value)} onFocus={() => {setToolkitDropdown(true);setShowPlaceholder(false);}} onBlur={() => {setShowPlaceholder(true);}} onClick={(e) => e.stopPropagation()}/> - {toolNames && toolNames.length === 0 && showPlaceholder &&
    Select Tools
    } + {toolNames && toolNames.length === 0 && showPlaceholder && searchValue.length ===0 &&
    Select Tools
    }
    clearTools(e)} src='/images/clear_input.svg' From 1ea8d16050afcac4a94ab9034934cc697d575998 Mon Sep 17 00:00:00 2001 From: luciferlinx <129729795+luciferlinx101@users.noreply.github.com> Date: Tue, 25 Jul 2023 17:55:13 +0530 Subject: [PATCH 34/77] External repo support for marketplace (#847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added support for parsing marketplace repo * Added Support for marketplace repo * Improved Marketplace logic * Fixed marketplace install * Handled no path bug * Updated .gitignore * Updated unit test * Added Unit Test * Updated .gitignore * Refactored Code * Updated Marketplace URL * Added Unit Tests * Update tool_helper.py * Update tool_helper.py --------- Co-authored-by: I’m <133493246+TransformerOptimus@users.noreply.github.com> --- .gitignore | 3 + entrypoint.sh | 2 +- install_tool_dependencies.sh | 2 +- main.py | 17 +++- superagi/controllers/toolkit.py | 41 ++++++---- superagi/helper/tool_helper.py | 67 +++++++++------ superagi/jobs/agent_executor.py | 36 ++++++--- superagi/tool_manager.py | 85 +++++++++++++++++--- tests/unit_tests/controllers/test_toolkit.py | 67 ++++++++++++++- tests/unit_tests/test_tool_manager.py | 59 ++++++++++++-- 10 files changed, 303 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index ab92910c2..1f5ed072c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ workspace/output workspace/input celerybeat-schedule ../bfg-report* +superagi/tools/marketplace_tools/ +superagi/tools/external_tools/ +tests/unit_tests/resource_manager/test_path \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index da37ae04b..2403d55c0 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Downloads the tools +# Downloads the tools from marketplace and external tool repositories python superagi/tool_manager.py # Set executable permissions for install_tool_dependencies.sh diff --git a/install_tool_dependencies.sh b/install_tool_dependencies.sh index 87dc10c2e..7f37ee224 100755 --- a/install_tool_dependencies.sh +++ b/install_tool_dependencies.sh @@ -4,7 +4,7 @@ pip install -r /app/requirements.txt # Loop through the tools directories and install their requirements.txt if they exist -for tool in /app/superagi/tools/* ; do +for tool in /app/superagi/tools/* /app/superagi/tools/external_tools/* /app/superagi/tools/marketplace_tools/* ; do if [ -d "$tool" ] && [ -f "$tool/requirements.txt" ]; then echo "Installing requirements for tool: $(basename "$tool")" pip install -r "$tool/requirements.txt" diff --git a/main.py b/main.py index 7de018229..33c11eb98 100644 --- a/main.py +++ b/main.py @@ -42,7 +42,7 @@ from superagi.controllers.user import router as user_router from superagi.controllers.agent_execution_config import router as agent_execution_config from superagi.controllers.analytics import router as analytics_router -from superagi.helper.tool_helper import register_toolkits +from superagi.helper.tool_helper import register_toolkits, register_marketplace_toolkits from superagi.lib.logger import logger from superagi.llms.google_palm import GooglePalm from superagi.llms.openai import OpenAi @@ -117,6 +117,7 @@ app.include_router(google_oauth_router, prefix="/google") + # in production you can use Settings management # from pydantic to get secret key from .env class Settings(BaseModel): @@ -313,17 +314,26 @@ def build_action_based_agents(): workflow_step2.next_step_id = workflow_step2.id session.commit() - def check_toolkit_registration(): + def register_toolkit_for_all_organisation(): organizations = session.query(Organisation).all() for organization in organizations: register_toolkits(session, organization) logger.info("Successfully registered local toolkits for all Organisations!") + def register_toolkit_for_master_organisation(): + marketplace_organisation_id = superagi.config.config.get_config("MARKETPLACE_ORGANISATION_ID") + marketplace_organisation = session.query(Organisation).filter( + Organisation.id == marketplace_organisation_id).first() + if marketplace_organisation is not None: + register_marketplace_toolkits(session, marketplace_organisation) + build_single_step_agent() build_task_based_agents() build_action_based_agents() if env != "PROD": - check_toolkit_registration() + register_toolkit_for_all_organisation() + else: + register_toolkit_for_master_organisation() session.close() @@ -395,7 +405,6 @@ def github_auth_handler(code: str = Query(...), Authorize: AuthJWT = Depends()): db.session.add(user) db.session.commit() jwt_token = create_access_token(user_email, Authorize) - redirect_url_success = f"{frontend_url}?access_token={jwt_token}" return RedirectResponse(url=redirect_url_success) else: diff --git a/superagi/controllers/toolkit.py b/superagi/controllers/toolkit.py index e04200cd4..f6ae7aa23 100644 --- a/superagi/controllers/toolkit.py +++ b/superagi/controllers/toolkit.py @@ -6,10 +6,12 @@ from fastapi_sqlalchemy import db from superagi.config.config import get_config from superagi.helper.auth import get_user_organisation -from superagi.helper.tool_helper import get_readme_content_from_code_link, download_tool,process_files,add_tool_to_json +from superagi.helper.tool_helper import get_readme_content_from_code_link, download_tool, process_files, \ + add_tool_to_json from superagi.helper.github_helper import GithubHelper from superagi.models.organisation import Organisation from superagi.models.tool import Tool +from superagi.models.tool_config import ToolConfig from superagi.models.toolkit import Toolkit from superagi.types.common import GitHubLinkRequest @@ -20,7 +22,7 @@ # marketplace_url = "http://localhost:8001/" -#For internal use +# For internal use @router.get("/marketplace/list/{page}") def get_marketplace_toolkits( page: int = 0, @@ -51,7 +53,8 @@ def get_marketplace_toolkits( toolkit.updated_at = toolkit.updated_at.strftime('%d-%b-%Y').upper() return toolkits -#For internal use + +# For internal use @router.get("/marketplace/details/{toolkit_name}") def get_marketplace_toolkit_detail(toolkit_name: str): """ @@ -66,10 +69,14 @@ def get_marketplace_toolkit_detail(toolkit_name: str): """ organisation_id = int(get_config("MARKETPLACE_ORGANISATION_ID")) - toolkit = db.session.query(Toolkit).filter(Toolkit.organisation_id == organisation_id, Toolkit.name == toolkit_name).first() + toolkit = db.session.query(Toolkit).filter(Toolkit.organisation_id == organisation_id, + Toolkit.name == toolkit_name).first() + toolkit.tools = db.session.query(Tool).filter(Tool.toolkit_id == toolkit.id).all() + toolkit.configs = db.session.query(ToolConfig).filter(ToolConfig.toolkit_id == toolkit.id).all() return toolkit -#For internal use + +# For internal use @router.get("/marketplace/readme/{toolkit_name}") def get_marketplace_toolkit_readme(toolkit_name: str): """ @@ -93,7 +100,8 @@ def get_marketplace_toolkit_readme(toolkit_name: str): raise HTTPException(status_code=404, detail='ToolKit not found') return get_readme_content_from_code_link(toolkit.tool_code_link) -#For internal use + +# For internal use @router.get("/marketplace/tools/{toolkit_name}") def get_marketplace_toolkit_tools(toolkit_name: str): """ @@ -111,7 +119,8 @@ def get_marketplace_toolkit_tools(toolkit_name: str): """ organisation_id = int(get_config("MARKETPLACE_ORGANISATION_ID")) - toolkit = db.session.query(Toolkit).filter(Toolkit.name == toolkit_name, Toolkit.organisation_id == organisation_id).first() + toolkit = db.session.query(Toolkit).filter(Toolkit.name == toolkit_name, + Toolkit.organisation_id == organisation_id).first() if not toolkit: raise HTTPException(status_code=404, detail="ToolKit not found") tools = db.session.query(Tool).filter(Tool.toolkit_id == toolkit.id).first() @@ -132,15 +141,18 @@ def install_toolkit_from_marketplace(toolkit_name: str, dict: A message indicating the successful installation of the tool kit. """ - # Check if the tool kit exists toolkit = Toolkit.fetch_marketplace_detail(search_str="details", toolkit_name=toolkit_name) - # download_and_install_tool(GitHubLinkRequest(github_link=toolkit['tool_code_link']), - # organisation=organisation) - if not GithubHelper.validate_github_link(toolkit['tool_code_link']): - raise HTTPException(status_code=400, detail="Invalid Github link") - add_tool_to_json(toolkit['tool_code_link']) + db_toolkit = Toolkit.add_or_update(session=db.session, name=toolkit['name'], description=toolkit['description'], + tool_code_link=toolkit['tool_code_link'], organisation_id=organisation.id, + show_toolkit=toolkit['show_toolkit']) + for tool in toolkit['tools']: + Tool.add_or_update(session=db.session, tool_name=tool['name'], description=tool['description'], + folder_name=tool['folder_name'], class_name=tool['class_name'], file_name=tool['file_name'], + toolkit_id=db_toolkit.id) + for config in toolkit['configs']: + ToolConfig.add_or_update(session=db.session, toolkit_id=db_toolkit.id, key=config['key'], value=config['value']) return {"message": "ToolKit installed successfully"} @@ -228,6 +240,7 @@ def get_installed_toolkit_readme(toolkit_name: str, organisation: Organisation = readme_content = get_readme_content_from_code_link(toolkit.tool_code_link) return readme_content + # Following APIs will be used to get marketplace related information @router.get("/get") def handle_marketplace_operations( @@ -289,4 +302,4 @@ def get_installed_toolkit_list(organisation: Organisation = Depends(get_user_org toolkit_tools = db.session.query(Tool).filter(Tool.toolkit_id == toolkit.id).all() toolkit.tools = toolkit_tools - return toolkits \ No newline at end of file + return toolkits diff --git a/superagi/helper/tool_helper.py b/superagi/helper/tool_helper.py index e8f3d7f1b..3dc4d1587 100644 --- a/superagi/helper/tool_helper.py +++ b/superagi/helper/tool_helper.py @@ -118,13 +118,17 @@ def load_module_from_file(file_path): return module -def init_tools(folder_path, session, tool_name_to_toolkit): +def init_tools(folder_paths, session, tool_name_to_toolkit): # Iterate over all subfolders - for folder_name in os.listdir(folder_path): - folder_dir = os.path.join(folder_path, folder_name) - # Iterate over all files in the subfolder - if os.path.isdir(folder_dir): - # sys.path.append(os.path.abspath('superagi/tools/email')) + for folder_path in folder_paths: + if not os.path.exists(folder_path): + continue + for folder_name in os.listdir(folder_path): + folder_dir = os.path.join(folder_path, folder_name) + # Iterate over all files in the subfolder + if not os.path.isdir(folder_dir): + continue + # sys.path.append(os.path.abspath('superagi/tools/email')) sys.path.append(folder_dir) for file_name in os.listdir(folder_dir): file_path = os.path.join(folder_dir, file_name) @@ -147,15 +151,19 @@ def update_base_tool_class_info(classes, file_name, folder_name, session, tool_n description=tool_description) -def init_toolkits(code_link, existing_toolkits, folder_path, organisation, session): +def init_toolkits(code_link, existing_toolkits, folder_paths, organisation, session): tool_name_to_toolkit = {} new_toolkits = [] # Iterate over all subfolders - for folder_name in os.listdir(folder_path): - folder_dir = os.path.join(folder_path, folder_name) + for folder_path in folder_paths: + if not os.path.exists(folder_path): + continue + for folder_name in os.listdir(folder_path): + folder_dir = os.path.join(folder_path, folder_name) - if os.path.isdir(folder_dir): - # sys.path.append(os.path.abspath('superagi/tools/email')) + if not os.path.isdir(folder_dir): + continue + # sys.path.append(os.path.abspath('superagi/tools/email')) sys.path.append(folder_dir) # Iterate over all files in the subfolder for file_name in os.listdir(folder_dir): @@ -214,11 +222,11 @@ def update_base_toolkit_info(classes, code_link, folder_name, new_toolkits, orga return tool_name_to_toolkit -def process_files(folder_path, session, organisation, code_link=None): +def process_files(folder_paths, session, organisation, code_link=None): existing_toolkits = session.query(Toolkit).filter(Toolkit.organisation_id == organisation.id).all() - tool_name_to_toolkit = init_toolkits(code_link, existing_toolkits, folder_path, organisation, session) - init_tools(folder_path, session, tool_name_to_toolkit) + tool_name_to_toolkit = init_toolkits(code_link, existing_toolkits, folder_paths, organisation, session) + init_tools(folder_paths, session, tool_name_to_toolkit) def get_readme_content_from_code_link(tool_code_link): @@ -240,13 +248,18 @@ def get_readme_content_from_code_link(tool_code_link): def register_toolkits(session, organisation): - folder_path = get_config("TOOLS_DIR") - if folder_path is None: - folder_path = "superagi/tools" + tool_paths = ["superagi/tools", "superagi/tools/external_tools"] + # if get_config("ENV", "DEV") == "PROD": + # tool_paths.append("superagi/tools/marketplace_tools") if organisation is not None: - process_files(folder_path, session, organisation) - logger.info(f"Toolkits Registered Successfully for Organisation ID : {organisation.id}!") + process_files(tool_paths, session, organisation) + logger.info(f"Toolkits Registered Successfully for Organisation ID : {organisation.id}!") +def register_marketplace_toolkits(session, organisation): + tool_paths = ["superagi/tools", "superagi/tools/external_tools","superagi/tools/marketplace_tools"] + if organisation is not None: + process_files(tool_paths, session, organisation) + logger.info(f"Marketplace Toolkits Registered Successfully for Organisation ID : {organisation.id}!") def extract_repo_name(repo_link): # Extract the repository name from the link @@ -273,10 +286,12 @@ def add_tool_to_json(repo_link): def handle_tools_import(): - folder_path = get_config("TOOLS_DIR") - if folder_path is None: - folder_path = "superagi/tools" - for folder_name in os.listdir(folder_path): - folder_dir = os.path.join(folder_path, folder_name) - if os.path.isdir(folder_dir): - sys.path.append(folder_dir) \ No newline at end of file + print("Handling tools import") + tool_paths = ["superagi/tools", "superagi/tools/marketplace_tools", "superagi/tools/external_tools"] + for tool_path in tool_paths: + if not os.path.exists(tool_path): + continue + for folder_name in os.listdir(tool_path): + folder_dir = os.path.join(tool_path, folder_name) + if os.path.isdir(folder_dir): + sys.path.append(folder_dir) diff --git a/superagi/jobs/agent_executor.py b/superagi/jobs/agent_executor.py index b321361bf..cd8f0f520 100644 --- a/superagi/jobs/agent_executor.py +++ b/superagi/jobs/agent_executor.py @@ -1,10 +1,12 @@ import importlib +import os from datetime import datetime, timedelta from sqlalchemy.orm import sessionmaker import superagi.worker from superagi.agent.super_agi import SuperAgi +from superagi.apm.event_handler import EventHandler from superagi.config.config import get_config from superagi.helper.encyption_helper import decrypt_data from superagi.lib.logger import logger @@ -32,7 +34,7 @@ from superagi.types.vector_store_types import VectorStoreType from superagi.vector_store.embedding.openai import OpenAiEmbedding from superagi.vector_store.vector_factory import VectorFactory -from superagi.apm.event_handler import EventHandler + # from superagi.helper.tool_helper import get_tool_config_by_key engine = connect_db() @@ -82,18 +84,18 @@ def create_object(tool, session): object: The object of the agent usable tool. """ file_name = AgentExecutor.validate_filename(filename=tool.file_name) + tool_paths = ["superagi/tools", "superagi/tools/external_tools", "superagi/tools/marketplace_tools"] + for tool_path in tool_paths: + if os.path.exists(os.path.join(os.getcwd(), tool_path) + '/' + tool.folder_name): + tools_dir = tool_path + break - tools_dir = get_config("TOOLS_DIR") - if tools_dir is None: - tools_dir = "superagi/tools" parsed_tools_dir = tools_dir.rstrip("/") module_name = ".".join(parsed_tools_dir.split("/") + [tool.folder_name, file_name]) # module_name = f"superagi.tools.{folder_name}.{file_name}" - # Load the module dynamically module = importlib.import_module(module_name) - # Get the class from the loaded module obj_class = getattr(module, tool.class_name) @@ -138,7 +140,7 @@ def get_embedding(cls, model_source, model_api_key): return None @staticmethod - def get_organisation(agent_execution,session): + def get_organisation(agent_execution, session): """ Get the model API key from the agent execution. @@ -155,7 +157,6 @@ def get_organisation(agent_execution,session): return organisation - def execute_next_action(self, agent_execution_id): """ Execute the next action of the agent execution. @@ -197,7 +198,12 @@ def execute_next_action(self, agent_execution_id): db_agent_execution = session.query(AgentExecution).filter(AgentExecution.id == agent_execution_id).first() db_agent_execution.status = "ITERATION_LIMIT_EXCEEDED" session.commit() - EventHandler(session=session).create_event('run_iteration_limit_crossed', {'agent_execution_id':db_agent_execution.id,'name': db_agent_execution.name,'tokens_consumed':db_agent_execution.num_of_tokens,"calls":db_agent_execution.num_of_calls}, db_agent_execution.agent_id, organisation.id) + EventHandler(session=session).create_event('run_iteration_limit_crossed', + {'agent_execution_id': db_agent_execution.id, + 'name': db_agent_execution.name, + 'tokens_consumed': db_agent_execution.num_of_tokens, + "calls": db_agent_execution.num_of_calls}, + db_agent_execution.agent_id, organisation.id) logger.info("ITERATION_LIMIT_CROSSED") return "ITERATION_LIMIT_CROSSED" @@ -267,7 +273,11 @@ def execute_next_action(self, agent_execution_id): db_agent_execution = session.query(AgentExecution).filter(AgentExecution.id == agent_execution_id).first() db_agent_execution.status = "COMPLETED" session.commit() - EventHandler(session=session).create_event('run_completed', {'agent_execution_id':db_agent_execution.id,'name': db_agent_execution.name,'tokens_consumed':db_agent_execution.num_of_tokens,"calls":db_agent_execution.num_of_calls}, db_agent_execution.agent_id, organisation.id) + EventHandler(session=session).create_event('run_completed', {'agent_execution_id': db_agent_execution.id, + 'name': db_agent_execution.name, + 'tokens_consumed': db_agent_execution.num_of_tokens, + "calls": db_agent_execution.num_of_calls}, + db_agent_execution.agent_id, organisation.id) elif response["result"] == "WAITING_FOR_PERMISSION": db_agent_execution = session.query(AgentExecution).filter(AgentExecution.id == agent_execution_id).first() db_agent_execution.status = "WAITING_FOR_PERMISSION" @@ -360,15 +370,15 @@ def handle_wait_for_permission(self, agent_execution, spawned_agent, session): def get_agent_resource_summary(self, agent_id: int, session: Session, model_llm_source: str, default_summary: str): if ModelSourceType.GooglePalm.value in model_llm_source: return - ResourceSummarizer(session=session).generate_agent_summary(agent_id=agent_id,generate_all=True) + ResourceSummarizer(session=session).generate_agent_summary(agent_id=agent_id, generate_all=True) agent_config_resource_summary = session.query(AgentConfiguration). \ filter(AgentConfiguration.agent_id == agent_id, AgentConfiguration.key == "resource_summary").first() resource_summary = agent_config_resource_summary.value if agent_config_resource_summary is not None else default_summary return resource_summary - def check_for_resource(self,agent_id: int, session: Session): - resource = session.query(Resource).filter(Resource.agent_id == agent_id,Resource.channel == 'INPUT').first() + def check_for_resource(self, agent_id: int, session: Session): + resource = session.query(Resource).filter(Resource.agent_id == agent_id, Resource.channel == 'INPUT').first() if resource is None: return False return True diff --git a/superagi/tool_manager.py b/superagi/tool_manager.py index 674dd0e03..dea5f4bb0 100644 --- a/superagi/tool_manager.py +++ b/superagi/tool_manager.py @@ -5,6 +5,7 @@ import zipfile import json + def parse_github_url(github_url): parts = github_url.split('/') owner = parts[3] @@ -12,11 +13,11 @@ def parse_github_url(github_url): branch = "main" return f"{owner}/{repo}/{branch}" + def download_tool(tool_url, target_folder): parsed_url = parse_github_url(tool_url) parts = parsed_url.split("/") owner, repo, branch, path = parts[0], parts[1], parts[2], "/".join(parts[3:]) - archive_url = f"https://api.github.com/repos/{owner}/{repo}/zipball/{branch}" response = requests.get(archive_url) @@ -31,19 +32,15 @@ def download_tool(tool_url, target_folder): for member in members: archive_folder = f"{owner}-{repo}" target_name = member.replace(f"{archive_folder}/", "", 1) - # Skip the unique hash folder while extracting: segments = target_name.split('/', 1) if len(segments) > 1: target_name = segments[1] else: continue - target_path = os.path.join(target_folder, target_name) - if not target_name: continue - if member.endswith('/'): os.makedirs(target_path, exist_ok=True) else: @@ -53,6 +50,54 @@ def download_tool(tool_url, target_folder): os.remove(tool_zip_file_path) +def download_marketplace_tool(tool_url, target_folder): + parsed_url = tool_url.split("/") + owner, repo = parsed_url[3], parsed_url[4] + archive_url = f"https://api.github.com/repos/{owner}/{repo}/zipball/main" + response = requests.get(archive_url) + tool_zip_file_path = os.path.join(target_folder, 'tool.zip') + + with open(tool_zip_file_path, 'wb') as f: + f.write(response.content) + + with zipfile.ZipFile(tool_zip_file_path, 'r') as z: + for member in z.namelist(): + archive_folder, target_name = member.split('/', 1) + target_name = os.path.join(target_folder, target_name) + if member.endswith('/'): + os.makedirs(target_name, exist_ok=True) + elif not target_name.endswith('.md'): + with open(target_name, 'wb') as outfile, z.open(member) as infile: + outfile.write(infile.read()) + + os.remove(tool_zip_file_path) + + +def get_marketplace_tool_links(repo_url): + folder_links = {} + api_url = f"https://api.github.com/repos/{repo_url}/contents" + response = requests.get(api_url) + contents = response.json() + + for content in contents: + if content["type"] == "dir": + folder_name = content["name"] + folder_link = f"https://github.com/{repo_url}/tree/main/{folder_name}" + folder_links[folder_name] = folder_link + + return folder_links + + +def update_tools_json(existing_tools_json_path, folder_links): + with open(existing_tools_json_path, "r") as file: + tools_data = json.load(file) + if "tools" not in tools_data: + tools_data["tools"] = {} + tools_data["tools"].update(folder_links) + with open(existing_tools_json_path, "w") as file: + json.dump(tools_data, file, indent=4) + + def load_tools_config(): tool_config_path = str(Path(__file__).parent.parent) with open(tool_config_path + "/tools.json", "r") as f: @@ -60,15 +105,35 @@ def load_tools_config(): return config["tools"] +def load_marketplace_tools(): + marketplace_url = "TransformerOptimus/SuperAGI-Tools" + tools_config_path = str(Path(__file__).parent.parent) + tools_json_path = tools_config_path + "/tools.json" + # Get folder links from the repository + marketplace_tool_urls = get_marketplace_tool_links(marketplace_url) + # Update existing tools.json file + update_tools_json(tools_json_path, marketplace_tool_urls) + + +def is_marketplace_url(url): + return url.startswith("https://github.com/TransformerOptimus/SuperAGI-Tools/tree") + def download_and_extract_tools(): tools_config = load_tools_config() for tool_name, tool_url in tools_config.items(): - tool_folder = os.path.join("superagi", "tools", tool_name) - if not os.path.exists(tool_folder): - os.makedirs(tool_folder) - download_tool(tool_url, tool_folder) + if is_marketplace_url(tool_url): + tool_folder = os.path.join("superagi/tools/marketplace_tools") + if not os.path.exists(tool_folder): + os.makedirs(tool_folder) + download_marketplace_tool(tool_url, tool_folder) + else: + tool_folder = os.path.join("superagi/tools/external_tools", tool_name) + if not os.path.exists(tool_folder): + os.makedirs(tool_folder) + download_tool(tool_url, tool_folder) if __name__ == "__main__": - download_and_extract_tools() \ No newline at end of file + load_marketplace_tools() + download_and_extract_tools() diff --git a/tests/unit_tests/controllers/test_toolkit.py b/tests/unit_tests/controllers/test_toolkit.py index 87640ba05..0694c7666 100644 --- a/tests/unit_tests/controllers/test_toolkit.py +++ b/tests/unit_tests/controllers/test_toolkit.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest.mock import patch, call import pytest from fastapi.testclient import TestClient @@ -59,6 +59,44 @@ def mocks(): return user_organisation, user_toolkits, tools, toolkit_1, toolkit_2, tool_1, tool_2, tool_3 +@pytest.fixture +def mock_toolkit_details(): + # Mock toolkit details data for testing + toolkit_details = { + "name": "toolkit_1", + "description": "Test Toolkit", + "tool_code_link": "https://example.com/toolkit_1", + "show_toolkit": None, + "tools": [ + { + "name": "tool_1", + "description": "Test Tool 1", + "folder_name": "test_folder_1", + "class_name": "TestTool1", + "file_name": "test_tool_1.py" + }, + { + "name": "tool_2", + "description": "Test Tool 2", + "folder_name": "test_folder_2", + "class_name": "TestTool2", + "file_name": "test_tool_2.py" + } + ], + "configs": [ + { + "key": "config_key_1", + "value": "config_value_1" + }, + { + "key": "config_key_2", + "value": "config_value_2" + } + ] + } + return toolkit_details + + def test_handle_marketplace_operations_list(mocks): # Unpack the fixture data user_organisation, user_toolkits, tools, toolkit_1, toolkit_2, tool_1, tool_2, tool_3 = mocks @@ -68,7 +106,6 @@ def test_handle_marketplace_operations_list(mocks): patch('superagi.controllers.toolkit.db') as mock_db, \ patch('superagi.models.toolkit.Toolkit.fetch_marketplace_list') as mock_fetch_marketplace_list, \ patch('superagi.helper.auth.db') as mock_auth_db: - # Set up mock data mock_db.session.query.return_value.filter.return_value.all.side_effect = [user_toolkits] mock_fetch_marketplace_list.return_value = [toolkit_1.to_dict(), toolkit_2.to_dict()] @@ -96,3 +133,29 @@ def test_handle_marketplace_operations_list(mocks): "is_installed": True } ] + + +def test_install_toolkit_from_marketplace(mock_toolkit_details): + # Mock the database session and query functions + with patch('superagi.helper.auth.get_user_organisation') as mock_get_user_org, \ + patch('superagi.models.toolkit.Toolkit.fetch_marketplace_detail') as mock_fetch_marketplace_detail, \ + patch('superagi.models.toolkit.Toolkit.add_or_update') as mock_add_or_update, \ + patch('superagi.models.tool.Tool.add_or_update') as mock_tool_add_or_update, \ + patch('superagi.controllers.toolkit.db') as mock_db, \ + patch('superagi.helper.auth.db') as mock_auth_db, \ + patch('superagi.models.tool_config.ToolConfig.add_or_update') as mock_tool_config_add_or_update: + # Set up mock data and behavior + mock_get_user_org.return_value = Organisation(id=1) + mock_fetch_marketplace_detail.return_value = mock_toolkit_details + mock_add_or_update.return_value = Toolkit(id=1, name=mock_toolkit_details['name'], + description=mock_toolkit_details['description']) + + # Call the function + response = client.get("/toolkits/get/install/toolkit_1") + + # Assertions + assert response.status_code == 200 + assert response.json() == {"message": "ToolKit installed successfully"} + + # Verify the function calls + mock_fetch_marketplace_detail.assert_called_once_with(search_str="details", toolkit_name="toolkit_1") diff --git a/tests/unit_tests/test_tool_manager.py b/tests/unit_tests/test_tool_manager.py index e59a874ac..064c21642 100644 --- a/tests/unit_tests/test_tool_manager.py +++ b/tests/unit_tests/test_tool_manager.py @@ -1,22 +1,36 @@ +import json import os import shutil import tempfile +from unittest.mock import Mock, patch import pytest -from unittest.mock import Mock, patch, mock_open, MagicMock -from superagi.tool_manager import parse_github_url, download_tool, load_tools_config, download_and_extract_tools + +from superagi.tool_manager import parse_github_url, download_tool, load_tools_config, download_and_extract_tools, \ + update_tools_json + + +@pytest.fixture +def tools_json_path(): + # Create a temporary directory and return the path to the tools.json file + with tempfile.TemporaryDirectory() as temp_dir: + yield os.path.join(temp_dir, "tools.json") + def test_parse_github_url(): url = 'https://github.com/owner/repo' assert parse_github_url(url) == 'owner/repo/main' + def setup_function(): os.makedirs('target_folder', exist_ok=True) + # Teardown function to remove the directory def teardown_function(): shutil.rmtree('target_folder') + @patch('requests.get') @patch('zipfile.ZipFile') def test_download_tool(mock_zip, mock_get): @@ -31,7 +45,6 @@ def test_download_tool(mock_zip, mock_get): mock_zip.assert_called_once_with('target_folder/tool.zip', 'r') - @patch('json.load') def test_load_tools_config(mock_json_load): mock_json_load.return_value = {"tools": {"tool1": "url1", "tool2": "url2"}} @@ -47,5 +60,41 @@ def test_download_and_extract_tools(mock_load_tools_config, mock_download_tool): download_and_extract_tools() mock_load_tools_config.assert_called_once() - mock_download_tool.assert_any_call('url1', os.path.join('superagi', 'tools', 'tool1')) - mock_download_tool.assert_any_call('url2', os.path.join('superagi', 'tools', 'tool2')) + mock_download_tool.assert_any_call('url1', os.path.join('superagi', 'tools', 'external_tools', 'tool1')) + mock_download_tool.assert_any_call('url2', os.path.join('superagi', 'tools', 'external_tools', 'tool2')) + + +def test_update_tools_json(tools_json_path): + # Create an initial tools.json file with some data + initial_data = { + "tools": { + "tool1": "link1", + "tool2": "link2" + } + } + with open(tools_json_path, "w") as file: + json.dump(initial_data, file) + + # Define the folder links to be updated + folder_links = { + "tool3": "link3", + "tool4": "link4" + } + + # Call the function to update the tools.json file + update_tools_json(tools_json_path, folder_links) + + # Read the updated tools.json file + with open(tools_json_path, "r") as file: + updated_data = json.load(file) + + # Assert that the data was updated correctly + expected_data = { + "tools": { + "tool1": "link1", + "tool2": "link2", + "tool3": "link3", + "tool4": "link4" + } + } + assert updated_data == expected_data From bddffb52753b5971153df742bbdefaf8f22cd104 Mon Sep 17 00:00:00 2001 From: Kalki Date: Tue, 25 Jul 2023 18:34:29 +0530 Subject: [PATCH 35/77] instagram image added --- gui/public/images/instagram.png | Bin 0 -> 67772 bytes gui/utils/utils.js | 1 + 2 files changed, 1 insertion(+) create mode 100644 gui/public/images/instagram.png diff --git a/gui/public/images/instagram.png b/gui/public/images/instagram.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a8ecb596974b4b9fed6ef8df80b3a0b8373ff8 GIT binary patch literal 67772 zcmb5W2{@GP+dqEK3>^9n?914VZOqL7o_c7$&+~oX_xGRUaB!&Oy3gxeKIi9LZud_a>Tz=laRLCqZJ@7n z1^`%~A6Wo)IP`4>SKI;sXuv?{ue07Uq;7jqML zn~mZGSaQ%J+sghrvBv}8YEu(V|E&J=0Gs1Y*sZT-f{IvIUG|+}Vaf)48a+Iz&&Q_5 z-=(FEpXw&BX$2>`jTeuk3UM^V=xK`TCp8BJ>E|Z!$)IT!ioCsA@bWxv*!>>CFp?Z1xv>kYixB+_8aP!S9fW%+u z7BTq(m97pJmcuag{=)6-%6CBQ1wr=QyWlD4o0m$cwfI!S4&7i7<8#&s!vX!IG&}hj<2Vl}zE~-^*tVQ*bAb&XUUv&~X~~CPo{X!Q zYF-k7MOX``=bK+Btge_nOjkH#erIN37s1DCS0h}xq0ZkC@nR$o-n5d6akrXS*1v3? zZE$k)=FC?t7#-+SuJ;583JNwz#D{ZJiK8F1u**YM*ufz!>}5-F#RXR$#VlumFfqf+ zd-HIsR^#Nrudv|v6rNl*{L`Hg;M1Lx821Jg!J}buHoP`HdL$o?Cs#6~3z{7d^H=*ts066#`4N}32x?i%w;Zj(h`x7yLZmBoC~5&7f1 zJ3O^U8a64r3*iKPPxbRsT#R_6qazf(1=!5^h@Xa|CAcL-jbM@ro(M#!GlEULo0H`0 zE~~iQjzVibp(@RlTwk4vH5AfD?sg9~8wf`?e#jAxa_`%U{f3XlRN^g(35X&Pd%P+k zD@b3HaUS&PmxSa_2u%1!oN?2@Tk@2!aUEI~Qk)5GgGA0p2==hGZL%M%hQ*vyeCLhA zdfG~{=O|1AyTsM`qx{>UqhA^e;;3L?by!df$4-E8q=Xhj(}$!BW!bpice9W_;P^5o zKJFzFKVQI~#W}7Wo1ed`5El;J|NO_dc52{PZ*NzY(Sr$_Mr#KV4sYPGdWNirCoe{D zN=W3sWU`rl-DSg7r1%f6h3-$HU!Lo*CF?6c z55N*S%GfXpc0w&so%i{V%US1j60#Vkhd#?ekXn-qTJ+(9rbU}a%bY|Ty(sF~nORj3 zvuECV$3#Tn5Ks*pKY&zhtz9c7Jo+91%1GC?MF@j|kt2tI`;1VR+skrLl1F*xXI|%b{uLj$~n`sRv2u?@u z8Q5;#EX^#}joq>Ouvs z;jclaojb~w-#_{V*OZU81{XC`?Fq=CyE{i;cB8Z^r)tKGD39V~;6enV{@z1AUAavdw z=t$co5n{jq3g{EM8^${u+j3oGwCtxDfr1lf^X#6J2z!*Y+w+l@=b01{;-;kOzlAfA zKvr$%W(Dg9-oUdPVt7tyJMo~qHs@KI=?LMOg6v!0x360>CWc4wamU4BtLZp4ebkri zv^|qst<^A@FTTGg#Tq_AJV|m*bORk<=AjkIPdbPsrIG!MVxOPC$LI(}G9L!&TKgkL zOGu{)l*c$AlUvhlen`#M%jD&<`I6`!uKGT>E zh=KHaFR!+jt^fnTjum|iK!us{KkwkGrHsO{AJrb>uiKZOKCn6SIFpi+gDy9q8|PnN z0H@-+xTO5_06*#ekQa!vHeSqL8t4WF>zfEh;TE@GX&nzw38BwyXR@1qhg1;~jEnDr zFRU%^ay6nJ*KTt8w?tXRkhT*fxoTII4`Y7{EV4)#KVb`ca*s*Z{l{EK9pG*x6e+2L*Zy zjud=~VJJt7tCra>)~)57AcB_E;5J=IOvHGfX>XF3pd~UToA$?#c7n9yQ*54bIM%TC zA3Ub9UcHt{GNZ|_@0v22MAPIdz3_B0K>uQH|I-xa5?9uKLh+2I* z_Q-SV)yqt_sdCO7Q9)>wUFT5r{wx_4Uu3@<`SvmjTZS7cDw_DEo*^L7(u^vCwpsnE zgf9s!R%pyB_})}15u(fFDb~kJd$>t!en?0)3iim54$v#3)K zf1zU*&K1Nc19EG%al}^&lN`5ykZB+Lt(hcRHdK!TR&kz^jR8I4__K6H!-j6JHt2wl z|MKCi^U&>}Jc6BN^@F)-9pYJd1nbveKC5ab)(Fthrf$M6JP$Eba&BM*Dmf zg0(*kkSExlQm?Q!dbDg6_^va4pULI8+q&ndh|Cuymk5os~z!b zn2MYTd$^x?;+hQoY2b;+m{PC7V&B#wSo0CErh zKIC6ft)Yf_6R}9u00VHj+KOX@ukw2ydOu{kkB7qiG=)+cz}mP}R|B4v<7ioB`2|-{ z3{C!uAz37vOOZbC5KQcC7iQ1BW6>gztjXkb3J5z~s)@krJ#aCZET$RX8U{Xu00T~5oTCcHYOqi&`@yg!Re@W3!oxli1&AZHl zC4o8cpcD5C+K(puU<&DE;m^1d@%EM3X~fq$jcmK|A?OT@hbus#YV>wn{te^1Cy%)d z1KUq#zW@UQv$jO7)N5LhB+h6uxrw*=961)VR9TATvd;JPFOD<%?^gm|Kc9|bB~g22 z@xx|y@{3!N<(Twx=-S{g5+5tRsvg$$gIl?k!`G(kPIpIFT-~0tmr`8;&PTr-FRi*7 z7X0Fk9>`zTBMxz^69Rts&pu$Xkl14}G1gVud>NkhFdhCdezsRodNkyQfTz8ipGwN9 z`?=My-3MQZVAL>$dt{M{N|f3{qL#Kb6iSEXJ-5n8GmEr?)>4C6qSPb$jZ!7)j-8~- zANCnG3J=JW>W3E{&%Zq5FiD>F_ws@$2ZLG>a%3lVBCm87iJGodeuv6}T}ADu{{u#o zW8H-Wm%s93dIjX!zGbpt1M^Zvh!n4jUMl(a*_@H)DIx^ZKcvG<4NTStOy?34iT(3W zp^7Lv;Ao8TAL^38_V|JqyhQv1AXvD9pS>~QQ`S-73b^!cD9-7Cr&sSwSJ2$Ozc}lL z1p}{@EGvx==V^gBw7Tl}CF}nn$;t1+9i{+ZZPy)PU4@%6+Bbdq#*!gL~vP6XcedHrU5@9z z5P$JPh%9nu(Xu_ZZnES3;hnALH_~CftZ-!{1TYyYBkxxr&^5>RD4Fgt`U8|Bx+UYM4_`HB0mL&O0jN0*UVjK2A zkUG(gnWoVB^vNhYNF-Qv?Ej-w+opm5*}!~p#rXv@c~1eE6HLd>8SKL29|pomA4W$o z=*WsGhABF>=Y9V_?cp#65c?HK9HbR;35cmW$s@tX6s0n^TI#I`(Zy1B;Ohnj~>#n#q-C~F6 z{V!nT-wE^|>(8{u_cUw6q}s~Zo(%l=DF>AQw*X))_$5}+w>X`XO2jkDRm@0Zk7I6Ur|?|ky;Un@TUBUpGd^klwYj4+{bnR zQq>UPd7kwZ47*}v0*6vk!%?39$Zk*2nnDP+(wV*3W$7{`XY7*AerM0eM7UEZ@F>5cSWdb3`{~r#{%-hI3H>^4i zra?#oQqRTexx*>eZoG}k*&Rov`vsWbacj8$@KM6_AP@YjiFDw%vXc40>>Jro!)d_I zzWqGpu*Cs?!08VapRcaC!Qh&t^at|SSW(5ZSb|f$r$DU!022U|pIxh`;7gme;f)&L zN^+qqFda)!t@vMKt8C9Ve%NBsFa}$lX>92(5yI@oa9td3XCpn_4UduIXjeO6g6b>b zTc{}5RFqw)8GY_;`Hv08xM8tH6Wbm>>`J-`3@QKzUh!Q$!F2mzgCzXRbSfbDVdfqN z&3%4^YByk)6xs@1zU04n#1@B?%pwXZ64CN%Bb=XP>*W`i@vvf$uN{%Uz3VTY|g>NzU`<>c`I!f9jTMwj|b!cqh z7@X^;xliM(o*0tNFpwwgN1mI1absb@cbNiL z@Nk>(^rwQ>l|2kMt8M)-k#VR!AFeHFLx^^zYf79`1x()Dlfp1HHkt%^7@y~-OVCMGJbFl5n1vS@{H z_Lt2K-eupIpzJ;LpUQGb|HZ}s%iWHp_pMb|RKadK{e>mxA`!+$QNz&nc>Y`ubL*kC z=T61tkD;(~J}`QI`qId1;-lzTE%VUm*aUT9uQ?GY@i4*J-*n)q{h#XmcizQ}@yA$q zpw%#LfT_n97%|w`zHn5p*znKYu9#*%#KE{?sYlUaWCKqe&5~qM)^a1eS#RP-cEnlx z0mHak%&GlaY6&fNdMfSn9Lb7wY~ptZmExWHX|2;y}%Xa3Lz zp;IJ^HcVHGzO$N<;hjC@Cm;k}X!7kMNqz4qh!}yg`k$O{{(ox_{u0PoGm*usiN6?m zB|aWu!9e+>x62w%yP6|Gu-2Uecrz~#wlOc5fc@tG({!g0|Ijt6zR>I zEEgnum|#FTs6D?78B7~7NghxIKUC2Ps{}h_+c~ss}b1b)^MlT7zYu9 zidao#_6_NVznH1dztS;bfwMl{F+l{|YdwjqU70xq>4dz_c%V|y15KNchh;Vd-pw4K z!AGdoFvk!LKa}17yZz8xf%DWc06RRE4XNWh;ZI%WZQlQ&1`N;E;$k@?(@vc%bOkSs zl(r7k$KCo@SpAP~j$y%)4Sr9G5<~G`^)u=K$1Wk%iiWWzWdws%n_GJ6YbB&SJ3;MTKHIk}0OW4P7Toh%*|7 zF!*Q5Q==BzN1*BjggeBh|Iid>hriq&fo}|!M=PgnV)x8Ld`$C1D;IJ@Kb{P>*HwRo zAspajk#%`lxdJAO^1gMeop;%t}Fd0>*lZ1|rZMPRr`Q3X!3*cr@}+VKo}JR@sBkO7rgBK~a`F_p9k$mNEZ zS^?gNeFxaE%jE|!t09x8X5_RfuNf>Fz`xmr&hp>EYgXvJX44#a9ITpS#dy~%gJ859 zdh_hF<@(dunK3Ku%vUjNU$Pi>=DroS@3|FPEvm}=CDdN-`W2nZb=Tmifa~0iS(tQK z3R4?{Q1Oi#K&&b&=Fr!qC~wuIG|=9P5Eaxg-eZRdD;fNl=8Ll6u;h7oIRB=mvNnP6 z@#8*xhmeiPs`cg`i=#opUTMl3qci^3urxclhr-tA);WP$ za=L(jDX_}jwg2f-o~IBMya0y>c-USgiE!)m`tRno+Ajc^O*!iZ?`WZuY{tN99rT2BMDowGA15La zQCEXSu=V$bVX}cm)ldx(^QRy>g_xbTWGOGBb6@vmoSo(h{4mFl8aRMYn;#)x7^!bz zH+0t1DNlcl;2=@@@o4|qf(SdL(tH&YO)_HkS}F|#-jqbDpOaY_EbV-{cMUQS19FPXUL zpo&N?n04SE0y;FFn)g!Qt>xCcg6?SU_r;fS*hLC?Pr*3TIO3fYxi5e?<7WheXP#En z^n&JQT2tL3=$z}QM&H&kObx{dc6i=yhlIpbdEO(SlU5+sK3C&GY&>3+H5`e!JZ*X{ zU@2v;P2FD;!_r5HMzQ|tY#ElzGiBQTGYZ-~!EwHa!lou-qcRTtehOX$J)gc7s=f6J zwWO!dHmV?5=;0?GVU8paK)xvt>j%9L0H3uK+AIGk#uZ#@!hyx9Rg zYckkT1g=8u^XbdAE`v)ULvBD|bmsehWDuh|^Xp?LT(!%7XjsnFiaNDD^E7r@QyM;2 zDF&O*_Q`@8+)vc4Iz-S1oj4okzA9B}vn6>#4%oxjyN}b6HvJ5&6DVUT_j+Up^~8T& zq+U`Te``uzgk5DU=fm26s4wv4x@cGu9g9?Mn*bxBVdLF$xZ4abc)1-J~v{*N9PR2bqfjh2E%?FqGVXjHp zxoY(adN$oU!8E|D zMt8R%f@4bA^j&=Uk7+eQ{oy(hbb;f(#=u!RLc&E$2x)@y(lY$3xzL+`WOQ_8GE0{;V9uo<#3hf~f2PIL+%y117UvNh*+{|`LgiwGWA5v?QQ0E7gS zR;>=l>tNe(9x@ptcWNw1Q(k|75*dnPSJZ(F`Z**_sdrz^mnr7q%@etj zpSIWqO+DBc-ZIQ<80uWMyOCYNzMCtU&d4&@CuF*=j!t=UPp$@N-$V7nF63T>nj@5w z_iaDu@07bX!%)=9a7<^nBva6DJ#vd!MR~;bExj1nHnIRUcr{H_knEy3<;K93 zbUZQmDpdbHIpR;*JyTnz$?o3!fgj@YfeI73i)AWK)*UdBRiY5OWt(eeQ5h4{+Zhx~ zKaKXGCSvDfpm}=cNY*g$Qju44dJ|?rHXg}7?q|MvvrL)$e(v|$(^vbxyJ&49(Jm@? zw2^4mL%{cX*l^%wSjO5jSxeG{!s;~xM(i=RT-qNO&-@bn^vlYU^M4y-__`Y4LG%Zs(q z_w%{nl7V!{ADlv*#o%$?odHDRK!8tnHLNSO+S*~np*30B5c%7K-LqXJnW)c+oq==& znwCBPvK?wC0CSZ!(TLlvA{;qA-rT%VEqKUi&!l_@h6u^-vDl0$NCZCRQOsS-uHd!5=Dl4Cq0{6%=dICHz+k zS2@8eTQ_eG_YD&|32P`GnT8Uhbxrg+RG;%Xn)yWfrOszwz&*_@Huk*R7NqtoOvj4M zPRDRdrBg)1R5asNf}!E11BzE`@+{hSB@sAz61sm&^jSI9Lv9ra=+L~ux;@D2`Fn7w zk9d2tY8_FE8&3HgZVA_qJIg8oPoaGie^L zG$tA+_41IGCni?1i|8iMf{q&_Yl~d(-a%Z*uXE2d8DK(3{pl{wa;@tJ!=^f)AJ{j0 zS`7njSTtr$WJw0gOfJ>jub)ptkSz$66C;9-ET>Gx+ zU|3JUlo;^1$Y>#Aph((}t-`)JQ)(cIR%CQ*|McFIJ?v(ZS3m1d8Jzift~D|?HESwq z;oRS2q2%1fRj<;bB9ye51qugQXMd^6Nc!*tHRK%Op`WIo+nn(uZ<*#2&L zXg)(6d?zw~1^()|Mx}OM>I=@CX>HLf7c^rtyD)p~8H}OoVm=}>_4S6H*N&;~Pvtl` z_uK}2vpsO_K4v{lmM3vwNPh7wXH=;dFICkWsH`yPQ$6g}UQEr&eE7#(Yc!#?ECZ?S z8UvxJ0b7I8snWcDqcZ41Tff%C4tt`V{b$CI>C}SI5I~MYn3~iV8xjT@pA!dP+aV$z z#jxgxDC`=GJl zGl-fCm5p~#kKY2JIqlG^z(2c4fyaPL5Q6HVkls$31sV#{I2|>ROb&M|V^I!RIh3$9 zxh)U&+N7H+*usa8w~QVitWEFEGTPZ4vlI;t`~D%@5b)*N`RuIkWl9z?|r5YqPw==I9(REftTVtf&0C2 zJW{kZ$qXMFL`M=nZ-KCFUnQa`g~rLtxnuYn6+s-0RE0}Ev*anC4rJF)%F^Ch!5F|W zdfx9%I)2X}6QQM#-RGet7AFgFE_iHZE!&$i7a2ur>_dEG!mcBt&%;)x*Pt}E$-Osg zoqBY{*3t(LW2YZ3d7Li%Ba^ihFlG|RrdzMJjlF!})VGyT5Am01_CX~LOR-!5?3ZPwTg8lq@01Il^>729n9 zHN)PN2JBFz#GHcyV2wQAHaUvBB0V#D+6zABy-3$M0Z}z5lm0j%Jt?aJeiqjhV%O<9 zyQsbn9pCymFmvz3VYA2OL5W`?MI&_TS0ox2tOd|E{i;1Eu2{MydUn(p{5?7Q)e!&R z$7dw3Z-I?m*pGLRFykKehK$xS*WT^u&&xh?jNw2BBuhSlJ&h>gJ;M1#h|&HqZ46o zRl6TTZzQ+i?U>~{d?j4u1D92MkqKhOn;*n7_7qJ}yw31fs;R6uYfSo0#(JR5>zmWS zYi%lbn9AC1afYXl|NPg;)plG8P@J!|CH7B?v+2A!%I&dsX>=v9PaN6BnQ%A4OsOKt z#2cD2XXh&QY6>%k7V__Wo~@UPSBWfWa(OpqZOvHvE>}_;G+)jx^UF8IV0b2Nj13k|7zwH)MFvg5HeKZ7@S*3?1F=YLy19eW_uv zKjSbpqC5!Y0|U}U^e=Sj2G&cs;%k?RZ@b+{F}^+o+(}dIxpO0R9;$7hAfi$|p!Enq zHMaJUn@q-Fza%(w8Ap9mSWAJ?ZOEy_KBa)WJ_F6Nklzqma-C`z9_*5{#CCptn!+6k zNTpNvg)JXhq|ml$D?o;$mA+6|*~F_(+q-NqcOM#mHq*KF)BO0ayFnNQ?VV`;*=zZG zMgToOhor_~8c4p6>3k=1_gct%uSw6oc)ebZl}e~&hvNEqYWIYmEVyE?TAy-iorCJ% zg3u<;i8$JUgyUcSwR@$QVK54330ax2GJ-2N>VIYhf#)!}@$>xu?P-d((HnL20GY>y`QccG4Kdo6ay68BqQi+?*N*7|KWhw1#M zOiXjIu6OF|4ReICpl9ceU|);mSxp#$e5@e-&F)6m5k3rd@sN;}I(M+uj0bk%m5UOs z+#<#;I`l&9O~`%SOJ?PRp|0*3#xjXxY7y#>Fs$kvfZRbk2K&(mxwR+bXStO)o0zGu zB=qXU3kzF+-#gH@NW1Ei%_e9rl=tlh>+}Y#^oR!%RJ9PnfwO|>u-a8EQ#H&TH*khd zRc>4ua%r0BzkywTABvczm&iH1IyBNXJa9g#uNuZ_=~BFDz?s@5xW5yX^U?`;`4HfZgz?j5vHEwWg?4?}r?(xdnu9iVeF_A54r z-+;s@FY{3k^Ocye?|(YRFi!^=|HuHdkY*2Yx+8S1VN)(atpO^g|HZ`HU!$x4I+}$x z-9U=ro0vo5rh!8o5Z~P2;wZx4XT-3=j410;x* zi3Z^)v3y7P4?+%#E#B{1?}HiSvrfQs^B_JamGJ3zx%##0*ftc_Ri+w#gvQq&db|ZH za_dcG7`q;Mt&H{HF0XIor-j4^Wijk57e3v65n?m26pyr`tqqvjRM?v`up;^#vddLa zkzCg-I%(>+fmm96;MpyL)b!VYiYCv%z3)&DigOsie>Dc57R4j3I3?`}c?AEQ6o-;> zDdktxXgUjd0@OHg1+sG)yN`XP5#4G;4;QwZbs;OhV2l2iWy-5EcxEcJTJraUv(HVY z?1J5+&R=P#Hsi-PHujQhaDvZ@mt4WU4WE})9btkR?^ObYpT3znXy%Y@RLX5)XSvTx z3E<6$jVGm95FUSx>?pD~yv3Db(QERudwyEBXkdn-%@*Rf7=HpTDZY#jD<+>8EL&M9 zSICajn;QA%=zmEC^MmYdmo^iB?_1^kbXAWxZMrMOsP+dA^#&)&Y4Nv1xlX@5lrm2L z8-{oIExD$C@wn6P(CsxExRj}GXL89*377XcQuez+!`^88NKXLLMq6Mv@6{Z)hFT-_ z7ha22#1C{|35N<2eL2KPLFpQKduAF|L^b`Y<;SY2$U+>B)aKR7Pb5ZAiLEtxfM1MS zetF5Y^d{Oa#`HWW=#H-+H^zT!Pyh{!E^(VL_g1Ia{J1QVGE2&RYO=aO2KG)urN#(h zGj0(F#j5|Z9D4TN@k%{T&4}9P7KSL z)t|3?v?W@VX}+1c>mlDHj-+YPeJgsz(a$=#h2!Ho$eyx-m30p0p&DiTmnO{Z;~k4q zpgjh--XWX_uVY%0G1+(g9HafBWAQG#4Un2%<_??c;IiqB<6oA(0K=pstIn;Ja!zX9 zu>R>4z%DdP5(nGqMeO=(vu6Zx`*+2QmWZM457E7*Dx~emzMyX5-zyCs;TkAM>KJBb@r@YVRAo+1%TZ|VFvh$D(99pf8Bi?nLT4OOQH4XZ4-7|PdD<#1w;=75e<}q<_61C06A;9Q_T9*w zsbiHCcm2Zj%%$_jRr>|dG=rLHDU^849r zZC_|66JOEI%`Z`ZYycp6MhL_^V+ZFcFnrTrkj!#LvpGL)@U!ms+k{)qtvcSOBd^eg z$e5X#fva1`O0BS$>o#M?BR$Oc_{2B)tArTIji}kz4`vy$XX(AK{N+Hdn&R0Sd)718 zp2hN*_wVR`>g|8c(`$MSYQB9Xj^8t4g_;Ol-^b5Q|Cl6dhknp>s9k=4Ojuz-y3WaV z{slBDPOqi(%eC*QdcIBt_su6M&VE=>=t~%nAx@7LgDzR?-MYgAtoqF z`&(drGf(jp8_%6j56s)knia0aa+S3p-)m4{*!1I%UY>SOdyaB{KLz*HY(+d-NIdg# zHZ#eNyj>-X?I>n}}w7aBVlLI>t+J<=8V1545(6x=8VNW185zC{v`h^M- zXUfZHM$cB$S2kIgU1A+DuaOlVAOqnI{s{Imdb9F_a*sCP^)yc5$;@*|+}f$Cp7q6o zsP5D2MDkpw^zh|#)PL@~Qo6I#xs04(FGh^uhbK>B2x=@+6q^C@d)8yfZ<&`3BJ1qx ziac-uvxZNt10)nDMac_>X{~u{Ydibyglb=k+NYKBtzr|^!0kdu@f&i%ysTTn0!8(EW^UczF`$-z5h9`6p)IB&de58$ZRgU(~%bH_jVWId1z(t0=wl?QxVWSY*A<>J8&>*05 ziQjyF8)2-{q-nRV_WTX>ET#8moiyX6qu*OX9d3&MCtlQ~7;s#fk|b8u1o*UhD}CF%ELnj(`k6R$A(T~r*UKmN5M8hO zy%zRgwowvWTTM`nE3G?;H^t)souwDYauXu1Vky({tSW0G=IGJ@?@O%A8hBA$;0^yS2Q|z{W;XtuSFEGy1>CpUt=H+KGGJEp zHQzv9+~unS&_pFg!dQZMTG$YPUIEqnzGq`2+IymNGuo$j8YEs94-VjqT=A@3c8a=q zC`@hB*O;;&1*NBkfZ*7^AIV{oDV7nk*)CV!+W}~-#=Q>9Ld;I+7hVl9{M*mlT5eY# zwkG^lPm9Q>a_egqY~u`gni_BQ=u^De{GMG(WA3e8RyT=@~P#LJi+INKo4)+7S^ZK zF)6~pTQy?JNEdgQa*@WIu50`en&Vj8NP3+`uiugbX6a-!DB}`qvKr#6kKV%arX)aq zX4R$jWN@eNholqLa|A)2Fh`Ttc#_oJv{#2*leB9mLE@t>6e4c zCg%}jG$#xmcbt1ga@PR#l3!k2=+EjmE|#3PANgsJT?Y5+0m;&0ult%+0^c|W$gTbV zfU@8Fea<5<@+600HO>bS>|RG_^QIiyUVtS1cGuE`4TU^!a-bmu?$8Rg>{Wb*2D@bge$hdFe(%u(K=M5rOM;hE zwBUZ)aj#A~S1rT?cdi}D0{ULJA(Zohm!A!Dii++fidk$*i1X$0?9E+K%9>rMO>6Ld zx3jHn0@^^D$Bj7qUpfyZ(I@K}kknmM_-iFSzx0jAcP?zYa075S)=Hbw5al|)1=|sC z1h?|8W?oVQR@wyKk)Da5-P@x9bMwim ziHoz=!nC=Iab4@X0CM~G{jM^=)#8V~Utw=Yrz#v?40{4K&KeP`YLo_*A*YA;6AlX;;Llk0OoOX|W}?>6Eb_HsR?)M2iK=PH$N6;b6Y+N$)p;uv zt=oJ|RK0pdMgQ@ZRS>HbxRy5Ia4-e>s5BLoMt0W zu*e54{ps<0H;oErnj0xB(V@sJ=PIgq;K<_oTHr_0qmA%X|AXa2}9gQ zUArAmk0_=!U}-tf*iNXSmQnoo4HdnG;>fS%>r}YRY3vU(WK#jO?yETBhJ;UO`Rds@ zLaA~kLiJ3K2$nLWa2kq@w)=j7aM_X^mcB_~2(XJ80xl@@a>-GLT?tpuXNLqYW_j_*D4(k7g=V|ox6w6=ch zN*x05cPIe}j2w-B3*H5Yl_;?SC#tSg-$Vk6*gpnx#Nwkx=JlH`^86N_k z$FrU-nk5U)0Yw!-jFfl9hEz#vC!twDi^~}yP&C|aUR7Itz83TpI+6XtQjJA8r6w;(DiM(yr}|fOulZCNMhN6Y?F>2oR0SgssEJ z>2oK6Z!X0^f(q?cvE+|ch}R&E-b9wfAzCO7z~iMJ&P4ykx)gbs$H9xk5DQ(!o1@6#L(a~rcXx?uX-;@KH~fIs_%b7)e=0!; zdfN#!mcII2F*UUD*D~HgBvgEPuT8JaQ`%%qCDfo&CqG8$T)-2H6VJDKTaEWzAg+x? zzCg5_CNer$WN3f|y^Ocy2FUGER(O7s&hqIm^oQ$P6xYtUw4Q(#)uz;q0P}5LP>r_> z-6V{FhNdalv-Kj_`E(V)kAB5knDTVv1@7eYa9&}SGoh9T_Vv#QA2u2WbmJOeHy=Jl zu#7Ki5WTtdwfx0x;!T^gz3kP9&WtMKMC~3%R{j;h%E0ZUoPLv^X?73d_n9alM~iHf z!VD9zUb?64;lL!t6JFxbTlE5RWy_Cm52+Cy%6eJks1;h6x4A;vV<{+l%Y$FM!T8)U zXi!NTqGvA+;~$?L>ZT`sp!8^Xso3E!_FgtV8+@NVz0*kNM#u}ne8QCzEd|1S16yA2 z+P7M9MOoJ%xCyO0dq)s602e%4p1~2sSyi=Pv6id0Ha1|Y$FfX&+|%n^topg657Q;h zNfgS8&6{zx2A}%#-Ai#Jekq*nZgwLy$2A7{J+}u1+|GxHeSlthw&a}-*KDw z5@;&LN}x5T<1eUaezFz?b8zPe5$TAtk8nAuFoi2!uA+@6B0ICn*kZ?k!S9bzXi;>p zyeJ`G@8?4lz-!pH{j892Q+szd`Qb|hsnBzimMl;sRwSfvg};%27}Qf7JCAJl|EQY) zFrEfIayXXN1o$+$1pIsxZLo7{1O;*6j3yg(%=O}QBZWs-)=0irxLxrcAPeKRImZ(b z3pI=z18=8eFP*wi8!k0S9|imZvse%IizY>ipEAlIWu{!R{|0m1H!m6Go`xVTH>qIz zQC0br(yr znl)CG==o25TOy-61aUl5*nQMKvCp1;9$-nEcFRXo-+Q)6Y5N`5AN>lUQ}#(`n&sfn zbn&sqVj@$Sd(^V=KVcF4MK=zUg!cMFUb~}4EsaYRsx?2=Gm$oc?0eRHv}S=xbDJgd_FvC}1qS1XJpi#=pGv?1|_PSW=#^prE-Dyp|Nydr?A^xJ^m>rh&9N z@eK|Kc$eQZgx!31Ts9WXiuq;OAlM}sKR7d$g1?RW+Cd{UE z60T^J9Bg2%PTeUA{l2tY{ngNM{7*e`9xlx(DeQ6%ANbNZ6mhLm9M$qjorQ3I2*Wo- zvlkaZ*iHKhMtum7RHV)$!Q^-s;Egkm_X_FZ+{14$^>mHEW?i<>09EIUT3O^xNeEJz zlSalk>9x9`r9#Hw?V$vIkbMlui(1)yqIwK4<}m~`(t1$u16mK4*dFEte@6kL9KHt# zi5JFE%dU4Ua52oY>tQ-VB(V5MzVEZfqp&D9%dMhl)jWnxRQx8N*nr{Ev20x0E6UhG zgHYDHhH7ktem&1!Z_nXh4z%XdL^UTeuO{iTpP*mkQ=BdHg7Imu@GJUsr{l+p^3bBh z)Tx&Ad|0!v7wofMbc4&7(V|5FZlXZd1Egwq-fiH7d{-7MQFTH(mU%)p^B2X|n5ann zt!wsu3Ua}4MI}^Ao3(*kn_$ByR1ONeEAD-`z%e8{N~8XK-6o->t|qN9R#9~3o8NKt zdX~+B2mUn1ZCA!%p%L5DLzEMQEawA=!#jbyMw;))(|_UIJySAhGtO8#e?E*->L;rh zIHUlsKBDR)tKJn+zFEWX6;ovvG|W`b{=KN7#s;b1AjKAR=QI$wVjWg^;4uoj94(iD;uYzifd1l? z5RWb=X6o9HMmKx{VrzS|{mLS?TL`-l*?`csra5AlYby^=yx1&yIu&?rq+Up#{$Lcd z>BQQ!FSg-}D&Qyo0rl3Uv-zF!L3QG?kvOnu?I)f=k_puQzBhhvd?`m%@2^mdnDBZk zWRkwm)iD8hLiwVvPV65AF39`xt}8Q0{<#Y))}QN&W5M0CKQtl5qw)#!uMsQ`sBAUz z!JqA)0X_=R=|C!en<+BG(q-Q^9pvb)Qg`sSW`K&<*;Tzg*g$pnN5lvzyO3Q`LyIaDj+=n)V|ES}pTitZRNbO*6RoY=7m zYfia<8d?1KY+&!vW^tC!2e4m`H(a4=xJ{qOj*O(iJ*l0c;A-4`Z*D@oJ6K}U#Y$S4 z)&L_F4u01uZR*2kWA9AAgIJbRlD zFh^>IU0;=I?TBB@diL6fsm+F;DjSHNd+Vt1 zH`s96gQ&mMFSbG^ayK1-29?dz1FO?^l=gW`QBdE zUDKV%nH2lOt&OJw9CI=z&g__&$jL1-0P=ivbD5S_TW_l_TMJY?RDQouZjAgV@NRlmLN|wql`!<#A zTlQVD@B55xW`4I8^}OHj`~Loba(~W!&ULPHopbK#+etWvQ?7dR6#3NS`UQk9<@D)s zTU!T(Puf(fD8fW)s?-aGYdeU6^U+3w zNUvrvT^@FB_dBKrNUMD`8MQtMrHFLO6Qo#?tJm8r6yDE1qOaaU=Pn>dQ7C~V9}?LH zy>;(gzTw{SU4nn|9Bl`opqRbZGErGxM}|;<#CDdPMSUZl^=F7-Dd%Iz*%@(G20QuA zmxoy!!4fJ84=tXlC5;x)Sd6KNN7ARhv75pWMXQyhcCh&GPwk^K^R6Y#X(QU_y=S17 zQ?aCvUh8kJO4t(N2|@rM1;g->;u0AwC#38-vqH*Hb%zr7 z+TY)%x^>sbQ8C+Dgg#4>TTc_gYGW{eqAvyZNnvm(1j)%Dw>)eEEupv;$y7)plEN?% zvvwV~KB8lIaD`Q6W*lmY#xNyjyPbAAgd*;C1Twf4+{CF4I{V&duldlvKlbC>K`29O zGMqWh_I4`Wf#5pj`iT9F@o?A*RkF!;L_>fjW1+Ya_Sjc%r}$Z~38*?OO_?YXiWhmaRWDLHN8_e%rEbH zQiSP-y6vZ>2oddlwOxu~2A#qHthU{rI0uWE7pZV=@jCLJU9li5#dkC&VV!;BY4>HUyAh_s&teQw&TjtUP(RR*bg5P zzB(6e(^_qtVdkDD+Z_DzO>Ff|Fk!%kYJn@qxO23xzN#kJnr9CUcq6gUWbxt%Ne|P- zxOXX{52jK4t+`R+1b?%$Dnq3FG91@6V$I0w*fqKG4qh`IUi0#ve!>s`^i3*Cgj>?% zH}733UmY56!cuAc!55t`A}+M=#v;;!QsK+*q5Jzdr@P?V3xa@lLC{(Hhy$)8OrGf) zzw52C)Awx!-3&>c6fllK>k33jmQ4GRO4%vd9&%q{@iTki)6^+Z2H`qsrqh$~dWKN+ z(sD-A)yHMUx53eD82DD#Vju~Q8Na2cPZSib$Gn?iCeD|KE-BUrUxW6)fuC#bZG@Gj zxUiV1ND-=RM_k?i#Jk=Rs2|?O@KI&5Bc~CzOMRmR+6slby@&i~m{=yP{2U1WRRE=% z%}LKN9ujGbHob6r3dACA;2-72Xxp7NgUF^&dBMAA!;z4qgeK~%pA6Qh4?m=r?4{_n za2QwXstO_9<_rn_hMHl~FAw#rpxPUtlthhl2dm7Dl zvdq-Gs&U1ovhn-&BT!Evn>GmVqCF^_lPVZ!8xY6UcK8^r=Ki6)uVtCO^}Z(+qn?4+ z#%_LefEsB$+X_86u(=p&8@+WU2q`885!SiZX;^ z*2GEinp9yik9tY>CfNS9(qkM9k=7lIa?5qq-a%YQ=Fn)*lVWyNGPvc$dxY^b%Fx4i zSVMQ_#J~W!x^p0m%h?i6)}ufT?HC~ZJ=lvv1$y*55Jt7JpOwP~_RLAalo6vID1pBq z;yzc;5s0F9-U169Wf5WAE zG(rd}r!Sr*4KEZ!lhjtL<+t$CiOW5isPKH#no+N{(i$nK^2Cz(1VLiY_z7(Ssn{*h4Ujzh-5_i6!_{f}5i z&EeU5w(k7xne&aXr1aGb2o?t1V0|CV-*ZtEcC^aC7p~;ka`oG2bsy4_zU=Is>@{bA zRSQhfF>r)*^m1s zxdmjdLz24eJS+Gy2>k}cx9Ou)Rw#Jw)nR^76p6Rm21U#;*_pQZS`?OLAKfp%2v%?Bg+m;~RetgCg3kr;{>f+~J9G^F|}lYaD_Sby+8#Tq0XU*(=%wOIwOX`=mwA^o*>f}5IWvv*Wii% z&#T9jH?@Vs$K`}omUQ8y%w|Q)7_vm+AOh9txAXAXvX7JF_*i(dy z5F`5-BtHhqNH^}^aOS(WN*Jirj1MC4opdF{C=&`-*{F4r<53@+Q&9%{2_xzxIss+1 z(f6ed8pyM$UD5bw5!J$KI~b;q*kCSgyHn%(0O=92p-(`xUN|RvoXicT@io&U!O3Ar zTbv^yU6`Nw@^YklK)&m!Y4WeC^&Afq&Te-Dcfq}yYq1UlzE+r=CmdV){5;I}=Lhy! z$dDRzdKvxQhtHq(%2VJ!yy%3M47SIB2{J3@IDK^kw-<|% zX_Vm}(qsw`F}rFBDf9a~w{jlbK^X1=vU}FcU}QtJ4C&d>!(i3>!DZ@%6H%Zx?j42B zrb=^guatX1L$ogf+ZahV=?=#^>zch^(%smp@$g(q3HZ{$85s1!1|B}PUq=*$n{yohjCT-f+j3Mdnob+Gyn%}pBOCfCP-zg7(0}W#Bhi7-sjSc7 z@7}*aG^P2T0R_xw`idMe-FbVv$JYRGb)>u5y%sWrk{OtMQ5p?1WWU#4Ykg}X6WH|#%164w+GPsvm{f zmWI(;%qAll$et6DKTOzIFGHB2Kp+h3pGL%hA=8aQ*hx3DcJ?oyydTLRmdRn}-*eeM zutlX4-UyRxfH440#}bQCR!Z(O?#w54+==18MZOW*o2+~8d{$UM#fd;A*rD;>h|h7>4VI3&;k8o9yt3eH%L;tO}}q=+v?88{~1(*aki=yC6ua zc_c*LG3T=n(m zzpxW&q_fFhLM2}6B9~?DmeTHd2PMcOwdQM@ybq4ZZA(OrtX{sj&2+F?kPv&^lgpNF zk6~h5GSgdCLfBT$uO`4x#*Z(n7{8%D1otjK4&hV!tX)ig2Y@f^^#vHq9&9dEGNZ8x z=$Pc_CRm{+WI}XBL%`r}6G`xPH!ZVcEa@>(AbM&nbI0%%{F|*^xFVZ3a)>!YewD}xwEB@lB$r=7WN67ww=LFR6;B^@C3j(;8#wkNe+2aJ=6lA=6u{V|X zD)fVr-{%%!-9fNza}x;fVAIw)F$Qg^|4`)P0D($z_2ir3T|jqUYSsQBdvrSz#_bKD ztwj^{eqAURUJh&6p%OP9v6VPw!s;r!(a74P4lB)1-fgfN)zZlx4u&=PHGworCY`DI z33{v6>f=oBovCyyqSL2sEV4fCvkypvRvU6t??1pbrl~WIARo1GsKU(m16jOjQdMZ% zuqFZ&ZcIOD2ELi~b7sJAM`8Be&xD&eHWj?R!x_ZjS&+TX(2Ed+>@zZiqMU<{(7gH3 z1oZJvIZQByepTeI@3x1eMduXeL-KQ}q?$l$;!>@cRKf8TPr3skmp)15fZ=J=7NqE7 zv9$NUX8{5Y7)4xH2fRbP$gH@1F0evy*p6@9=cx5 zgiX6;T&c18LgAi9D?Js%RMYLS=V=Ya)BrOdxIfv;iHRuBbdubC>FAcjDAL|5-lmzS;R1hmW=Pk)nW>_BqpIs6D~8Iz5cC2 zl)lZj-o~Ai#ZWbROc6yoSluNogZ;dKhwk+r`2M&{_>Bi1ZOGd)RS(4>TbzP1+8vg||}hYW+K* z+cN1qo`e0QeKUCZw588sa2|=!8{YAOF&oTNYby$xnSq&Mm@dc=dipp}xS8aFr@usk z`vNn31XKwzWN(bbnQ9}G!+_amAk~)0e1wi=19~J-t;%(AD2Pxc$j`pDXh;=BU3+AE zYkRm$Io^9rRF_+L%hhsi*SSRCM{P3DJ~c11t?lHhK!~dkND1J!3Am2!f_9ipvWF-{ z*0tjjb6qpR%8sal} zD%V7Q&;(-$+({TC1L+0+9)9g8X|}u`L5~r>{%jTeZbO*?`3BK*#EmGOIyB!`3&0hq z%I78vW-sE#hnGnCei)XmFWr*1^4zcpXdYf6E`YGNAn9IK1kP#1rFvT#a{M zx#(-j5c*_D#X8PF!q~h?LPclvLr3DaB7{J;@iU~F$v_)u{(WhCsn6{N$F{B5jX^!5 zP*$Jp?nF77gU|4qPeiINqe|hJD&QcD3qf@2P$fjTDhfADW+x5zts3H%`ifv$E95@$ z{S={CRrxJiD^(H%v_V1iq$9{7E}*nmN6f0hh%YD;4;-KZFHw*My^6V5_Uz;2c7C(G zihuQ!PQQj{WGCQf5O;0gSp^}#6(GMQGho}HoQ`Oq=gAGr0e%<;p6sKgoelOl!_5#p zchy?p^rEd#nVhK`Z@A0U9zulQ@!Jsv_4Zv;S0Wq;E6uR|F-3HTBMsbL z@V)LIW8fd3`gjFTsZ3S0kUE_)9PQ+Ok9Q-uM=PJpycJnMWc z*Y8xqVp3{@E{7gqJGF}H^f=zvw+34WfkOM> z0wA*F#>#8Be_wjnENs)%J9t>JX%^^Ybb$RZD2O8hVuJLXdcQd)!WY3(l86NVSI1&G z!Fwk`2Db%kT~o%XcUVS_4GfS5{XBP@bvtlPUDxuL%jrRqCjfwkmRM-uSIcC*eWKUo zzoos$oT2w}q^$k)W`b(5i;Q2`nX=epEA|hai^jGbOp46oV zUUw%2`~LZEC;YWg4>U-Y^Uk&As(p*XxCuXbmc6Sl0~tP6Jl)o3b2o@LjZ76~X1<7W#PRteFYVFXJJvnd6TSjEC{KW{; zi3R&C6|dM7*Hb3=54U(h!R0!%+oen=#|T}(r>QY9cIP@0!<$$%8iEW#zxfIsqJMM5 zQmcgbEv#17nH|@S?y?T7%1A|wLE5z8!!`LZG1sMIByt-SZCjz)i+JlyB*Wk9Q%>U6 z`gECVtb3V^lKm$9J1p`-!7c5c_k_Y|&b@Bk<)C&}t6lqY{^wo2g`XtXGN?x$A_T>D zI-`{~Wt$6!q5fj_r+G(CWF#Xu$f~iVr-D<5>%EaTaF=yHLihW+!V_GMm;R40n!3}X zPcE8coC~u0KD6-}TWQrl1C!dn`G`OqU8uysv zrV@IM&XFT=xy(tfYtp1v3a!Lt3$ET*k*`ByI8?D-7+lvHO_$ivl>iy{{MXCsEkcTU zQuH^uPTB!b{R2eww+alha;zv;{sl^PdkPs)0=7yFlmh`WJL;m1hm&avdJErFn)L9ZZ_1=eObkav=1pxBC_eD3NwWBYMWChP zG1M4R#Q+~n#A_-upu#hA^^z&vkbiBLS|6HESiJ3v<4<5Z-r*1!!!`&cv`4ZxcL}ZO zw|e!vHM$NSn41u?at_3G7JP*rn(jp;d9LP%0wVrf?iuGJtnIT9d>$z(+TM8X$(gRG zAa=+4R@3KYcIj~w!vXJ89SAf$t6Eu6~nlB@ziyOdY6gNB-6Hm z$LjbFeuj^aLtHQOHRcXDz3`%9C}YeSZKSJ!_T5yCaWu6F;{$EH$yz$gRt)VNZuyQ7 zcaTS1hTZdFE^jCIalBDLPjqIWT!v|lesxy!!N}<$Dg>lNZBh0cQImLW`hy9jk)Ytx z)R+%F=)1RPR1o30rUvg+{(MOU9v*<9PvM=VN5}?E z8-^!gx1Qhlzdj?H>pFgDVqtYaaA3{*t!50H*tWHa5#o~SFt66dH}C_KW0}*FQ;qOA z_;J}uc&$)`ih3`6jF=Seoi)BiGI#t%Wq`mnH0HSuxtD;q|5Q$ zf+7t7*=h$QF?X&eI-IMzMu!@ZKim>N_4V=Xq-*lSAqL-gYJt?cqHTQ%@r!j}~@hk{PkZ)d}fK@vGOP+9LP&$n-)MP0sQ!e@o{vWlQ5u8@{n zoS&j?hVysSYh5|kc2!5RC{m>m-!pt+_^qQ22!(_Pac~DNX{Gg5rLG>qa3^QbAK<|5 z{fk>9#6kD5hJ6aplx!yy4oey6r^P+mdoMdr+q_au=A3fqFL$d3<6?-g z-AAuPlz9(rrvH;KD3FS!b=Q3Mck6QQ4h^48z8Y0>ei7(okyB==met=)o4?F{_}{*y zz8icA^=e!3l3YNSHIqs4`gz-ee}*gcwb%X?>Y(#Mj`HG3Tjz*|ZvdCz3$G;il?YN@ zz226=z#=P))%La`%VTYgNrg!khl&gGhc844y1a+Nfq>J<)&=Xx+5F7a(Kl2iVCa@|TkW1O1Yl8>9G@2PP?8KG!sT%n7Z)zV z^?k{ewnE-a^bPTVkgD>+c{aI$WSxxY_>otNu#j@Ok6D^UDzPw?q7f7nAd_3Ge|It@ zOELZV1<=TCGVST;it~F1Z=N8v@4;-@>q%W{sCu?qH5#l6d-z0`y~eV0i&lB`+?N*~ z51eoOxT1-tk75$?YlCtXSk|VGVBPy@{dZi3RWkFcUKs-g>2Br-4?xz zi|A^L2y_p})1e=S+6sGD?-FFe?-l_B9%@awl84~|+i-NGm!-zkmA_%;);LsvAz#O> z%wV_*OSr%UQS=3M*x|J&gGr8K#i16rHS}BkIc+@TCnOc0?fz)E9dich{zMlP{TR`^ zw*yxtR!GHv<>b;R22K_0xU8&UbzO2*=o8V+w0j&3qu|S_q~$oEq!bU{R$z?brecKP zEvKqCCh>Lz01|WEoVVz z;|!~$lEb*7%yvrGSRinKLB16U|+S{)WRllD%7 z5NC?vZO4P(ma1-#gP%5brBdhioDTdt55NA)w@4t>|I;m>d42O7N`TonbqNT+1tr&8 z>)u?qe-N`$iaX1Wmw&H42E3_SqSp<&7Bje_UIR&gn@GZ{9(^vFN*nof$4=JM&tTq!rnasaC2uWan_%8`KfqC2i)VJ_qWv%sU& zokvilB5V5tUn%$2axMl2-QmEB4p+p%+o=otYb;|JK6P+z9%IL;_%eb(sI$u8J1!ju zK_T<5!RP2hQYShb6Rd^_CgO5s^$iv<_ z8aTH$6C!9*GRKcy5EjtDc~1Lxq}5-Uo7AQb^hFsDC~txv+iUoJXiapL{~M7~QZ-9? z^fe)hp{9FHtV04<7m|0kBkS2jqsLEJ=h@f0>fRE7>?u)l3tCU7-1r%~vY!?|2mgWb z#0ToKou*}n&dFd6fc%}wxq;Og3fVR8o~qkl9n-wh8M{-$=~XUHe}y@uJbH{b+2^g- z!mYIW?3Fx;4Xe6lhMoT(F>@bC6V-jO96Ja?nCWhGzEP9_i^wh*;u))TMg2_{+^*d6>ZOFTe7< zUEBRw4UgDJmF~yM8@qi4u?nFl;EdNo|0Z*p*sce?ag7`n2#+4y_DDHe3QllCYSSlwi&D^WSNLaK+Gx!^rBqNcqO7v&c~)(+^`nr}kG9>ZEziF1^FUL(Io zJ*f;(anc)uDzD%#lbxi5xuSkpX~dK8?S6UgD&;``Z~!*d~BKp931kNlJtZa%)C@F52OAJrLWL!~EpHO_+Z14_ z79g3|;gOyv=GzT#l)p4y1Xx869ley#kGB~DKvy~?;l}#(XWe>Fp;eu*e_qOQ&Qe28 z^QA9t@Eq#3G%9#c#AJRWtgw_irZD_MW+jK6>Vd)9P71PlxJ8r&U>MdWJ2d(pu3R#1 z&L}?`*S(Kr{kS=L$q==b%j2Qp`5gnSSI9@O?>;onZ(^D1Sn?QH5zcm9irQ4AR4 zYTL2NOAnhjmv@<(Ts+{VzPsM!i+ziU(lKmO}TTtS7Qvgy2fRL{+k1^EDCFcJ&U*&mQUMm!Uq}; zLMURffhuhK&;AuD@k$IKsaAo^T<{H41%cxbZF?#0SWeG*3cTh@GVJ3=#dh9_JD+Mf zp#Ij{hINs1P8Li87E24%%J;^BL*0+Z`02wUd;TQJWAEY3Qu14yrL39WDW-mw0&Q&u zwPdJyPx^NI=HVOR7M-rigMUMMv|gJbLs!kYkuQdzbuDYC(pc=aXMp`LK>xIAv?gLR zU@Cg*NgVtFBHYw+f}tbyeornZ+qz+`LI<*b2Eo#b+F~z52;9J=1M<+6JwCfOd-bwW z8@RTvi~mN+&=#1t`>7){?St^LJhFGgtMFQQk z7G~@x$S?tuY>g*Ttc8uV!Iq42JL@ltfnN5Z0e|>be*Ndk)MjLQmcS=InyecSC&b^6l$Ebf`OOBBG z-&|_^1c$@~Kgsl+PcrkQ^gN5b_PixhTYW>*8tkBct%3C}y9fzxHbxb2HLCQJz zBplU-{=M|XeTo<86xb80;6OFJ!&W3#&0`{@4aXppRxum6h8Ay(Zw6qf*NJc!70anx z4@>33C!Y1+?)-I~6P{q~-`wXo`S`0#uyElg9*~*rjZE9@7U%s1p=gZ`jeqC7W=W1r zXDeVQ3qdB*LB-3}iZWPF5eI>hTJ&|S=AHw?o9=J;3PHtaINVa;TA5;{sV?f_EweQA!4AMUO=8)n_>2^w=0 zM}n=!UsgI8tzpsgjK{TVGQi;S{YB8GJKq9Zh1;JS{>wa=-nWq3swWhhYB#DOx_!EX zsaEA(Q+Ju_D{biaEPy=pK{%pgG~R3Q+Bf5U|IK74#e3aQW|N(WYpqG8Qb*s1HSxX0 zGZ#&P_7kXkdMso8hK1yFRwQQRZHk@=igM*SyM5=)yYi>gv*T3wrLH8MjP9*&KT(X} z013@E3I93j*SB9r#Rz;w`~uwix8Ao-O!D4)-6Hjs3Yk3mv5c}oNO}2f3@xQRk3t_W zuvb*g*M*c{jE$W%U;y*;vlK80Fs~366|>wuT<<tNVSu30A8y z8w~|bw;)@0(RPe6Q^){6!Rerde%)X2we|U5T^Ni>E0d5|KN!RZ9t#+xTz!1kiH|%B-^5~gw)wMdxKBf){eu0?088D7ykm#9*eRp;#nSWM z-#$x$hyRujo%+&^3-;lMYh5d72XSH!P|2OjQZSb9*DZ@yY76;I6rn4Ei468L@Vffv z!8%7#0#|43ug1Q%xe>eQvSaAxGBNn zVz1bh_5ER%&Vx7W-0vhOwKxY%dV}P|DCTo4Hj2sy=AnS?8-t0veygzLihyID z+!HuyrSl{qC_x0Z&qD-76h;XMmJIJZvo1!Ufgv!XbCM}F?p`(^Nq4YT>fQOkO;3_ =iO%D3NMIQQ387~U;sf00ltQx)&^PkZ6eLqrl>Pc^o?n^VBHTvk zdR!CqFN^233tMRL4$R&gJ%X)G&;9!dgJ2A2U1%+A%NRs`1s~=6y>Is9#=8Dep2s=b z@KJnM=)mp4Bg%>wz~J-YWRQJCym@Z==WsYedw7BZgqs%oN~-rch;C2PWwKWliSISr z-ENYrojP^x8_yrFi)i6P8{%774H-Qf+VeOns@fEQEyi*+TE`0%rcVLIKLI)@ztmCU z3%H6Ng5@3TuK;8``?Nb(!DqT-ZgT3*czcC<=#!`Np-8oV5ET{it`8zq#LK#XDeUk2 z3rkEF+xTqz#C7yK(XL_}cAy{86qmgcA?SR{N~Deo4KMBb%vYTkqD%k9jwPD((c@}_ z$Ehn>tK=T6bKY#-kGmNJqkVFRc=g;T$G~jVtR{lRe3PQX6#~Yek+tP)P^i$_=X>rv zE8o1D%V2u8l>V%-<{R%o2=Xad@kX z6?#Rb^nz(Y@2z&jkXq3)unG6-S7j<Z?HvP*Jz}#ehgzP6Cj*UU19W0X)Fw zwGB*N#3I#DoAFAxBB;gC>b}x_vqMGtFLyaFLl}|X(v?`5t&H$Bh~VpkL!fX_f~4r2 zc)m6nOfC0@!@AV8n;u0SY{J;t)iHFYHLHZtJkl#*MakuRvf1M(B7ucMVeUEn5l2pH3Z0e+A{3Ew$IYNEG+g5ap{fcZH z!S084kfG-Abh1LQ{1s`k5z)FXFsMAM&i1{2^;?nzp9DGLqGIJ@AfTgL>4oiNt-9n= zDyXwsqWOX%(H_GE3~F93LSr3u^%e0RlM<~;KVsUQsL(H7hK4G?ekqTpoj>N*O^gY}$zp_aH) zf6*p}YCHsX4&{I7RJ#Gv)+5OW=Bh8tV7r4ql<@59&I6NgY--6om&h_7(}nIW$%G-l zwTD@Hf%KJ?pg3rPNQ|~C{3?oJNig;9sO@?!Az+a$Tls(^3i)l`qcQS8@%H(QG4O|l z=hrC*xKW!+jqvjUomVR=txQd`2DWHVlnLUyH1d;AY-91m|8m|Fc8A`%f>L31~SV;I!^xB(EsBS5z*le6g0@=~8AGpNd^zJM^A|N&lgj zz9n8{M8l4%Tposmuv4R-y~n@$@Sx}Kt-?9rXNu0uS;~%lsw|ewf7^1&YnR0i+V(Qq zgUKu9BM0Qk9|9c+0DIoKas2An-UC6EL5^^YeEzZ}T2>_T$V}p|gBA)2Dn{3Rx}j)Q zy^9iz#Caa~XX(#g`>4z$@OnN{m1$hly~rw~HZa0Rc=_C5N_+sV2?o;5hNslZyOSdn zMBQJntn*7O@ZPM;RzBXXJGZiNAh3|!A6+S#8oF^5{U+Ga>ct%bzt(O2d9y;kg?H7} znb&WGc$3mEA9e|p1~S$0i9L?9A7x>eJ>H#tp#NrmIRUmLToQde?cZm}4+Nv2q*;kN zu};dPF@s*V#jDIkxf)Vb?c6Ia>%meVqMxG2m!nEeMF52)Iy(^hPi-!~+#)85JE*PI z9Lah4%N(L+Qd0M(ckJ_!BW_>}*d?hAO2|xe0>iuji5Kj*MM(ADWL$bwWrIddx}NG< z59f!vw&sjyMQwmhlP2z?Pkr_(6U{Y!D!s~cTWN9Xt%r?sh^q*0nQE}CVd8nmW^xj1 z(%{hsTTjSTaXKk9$D_OkmOGhH#B6iMKRGFjn0>doxaGf{%mpA##G`fg*(mhO2U+;| zDUU>W_;vn89T=#3nHUxKBJM{@8Oe4Gm)F#EnB-nh9RfKbFrt`&E#q!>izwJJu?oB{ zc{TzKj~n3dN2;NRwqchJ8}vb(;tPIws4Gxw3_7?vFdy(W@_>A1wn6sy)=2{oAYy3@ zUUs{RzOX5#|IkpP?7BL%r}92$M9klHvX>Z*!y~swBfklr+5fVuO!7D=to)#pl092E zB_Rd_XIh9vG*h$a+CS03R-XP_HNGZ*oROTIkV=}I)Xx9Q-&N?#Ql=fta3W}$tsPO2 z)xTl`NjO^)n{;y>2u5D+LcCY>`CjJ57FyI|jwmn?1p5v=rywI~i9d5iO?0dlL<4HrO+Vx+F0s z$#?6Or_FEZs2X@{Yg=Xh=eazR;UcaregV7OZw=S6V^7P&9D>;ZoKQJ>OHuc}`p#!e z9!Tma0ZU?9Nfjxy>u9D6KcA#H6RR*K5FTdf=A?n2PP2?JB=&$pWh@#_O@S{`)CrcZ z+GCJ{U%-+SbRCeLs<*0)cVkro#$^%`b?pppy91j(yokT~9j5&)@sWo`Q;^v<+o2be&KeR@jyQ1*(r z#ZGs9JI$s}w^de=(R8aNw*PT9L9mEqp?|A%A@FU?`reA@$jkQq8=6}#LO}YntG3hj zO?Q){zjBQIOCc9`x9ybV=vbUgB5$N$*E0oqJd2HJ#OhVVtmK|;fOMXtiF^q{a)LJ< zihC1>`CIHg`NNB54WWQ`pM97KlSux|P^*VJOE)fqc69ida2m1vPehphhzPJN$G8(W zNHH)vHy=0_vmQm>YFc|KA#e6f3v4|;vSfHh{_c-`i-#P4Wn)7#cFwCX2HvS>|IKh9 zyL$!RH(&NfVU@3r!TP{H`48F4;W2p$ll$41sZkzO09T`l=J+3na5rm(nE ztHF?R=itB1N6`RSGND3r>%O9}H6{_0yEDAVhN~fpM@JqnE-^a%xL!5+Jnw@XRT%5% zW#4(bexTg#Fl_==r^aCR!9-@PRqgh_k|v8e^!Ytx;zZCXmMb}VC+cq;)XyKmmW%=g zkXz~e8065kP$rtn{7%*L!IfGeb5V~yVjf*tR>5l_Hvh$}#+a|!6UQC`Nn|EqlIJk0 z?<@+pSlqF&ixvJImjun;Xs6>~FYmK*}WFVV@wp8OirR8im~inRCf2 z^p6eKpzejCmADn{uI0Zk9=yhm3uJ}?aJ&4 zRh1>Mldbe5y>FgFT^g-P#*zIRMWO26rgA15{&}ZE^~02#-_5}5WV|4r`C>Wc9D;i_ zpd9*K@9I`o;chrzK<|{W+|OGsEmv1d9zFh6N<^ zE$>eE*Q@gw7%#u;rF@bkh^CNJAevtMiKeyO?LTwN@Tp8wqQiQ08%%*dk0O+)x&(EUWI%sZY+pxaD^| z&KQV~`~07%@) zf1UT=DaUP6KIQ%|4)Y7s(O~eF&*OyH`u7nd|IbqKscX`l)1B}GqD_;oPeCgkFqoLQ z%Xa;syJY6Sap*dm!T!$*I^~3l%+{KBDQd++P=do(`d>6L5?xnN}~H+ zo88e_|8f-rZzi-8zb4p_%tnXW3&(q{M`l{C^EJUji|JEPh-byiOY0*Po$~1g0Tjt< zNALD*`oy8Xtz4%$7-XpNL2k8QN9E@rR{w>GBUr_!oiB$>%P%SW{3nDJeuc@^U1Z#O zI4U^zOE~{LHDDJ)0b#bjc!Ic^QF3=!4zIk#BMKkyTa_Z-8M)MPIa@o!Et|a&ws-B> z$PdlE1Oe6p$4X#ZOTK^MDIz~nxR=dWYI827Vd~o`$VN%v)zn9*>x`)|pB1 zsZpLdZ@3Y5AJ!@a6-8}AR;cO*zvjjsx1u3Mqp*?xI4v@7Wjh+--T+%am;wrItA|tX zEnBm+hbPBGl6oYpDCZF@&eqJI@YW1>x$4C}xt@0rBI#hZsFYAe2yW=ECP`(DdM&~1 zB-4+k$9`r0yL<(K!u>J3yFkgfvk611)t01z!K-&WaUCNVn8$n@y$Msi)6BA$^?+!( z9ask^$vl3S)20*)tFbwxPl2BtXqVfF$f-jao0`V+Hw-xCioyRl{?B~jrUSMuTrR5Z zb7cSX3BIQ~+5mFIR}7eC^f!K@6CRx3J~mp@W%C2Y}}k$mBI<>bM%Yq8x! zcgwG`DV#QEQ{5Pw@3vX_*388gVCR4ugs zbKkyuz2}n$eTNe?I%tewvht?j3!l0ZLp>%PN?Kt0ei}bofE2U)uz3g1;u+L**zbQl z%z2-S;6S*HM=lb92!)1P+Qos!d@V+>;Lg4>DkTsW5aR(hD_()8cZ(gE)bP|k1_>{U z&llQ%__!B%gDsA}EH0qss5VECyE$J{%yGZVnW1)N)ZOz9ynS_GuIV-JT)IQ(k(oo) z(Iu>4UeXzDq<_PvEZud3?Tl~F%dq24MxwQ4q^MugTd(h0d_2U8&)~Ck* zw-9v$YhYlNYuF!b5HyWc0P$U%UKS|&#%2cuy;G&5iFc~$(9-*24c(6$UcRwHXJ|0L z{$p@Jlz?h) zej?wnA|HBRveCug{R#q@g8n#~4ski3u?Xf;&Ed^j$owC`t*6Xf^f?-iehptvfofiY zjY&&a^&FtxaxTt?Xv}jIpl6H0W{{NdW&HMviyg9}c zN-Oa~+>Ha9{FHmt80t~Z`|O<(>eimVAr}#cM+0+Mp@7kx2P?JDopr+UVjr0Q9hz4& zr$-G~ZS;3JgEqth!NCFlfPSn0Ua)Ob@8Eo&hEGv8Y~>AWQGJxu@wBr73^S;hZ7 z+7OIeE=p%4h?cHq?Ph2hq4SU$X|7~f&{_6=CFQ{ozOtyJ`(E3ma+=5DYv|I4{~yfA zHo`*Pr-QLW&5AOVRt{cS3dCg~vbN42S)_LqMe2-sm($Ur;I6v>3Vg}x%b#TE$t0dF)tNPw9wk9m`199S4`ILo1)r;aXPT8h$y^*`+L?x?PFPBy8#>TTN9;3EUMW=*C? z>4^Uvospe@KF_%A5nF#@{}MmS#T7%B0dK%?7!75PRrDSYLE^|84t0uG1k^F#k!MylHSrh*Mx&t;(E9h$d|e+@q?a>yuO8l# z*~g-WIswi-wdBQizaSgF#IHYjqc%Kle|`3=R0l|hgDn~P8y90h16zOTBCb+Me!N>a z2H>LiT)nO@T0J@{D}(F=ItKSuzjRHSqj~+T{1-z0WpBV3MmNoU0_Vfoa&`3iAY0uznXIuSkYioUNF7+dNQ-aziZ--;aPA zWmInj-e0VT9Z1m3Z*{B>`@wE?g8pW=>A(5n<~8h*!7fFZf}bT=H#G|=>I+}~eh_O| ztwD?syq~1v3TB?c2T9A7Ac&3%S3-*lR9tf$_1q4li1~%ATlv$Y{*G~0><4}=CpoZ$ z(mA~T!;3zsskNp3AY@@1ip~o_$a*vq(1>07h*_|AD9wbM=005W*GDCKQMj-}HDR!3 zg{_+PZ`om=z3%K2qtbT617uU3lPCVS5Wku0vZPBKA5(=CM2om_EzsFuHAU&2f;s6DX&dl-2EC1Blp3`?oJOk zeC*zE+aDi`%N&0BVFS9DT3|FUa-AIZS%y44W_&uLBhSfHbz1qd^!d8O0s;as2iQlJ z*LUwKZfD?o^l0V0lD{IB^XcL5JlJNIEiQLlpeutruH+5MJ8SPEM@PJjm`>^AKP92p zV^id~DBjev^21j zL;r&!ee^n>0#6!&>Mf1QgV{2h)Awfwa~G}i64yW39i>FRy|`)+dBetIuBt<24Lafk znS)KaWR=Y4^9t9U-o37!DVp0>7s}C))3yKZQ~1xPsm?uQae$nRR-Hp_(Xs;H!*psJ zx4dt88;_V63ieUjx#;U@XtuCL0r{Z}abXRhXcaodYw*_>4Mi3+4))C+C!|O#TowAZ z^*EG|p$OvE?Pp+BC2Qxq%jTBsx(@e9<(z&WW(r2eCL!;7x5 zRuOXM*JlC(p2jcool-R4PwnM{?lt$u{-k2QgN&!4#VM^lTiP<=`#tko5@SBxcxmpI zrZ4Xru5~6gA=OP0d9nv_IOu9k0_0kHb=sqLFXC<&2=KSTNzy*&*{jTp>sl0xgbtZL)3sZbh8`y{@=J}-l zk4t*P(IEV*CC##J5L%pOsdQW=_!?NBa#OmIeEAewOEgAKg-9$PlX@r_gB!#yxD)E| ztegr5{%J|k6JLSm%60RHt_n~dI>YK3OcW5PbH$Lu%l}8(cZN09b#0!L0HFs26r=A;gA?1ylqPfd~pHy+{pkup-h_P^2p=NJpf%s5I%Q^xk_fA%rp;9~GbPo9~^O zYkv68E3CctTKBqF+h_I$d6=KhyU}BpW$U6#_IP8=1TP$WG)rao87@xFz zd1u#Dn(v-|-;5hGv6u2?WsZb=bNwLzFF8E>ZEOQfwvbB(!_}BA*1Yt&E`nmf;Cu_7 zYQoaynft+5tW`SPQZ}6X$#AlcD@N5vZZ*#^^FyOr z(_f@& zX$`*65nJ9d(!r0rHcCR4iP+k-`Ky0>ku4Oa6foM#XZ;X|h|Vi$6=_(f-YD_<@ynG` z=NFw;N@@qVd<0A@F()T4BDQ)OZcqBqXM_|aqwxf4ff-B*kb(xKY5Z>$pDlM!(%CBJ zvGcAi4k6<{kF`}2|~@K zPI_C#$*0!l!^S;p6AUHpi(Gr-jde{fy0Q8<7Tglomv3#xBj1B$RYN!J1}RIVYg<~P zRu2H@_XPKmp;}s-r_M~QzP>(P`1V4*6zfS{8Y|wfgLM$W1X%{4xoeevz75= z+Za1x%~B)L$7TABf*YYoyoHdiP(Me^C(TG^2;q`Dtu64wWuAR_f^%5lQ!(gb4QfU? z@-iNIzq!T>+f}eEr;*cdDC|+G!h2jXWI~?Mh$)2n9GbDB?&Fb=W4(8Z)-Uio9!q-# zYOAilyo(uk?fRi1pR}*<2$aD>M(%TRd#+7}e;Fz?U=75%i%%tx5IJx6(Dl)&st$-XhKY9uCB;O*?-}Ic2#g?_MwSyc7i>L3iSQ&=hjhCpsO-`OS{p- z3ocXViF3K?r{27!G+L_AwP(8;*7{$mRI!avbWahZ%-CVdCO>1Azc+z(E{&pTD}S+AinF zcZQcbf8N97C;^XnxI90QThopWba#LQR3;*QY@4@du0#+hFDzlTOnnyaLdR`gc$%q2 z90{4?L*+p)qatLSk^84EIefbJgzjVTVSVcb3PCF@$yd_f9^3deo zmosqe+3D4S!YNNXemwae zOt}-`gJ{hTW>AbK3Gck~Sw$Rh}lQZyM&juQF zBg%fhnJ|H~q93Z7_}(0EEg(E!Z8BaGE7CY z!^}9PEoC?dO`liSIUVb75f}AvsIUu9yzg8Fx>Q_*hF4gL*x5Yqzb*bQfcO zj(fuapSVH`Nxo&6DfaLEX!{Z&2up=5hS6E^5om91cV1Ca*DW&5%&A)>bL~8VvU&oZ zD8E6V%BrBeW%cQ^)bWh=8Q6eUcN1Zcwo`=edlzAo(Q!ljuxsTvz;otUxPQ<98q3LY zJclQnFG24+(USVjXpXndu6#3aOC5$+NrU}Yr}*;;ps0LBs1F0Hjf&Y&p^j^6B&f}P z5*9Xb2Gjsp?{?JOjtE$HE78R(RvW&8h z^w^$dqeep7928O6NBvODRx~l;%t8DC1r8pW2nvocT@s0|e{lj&xJ=3*A?JWp26dE! zedv#mu^N{pF~MmUn@k|rvR{>u?MIKT#*IL8h3v$Yd)c_dz4IsOp0(bH98MORh~ptp{R`>K3v<~-J85EM zy#?`)Pq1C$9o%Jwt89b`33l`hMi7<@wGxaDU^MO#k%jqHXvM=PNh_`+VI+i)0xNum zgF}Ky(AFf$?|3poS&rnL?B2M{Y4=NlQ}R$MkK;dXAXASynTrmXd=UF`(;$FmwW#}e zM3(_vEaT8pSq)_JJ>2KW_N^wLb`w@>x!x7JWQiHjwVB}Vesx3Lm|V;*g(=OYo-^KC zAhmwA>^FD<9wQfOPA{iB3H4>Pb6RJ#^Oaeop}X>oMaa$9A3qq}yEjmX#LKHZE{@nT z1?MPyotu1bHvM=uwov=P>NzjMCk{@0rR+e-}agUzVwJZd6u9^)sy)lHJNW2cZ@2f?b^!kFr> zm|!NH@Jb}QOa7d%=+b=pQ*3P*C%aG{&-6x$;gyXc;-DC)YxBBY-kgTNO+ef(Vx=?i zg?`{Ekw)d1Aq7e))b6Ibt!{6aGYd4ZPk0FBz3@qp4^%+p=tWg1N|LvWpN_1|z@K{( z7hqer0+WWf9FCQ1h4fVD{S2HVK?)pkf``PBlry9^54_HD>s&R0$^ z|3^{`3EE*;ExJ8TrsD{%>%Iz9Y!yk*UA@dsz_-_wbG*I@z&~0fk|A)S;GuWMATVkEIrhv%Z*3v4Yro=kQRMQ0aesp$6q;TBKLwQ+I~aS>Y^@Y~LFNXQK1 z`**dab3gLENRVz(*!g3?8E*q;d=={1oD#7ve*ejPc*22xc7k?) z_JJArwM%B@@55qM7`z+9ShzcG??HC#I&z)0kzU4q(L8E#W$oQ-PM0!FEXcYPbpfY} zoKS}PzbZq*IAKfOtx2^4IO4>%)&RPdwtot-+mG(Ff%0n54tCHvK45XBKXXKqgl2LK z3HeBaC?uTd{2ZG4tN{A1t>NWYa@#YtP#GiYz7k5V9BKo1*;_RO+Y0tT!EsXGIjPY2 z09uZ!Bso=*6$y`R`xBs^6v1URXx=6s;^N^b9q(Kc;?)ZhQ-B?v=q~$tPWy*t7_X7` zgVA1&Tdf+ns6%RTdwxv7u~*co&>jk0 zPFdj)eeO-@RyKg$&ZP{A@7|60PC83yEO)1C%(ib$FfGTNjK&7^C__msS zQf7%&^kqlXnLq`h8Babci{{!Q$i|JIb0hP$REq9AmfKq}w<%@1hzt3)LOELHcn?rn zkRpyxqTe`Tkx?3Jmt0F8330NxnRVoH>oQTc)}!nBzfay}kFIRT5XY0PM0XIKVzW{vPS5l&(`bJcd6qUT^W0y3z>dud?Y1{W2CN93O^xkkFq`Tcn8 zeuw#2KC~{u7+q}|8@VYBxH}u3aPy;s-_2KM`!CPW$o&_Kv(}56n>`IXHt$$Bdio*$ zgXK&IxU%B4VIz}p_CR+V7P^k^Po4fVaNLPDc|5_=6tOqN)1Sl>#N3d@UTl)2$%|d& zFu^SsBsOC2yaFUN%TDh^E!+!`z+w!YRm}Fi+rHbrW;TCnWd}~!3=cv558c_%kh(RZ zrY4f1YCkK0Qz_;D) zw|w~%vg`3k?h-w*r{c8d@-`eaQ|hr5WVhTrudjI@C-$6PgMADZoe@K(?dcvWpK=OF z$OvalE2gXpw=+CYE0L1_6f5~`{R)PdXZ?bH8Qwjwvp@#2Cv*-6?GVOA<+~w|(n|s_ z+@YKzXYWU5=zs@mCO&gRzQ&;2SEZwt=kf{NA0^P$#=dbojyMJ4+!%RP2GN9JPueGU zkCN?+wDWkvyw~?ZbYD&`98>th{(`>LnUn|P+i+2U|Fv;$9+*d=t=d9LHe$4CC2XAo z4G5u97Ip_q|D%T!04-mOzG30^n9bI-6{tn<%|%gtnJJAU|MyFX=SL4$2TQ z2R0CAs~x2D!0+B34EZP?;k${!MGbZKMjpviNAtS!964k~g}Un%Ix$lYkDMa;uC9Ks zB6J5nhD>N^pKnKgTqvN)k{4AX*aC5DQgo!koejf@bb}yq5wcML8KXN2IpI8>G8sfd zn&;ulmzve=`4-wCo*PCWC4gj~faK_*%*&4|Fp@jcM$u>9xMGp-xpdZzD)i^2q~~nG zaZIGT&`uXg!!_ihzGLQeh<#ztS+S}r*>)R#@b5r&hQSk zDN6LZ6=ddOHJZ$~-83GtgL+K+&J31eOy6ROJ}EIQz#z?TGHkd)l{GYD2Q#dqh+mRl z?OP86aOq7CW{t^r9CnGZ%fJ_A@yH44r%=zC$Te&k&Us-@JLmgSn`IrxjxXe*#{H<> zNm;x^`;WE_3+{&XF=zi=hNS^cIMNnF)>G_R8GQ^WBn}4=hhhbh$%M7!PbYR2@@IxqNVB% zpYcH#kAhc6_pgX|C0%A;P^>ZYis@-@8Siq{$0o@w< ztu8d7Lso?{%#(b zJm$cpHCBy+ael(;kUuJ0HjibF`YHVLBCkf?LLu@fSs%)h*mpN-a>%+5eTbVU;2LEDsbD zoT-mFZVzH^jY@;N7o7+nJ%fu%p&DNB9!0%<(dZ=c?h8ojN&&YcVT22#{0IIe2-zrt zA|91{s~3o2(t;87M{&zS+zdY58Omk z8mp*2ZC@0r(E_^BTh;D}Q8k?$sK=H%6jDzneAi_vh~#Q~fz{wicB?;`m-1bvWcl0S z`B{&==z*aLkbTU+yX|rWi9^SJ8<7`-X$BfF7t<*Y*;78w{h_g7dpevvCQl{*K zrUm;N%n%=nR~LPtapzo9ZAEV^s3$x%9ZBD79=N2>w4Po*&aGL2K~?EIvbN$VT-f=D zU|HA;>dk0K;HKEww^td;v29F&mf4#{+wJ#tCF!vpo_yx+qbPaAHB)cJYdNLm(QOy8 zPZYZzAYL4({y#05ogGAF-=Fn)`S$L&)A6L_koYKVTTltUG({0=cj!pXts5I$a~g^X z{AcxQPm4HRNs|ZfKo2jguJ53OW#a=)iO(vv5Sq_>CXYxw5Z^Oqwyl9_1Y$8GdW} zx>tkO1v`IK**qM0;`6~)Rht?=EV(SL1i4V$wE)EnAv?Evgc%)`@Q()Nq?51TH^!x$ zA93AP+W@Gk+q`Ym+~W#Q(0joQg=6G$3z*8rZRi48SFD5QnD5bM9uue&2B^2W?(Bh+ z**w;biSxT)>3BgG=|u3cS5MBC-0_hUeGlu;t=ovcXVH8=S`$^gxbYtnm6rpsr4+E1LY$^Eu}X|qdBqVxHiPJ)654^EFIi?YHf+TnWi#{S2dXj?@ANKR1* z)PxlvYne`_q1ZvjpwkF#ED~V$1ghL454+V8PhRArX23%?@Sg{O)D z4iXLVpSX7G?6N`CJX39qnN&PeeuzvTnl8aE@r{+gOqJkU|>SV_Yt?hqOtvWRfnWohB`5jdJLO3F}PR84{bP0;C;cbs$DxHMR zJUCLnr0U0(uXJRegWH$)3TEd;jWpx&W^KnG1<|^kzhX`vG?uU=amnCqCF~4A0D_$WM$UvA*`uw>azHoi6Dt-9CK)nU9AZ z+vA48<5u+6+KEuxiwOF*po(iV&s;>zmI}}LHQV~e8EZ0ubW{F$3dx}G(I}u8z)ueL z>bZ`a`zV|RuRsBYmy0Jf%FKFtesOpcv{3>3IQM0b9mH~u&WS1(WG!$$+Jc+HJ_qmC zf&*nh+0N(iN9Fcecu9ty*ZKV>z*gfen)3S4JNeSK$QCg1q z7Jh5-`|#9yCy9f$ZAgf!m|ejoBfsf)R=T*fAdCC)4-B(I8am7-$zGhLk=mEsFI%`K zHsuFX3*3KWXuqI*=spJ9Q1L@0q3mj6TIl}UU&v#7e%e~c^qub%Oj)t>Y#eUG$Df0b z|5U2*5sS57UFm+j;}*cv4o^J!K`A^iZebtEZaYh1D36vsQq^{J{kE7A&sghHq4W8= z#_LJjBmqa(5{de7qyNi^9xJoZ+ZHAEXM5G|8|m&cdsj#y3T93_)6eTGCjXBT*V~bW zN_c@e-BrJs+miMc;N}OAD4b&tUYl4kRo-AwiH@MpNYC4ZAublQq}d$vedFy!D5_N( zjq5xQqh&fDXGMdob~mc2#@rF^kcFxh75BFVa^UWy9jqHs3#}nm&?Nq8Avo zaSN73#=dPk|340pH=?jOUo!NqXb~3~g15xd=RbXR7Dt>)8TXtR-%{uD7ZgqYWZq{m zdh;D!GH#1IQtt;lkU|YWu35{?4k3a55m_)H(HNfe+oUGcm&u> zqPY%r>WOAhMrDqSQO-M?x1UoJpGtr4bWQPVUCg`Vk{qtR-b0MvtCEnBuQjG$`NyWw z_PB2?HMcCJdp%|Zpo_l73g3b^>wCnjmV6gV^Kxxa8KB1C%Fb+1lmi#p!Dm9?=Wgs# zc`|Y$e@lIF67A#)eXFRogzm#g4?_SJAt6wD)-zBE3rEVI2VrrK3KK0F()qqi`-<`S zSp+4#TFiYlkPK#ja*rCloh+>yxH}IF zjhuG0%DB@pbY^Ho((H7E{CTsg9n1O zlkSKHi2{{gHjv%%Kw5*IdYGX!}*?Aef+*)gCQH$-Gpk2ts$izudRT%##CUe8&}w^Wpr)@o?~*;MOAjP5a)5i#DW*Mu7Vj) z{B`C1PMrjwAk14Yzp12x!qi)5bMEmB~3 z=;cRu1`-l+(NAjW6s|)EIV85O8bRYHo%Z`BDU12RY=2qam-TYM<_LzVnOzz&C!YwL zJm;oxFGBo6u&6Jp&(-(!FuB=qPZxep2T~(mit0y$nE3XcNGw;JzzI!B%c7DKmhOeR zV*mZh5^Gy0WN|-9u~ea#;P9*;c#fQ1xpx z?AR~J@kW7GFJ7pnmwxcoEw>$|?^?DV{U=Y7{n1}iKS%dRi#bm)ni&bC#goexN9X_8 z*nbhYlnUuR1jH_*sVFvlx6CTo8@A*NzB2!D@6dpU5jT^q@hKZC6C7D@mY;ko8Qqmyd;!s(KrYASt=}x$`(b!mqhIdz9h)68 zW_`QipnYOE{3ef9jeMo9#4C70J+I-~R9d?ge$(ij)*WqOn-%sMcpz*^CR%@qH@9Di zykPL~hv?9At(z~4m%p>qF^5*(SlB4_Sns|a-E26w4H5B)8QCqU8Ag&4u_=0;C6ZwL zJgoox{3N>^ZTZKA?YlsN*{{10icbo;Ih1NvBC_Mb_SS^nsq*I;mSmmYk~suqD^6-~purDt48cx3QD(2sg99@_CZmq1iHn`c$jb*p zW-7=6da+k93r8klQnY@WMx2_iv0mr_z3*8Ux|_7Vh73=(fA99uVyh90rL;I7U!O9H zoB499)l7P7N5fB_yG23I<zD<19I2Yp);`#kkVF;J)8O}^Li{d2lt;3g0nu%6@zX<_IW*_9VWrajUor|>7MuTeOlCJk9;dL6=zh~^?0GKJ6$}-Ul7D<0C-1#L_|vU;t-M|~L(PpbN-j8! z>wy1qaIa&%1T50e4!P-qkrL4F{=mQ9^;-weoe3P)CaQh>z`H;#t>HH>r9ul?j{6tg zhwwcdQ7MCFXyfNHs#aNUOPBHF%9__E{#BlyMhj8O8y0Me1<8Wo&FPnr1Zn%uxA-U9 z51)JPhG=`h8bau@dETmT_hv(+U`MJi(O?&7<(hzm5G6Q@)(FLE>fn(_BqU)G%8y(e zMVjBRIj#WV#nwz9O!8e4(okhsry{`ey|XUO{|_)93zdT))^NtDZ^ck?|0nqw_=;@Y zj3hZk5#B6t>(EIamIVxN3m35p2Zjdj+^YZQ?+u}wM(8x3V~N{wQJ{FcT;Ph*J0Tz- z>Hjfx+N2O}l4H>91m!Pd!_9iSY_Q$hEsqwaKPczr04B;%a4M zw*#=X2|D7-4|_qKOI>k;wRN9~J3M~WnXc=}U8p=hdiEK+%DIA<&*Tixw{dAnoz5U> z>aoCsh%0hReRqH0Bnd+nbJRN09nbAD7Bcs6V|s9fHMw~74L4OvU6@s}dgf1J9~ z9Q=Ym!b;gJK|P^k#YWb_*rF(X9vI-*JZs^*s;$m&^!gfld@&=nT@kN8|~Q^^`5GE zSlk6mu6S{5P|y^7V$9*fCJ*Z1;pYf`Sja~iS#wrz)2^5LN@Dp zaO#8=s0H-x9J}11S=#3-KbheBy%sP3oy$0?&q}QyC#u72kmsMF%G8e^8gF%-Ncc%o znp>~|c1Xvp3Xfdop%2qby08o@`kh+h3X~X8{MujWDDCE&&_*^hJ_tF9yQq-kJ7Lj8 z-)hi!{&7BAY!zb-#u9~3o zjA#F@{Yr0xk(p`(M)t}|gm9qb-1;nyuYa5pd&-PO2)%IG;)Dj-i_Xn(2Ui zTB4A{dH{P|U38a^J0%9WD1JdFRn_h32=3_&d`D~$BnqzzZ*~^0QvSjhD-5}nDWyvp z5|g;Q$6^l%uUmzJXx|*+7%2e=3Onl$H_ePAw9mJRjV*uaya5v>V}M8YrG>~Wz>H== z^tdRRTMVoxBCy4EzlHxOYKQi6t?OZtbrT%{ev|3dP^FAm^}Wr z6)9zhMJpwRzqPsvwVyGiE+xD@_pQ7nuWgDns$=A4I6vW^n<{0tBPo$WciS-8WKr5;z6^bE zG{iMbC%0gxTYK-an5qF7Gws-yxHUpMOi_|JF<&L-Am-}NX!O}~YI1M(I+&|Ob(sLM{9$rT27MhYi|4l2iFVp35o~zl8n=;xj`*kJ2;gfc)9h_n;*S3*Pj^(8IP@>){5g|Z za{2Sj%4FE7QP~tQ;5Uh0O3GzcP@_?Jv&V9gaf!y>1Z+WC3zow}i3Oh;c*yi`|Ge3R z?m907JB^(Myj{#@9FxLh+7KINNp7Azha??;zU>{4WAi*hquD`iA13KOm3Eu>8a4W; ztFj)?&?yB!dcNv%|6q`3 zkllR79f^BFm#H@G4vApq^k*%iM#DwwbOIy5(^}bI>Y%rqh%P?* zN86+^Pbu&C$G#TOrYeM>#i93V;U972<%0h}j97gCMT{gbC``=4F01pA!^g%ujru-Y z!GYe;tI>d+`ttEBP{y?Q@{dyiTgN+({vjCKm0cmFHYTLhpN`j~S}xRj|EU-uk(H|WX3sLQr8 zWEKyru=y3=n+dlF=^eFB?o*N8ySx|I$l0q)XFISOSFuw7TQU}-4kUc+g{mXAK!K`& zy{LP}6>2-YS1$3o_cs?+CETbef9PM-*&s98;ximl62x4Zq1FHdkI*;$c*{xuaDHR8 z*!rFY5B_tRsZZ*?ep;vevj*7(G`F9JSgmXEjnorQ@v8gtH(8%hMZQLw5)$LaWdxGL z>fIs#`;qeZKmIHH+-yZlp5p8J+6$}VSkKkDL%N=-s`dGF)(cc=!v3?l?OYl{m^NAm zHcQM3^M9%K{r^$zWw@2iQIrRxP)_dfv9{a%h<1iDw`j8dpknXZ=)zQ?S@QZ~NYx6| z4yG`g1_~rleLcJm;~TY}V@s>1Ou@_fH`3J6N>p~nrHXSuz4E$duLL^Jo`*bOvK2kT zXxomY)ilEH6qu~%%>UEI%kL%Y4yCT!IHJRyzqUe6j7V!vWVY1Dy6ZXDU(U0Y38lf$ zugvMX3q zM&#{=u9k~w!^xH&qFqTihIb!t7GjZI`mS0uxMvcyCHpyu&&=?uAsf;9WFcB|Rf%kL z0+!tTd=Dt^Mird5aF*#Z51;5njJh|&6#q!5ZHqhOVTHHO0`3i(X$d@rbC)_(!3fu_ zlBJ@NcARYBIWuDm5XkQ^8@JY;p+aYawa2uihSB!GhVbmnq{2A*}N z*I2y1(JVkh!Q_T?SQ1-L6fkpV=p6aCOkJrxC?v?hw>l6@6brCt&Bl|RSH_|C$NP>B z@4QNOTQ(%SjTu6jSKST{$t@?mH`Wp34KYpg1EfS#szF3+ay9Ff{S^=%1&7c(gHP+( z`AeBKYU7vH?t2suiEaPhnfvzAmsxK+jKhyXbQh$zHhGai5m7lMBdWZjlyd9E7r5jx zK`Fm;GpOP>VuII(FCz4V;c818iW}m_L@A-BnnGc3vDdTx=qBc!f?s;LV3X3v;&+G6 zwB2a@(w78z?C-iK6ROz(p^1lt2C_0ccT*{kQA zasvO5k7F@%kdRpt+Npc8cc219v6GSv7;6Wb5IGOM@9vO>j1p6zhvR1QQmL5DMeLVN zOv(;bi`R>4kA>a_vr)gXmF?~K5MX))obacUBmXm>@U5XQ&o{M-TRF5D3?XU|Ir5L zsbC4Elg&QTV_O$%^{&%d{H2I=E{;7gg2e6>y{^i<`mn+#o~C6#36$Jw#<1$2u=V;2 zpoyyBP2id9kjTOqs2DOAS@xULP9aq)#NmQj8-pB9>$SO4NUFB*$(R3GlTWY#QFoeU z4qe0(xCVG~FA3ntIv;t5vWE_WnehuB%FCR$prY57cI`dQuPqI_;|5}~FVGozv!@9) ztBT)sHfpi7cZ3 zF}`0(-XodO%Xq>u>3?`5e|fBQn#?y(;IaQHkJBaLk!NUTk$kS9)k-RoX1z`4?II21 znT`=+k&dKQ%mMvtfpPj69KHDJLg~=4jr2pI5e~l0K2^jCy{)@$vF;p2H|q&gU>#1nZf0lAPPWV(o-EZBw7xF0&U=_HBR(}j4ZThayY)VQz(`g0t zqWDG8fMZ~F1pq*1IokqC7r(SmxX{@oNBvJY=LjE#obr~~kQaDNy}~hppLc57Z%*Yz z601|~!;ZJTu%d5MF<^XFO(;@(Ahb?93{78GDLmiccMlHUs!H&+2g?<|c!yiLebIj- zcwsLGQT+=f<=Xj2v&izb6WibMQj1Iw-@!EzjpPoi#}ybxv-GKNh44Uxn^O4WzuZ?5 z)PcHgPNsR{?%kr_u66$&+or~3F+iU+?vBJ6KmV9v%Z#MaZbN8S9ffMi_zC*9m;yMf z!s@|$*ZMC%#>GpM_#%D&8xqE>BjF6ZNmH`*7tD|hW-)>c+^Yjdgw?Juob}vRx#-J| zMW>**2Wx|WN*HTV??yKB+&7U$C*1jUud_(a*$ztr+B7SGl9iJm2j28wvXfOy0~D-e{Pt9Iy#vD!aQotm+9BQCYW^3>V?#WFp>-RBqBbZ`^@lYv@r-2 zZekDLz9e<}imt`&{axnHNpDYIyVlDLJJ zf|y;)9^_Y9_mzDa5f-ZdmPpDDPgoNUQ0bBscAR=bSdH2No6IJ1%paV-y9Pf{m* z+}4l2JjgGQStTlpBUkl%R#_I(4atk@s=s^sXS}y80u)roB4I6$WSI#21M42#bfw<~ zHp#?6tYbHaFYGMZhe~kfur|owLhRQgIL}YQs@hwr4|caC7=bckz2RuHanZZ|y1$qK z?%nJ781fhB#NIuCdiZNLlxC4)!$Y`@a&gisdUqaM-79$fs@OmxWB^o+{X+dpFW6DAB@dEz`QyI-><_ z$@90k?Oj*gc;O6cs$O!F&U%kM-tP>q3K8IC}b+{$3;1)x_R8DOBtfAsby@?&j2)3 z90aMa?jqP+vIYY=3?l%(2J$(r9jKcLNu}T3axm?o380^Slk3{-X7Z&lO)2t~+@|oG zo1RQ6Do@y{jKn{mTKSadUPmlESHKb#Q~2q8afLm+^sonfhZHTK?w@&n2p$^tRp#Di zD5=2gBVebbkvHB*5VN;*y^2TBeKLBC=1UL7JeQv)s0^z8;Y~-_$=v*wXs3Y{$nVBk zFs-QpAVJbz5){18B3EP8M8JEIA62-x$U{t%khe-cr)he%g}+H7#yVUeW%0xoD8DqH z^G`MfrW2w<`qxk?Be~Z=&3CV0agE3sDdDQ$-D#{}fCZ<)Qj4BS?K8ebeG)Q{!*tGq5;)0u5%T4-1z7^W6uY z0@1uOuA8$qEmmLj>|s-RFdS+BX#`kW1Lzp$T4KbrCqdz=UW!3%gOZiOjD}+e5RvCD z5}yzD|2nRtNAMEZF-P8>23T*nVDs>yr>~U?ljus%3(5Jkgb}-U{piYjSb{n>rEg*q z0+UBxu^ZI$2s;w4UBI5wGg}8Ht$&`Gw~ZfMEIZ*^#6LgjVBMGi%k0-<4xGqid3VX9 z(_aC!W1ME)5a%r(th{oA+{ZXJT*4MQB8pZ5-vjIJR0Yj((q5FruSxvY8HEtB?!xI8;Gu|PvwKz+ z8Hl~K8i-fu!APVQxcINYIKC8fO17F6$`8*`NA17SUngN2*Grpzr|)MRoeQzflNeS6Oz z!(`ot5oYS=L}JcCysk=?Vn2^Ph=!i2)7d)Yu|~N*dTRuJbUWWfd@{-irz8mOevl(} z$khPo@*|k4Ypf=JRxD(Z%UAYt`rDdLRxhTl+o)*b<5o310X@&P2(_OMUV!KJz~Izd zqezqOok{D7PQWwo(NuUTYoBG`yE5h;Kh+$xt*)LD^ zCPT`l0(VaS>vl?3EyIe9H%plyK2~jFY%LCgu4J zO*y(LTpv$nU4qbS3yx%RA&K|=UyCEQ(AGDsQ)4e7e#_Ghnm-RsgSerLWnFS}&lgBu zdEaHmZ3oqDP{CO}733oNNVqABbEJN_AN{cjGbBGDNgR5R4E4P+Z1it9_miZ0f%}Q# z3v56s<#Q9qve#1(07R+SuHysdXt~yKjO3bR#rpdx_6PfOJ?*7bQO$xwte1q%;@h6GW|(?2gZM8Hit9{&*8{MpL8LUTn~7t`P)HmpwDn5 z*HB(^x>+4nJn8|#E?{e_Z@OU#I%g#zx#ow1?&mumKZO1pnyQS)*&qpbx+~GS+WkNX z?vn}(F;GgMAd*&p0?OH<%K%>5-40e4M?HHkX+y;o)tEN6(QO(RDJMYvz&EoT6HE(d zLF6ZuIqUvfC+&kiKIo4`U3LQl>t1{}(KzysMr=y44w6RIfvD~kDeG{&i~oe@qxb>_ z?!txRWW9Xmr=oM9W3;I>bQnDsVaFh8JzXdq@>hpL0BPtt2PwINCm~nBs*&*<3ot?d z6bCi+U4N&dlI8m7hBYu%V=hN0v_{T^8nMUwF>&UliVe5h&jmql_6}U|T7?;u!|Y|e zEwqvwio9s|^Jl9_kbj#1a<+p>PB)CcHtqfmul3(WQG$2~({XFy9h9qZ z*62RvuN!eFUFwWS#|MemY{4m;aPpR(vwYr@2`?HCsrd4|i1@^m&gCYTyjDeN}NVV z4ly(Pe5zvSS0D{|05sax1$w}a3cG&-jG@vItDipypTT-!*>*_+Ecsi}gGDe-@Fa{B zd5Xc{={SJ4bD-PM;kdVUt?DsH+QzaE2oKF)_Mwy<8OT$D1y0`-PJs&CW#*4xVh7BD zZxhKcJvhjDD{#($t~c|xW1-*+CV^}ATQmwLwW91N6A@a`UFgOZ>%^sxoM3g-1`PXN z!@zfCSD>g|mIM6%x&n)GekekokJuZzJ_M1vU2p`g`CgHo&}jxX{)DUtCeV^rIHX`4 z4$FHhwe6lM&%|SOxc?z|K?&Q*l7;PH3ydT&u6npqwqF0L)9JX}IvDn{Ib3gie~vIy zH2>u-0f!IpN^WAje$?kcu&!Yks+jo&a-Pg_=cwlH&wgG9cm=}b4xeLgmV0t%9EmQ< zBEMy33Y17iC-pq{qSN{II`?B^b9i(3%j(C?b#q}cxizzMFPq2ZS61?ZU7VJN*Vcxm z@?Vco$E(TTHu=c4%VLJEu-6tYAME;=ysRzUn#Lclup4^1^VEfh!O+JqJ!q)YZM?t6pt}ZMF3%)>|i9DambZYMl@HwYpCc`-M+L+@UAMj!sLue^oN&r(7PHsF7(Y!iD5(FA@?$hGF=}~X&n5CLLf%Lmf$j(zwe{9YWg$r`gUynSSJezzQd*AWw_`MIE`L$#1Ne2a2xIW0P6f!+}@nxRnAvDupSHDT+OP*7i z^ITWiyoi7bvo-0}JEH>I`KG$`_t6ib=P#h=TRfMCy3^xl?V!`8@bee2U&6SE0Uu6f zVX=I9Xq&LXRY=a()Jf$0VCSdEWHx09Sxgf$xVQNfRqxpg^z~NoEr*&lAxWoc77bc3 zbP`yXpdqT(@?jyXb&W10S0Gb3Y~ub%*1W`hL_H+h_)c1YYW?S@7EPCC>Ziu`2G^X{ z^6SjZ)BYZ=JtkG4oo3_K@9jGKG@eH!kh6Yojrg<4ebuZg+WLhBGq?= zi9Ix#F*fNQ9f($`S(+SH9arks*{bYXd2#Um%zWqjrDVE%)dX>e!%A;vsfb_3NToZZ?`4^&xcLVbdod1-cE>;E0 zbY+ipX2$wGilmeqHUjkoBm2#JeSn8cB3y#XXoeTN7umXQ3d_^Ark^Z zYg1}_kImhZd6#jppAykjofCh*A}D{v@xwu`yyHxi2Ia<_8ktu>HGy++!Ay~+MdjU8 z^M0j+V-XrLJ+w`iwmET@b~@YNe=2KNwA9rR7bB!U?NrsJcqw`0V%$}ou>#u;fi~#{ zyP(@R_ash5KaZr_qocR98@KkZ89rX?Ka&_ac;D>H+RfHg{=x?)Yr4LF5J0yoqL3|x z#M|ZRks)Kn&LM5Yt|2XhIr-BqgVABJH`z7Xj%L~-7KOH4k}C_D$0mFJqD)gpe zu8Uk|29GZkzWqANW!2=fa?k9k$=-8qT=DmJwWh@P#gCSJ`g+5L_tLfkE9c~QipfnT zq-0|wkNve?w>}MW)#uAao=6smcu9_A2~2sWS|c?3!}%3y#W8MG>~J!t*yDDwiLJD3 z!~fOVl?OtZ|NrNi!8k{e+A^Z0R7%r2nvr9++E9*Kp>oW0v58odGxErm?WA~FVi_xt_vx6k|Yc^|LW>;3*bo~MN_!+T6v?335u zi7@Bapn|()o1y$Q!o;8&8%hjR8=vpAqGd1G$sfvR$)C$78&99#hjPpgn~a&AU{wCL zI4BcSU{Vk-kUp|0NZjq10IAF_zBH8a;BVU2z^MH7cA??m`O0^ekJkA8b((6lG@YM! z1{qphcb1`^dirNWT=xTey#5h%c#h$t0`Gfxx|qH#cWPT8ZfK9TKK>D9R8mF|Dr$+)^6$t3WHUz= z^dv*I|E~CBXCUb>i@#VsoHI$!w``G?Zt>k1wLxetW4jN~Oc~lTb~Upy`OLG3di9p9 z*%Y7CL&@2{kO&bJQ@*4X&l4=}Wa>Gsb&%pj}8M~UTg-zEnarx8QeToY7D#HWGV zz!()t@;d|jxG8R-k7Zo`IS75h$FCjo8HlEnT zTkhku1$fjPcJf+Au1#nXON%BuLJW|JIwkP+sj}CZ z$mT#L`Vcj5qSjW$FR)D}Iy>IOe8|jWMeY5A`*UO{yQnMT97Kox%E||)ZgE_m3||a< z!Ew>mMSZh(<9NXDPnLCtL{ydEe5S<$*7b&*K4BY$bfuH)XzqKJLhGHA`@wu}a#L^% zIquSvVv$+ET_g{EisUbSz&j1y3w_J7BL31oI?IGlb;`7gkMFtba;QwVZ~cUeueU-) z=W30YY`d1U?kbJ3<;=8(cNo%+yw>3hJ&rN57CSX;(TqFD#0RpU(jAVYvp|M5pZbGW z1sLax&f;?TQq`2{<@a}+0WhZ}qBnzGsU;}jlb_TBCxph2#L)j&vvuBm4&L~z{(J`5 z5W(2~{)j;86>MaH%~KZ8<4x(6$Y;8uF98hkyqtswX$hb6K zpp@Piaa@&Nk$h4#$APlfHgaBSsFdCYRz6xxVy2&)sMSjmg7(?}+2$)yER#>6j~Me6 z=(S6FbOq@Au;oYr(9B;2xh{VjukqZ(G2Zu?Q=hthguZ-?m`>`_TdvPCu4kTiwk-~D z%<-I_CC$Ra@q=>vJVF40bkB{_iwv1g?ULTa)Lz5d> zk)89ds4XKqUV|mm+=@{d-)TOS#uK)k z8)#{~$pRDy>@{|aY0)cQ@@25Tm6odM0x=JzDow0^z{_v#;AZhK-|k!}m9+2wW2w}c zGKZFO>wlS*O5b!o&Ut?tieyepS5JLd_U+Bh^Wuzf7ei5O{nQIwWyBiIldW6Ii83U;N$(2 z8Rp|#0=KRvy^o%(iA3&G_^EO>hduv3ho2X~ISgk{IV|I5vPb>JnZ3I-jn8x-eaAh1B33tAQwB#PU~O4h<&q1o5#L}4h%FLCs{H5 z$}YI&o)J`gLlC+~@mHbX7*7@2PBf!z85Pw=-CtiF1#9Vaqr8piyX&8SNx3IyEhO(g zxMdD1W{M;k`b&lPH->1h6qbFkCuDq5%(vAR%;`w9vpTrCCET9&dZtkjW`7>9HXnC6 zb4rPR%(z7yz>3XNAuz~xZqiH`*=JjZhj?_51PShuZ=s*J z+?@O_YCcqN;~STT?p0{}5_!op=g3P!;VDdB79@1mM{Uw%5>sT-XlPgB6{mR^hjia$ zQ@~GwiuCE&Y@d77>ny-}rWI&U8%@qH5i|jRfoJo%Foz@&*N`KcXT-4FY`fsT!b{=0 zp~6rFyfOwp(IiOxpvy%Xqa|MtRmt&F6=rqap0K8il&ApZ;_ki9eW~x|xR%I|P37;Z zKJmQ#Fy|u0CH}-Q+~|`nNGmq?7!=uW5?v-h%UF92nz}C=evFS3<`_Te|5eAOw`uD+ zCH0(oyfDMEY6h}160m%)}$h`=N>ijtBy;W zqUVsYKqDWEhX2)ooB<}e7cMWY(p%Ooluv}@J?MchcTj)e5O6ijKJG_YlebS^c{?K* z?FEAIOHo>AOe+{qcxv$%9W#-lq3nuGV)`Soz8@Ljh<<2Y%7c<94c;51Vd z%Ma?YNup-`+XK$xVMj_O_-H_mG#L_)@Ly&m-@Q*u0QYX!Hv#(N8bH{zF2QE-OcFK! zUzwOaR<5@H*q!7msg7Qn)hJQI-#VZ&SNfa6Ha$|0gEIX1DVt+$Cm2*D)^LDqK}*6_NO*-M%Vw&$bA->`le%{)I7Npp5w8+=&iBJV2*msxmp{6fuDMD z<9_?ikxMx;MDxIFm0+smLnl~7_gcDLdUjuO@J|DTz&NFfo#ojWg zIE*ii9nFshoGiCD>ItN4wT*YzC4fm;OzJL%wfgNwOhPK*1U!NHBCx*hZ20gQ8w3N9 zyPnXnrf3qA+e=^%Der8I60ZNY;FNgl2O{zvXDje(_a5)V2O0my#4P-H*K{=;iPPJ3 zV^fv17`-+v6D3yP0$_DQ9^Mo(UfFkib7<-$fSdGMyLEH=*BS5Po31;N0~d6sw2OMk zCM{$SwiCPX=&`%bVSYUUZl>&-PJ|VE(o2QX`dy;w{LDfETCYFc~JaQ%|=7nTJ$Azx zp49Wn2|f+O;lj2G@M4rR#2YyjKz4dP+|&I^k>e*O@`^7WU`EMv<_31N>0)NE&b{^( zMkOx_58(+7<*yZT3_1bsmS*vh%rgdrTdTY-2r|4Uzr^^^sE=qLMMi~~Vdp-N1S@{l zC3;dTiVHmuBm#P7&Kk{--ZU@6s_ z3eVW*ia@8$mD(O);M9EoHlE<3Lx#{DK%PR5R0becpD}%k->G`sK%iF=SG)qI_(1D# zmH>w0<9lHadMZ-DYXBgwYP#e5eJ~6?ZwM%x@UM6@AI0|-U{0oBC4Y}m0*5a?u_|N?m8~0-S8K= z`lx{}Hr-`K)+3|zv#>R$^^i3g`WYS%uz(|-z{FTAz4Zron40-`sNubdi?CZd{&C40 zDDB88ZUQN*&ZU&aP+@dQUvE4~OjHcQFuLc!4Z8_3OIAhN6aLX9zzy?hLHt z6#$nw_32wsxNcbXx}dRD?=Y|fuo3-u*{-7YegePz1nA^+?O~+wycQV&i%IHXg<52bmL?dTON=NHXrAsR=w%G9N-U55pHxQwSikz@fEZdQV4F$q3pQc%mKOq9Q6kOW*3-TSZtK6Z(i zaDUdrmTQ5%+2Myp;Y0;nC$)J(gyQu;_F)Hj56tVRQ_FYY{)m2+CBsGcX8t1}AruBeVn{b4a;h^*Rpl@R_DqH*QRVZ!l zV39LX>S!|Y*Te-{QG4(fh~`VssSHFPA3-0w_PQXS_53}CR7hn3#pp`V#^0PRGI^Vd z>2esfB6z>H1|mB{YSe};AAP>-`jOKv1%y%&Va^Dm2r2t8cO0HfSs#JZ0&X|H!4u-l zkwwXc;#~$@_28B_jv!X&Qlh4?^OW1rk9x;922i|3tpo93`|cAb4wBnWqOp!dGDHfM zk3zOLEUDhgpCGWiGeQ5`MHo`kN;R)&L8PviZ-DQ{d{K0&kqB<&7&JXT0FUB?xp~DC zlJDep8Yp)K@^>FF7qQ(FK|Y9$BsMA9PZ!A4@Z5hi$x1=oVBAhGlC1j8b!F5>uM zl_}osZ2um{$Z?aJwZfRWxO6y+C{3)O2X6)7&k1D>#6FE0$Y>3^gS>IM5`LUPJ{CFR z?ErWb*8!G(>#8XODo=+drD3Y*_CXU0AtG?&3Q&j!&APR;5q_s7>^_&hA2-G! z+qp^vc3~RSH=P#T_w)qh%CHJgydoHOL5YKe$<#$&z!Yk3tMK$`J`y)= z3y_IO+=vdO>VwF_J#3y!NWf25bY#q4`lw}qZn(kV0l?Wns&q_W0g^Utloakmd)?3v z@SDOA6@f~mPl|vs4Y`34h%8;o3BeOWs1fnV_K;YDd%YDDP`{eMu5boo2F+Bq4ogBj zvn&VGMt|W-wX0vQ{6#(zFlLJksq&YVhP6mwIc_R;QFbsfV;AG;<;+%w1x38nn<$+% zC8yF9xQ_nmQawwHQKDvU$lpERv8WknqHtpXefm8x48QWym@S6$-x9v5S=6I_rHWmg z9px3KH4&zbTFr!lj{y+;RfE1q;-wapMWoJiFf?t08puGV`p?(pPRM_J4}>@XZz#d8lhRP67MNs-3L3kr7Q2tP e#dh|(kT3gU(%un$2g?B9AJZQ!c4TZLo&682KmnKl literal 0 HcmV?d00001 diff --git a/gui/utils/utils.js b/gui/utils/utils.js index 957cbeccc..e8d05ba92 100644 --- a/gui/utils/utils.js +++ b/gui/utils/utils.js @@ -21,6 +21,7 @@ const toolkitData = { 'Thinking Toolkit': '/images/app-logo-light.png', 'Image Generation Toolkit': '/images/app-logo-light.png', 'DuckDuckGo Search Toolkit': '/images/duckduckgo_icon.png', + 'Instagram Toolkit': '/images/instagram.png', }; export const getUserTimezone = () => { From 84b593ce170ee22725cbf1589818f8b336eba30d Mon Sep 17 00:00:00 2001 From: Kalki Date: Tue, 25 Jul 2023 19:16:18 +0530 Subject: [PATCH 36/77] apm bug fixes --- gui/pages/Content/APM/ApmDashboard.js | 2 +- gui/pages/Dashboard/Content.js | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/gui/pages/Content/APM/ApmDashboard.js b/gui/pages/Content/APM/ApmDashboard.js index 5c0bd8dde..8da446ab0 100644 --- a/gui/pages/Content/APM/ApmDashboard.js +++ b/gui/pages/Content/APM/ApmDashboard.js @@ -11,7 +11,7 @@ import 'react-resizable/css/styles.css'; const ResponsiveGridLayout = WidthProvider(Responsive); -export default function ApmDashboard() { +export default function ApmDashboard(key) { const [agentDetails, setAgentDetails] = useState([]); const [tokenDetails, setTokenDetails] = useState([]); const [runDetails, setRunDetails] = useState(0); diff --git a/gui/pages/Dashboard/Content.js b/gui/pages/Dashboard/Content.js index 8d9270712..74271493c 100644 --- a/gui/pages/Dashboard/Content.js +++ b/gui/pages/Dashboard/Content.js @@ -33,6 +33,16 @@ export default function Content({env, selectedView, selectedProjectId, organisat const [starModal, setStarModal] = useState(false); const router = useRouter(); const multipleTabContentTypes = ['Create_Agent', 'Add_Toolkit']; + const [isApmOpened, setIsApmOpened] = useState(false); + const [prevView, setPrevView] = useState(null); + + useEffect(() => { + if (prevView !== selectedView) { + const apmTab = tabs.find(tab => tab.contentType === 'APM'); + setIsApmOpened(!!apmTab); + setPrevView(selectedView); + } + }, [selectedView, tabs, prevView]); async function fetchAgents() { try { @@ -262,6 +272,9 @@ export default function Content({env, selectedView, selectedProjectId, organisat } }, []); + console.log(selectedView) + console.log(tabs) + return (<>
    {(selectedView === 'agents' || selectedView === 'toolkits') && @@ -372,7 +385,7 @@ export default function Content({env, selectedView, selectedProjectId, organisat } - {tab.contentType === 'APM' && } + {isApmOpened && tab.contentType === 'APM' && }
    }
    ))} From 4b202babf8db14600ed39742518a7f290fad0610 Mon Sep 17 00:00:00 2001 From: BoundlessAsura <122777244+boundless-asura@users.noreply.github.com> Date: Tue, 25 Jul 2023 19:16:56 +0530 Subject: [PATCH 37/77] Dev rebase (#858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix/agents delete (#795) * fixing the send email attachement issue * delete run label (#796) * fix * fixing schedule agent run * Updated README.md Provided Instructions for serper.dev API key creation and configuring it in the Toolkit Page * Update README.md Updated Image * Update config_template.yaml Modified Serp Configuration for SERPER * Twitter and Google Oauth for Web (#808) Co-authored-by: Tarraann * Updated DB settings (#805) * document fix (#806) * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * Bugs fixes new (#809) * csv file fix (#810) * ui bug bash fixes * ui bug bash fixes * Bugs new (#814) * Fixes (#815) Co-authored-by: Tarraann * ui bug bash fixes * ui bug bash fixes * Handling the rate limit exception * fixed add_to_vector_store_and_create_summary (#827) * fix 97971622 (#824) * Llm models fix (#829) * removed unnecessary toolkits from marketplace * Dalle fix (#825) * ui bug bash fixes (#839) * Fixing twitter creds * fixing google oauth issues * fixing google oauth issue * fixess * fixess * fixess * fixess (#854) * Resource duplicate fix (#803) * Supercoder Improve tool addition (#755) * rebased * Read file s3 fix (#823) * Updated * Read from s3 works * Read from s3 works * Minor Updates * Added Unit Tests * Updated Tests * Updated Tests * Refactored * Updated Test * Updated Test * Updated Test * Edit agent templates fix (#838) * ui bug bash fixes * ui bug bash fixes * ui bug bash fixes * rebased * first commit * instagram APIs implemented and AI generated caption utility added * Image upload to S3 and URL generation added * added upload to s3 instagram bucket * Refactored code * removed jpeg files used for testing * fixed recurring run issues * fixed recurring issues * Readme added * instabot config folder deleted * refactored code * Handled the case where stable diffusion generated multiple photos * docker compose.yaml version reverted to original * removed the utility to add stable diffusion automatically * added test cases and modified readme * Added instagram tool bucket entry in config_template.yaml * added instagram tool bucket entry in config template --------- Co-authored-by: I’m <133493246+TransformerOptimus@users.noreply.github.com> Co-authored-by: TransformerOptimus Co-authored-by: Phoenix2809 <133874957+Phoenix2809@users.noreply.github.com> Co-authored-by: Taran <97586318+Tarraann@users.noreply.github.com> Co-authored-by: Tarraann Co-authored-by: luciferlinx <129729795+luciferlinx101@users.noreply.github.com> Co-authored-by: Fluder-Paradyne <121793617+Fluder-Paradyne@users.noreply.github.com> Co-authored-by: Kalki Co-authored-by: Kalki <97698934+jedan2506@users.noreply.github.com> Co-authored-by: GeekyBaller <43145646+neelayan7@users.noreply.github.com> Co-authored-by: Nishant Borthakur <101320057+nborthy@users.noreply.github.com> Co-authored-by: NishantBorthakur Co-authored-by: Autocop-Agent <129729746+Autocop-Agent@users.noreply.github.com> Co-authored-by: Anisha Gupta <60440541+anisha1607@users.noreply.github.com> Co-authored-by: Maverick-F35 --- config_template.yaml | 6 +- gui/pages/Content/Agents/ActivityFeed.js | 14 ++-- gui/pages/Content/Agents/AgentCreate.js | 73 ++++++++----------- gui/pages/Content/Agents/AgentSchedule.js | 11 +-- .../Content/Agents/AgentTemplatesList.js | 11 +-- gui/pages/Content/Agents/AgentWorkspace.js | 14 ++-- gui/pages/Content/Agents/Agents.js | 2 +- gui/pages/Content/Agents/Details.js | 4 +- gui/pages/Content/Agents/ResourceList.js | 46 ++++++------ gui/pages/Content/Agents/ResourceManager.js | 1 - gui/pages/Content/Marketplace/MarketTools.js | 10 ++- .../Content/Toolkits/ToolkitWorkspace.js | 10 ++- gui/pages/Content/Toolkits/Toolkits.js | 6 +- gui/pages/Dashboard/Content.js | 4 +- gui/pages/Dashboard/Settings/Settings.js | 46 +++++++----- gui/pages/_app.css | 26 ++++++- gui/pages/api/DashboardService.js | 4 + gui/utils/utils.js | 15 ++-- main.py | 5 +- .../cac478732572_delete_agent_feature.py | 4 +- superagi/agent/output_parser.py | 1 - superagi/agent/super_agi.py | 20 +++-- superagi/controllers/agent.py | 58 ++++++++------- superagi/controllers/google_oauth.py | 14 +++- superagi/controllers/organisation.py | 34 +++++++++ superagi/controllers/twitter_oauth.py | 19 ++--- superagi/helper/twitter_tokens.py | 8 +- superagi/jobs/agent_executor.py | 6 +- superagi/llms/base_llm.py | 4 + superagi/llms/google_palm.py | 14 ++++ superagi/llms/llm_model_factory.py | 1 + superagi/llms/openai.py | 18 +++++ superagi/models/db.py | 8 +- superagi/resource_manager/file_manager.py | 5 +- .../llama_document_summary.py | 2 + superagi/resource_manager/resource_manager.py | 9 ++- superagi/resource_manager/resource_summary.py | 5 +- .../{README.MD => README.md} | 6 +- 38 files changed, 332 insertions(+), 212 deletions(-) rename superagi/tools/google_serp_search/{README.MD => README.md} (85%) diff --git a/config_template.yaml b/config_template.yaml index 331553776..fedd3b642 100644 --- a/config_template.yaml +++ b/config_template.yaml @@ -68,8 +68,8 @@ WEAVIATE_USE_EMBEDDED: true GOOGLE_API_KEY: YOUR_GOOGLE_API_KEY SEARCH_ENGINE_ID: YOUR_SEARCH_ENIGNE_ID -# IF YOU DONT HAVE GOOGLE SEARCH KEY, USE THIS -SERP_API_KEY: YOUR_SERP_API_KEY +# IF YOU DONT HAVE GOOGLE SEARCH KEY, YOU CAN USE SERPER.DEV KEYS +SERP_API_KEY: YOUR_SERPER_API_KEY #ENTER YOUR EMAIL CREDENTIALS TO ACCESS EMAIL TOOL EMAIL_ADDRESS: YOUR_EMAIL_ADDRESS @@ -118,4 +118,4 @@ ENGINE_ID: "stable-diffusion-xl-beta-v2-2-2" ## To use Qdrant for vector store #QDRANT_HOST_NAME: YOUR_QDRANT_HOST_NAME -#QDRANT_PORT: YOUR_QDRANT_PORT \ No newline at end of file +#QDRANT_PORT: YOUR_QDRANT_PORT diff --git a/gui/pages/Content/Agents/ActivityFeed.js b/gui/pages/Content/Agents/ActivityFeed.js index 8e592af8b..8c8d83cc5 100644 --- a/gui/pages/Content/Agents/ActivityFeed.js +++ b/gui/pages/Content/Agents/ActivityFeed.js @@ -74,10 +74,9 @@ export default function ActivityFeed({selectedRunId, selectedView, setFetchedDat }, [runStatus]) function fetchFeeds() { - console.log("In") - setIsLoading(true); - console.log(isLoading) - getExecutionFeeds(selectedRunId) + if (selectedRunId !== null) { + setIsLoading(true); + getExecutionFeeds(selectedRunId) .then((response) => { const data = response.data; setFeeds(data.feeds); @@ -90,6 +89,7 @@ export default function ActivityFeed({selectedRunId, selectedView, setFetchedDat console.error('Error fetching execution feeds:', error); setIsLoading(false); // and this line }); + } } useEffect(() => { @@ -166,12 +166,10 @@ export default function ActivityFeed({selectedRunId, selectedView, setFetchedDat } {feeds.length < 1 && !agent?.is_running && !agent?.is_scheduled ? (isLoading ? -
    +
    - :
    - The Agent is not scheduled -
    ): null + :
    The Agent is not scheduled
    ): null }
    {feedContainerRef.current && feedContainerRef.current.scrollTop >= 1200 && diff --git a/gui/pages/Content/Agents/AgentCreate.js b/gui/pages/Content/Agents/AgentCreate.js index 2db2c97ac..c01d945c0 100644 --- a/gui/pages/Content/Agents/AgentCreate.js +++ b/gui/pages/Content/Agents/AgentCreate.js @@ -8,6 +8,7 @@ import { editAgentTemplate, fetchAgentTemplateConfigLocal, getOrganisationConfig, + getLlmModels, updateExecution, uploadFile } from "@/pages/api/DashboardService"; @@ -16,22 +17,13 @@ import { openNewTab, removeTab, setLocalStorageValue, - setLocalStorageArray, returnResourceIcon, getUserTimezone, createInternalId, preventDefault + setLocalStorageArray, returnResourceIcon, getUserTimezone, createInternalId,preventDefault,excludedToolkits } from "@/utils/utils"; import {EventBus} from "@/utils/eventBus"; import 'moment-timezone'; import AgentSchedule from "@/pages/Content/Agents/AgentSchedule"; -export default function AgentCreate({ - sendAgentData, - selectedProjectId, - fetchAgents, - toolkits, - organisationId, - template, - internalId - }) { - +export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgents, toolkits, organisationId, template, internalId, env}) { const [advancedOptions, setAdvancedOptions] = useState(false); const [agentName, setAgentName] = useState(""); const [agentTemplateId, setAgentTemplateId] = useState(null); @@ -59,8 +51,8 @@ export default function AgentCreate({ const [goals, setGoals] = useState(['Describe the agent goals here']); const [instructions, setInstructions] = useState(['']); - const models = ['gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-4-32k', 'google-palm-bison-001'] - const [model, setModel] = useState(models[1]); + const [modelsArray, setModelsArray] = useState([]); + const [model, setModel] = useState(''); const modelRef = useRef(null); const [modelDropdown, setModelDropdown] = useState(false); @@ -94,18 +86,12 @@ export default function AgentCreate({ const toolkitRef = useRef(null); const [toolkitDropdown, setToolkitDropdown] = useState(false); - const excludedToolkits = ["Thinking Toolkit", "Human Input Toolkit", "Resource Toolkit"]; const [hasAPIkey, setHasAPIkey] = useState(false); const [createDropdown, setCreateDropdown] = useState(false); const [createModal, setCreateModal] = useState(false); const [scheduleData, setScheduleData] = useState(null); - const [col6ScrollTop, setCol6ScrollTop] = useState(0); - - const handleCol3Scroll = (event) => { - setCol6ScrollTop(event.target.scrollTop); - }; useEffect(() => { getOrganisationConfig(organisationId, "model_api_key") @@ -138,6 +124,21 @@ export default function AgentCreate({ }, [toolNames]); useEffect(() => { + getLlmModels() + .then((response) => { + const models = response.data || []; + const selected_model = localStorage.getItem("agent_model_" + String(internalId)) || ''; + setModelsArray(models); + if(models.length > 0 && !selected_model) { + setLocalStorageValue("agent_model_" + String(internalId), models[0], setModel); + } else { + setModel(selected_model); + } + }) + .catch((error) => { + console.error('Error fetching models:', error); + }); + if (template !== null) { setLocalStorageValue("agent_name_" + String(internalId), template.name, setAgentName); setLocalStorageValue("agent_description_" + String(internalId), template.description, setAgentDescription); @@ -266,8 +267,8 @@ export default function AgentCreate({ }; const handleModelSelect = (index) => { - setLocalStorageValue("agent_model_" + String(internalId), models[index], setModel); - if (models[index] === "google-palm-bison-001") { + setLocalStorageValue("agent_model_" + String(internalId), modelsArray[index], setModel); + if (modelsArray[index] === "google-palm-bison-001") { setAgentType("Fixed Task Queue") } setModelDropdown(false); @@ -392,6 +393,10 @@ export default function AgentCreate({ toast.error("Add atleast one tool", {autoClose: 1800}); return false; } + if(!modelsArray.includes(model)) { + toast.error("Your key does not have access to the selected model", {autoClose: 1800}); + return false; + } return true; } @@ -716,7 +721,7 @@ export default function AgentCreate({ return (<>
    -
    +
    Create new agent
    @@ -787,7 +792,7 @@ export default function AgentCreate({
    {modelDropdown &&
    - {models.map((model, index) => ( + {modelsArray?.map((model, index) => (
    handleModelSelect(index)} style={{padding: '12px 14px', maxWidth: '100%'}}> {model} @@ -826,7 +831,7 @@ export default function AgentCreate({ {toolkitDropdown &&
    {toolkitList && toolkitList.filter((toolkit) => toolkit.tools ? toolkit.tools.some((tool) => tool.name.toLowerCase().includes(searchValue.toLowerCase())) : false).map((toolkit, index) => (
    - {toolkit.name !== null && !excludedToolkits.includes(toolkit.name) &&
    + {toolkit.name !== null && !excludedToolkits().includes(toolkit.name) &&
    addToolkit(toolkit)} className="custom_select_option" style={{ padding: '10px 14px', maxWidth: '100%', @@ -1055,23 +1060,7 @@ export default function AgentCreate({ )}
    - {createDropdown && (
    { - setCreateModal(true); - setCreateDropdown(false); - }}>Create & Schedule Run + {createDropdown && (
    {setCreateModal(true);setCreateDropdown(false);}}>Create & Schedule Run
    )}
    @@ -1088,7 +1077,7 @@ export default function AgentCreate({
    {createModal && ( - + )}
    diff --git a/gui/pages/Content/Agents/AgentSchedule.js b/gui/pages/Content/Agents/AgentSchedule.js index b78b2b168..574c0e6c7 100644 --- a/gui/pages/Content/Agents/AgentSchedule.js +++ b/gui/pages/Content/Agents/AgentSchedule.js @@ -9,21 +9,14 @@ import {agentScheduleComponent, createAndScheduleRun, updateSchedule} from "@/pa import {EventBus} from "@/utils/eventBus"; import moment from 'moment'; -export default function AgentSchedule({ - internalId, - closeCreateModal, - type, - agentId, - setCreateModal, - setCreateEditModal - }) { +export default function AgentSchedule({internalId, closeCreateModal, type, agentId, setCreateModal, setCreateEditModal, env}) { const [isRecurring, setIsRecurring] = useState(false); const [timeDropdown, setTimeDropdown] = useState(false); const [expiryDropdown, setExpiryDropdown] = useState(false); const [startTime, setStartTime] = useState(''); - const timeUnitArray = ['Days', 'Hours', 'Minutes']; + const timeUnitArray = (env === 'PROD') ? ['Days', 'Hours'] : ['Days', 'Hours', 'Minutes']; const [timeUnit, setTimeUnit] = useState(timeUnitArray[1]); const [timeValue, setTimeValue] = useState(null); diff --git a/gui/pages/Content/Agents/AgentTemplatesList.js b/gui/pages/Content/Agents/AgentTemplatesList.js index 01cda96c7..dfec926da 100644 --- a/gui/pages/Content/Agents/AgentTemplatesList.js +++ b/gui/pages/Content/Agents/AgentTemplatesList.js @@ -5,14 +5,7 @@ import {fetchAgentTemplateListLocal} from "@/pages/api/DashboardService"; import AgentCreate from "@/pages/Content/Agents/AgentCreate"; import {setLocalStorageValue, openNewTab} from "@/utils/utils"; -export default function AgentTemplatesList({ - sendAgentData, - selectedProjectId, - fetchAgents, - toolkits, - organisationId, - internalId - }) { +export default function AgentTemplatesList({sendAgentData, selectedProjectId, fetchAgents, toolkits, organisationId, internalId, env}) { const [agentTemplates, setAgentTemplates] = useState([]) const [createAgentClicked, setCreateAgentClicked] = useState(false) const [sendTemplate, setSendTemplate] = useState(null) @@ -112,7 +105,7 @@ export default function AgentTemplatesList({
    : } + template={sendTemplate} env={env} />}
    ) }; diff --git a/gui/pages/Content/Agents/AgentWorkspace.js b/gui/pages/Content/Agents/AgentWorkspace.js index bd3372d07..71c80bd00 100644 --- a/gui/pages/Content/Agents/AgentWorkspace.js +++ b/gui/pages/Content/Agents/AgentWorkspace.js @@ -25,7 +25,7 @@ import {EventBus} from "@/utils/eventBus"; import 'moment-timezone'; import AgentSchedule from "@/pages/Content/Agents/AgentSchedule"; -export default function AgentWorkspace({agentId, agentName, selectedView, agents, internalId}) { +export default function AgentWorkspace({env, agentId, agentName, selectedView, agents, internalId}) { const [leftPanel, setLeftPanel] = useState('activity_feed') const [rightPanel, setRightPanel] = useState('') const [history, setHistory] = useState(true) @@ -235,7 +235,7 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents } function fetchAgentScheduleComponent() { - if (agent.is_scheduled) { + if (agent?.is_scheduled) { getDateTime(agentId) .then((response) => { setAgentScheduleDetails(response.data) @@ -365,9 +365,9 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents }}>Resume} {agentExecutions && agentExecutions.length > 1 &&
  • { updateRunStatus("TERMINATED") - }}>Delete
  • } + }}>Delete Run} - {agent && agent.is_scheduled ? (
    + {agent?.is_scheduled ? (
  • Edit Schedule
  • Stop Schedule
  • ) : (
    @@ -379,10 +379,10 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents
    } {createModal && - setCreateModal(false)}/>} {createEditModal && - setCreateEditModal(false)}/>} {createStopModal && (
    @@ -406,7 +406,7 @@ export default function AgentWorkspace({agentId, agentName, selectedView, agents
    {leftPanel === 'activity_feed' &&
    -
    } {leftPanel === 'agent_type' && diff --git a/gui/pages/Content/Agents/Agents.js b/gui/pages/Content/Agents/Agents.js index 3eb519868..1451acbda 100644 --- a/gui/pages/Content/Agents/Agents.js +++ b/gui/pages/Content/Agents/Agents.js @@ -30,7 +30,7 @@ export default function Agents({sendAgentData, agents}) { src="/images/loading.gif" alt="active-icon"/>
    }
    {agent.name}
    - {agent.is_scheduled && + {agent?.is_scheduled &&
    check-icon
    }
    diff --git a/gui/pages/Content/Agents/Details.js b/gui/pages/Content/Agents/Details.js index 8e1620daf..2a5c66f48 100644 --- a/gui/pages/Content/Agents/Details.js +++ b/gui/pages/Content/Agents/Details.js @@ -16,7 +16,7 @@ export default function Details({agentDetails, runCount, goals, instructions, ag }, [instructions]); useEffect(() => { - if (agent.is_scheduled) { + if (agent?.is_scheduled) { if (agentScheduleDetails?.recurrence_interval !== null) { if ((agentScheduleDetails?.expiry_runs === -1 || agentScheduleDetails?.expiry_runs == null) && agentScheduleDetails?.expiry_date !== null) { let expiryDate; @@ -182,7 +182,7 @@ export default function Details({agentDetails, runCount, goals, instructions, ag
    info-icon
    Stop after {agentDetails.max_iterations} iterations
    } - {agent.is_scheduled &&
    + {agent?.is_scheduled &&
    info-icon
    {scheduleText}
    } diff --git a/gui/pages/Content/Agents/ResourceList.js b/gui/pages/Content/Agents/ResourceList.js index 288628eac..0207211ce 100644 --- a/gui/pages/Content/Agents/ResourceList.js +++ b/gui/pages/Content/Agents/ResourceList.js @@ -34,8 +34,8 @@ export default function ResourceList({files, channel, runs}) { ) return ( -
    - {channel === 'output' && (!isAnyFileWithAgentId || files.length <= 0 ? +
    + {channel === 'output' && (!isAnyFileWithAgentId || files.length <= 0 ?
    no-permissions No Output files! @@ -43,30 +43,30 @@ export default function ResourceList({files, channel, runs}) { :
    {filesByRun.map((filesRun, index) => ( -
    -
    setSelectedRunId(filesRun.run.id === selectedRunId ? null : filesRun.run.id)}> -
    - arrow - {filesRun.run.name} -
    bolt Run {index + 1}
    -
    - download_icon downloadRunFiles(filesRun.run.id, filesRun.run.name)}/> -
    +
    +
    setSelectedRunId(filesRun.run.id === selectedRunId ? null : filesRun.run.id)}> +
    + arrow + {filesRun.run.name} +
    bolt Run {filesByRun.length - index}
    +
    + download_icon downloadRunFiles(filesRun.run.id, filesRun.run.name)}/> +
    - {selectedRunId === filesRun.run.id && ( -
    - {filesRun.files.map((file, index) => )} -
    - )} + {selectedRunId === filesRun.run.id && ( +
    + {filesRun.files.map((file, index) => )}
    - ))} + )}
    + ))} +
    )} {channel === 'input' && diff --git a/gui/pages/Content/Agents/ResourceManager.js b/gui/pages/Content/Agents/ResourceManager.js index d710dca49..f3acd2743 100644 --- a/gui/pages/Content/Agents/ResourceManager.js +++ b/gui/pages/Content/Agents/ResourceManager.js @@ -30,7 +30,6 @@ export default function ResourceManager({agentId, runs}) { } } }; - const handleFileInputChange = (event) => { const files = event.target.files; handleFile(files); diff --git a/gui/pages/Content/Marketplace/MarketTools.js b/gui/pages/Content/Marketplace/MarketTools.js index 6b7c800f1..1f76c4a32 100644 --- a/gui/pages/Content/Marketplace/MarketTools.js +++ b/gui/pages/Content/Marketplace/MarketTools.js @@ -3,7 +3,7 @@ import Image from "next/image"; import styles from './Market.module.css'; import {fetchToolTemplateList} from "@/pages/api/DashboardService"; import {EventBus} from "@/utils/eventBus"; -import {loadingTextEffect} from "@/utils/utils"; +import {loadingTextEffect, excludedToolkits} from "@/utils/utils"; import axios from 'axios'; export default function MarketTools() { @@ -20,7 +20,8 @@ export default function MarketTools() { axios.get('https://app.superagi.com/api/toolkits/marketplace/list/0') .then((response) => { const data = response.data || []; - setToolTemplates(data); + const filteredData = data?.filter((item) => !excludedToolkits().includes(item.name)); + setToolTemplates(filteredData); setIsLoading(false); }) .catch((error) => { @@ -30,7 +31,8 @@ export default function MarketTools() { fetchToolTemplateList() .then((response) => { const data = response.data || []; - setToolTemplates(data); + const filteredData = data?.filter((item) => !excludedToolkits().includes(item.name)); + setToolTemplates(filteredData); setIsLoading(false); }) .catch((error) => { @@ -48,7 +50,7 @@ export default function MarketTools() {
    {!isLoading ?
    - {toolTemplates.length > 0 ?
    {toolTemplates.map((item, index) => ( + {toolTemplates.length > 0 ?
    {toolTemplates.map((item) => (
    handleTemplateClick(item)}>
    diff --git a/gui/pages/Content/Toolkits/ToolkitWorkspace.js b/gui/pages/Content/Toolkits/ToolkitWorkspace.js index 99fc711e7..894cc9d0f 100644 --- a/gui/pages/Content/Toolkits/ToolkitWorkspace.js +++ b/gui/pages/Content/Toolkits/ToolkitWorkspace.js @@ -10,7 +10,7 @@ import { import styles from './Tool.module.css'; import {setLocalStorageValue, setLocalStorageArray, returnToolkitIcon, convertToTitleCase} from "@/utils/utils"; -export default function ToolkitWorkspace({toolkitDetails, internalId}) { +export default function ToolkitWorkspace({env, toolkitDetails, internalId}) { const [activeTab, setActiveTab] = useState('configuration') const [showDescription, setShowDescription] = useState(false) const [apiConfigs, setApiConfigs] = useState([]); @@ -25,9 +25,15 @@ export default function ToolkitWorkspace({toolkitDetails, internalId}) { }; function getGoogleToken(client_data) { + var redirect_uri = ""; + if (env == "PROD"){ + redirect_uri = 'https://app.superagi.com/api/google/oauth-tokens'; + } + else { + redirect_uri = "http://localhost:3000/api/google/oauth-tokens"; + } const client_id = client_data.client_id const scope = 'https://www.googleapis.com/auth/calendar'; - const redirect_uri = 'http://localhost:3000/api/google/oauth-tokens'; window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${client_id}&redirect_uri=${redirect_uri}&access_type=offline&response_type=code&scope=${scope}`; } diff --git a/gui/pages/Content/Toolkits/Toolkits.js b/gui/pages/Content/Toolkits/Toolkits.js index 6a9d31646..d103f2a2b 100644 --- a/gui/pages/Content/Toolkits/Toolkits.js +++ b/gui/pages/Content/Toolkits/Toolkits.js @@ -3,11 +3,9 @@ import Image from "next/image"; import styles from './Tool.module.css'; import styles1 from '../Agents/Agents.module.css' import 'react-toastify/dist/ReactToastify.css'; -import {createInternalId, returnToolkitIcon} from "@/utils/utils"; +import {createInternalId, returnToolkitIcon, excludedToolkits} from "@/utils/utils"; export default function Toolkits({sendToolkitData, toolkits, env}) { - const excludedToolkits = ["Thinking Toolkit", "Human Input Toolkit", "Resource Toolkit"]; - return ( <>
    @@ -28,7 +26,7 @@ export default function Toolkits({sendToolkitData, toolkits, env}) {
    {toolkits.map((tool, index) => - tool.name !== null && !excludedToolkits.includes(tool.name) && ( + tool.name !== null && !excludedToolkits().includes(tool.name) && (
    sendToolkitData(tool)}>
    diff --git a/gui/pages/Dashboard/Content.js b/gui/pages/Dashboard/Content.js index 8d9270712..4d4a9d924 100644 --- a/gui/pages/Dashboard/Content.js +++ b/gui/pages/Dashboard/Content.js @@ -364,14 +364,14 @@ export default function Content({env, selectedView, selectedProjectId, organisat } {tab.contentType === 'Toolkits' && - } + } {tab.contentType === 'Settings' && } {tab.contentType === 'Marketplace' && } {tab.contentType === 'Add_Toolkit' && } {tab.contentType === 'Create_Agent' && } + fetchAgents={getAgentList} toolkits={toolkits} env={env} />} {tab.contentType === 'APM' && }
    }
    diff --git a/gui/pages/Dashboard/Settings/Settings.js b/gui/pages/Dashboard/Settings/Settings.js index 291feea85..1fe3a88c0 100644 --- a/gui/pages/Dashboard/Settings/Settings.js +++ b/gui/pages/Dashboard/Settings/Settings.js @@ -41,7 +41,7 @@ export default function Settings({organisationId}) { function handleClickOutside(event) { if (sourceRef.current && !sourceRef.current.contains(event.target)) { - setSourceDropdown(false) + setSourceDropdown(false); } } @@ -52,15 +52,14 @@ export default function Settings({organisationId}) { }, [organisationId]); function updateKey(key, value) { - const configData = {"key": key, "value": value}; - updateOrganisationConfig(organisationId, configData) + const configData = { "key": key, "value": value }; + return updateOrganisationConfig(organisationId, configData) .then((response) => { - getKey("model_api_key"); - EventBus.emit("keySet", {}); - toast.success("Settings updated", {autoClose: 1800}); + return response.data; }) .catch((error) => { - console.error('Error fetching project:', error); + console.error('Error updating settings:', error); + throw new Error('Failed to update settings'); }); } @@ -75,19 +74,32 @@ export default function Settings({organisationId}) { const saveSettings = () => { if (modelApiKey === null || modelApiKey.replace(/\s/g, '') === '') { - toast.error("API key is empty", {autoClose: 1800}); - return + toast.error("API key is empty", { autoClose: 1800 }); + return; } validateLLMApiKey(source, modelApiKey) - .then((response) => { - if (response.data.status==="success") { - updateKey("model_api_key", modelApiKey); - updateKey("model_source", source); - } else { - toast.error("Invalid API key", {autoClose: 1800}); - } - }) + .then((response) => { + if (response.data.status === "success") { + Promise.all([ + updateKey("model_api_key", modelApiKey), + updateKey("model_source", source) + ]) + .then(() => { + toast.success("Settings updated", { autoClose: 1800 }); + }) + .catch((error) => { + console.error('Error updating settings:', error); + toast.error("Failed to update settings", { autoClose: 1800 }); + }); + } else { + toast.error("Invalid API key", { autoClose: 1800 }); + } + }) + .catch((error) => { + console.error('Error validating API key:', error); + toast.error("Failed to validate API key", { autoClose: 1800 }); + }); }; const handleTemperatureChange = (event) => { diff --git a/gui/pages/_app.css b/gui/pages/_app.css index 217eaf6c3..1656b217f 100644 --- a/gui/pages/_app.css +++ b/gui/pages/_app.css @@ -415,7 +415,7 @@ input[type="range"]::-moz-range-track { background-color: transparent; } -.custom_select_option { +.custom_select_option, .create_agent_dropdown_options { cursor: pointer; font-size: 12px; color: white; @@ -427,16 +427,29 @@ input[type="range"]::-moz-range-track { text-overflow: ellipsis; } -.custom_select_option:hover { +.custom_select_option:hover, .create_agent_dropdown_options:hover { background: #3B3B49; border-radius: 8px; } -.custom_select_option:active { +.custom_select_option:active, .create_agent_dropdown_options:active { background: #3B3B49; border-radius: 8px; } +.create_agent_dropdown_options{ + background: #3B3B49; + border-radius: 8px; + position: absolute; + top: -40px; + right: 0; + box-shadow: 0 2px 7px rgba(0,0,0,.4), 0 0 2px rgba(0,0,0,.22); + height: 40px; + width: 150px; + padding-top: 10px; + text-align: center; +} + @keyframes scale-in { from { opacity: 0; @@ -983,11 +996,16 @@ tr{ } .tools_used_tooltip{ - position: relative; + position: absolute; cursor: pointer; z-index: 100; } +.image_class{ + background: #FFFFFF80; + border-radius: 20px; +} + .image_class{ background: #FFFFFF80; border-radius: 20px; diff --git a/gui/pages/api/DashboardService.js b/gui/pages/api/DashboardService.js index f78c005da..ab9ef8df1 100644 --- a/gui/pages/api/DashboardService.js +++ b/gui/pages/api/DashboardService.js @@ -209,4 +209,8 @@ export const getActiveRuns = () => { export const getToolsUsage = () => { return api.get(`analytics/tools/used`); +} + +export const getLlmModels = () => { + return api.get(`organisations/llm_models`); } \ No newline at end of file diff --git a/gui/utils/utils.js b/gui/utils/utils.js index e8d05ba92..8f9682da2 100644 --- a/gui/utils/utils.js +++ b/gui/utils/utils.js @@ -37,13 +37,15 @@ export const convertToGMT = (dateTime) => { export const formatTimeDifference = (timeDifference) => { const units = ['years', 'months', 'days', 'hours', 'minutes']; + const singularUnits = ['year', 'month', 'day', 'hour', 'minute']; - for (const unit of units) { + for (let i = 0; i < units.length; i++) { + const unit = units[i]; if (timeDifference[unit] !== 0) { if (unit === 'minutes') { - return `${timeDifference[unit]} minutes ago`; + return `${timeDifference[unit]} ${timeDifference[unit] === 1 ? singularUnits[i] : unit} ago`; } else { - return `${timeDifference[unit]} ${unit} ago`; + return `${timeDifference[unit]} ${timeDifference[unit] === 1 ? singularUnits[i] : unit} ago`; } } } @@ -376,7 +378,10 @@ export const convertToTitleCase = (str) => { const capitalizedWords = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)); return capitalizedWords.join(' '); }; - export const preventDefault = (e) => { e.stopPropagation(); -}; \ No newline at end of file +}; + +export const excludedToolkits = () => { + return ["Thinking Toolkit", "Human Input Toolkit", "Resource Toolkit"]; +} \ No newline at end of file diff --git a/main.py b/main.py index 33c11eb98..0045c59a4 100644 --- a/main.py +++ b/main.py @@ -126,8 +126,9 @@ class Settings(BaseModel): def create_access_token(email, Authorize: AuthJWT = Depends()): - # expiry_time_hours = get_config("JWT_EXPIRY") - expiry_time_hours = 1 + expiry_time_hours = superagi.config.config.get_config("JWT_EXPIRY") + if type(expiry_time_hours) == str: + expiry_time_hours = int(expiry_time_hours) expires = timedelta(hours=expiry_time_hours) access_token = Authorize.create_access_token(subject=email, expires_time=expires) return access_token diff --git a/migrations/versions/cac478732572_delete_agent_feature.py b/migrations/versions/cac478732572_delete_agent_feature.py index e0c483975..15153145b 100644 --- a/migrations/versions/cac478732572_delete_agent_feature.py +++ b/migrations/versions/cac478732572_delete_agent_feature.py @@ -8,7 +8,6 @@ from alembic import op import sqlalchemy as sa - # revision identifiers, used by Alembic. revision = 'cac478732572' down_revision = 'e39295ec089c' @@ -17,7 +16,8 @@ def upgrade() -> None: - op.add_column('agents', sa.Column('is_deleted', sa.Boolean(), nullable=True)) + op.add_column('agents', sa.Column('is_deleted', sa.Boolean(), nullable=True, server_default=sa.false())) + def downgrade() -> None: op.drop_column('agents', 'is_deleted') diff --git a/superagi/agent/output_parser.py b/superagi/agent/output_parser.py index bd3747580..892c8cde0 100644 --- a/superagi/agent/output_parser.py +++ b/superagi/agent/output_parser.py @@ -42,4 +42,3 @@ def parse(self, response: str) -> AgentGPTAction: ) except BaseException as e: logger.info(f"AgentSchemaOutputParser: Error parsing JSON respons {e}") - return {} diff --git a/superagi/agent/super_agi.py b/superagi/agent/super_agi.py index cbcda55ca..5f324bea3 100644 --- a/superagi/agent/super_agi.py +++ b/superagi/agent/super_agi.py @@ -136,7 +136,10 @@ def execute(self, workflow_step: AgentWorkflowStep): total_tokens = current_tokens + TokenCounter.count_message_tokens(response, self.llm.get_model()) self.update_agent_execution_tokens(current_calls, total_tokens, session) - + + if 'error' in response and response['error'] == "RATE_LIMIT_EXCEEDED": + return {"result": "RATE_LIMIT_EXCEEDED", "retry": True} + if 'content' not in response or response['content'] is None: raise RuntimeError(f"Failed to get response from llm") assistant_reply = response['content'] @@ -210,18 +213,18 @@ def execute(self, workflow_step: AgentWorkflowStep): def handle_tool_response(self, session, assistant_reply): action = self.output_parser.parse(assistant_reply) tools = {t.name.lower().replace(" ", ""): t for t in self.tools} - action_name = action.name.lower().replace(" ", "") + action_name = action.name.lower().replace(" ", "") if action is not None else "" agent = session.query(Agent).filter(Agent.id == self.agent_config["agent_id"],).first() organisation = agent.get_agent_organisation(session) - if action_name == FINISH or action.name == "": + if action_name == FINISH or action_name == "": logger.info("\nTask Finished :) \n") output = {"result": "COMPLETE", "retry": False} - EventHandler(session=session).create_event('tool_used', {'tool_name':action.name}, self.agent_config["agent_id"], organisation.id), + EventHandler(session=session).create_event('tool_used', {'tool_name':action_name}, self.agent_config["agent_id"], organisation.id), return output if action_name in tools: tool = tools[action_name] retry = False - EventHandler(session=session).create_event('tool_used', {'tool_name':action.name}, self.agent_config["agent_id"], organisation.id), + EventHandler(session=session).create_event('tool_used', {'tool_name':action_name}, self.agent_config["agent_id"], organisation.id), try: parsed_args = self.clean_tool_args(action.args) observation = tool.execute(parsed_args) @@ -237,12 +240,12 @@ def handle_tool_response(self, session, assistant_reply): ) result = f"Tool {tool.name} returned: {observation}" output = {"result": result, "retry": retry} - elif action.name == "ERROR": + elif action_name == "ERROR": result = f"Error2: {action.args}. " output = {"result": result, "retry": False} else: result = ( - f"Unknown tool '{action.name}'. " + f"Unknown tool '{action_name}'. " f"Please refer to the 'TOOLS' list for available " f"tools and only respond in the specified JSON format." ) @@ -298,7 +301,8 @@ def check_permission_in_restricted_mode(self, assistant_reply: str, session): excluded_tools = [FINISH, '', None] - if self.agent_config["permission_type"].upper() == "RESTRICTED" and action.name not in excluded_tools and \ + if self.agent_config["permission_type"].upper() == "RESTRICTED" and action is not None and \ + action.name not in excluded_tools and \ tools.get(action.name) and tools[action.name].permission_required: new_agent_execution_permission = AgentExecutionPermission( agent_execution_id=self.agent_config["agent_execution_id"], diff --git a/superagi/controllers/agent.py b/superagi/controllers/agent.py index 0b7169330..b287b05bc 100644 --- a/superagi/controllers/agent.py +++ b/superagi/controllers/agent.py @@ -9,7 +9,7 @@ from jsonmerge import merge from pytz import timezone -from sqlalchemy import func +from sqlalchemy import func, or_ from superagi.models.agent_execution_permission import AgentExecutionPermission from superagi.worker import execute_agent from superagi.helper.auth import check_auth @@ -109,11 +109,9 @@ def get_agent(agent_id: int, HTTPException (Status Code=404): If the Agent is not found or deleted. """ - if ( - db_agent := db.session.query(Agent) - .filter(Agent.id == agent_id, Agent.is_deleted == False) - .first() - ): + if (db_agent := db.session.query(Agent) + .filter(Agent.id == agent_id, or_(Agent.is_deleted == False, Agent.is_deleted is None)) + .first()): return db_agent else: raise HTTPException(status_code=404, detail="agent not found") @@ -141,7 +139,7 @@ def update_agent(agent_id: int, agent: AgentIn, HTTPException (Status Code=404): If the Agent or associated Project is not found. """ - db_agent = db.session.query(Agent).filter(Agent.id == agent_id, Agent.is_deleted == False).first() + db_agent = db.session.query(Agent).filter(Agent.id == agent_id, or_(Agent.is_deleted == False, Agent.is_deleted is None)).first() if not db_agent: raise HTTPException(status_code=404, detail="agent not found") @@ -190,10 +188,11 @@ def create_agent_with_config(agent_with_config: AgentConfigInput, project = db.session.query(Project).get(agent_with_config.project_id) if not project: raise HTTPException(status_code=404, detail="Project not found") - + invalid_tools = Tool.get_invalid_tools(agent_with_config.tools, db.session) if len(invalid_tools) > 0: # If the returned value is not True (then it is an invalid tool_id) - raise HTTPException(status_code=404, detail=f"Tool with IDs {str(invalid_tools)} does not exist. 404 Not Found.") + raise HTTPException(status_code=404, + detail=f"Tool with IDs {str(invalid_tools)} does not exist. 404 Not Found.") agent_toolkit_tools = Toolkit.fetch_tool_ids_from_toolkit(session=db.session, toolkit_ids=agent_with_config.toolkits) @@ -215,10 +214,14 @@ def create_agent_with_config(agent_with_config: AgentConfigInput, AgentExecutionConfiguration.add_or_update_agent_execution_config(session=db.session, execution=execution, agent_execution_configs=agent_execution_configs) - agent = db.session.query(Agent).filter(Agent.id == db_agent.id,).first() + agent = db.session.query(Agent).filter(Agent.id == db_agent.id, ).first() organisation = agent.get_agent_organisation(db.session) - EventHandler(session=db.session).create_event('run_created', {'agent_execution_id': execution.id,'agent_execution_name':execution.name}, db_agent.id, organisation.id if organisation else 0), - EventHandler(session=db.session).create_event('agent_created', {'agent_name': agent_with_config.name, 'model': agent_with_config.model}, db_agent.id, organisation.id if organisation else 0) + EventHandler(session=db.session).create_event('run_created', {'agent_execution_id': execution.id, + 'agent_execution_name': execution.name}, db_agent.id, + organisation.id if organisation else 0), + EventHandler(session=db.session).create_event('agent_created', {'agent_name': agent_with_config.name, + 'model': agent_with_config.model}, db_agent.id, + organisation.id if organisation else 0) # execute_agent.delay(execution.id, datetime.now()) @@ -231,6 +234,7 @@ def create_agent_with_config(agent_with_config: AgentConfigInput, "contentType": "Agents" } + @router.post("/schedule", status_code=201) def create_and_schedule_agent(agent_config_schedule: AgentConfigSchedule, Authorize: AuthJWT = Depends(check_auth)): @@ -253,7 +257,8 @@ def create_and_schedule_agent(agent_config_schedule: AgentConfigSchedule, agent_config = agent_config_schedule.agent_config invalid_tools = Tool.get_invalid_tools(agent_config.tools, db.session) if len(invalid_tools) > 0: # If the returned value is not True (then it is an invalid tool_id) - raise HTTPException(status_code=404, detail=f"Tool with IDs {str(invalid_tools)} does not exist. 404 Not Found.") + raise HTTPException(status_code=404, + detail=f"Tool with IDs {str(invalid_tools)} does not exist. 404 Not Found.") agent_toolkit_tools = Toolkit.fetch_tool_ids_from_toolkit(session=db.session, toolkit_ids=agent_config.toolkits) @@ -289,6 +294,7 @@ def create_and_schedule_agent(agent_config_schedule: AgentConfigSchedule, "schedule_id": agent_schedule.id } + @router.post("/stop/schedule", status_code=200) def stop_schedule(agent_id: int, Authorize: AuthJWT = Depends(check_auth)): """ @@ -303,7 +309,7 @@ def stop_schedule(agent_id: int, Authorize: AuthJWT = Depends(check_auth)): """ agent_to_delete = db.session.query(AgentSchedule).filter(AgentSchedule.agent_id == agent_id, - AgentSchedule.status == "SCHEDULED").first() + AgentSchedule.status == "SCHEDULED").first() if not agent_to_delete: raise HTTPException(status_code=404, detail="Schedule not found") agent_to_delete.status = "STOPPED" @@ -326,7 +332,7 @@ def edit_schedule(schedule: AgentScheduleInput, """ agent_to_edit = db.session.query(AgentSchedule).filter(AgentSchedule.agent_id == schedule.agent_id, - AgentSchedule.status == "SCHEDULED").first() + AgentSchedule.status == "SCHEDULED").first() if not agent_to_edit: raise HTTPException(status_code=404, detail="Schedule not found") @@ -358,7 +364,7 @@ def get_schedule_data(agent_id: int, Authorize: AuthJWT = Depends(check_auth)): expiry_runs (Integer): The number of runs before the agent expires. """ agent = db.session.query(AgentSchedule).filter(AgentSchedule.agent_id == agent_id, - AgentSchedule.status == "SCHEDULED").first() + AgentSchedule.status == "SCHEDULED").first() if not agent: raise HTTPException(status_code=404, detail="Agent Schedule not found") @@ -371,7 +377,6 @@ def get_schedule_data(agent_id: int, Authorize: AuthJWT = Depends(check_auth)): else: tzone = timezone('GMT') - current_datetime = datetime.now(tzone).strftime("%d/%m/%Y %I:%M %p") return { @@ -406,7 +411,7 @@ def get_agents_by_project_id(project_id: int, if not project: raise HTTPException(status_code=404, detail="Project not found") - agents = db.session.query(Agent).filter(Agent.project_id == project_id, Agent.is_deleted == False).all() + agents = db.session.query(Agent).filter(Agent.project_id == project_id, or_(Agent.is_deleted == False, Agent.is_deleted is None)).all() new_agents, new_agents_sorted = [], [] for agent in agents: @@ -423,7 +428,7 @@ def get_agents_by_project_id(project_id: int, break # Check if the agent is scheduled is_scheduled = db.session.query(AgentSchedule).filter_by(agent_id=agent_id, - status="SCHEDULED").first() is not None + status="SCHEDULED").first() is not None new_agent = { **agent_dict, @@ -454,7 +459,7 @@ def get_agent_configuration(agent_id: int, # Define the agent_config keys to fetch keys_to_fetch = AgentTemplate.main_keys() - agent = db.session.query(Agent).filter(agent_id == Agent.id, Agent.is_deleted == False).first() + agent = db.session.query(Agent).filter(agent_id == Agent.id,or_(Agent.is_deleted == False, Agent.is_deleted is None)).first() if not agent: raise HTTPException(status_code=404, detail="Agent not found") @@ -485,7 +490,8 @@ def get_agent_configuration(agent_id: int, return response -@router.put("/delete/{agent_id}", status_code = 200) + +@router.put("/delete/{agent_id}", status_code=200) def delete_agent(agent_id: int, Authorize: AuthJWT = Depends(check_auth)): """ Delete an existing Agent @@ -502,18 +508,18 @@ def delete_agent(agent_id: int, Authorize: AuthJWT = Depends(check_auth)): Raises: HTTPException (Status Code=404): If the Agent or associated Project is not found or deleted already. """ - + db_agent = db.session.query(Agent).filter(Agent.id == agent_id).first() - db_agent_executions = db.session.query(AgentExecution).filter(AgentExecution.agent_id == agent_id).all() - + db_agent_executions = db.session.query(AgentExecution).filter(AgentExecution.agent_id == agent_id).all() + if not db_agent or db_agent.is_deleted: raise HTTPException(status_code=404, detail="agent not found") - + # Deletion Procedure db_agent.is_deleted = True if db_agent_executions: # Updating all the RUNNING executions to TERMINATED for db_agent_execution in db_agent_executions: db_agent_execution.status = "TERMINATED" - + db.session.commit() diff --git a/superagi/controllers/google_oauth.py b/superagi/controllers/google_oauth.py index 4eecd6ba3..1b3b99221 100644 --- a/superagi/controllers/google_oauth.py +++ b/superagi/controllers/google_oauth.py @@ -11,10 +11,11 @@ from datetime import datetime, timedelta from superagi.models.db import connect_db import http.client as http_client -from superagi.helper.auth import get_current_user +from superagi.helper.auth import get_current_user, check_auth from superagi.models.tool_config import ToolConfig from superagi.models.toolkit import Toolkit from superagi.models.oauth_tokens import OauthTokens +from superagi.config.config import get_config router = APIRouter() @@ -26,10 +27,15 @@ async def google_auth_calendar(code: str = Query(...), Authorize: AuthJWT = Depe client_secret = client_secret.value token_uri = 'https://oauth2.googleapis.com/token' scope = 'https://www.googleapis.com/auth/calendar' + env = get_config("ENV", "DEV") + if env == "DEV": + redirect_uri = "http://localhost:3000/api/google/oauth-tokens" + else: + redirect_uri = "https://app.superagi.com/api/google/oauth-tokens" params = { 'client_id': client_id, 'client_secret': client_secret, - 'redirect_uri': "http://localhost:3000/api/google/oauth-tokens", + 'redirect_uri': redirect_uri, 'scope': scope, 'grant_type': 'authorization_code', 'code': code, @@ -46,11 +52,11 @@ async def google_auth_calendar(code: str = Query(...), Authorize: AuthJWT = Depe return RedirectResponse(url=redirect_url_success) @router.post("/send_google_creds/toolkit_id/{toolkit_id}") -def send_google_calendar_configs(google_creds: dict, toolkit_id: int, Authorize: AuthJWT = Depends()): +def send_google_calendar_configs(google_creds: dict, toolkit_id: int, Authorize: AuthJWT = Depends(check_auth)): engine = connect_db() Session = sessionmaker(bind=engine) session = Session() - current_user = get_current_user() + current_user = get_current_user(Authorize) user_id = current_user.id toolkit = db.session.query(Toolkit).filter(Toolkit.id == toolkit_id).first() google_creds = json.dumps(google_creds) diff --git a/superagi/controllers/organisation.py b/superagi/controllers/organisation.py index dae46a532..39403d56d 100644 --- a/superagi/controllers/organisation.py +++ b/superagi/controllers/organisation.py @@ -6,8 +6,13 @@ from fastapi_sqlalchemy import db from pydantic import BaseModel +from superagi.helper.auth import get_user_organisation from superagi.helper.auth import check_auth +from superagi.helper.encyption_helper import decrypt_data from superagi.helper.tool_helper import register_toolkits +from superagi.llms.google_palm import GooglePalm +from superagi.llms.openai import OpenAi +from superagi.models.configuration import Configuration from superagi.models.organisation import Organisation from superagi.models.project import Project from superagi.models.user import User @@ -35,6 +40,7 @@ class OrganisationIn(BaseModel): class Config: orm_mode = True + # CRUD Operations @router.post("/add", response_model=OrganisationOut, status_code=201) def create_organisation(organisation: OrganisationIn, @@ -141,3 +147,31 @@ def get_organisations_by_user(user_id: int): organisation = Organisation.find_or_create_organisation(db.session, user) Project.find_or_create_default_project(db.session, organisation.id) return organisation + + +@router.get("/llm_models") +def get_llm_models(organisation=Depends(get_user_organisation)): + """ + Get all the llm models associated with an organisation. + + Args: + organisation: Organisation data. + """ + + model_api_key = db.session.query(Configuration).filter(Configuration.organisation_id == organisation.id, + Configuration.key == "model_api_key").first() + model_source = db.session.query(Configuration).filter(Configuration.organisation_id == organisation.id, + Configuration.key == "model_source").first() + + if model_api_key is None or model_source is None: + raise HTTPException(status_code=400, + detail="Organisation not found") + + decrypted_api_key = decrypt_data(model_api_key.value) + models = [] + if model_source.value == "OpenAi": + models = OpenAi(api_key=decrypted_api_key).get_models() + elif model_source.value == "Google Palm": + models = GooglePalm(api_key=decrypted_api_key).get_models() + + return models diff --git a/superagi/controllers/twitter_oauth.py b/superagi/controllers/twitter_oauth.py index 6bdfa3761..131c57ccb 100644 --- a/superagi/controllers/twitter_oauth.py +++ b/superagi/controllers/twitter_oauth.py @@ -1,26 +1,23 @@ -from fastapi import Depends, Query +import http.client as http_client +import json + from fastapi import APIRouter +from fastapi import Depends, Query from fastapi.responses import RedirectResponse from fastapi_jwt_auth import AuthJWT from fastapi_sqlalchemy import db -from sqlalchemy.orm import sessionmaker import superagi -import json -from superagi.models.db import connect_db -import http.client as http_client +from superagi.helper.auth import get_current_user, check_auth from superagi.helper.twitter_tokens import TwitterTokens -from superagi.helper.auth import get_current_user +from superagi.models.oauth_tokens import OauthTokens from superagi.models.tool_config import ToolConfig from superagi.models.toolkit import Toolkit -from superagi.models.oauth_tokens import OauthTokens router = APIRouter() @router.get('/oauth-tokens') async def twitter_oauth(oauth_token: str = Query(...),oauth_verifier: str = Query(...), Authorize: AuthJWT = Depends()): - print("///////////////////////////") - print(oauth_token) token_uri = f'https://api.twitter.com/oauth/access_token?oauth_verifier={oauth_verifier}&oauth_token={oauth_token}' conn = http_client.HTTPSConnection("api.twitter.com") conn.request("POST", token_uri, "") @@ -31,8 +28,8 @@ async def twitter_oauth(oauth_token: str = Query(...),oauth_verifier: str = Quer return RedirectResponse(url=redirect_url_success) @router.post("/send_twitter_creds/{twitter_creds}") -def send_twitter_tool_configs(twitter_creds: str, Authorize: AuthJWT = Depends()): - current_user = get_current_user() +def send_twitter_tool_configs(twitter_creds: str, Authorize: AuthJWT = Depends(check_auth)): + current_user = get_current_user(Authorize) user_id = current_user.id credentials = json.loads(twitter_creds) credentials["user_id"] = user_id diff --git a/superagi/helper/twitter_tokens.py b/superagi/helper/twitter_tokens.py index 10b36d59a..3cd8dc031 100644 --- a/superagi/helper/twitter_tokens.py +++ b/superagi/helper/twitter_tokens.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import Session from superagi.models.toolkit import Toolkit from superagi.models.oauth_tokens import OauthTokens +from superagi.config.config import get_config class Creds: @@ -29,8 +30,13 @@ def get_request_token(self,api_data): http_method = 'POST' base_url = 'https://api.twitter.com/oauth/request_token' + env = get_config("ENV", "DEV") + if env == "DEV": + oauth_callback = "http://localhost:3000/api/twitter/oauth-tokens" + else: + oauth_callback = "https://app.superagi.com/api/twitter/oauth-tokens" params = { - 'oauth_callback': 'http://localhost:3000/api/twitter/oauth-tokens', + 'oauth_callback': oauth_callback, 'oauth_consumer_key': api_key, 'oauth_nonce': self.gen_nonce(), 'oauth_signature_method': 'HMAC-SHA1', diff --git a/superagi/jobs/agent_executor.py b/superagi/jobs/agent_executor.py index cd8f0f520..3c2aad9a7 100644 --- a/superagi/jobs/agent_executor.py +++ b/superagi/jobs/agent_executor.py @@ -263,7 +263,11 @@ def execute_next_action(self, agent_execution_id): return if "retry" in response and response["retry"]: - superagi.worker.execute_agent.apply_async((agent_execution_id, datetime.now()), countdown=15) + if "result" in response and response["result"] == "RATE_LIMIT_EXCEEDED": + superagi.worker.execute_agent.apply_async((agent_execution_id, datetime.now()), countdown=60) + else: + superagi.worker.execute_agent.apply_async((agent_execution_id, datetime.now()), countdown=15) + session.close() return diff --git a/superagi/llms/base_llm.py b/superagi/llms/base_llm.py index 12b9eb452..b5d068035 100644 --- a/superagi/llms/base_llm.py +++ b/superagi/llms/base_llm.py @@ -18,6 +18,10 @@ def get_api_key(self): def get_model(self): pass + @abstractmethod + def get_models(self): + pass + @abstractmethod def verify_access_key(self): pass diff --git a/superagi/llms/google_palm.py b/superagi/llms/google_palm.py index 707a1579e..40e25d82f 100644 --- a/superagi/llms/google_palm.py +++ b/superagi/llms/google_palm.py @@ -90,3 +90,17 @@ def verify_access_key(self): except Exception as exception: logger.info("Google palm Exception:", exception) return False + + def get_models(self): + """ + Get the models. + + Returns: + list: The models. + """ + try: + models_supported = ["chat-bison-001"] + return models_supported + except Exception as exception: + logger.info("Google palm Exception:", exception) + return [] diff --git a/superagi/llms/llm_model_factory.py b/superagi/llms/llm_model_factory.py index b13f46067..3ec7070f8 100644 --- a/superagi/llms/llm_model_factory.py +++ b/superagi/llms/llm_model_factory.py @@ -22,6 +22,7 @@ def get_model(self, model, **kwargs): factory.register_format("gpt-3.5-turbo-16k", lambda **kwargs: OpenAi(model="gpt-3.5-turbo-16k", **kwargs)) factory.register_format("gpt-3.5-turbo", lambda **kwargs: OpenAi(model="gpt-3.5-turbo", **kwargs)) factory.register_format("google-palm-bison-001", lambda **kwargs: GooglePalm(model='models/chat-bison-001', **kwargs)) +factory.register_format("chat-bison-001", lambda **kwargs: GooglePalm(model='models/chat-bison-001', **kwargs)) def get_model(api_key, model="gpt-3.5-turbo", **kwargs): diff --git a/superagi/llms/openai.py b/superagi/llms/openai.py index a25c0c847..4822ca1e5 100644 --- a/superagi/llms/openai.py +++ b/superagi/llms/openai.py @@ -101,3 +101,21 @@ def verify_access_key(self): except Exception as exception: logger.info("OpenAi Exception:", exception) return False + + def get_models(self): + """ + Get the models. + + Returns: + list: The models. + """ + try: + models = openai.Model.list() + models = [model["id"] for model in models["data"]] + models_supported = ['gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k', 'gpt-4-32k'] + print("CHECK THIS1", models) + models = [model for model in models if model in models_supported] + return models + except Exception as exception: + logger.info("OpenAi Exception:", exception) + return [] diff --git a/superagi/models/db.py b/superagi/models/db.py index d49b5793e..e711280ff 100644 --- a/superagi/models/db.py +++ b/superagi/models/db.py @@ -29,7 +29,13 @@ def connect_db(): db_url = f'postgresql://{db_username}:{db_password}@{database_url}/{db_name}' # Create the SQLAlchemy engine - engine = create_engine(db_url) + engine = create_engine(db_url, + pool_size=20, # Maximum number of database connections in the pool + max_overflow=50, # Maximum number of connections that can be created beyond the pool_size + pool_timeout=30, # Timeout value in seconds for acquiring a connection from the pool + pool_recycle=1800, # Recycle connections after this number of seconds (optional) + pool_pre_ping=False, # Enable connection health checks (optional) + ) # Test the connection try: diff --git a/superagi/resource_manager/file_manager.py b/superagi/resource_manager/file_manager.py index 7379f7585..4c20ba16d 100644 --- a/superagi/resource_manager/file_manager.py +++ b/superagi/resource_manager/file_manager.py @@ -75,10 +75,9 @@ def write_csv_file(self, file_name: str, csv_data): else: final_path = ResourceHelper.get_resource_path(file_name) try: - with open(final_path, mode="w") as file: + with open(final_path, mode="w", newline="") as file: writer = csv.writer(file, lineterminator="\n") - for row in csv_data: - writer.writerows(row) + writer.writerows(csv_data) self.write_to_s3(file_name, final_path) logger.info(f"{file_name} - File written successfully") return f"{file_name} - File written successfully" diff --git a/superagi/resource_manager/llama_document_summary.py b/superagi/resource_manager/llama_document_summary.py index 8f83eb0ca..5eca38913 100644 --- a/superagi/resource_manager/llama_document_summary.py +++ b/superagi/resource_manager/llama_document_summary.py @@ -22,6 +22,8 @@ def generate_summary_of_document(self, documents: list[Document]): :param documents: list of Document objects :return: summary of the documents """ + if documents is None or not documents: + return from llama_index import LLMPredictor, ServiceContext, ResponseSynthesizer, DocumentSummaryIndex os.environ["OPENAI_API_KEY"] = get_config("OPENAI_API_KEY", "") or self.model_api_key llm_predictor_chatgpt = LLMPredictor(llm=self._build_llm()) diff --git a/superagi/resource_manager/resource_manager.py b/superagi/resource_manager/resource_manager.py index 2004d3704..3a417cc7f 100644 --- a/superagi/resource_manager/resource_manager.py +++ b/superagi/resource_manager/resource_manager.py @@ -31,9 +31,9 @@ def create_llama_document(self, file_path: str): """ if file_path is None: raise Exception("file_path must be provided") - documents = SimpleDirectoryReader(input_files=[file_path]).load_data() - - return documents + if os.path.exists(file_path): + documents = SimpleDirectoryReader(input_files=[file_path]).load_data() + return documents def create_llama_document_s3(self, file_path: str): """ @@ -44,6 +44,7 @@ def create_llama_document_s3(self, file_path: str): """ if file_path is None: raise Exception("file_path must be provided") + temporary_file_path = "" try: import boto3 s3 = boto3.client( @@ -61,12 +62,12 @@ def create_llama_document_s3(self, file_path: str): f.write(contents) documents = SimpleDirectoryReader(input_files=[temporary_file_path]).load_data() + return documents except Exception as e: logger.error("superagi/resource_manager/resource_manager.py - create_llama_document_s3 threw : ", e) finally: if os.path.exists(temporary_file_path): os.remove(temporary_file_path) - return documents def save_document_to_vector_store(self, documents: list, resource_id: str, mode_api_key: str = None, model_source: str = ""): diff --git a/superagi/resource_manager/resource_summary.py b/superagi/resource_manager/resource_summary.py index 58084b28d..fb76d3fb0 100644 --- a/superagi/resource_manager/resource_summary.py +++ b/superagi/resource_manager/resource_summary.py @@ -28,7 +28,7 @@ def add_to_vector_store_and_create_summary(self, agent_id: int, resource_id: int agent = self.session.query(Agent).filter(Agent.id == agent_id).first() organization = agent.get_agent_organisation(self.session) model_api_key = Configuration.fetch_configuration(self.session, organization.id, "model_api_key") - model_source = Configuration.fetch_configuration(self.session, organization.id, "model_source") + model_source = Configuration.fetch_configuration(self.session, organization.id, "model_source") or "OpenAi" try: ResourceManager(str(agent_id)).save_document_to_vector_store(documents, str(resource_id), model_api_key, model_source) except Exception as e: @@ -67,7 +67,8 @@ def generate_agent_summary(self, agent_id: int, generate_all: bool = False) -> s documents = ResourceManager(str(agent_id)).create_llama_document_s3(file_path) else: documents = ResourceManager(str(agent_id)).create_llama_document(file_path) - summary_texts.append(LlamaDocumentSummary(model_api_key=model_api_key, model_source=model_source).generate_summary_of_document(documents)) + if documents is not None and len(documents) > 0: + summary_texts.append(LlamaDocumentSummary(model_api_key=model_api_key, model_source=model_source).generate_summary_of_document(documents)) agent_last_resource = self.session.query(AgentConfiguration). \ filter(AgentConfiguration.agent_id == agent_id, diff --git a/superagi/tools/google_serp_search/README.MD b/superagi/tools/google_serp_search/README.md similarity index 85% rename from superagi/tools/google_serp_search/README.MD rename to superagi/tools/google_serp_search/README.md index 2ec1bcc08..efe049054 100644 --- a/superagi/tools/google_serp_search/README.MD +++ b/superagi/tools/google_serp_search/README.md @@ -18,14 +18,14 @@ Set up the SuperAGI by following the instructions given (https://github.com/Tran ### 🔧 **Add Google Serp Search API Key in SuperAGI Dashboard** -1. Register an account verifying your email and phone number. +1. Register an account at [https://serper.dev/](https://serper.dev/) with your Email ID. 2. Your Private API Key would be made. Copy that and save it in a separate text file. -![Screenshot-google serp](https://github.com/TransformerOptimus/SuperAGI/assets/43145646/7f20e9ae-3a25-49cd-aa72-b96f7e6ae305) +![Serper_Key](https://github.com/Phoenix2809/SuperAGI/assets/133874957/dfe70b4f-11e2-483b-aa33-07b15150103d) -3. Open up the Google SERP Toolkit page in SuperAGI's Dashboard and paste your Private API Key. +3. Open up the Google SERP Toolkit page in SuperAGI's Dashboard and paste your Private API Key. ## Running SuperAGI Google Search Serp Tool From 8eeaca4ad3bb88772907d8c1aa7b52ad2538f1ae Mon Sep 17 00:00:00 2001 From: Kalki Date: Tue, 25 Jul 2023 19:20:22 +0530 Subject: [PATCH 38/77] apm bug fixes --- gui/pages/_app.css | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/gui/pages/_app.css b/gui/pages/_app.css index 1656b217f..5637cb77c 100644 --- a/gui/pages/_app.css +++ b/gui/pages/_app.css @@ -996,14 +996,8 @@ tr{ } .tools_used_tooltip{ - position: absolute; + position: relative; cursor: pointer; - z-index: 100; -} - -.image_class{ - background: #FFFFFF80; - border-radius: 20px; } .image_class{ From 97c5ee94ab4045dadb08d1accaf0a19b47f36dad Mon Sep 17 00:00:00 2001 From: Fluder-Paradyne <121793617+Fluder-Paradyne@users.noreply.github.com> Date: Tue, 25 Jul 2023 19:24:17 +0530 Subject: [PATCH 39/77] Docker image (#836) --- .dockerignore | 20 ++++++++++++++++++-- Dockerfile | 38 ++++++++++++++++++++++++++++---------- DockerfileCelery | 11 +++++++++-- docker-compose.yaml | 13 +++++++------ entrypoint.sh | 3 --- entrypoint_celery.sh | 10 +++++++--- gui/.dockerignore | 15 +++++++++++++-- gui/Dockerfile | 18 ++++++++++++------ gui/DockerfileProd | 40 ++++++++++++++++++++++++++++++++++++---- gui/next.config.js | 1 + 10 files changed, 131 insertions(+), 38 deletions(-) mode change 100644 => 100755 entrypoint_celery.sh diff --git a/.dockerignore b/.dockerignore index b090bf637..dfb96d0a9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,18 @@ -venv/ -gui/ \ No newline at end of file +# Ignore everything +** + +# Allow files and directories +!/migrations +!/nginx +!/superagi +!/tgwui +!/tools +!/workspace +!/main.py +!/requirements.txt +!/entrypoint.sh +!/entrypoint_celery.sh +!/wait-for-it.sh +!/tools.json +!/install_tool_dependencies.sh +!/alembic.ini \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 775a0d5dc..86f98bf95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,33 @@ -FROM python:3.9 +# Stage 1: Compile image +FROM python:3.10-slim-bullseye AS compile-image WORKDIR /app -COPY requirements.txt . -#RUN apt-get update && apt-get install --no-install-recommends -y git wget libpq-dev gcc python3-dev && pip install psycopg2 -RUN pip install --upgrade pip -RUN pip install --no-cache-dir -r requirements.txt +RUN apt-get update && \ + apt-get install --no-install-recommends -y wget libpq-dev gcc g++ python3-dev && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +COPY requirements.txt . +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt COPY . . -COPY config.yaml ./config.yaml -COPY entrypoint.sh ./entrypoint.sh -COPY wait-for-it.sh ./wait-for-it.sh -RUN chmod +x ./entrypoint.sh ./wait-for-it.sh -CMD ["./wait-for-it.sh", "super__postgres:5432","-t","60","--","./entrypoint.sh"] +RUN chmod +x ./entrypoint.sh ./wait-for-it.sh ./install_tool_dependencies.sh ./entrypoint_celery.sh + +# Stage 2: Build image +FROM python:3.10-slim-bullseye AS build-image +WORKDIR /app + +RUN apt-get update && \ + apt-get install --no-install-recommends -y libpq-dev && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=compile-image /opt/venv /opt/venv +COPY --from=compile-image /app /app + +ENV PATH="/opt/venv/bin:$PATH" diff --git a/DockerfileCelery b/DockerfileCelery index 0f8625679..682e50824 100644 --- a/DockerfileCelery +++ b/DockerfileCelery @@ -8,9 +8,16 @@ RUN pip install --upgrade pip COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -WORKDIR /app COPY . . -COPY config.yaml . + +# Downloads the tools +RUN python superagi/tool_manager.py + +# Set executable permissions for install_tool_dependencies.sh +RUN chmod +x install_tool_dependencies.sh + +# Install dependencies +RUN ./install_tool_dependencies.sh # Downloads the tools RUN python superagi/tool_manager.py diff --git a/docker-compose.yaml b/docker-compose.yaml index 6dabb981d..35a089433 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,22 +9,23 @@ services: - super__postgres networks: - super_network + command: ["/app/wait-for-it.sh", "super__postgres:5432","-t","60","--","/app/entrypoint.sh"] celery: volumes: - "./:/app" - "${EXTERNAL_RESOURCE_DIR:-./workspace}:/app/ext" - build: - context: . - dockerfile: DockerfileCelery + build: . depends_on: - super__redis - super__postgres networks: - super_network + command: ["/app/entrypoint_celery.sh"] gui: - build: ./gui - environment: - - NEXT_PUBLIC_API_BASE_URL=/api + build: + context: ./gui + args: + NEXT_PUBLIC_API_BASE_URL: "/api" networks: - super_network # volumes: diff --git a/entrypoint.sh b/entrypoint.sh index 2403d55c0..56b75aab3 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,9 +3,6 @@ # Downloads the tools from marketplace and external tool repositories python superagi/tool_manager.py -# Set executable permissions for install_tool_dependencies.sh -chmod +x install_tool_dependencies.sh - # Install dependencies ./install_tool_dependencies.sh diff --git a/entrypoint_celery.sh b/entrypoint_celery.sh old mode 100644 new mode 100755 index 0b2b8a531..a5d7b5491 --- a/entrypoint_celery.sh +++ b/entrypoint_celery.sh @@ -1,5 +1,9 @@ #!/bin/bash -Xvfb :0 -screen 0 1280x1024x24 & -x11vnc -display :0 -N -forever -shared & -exec "$@" \ No newline at end of file +# Downloads the tools +python superagi/tool_manager.py + +# Install dependencies +./install_tool_dependencies.sh + +exec celery -A superagi.worker worker --beat --loglevel=info \ No newline at end of file diff --git a/gui/.dockerignore b/gui/.dockerignore index 220520759..42cdf5d4e 100644 --- a/gui/.dockerignore +++ b/gui/.dockerignore @@ -1,2 +1,13 @@ -node_modules/ -.next/ \ No newline at end of file +# Ignore everything +** + +# Allow files and directories +!app +!pages +!public +!utils +!package.json +!next.config.js +!package-lock.json +!.eslintrc.json +!jsconfig.json \ No newline at end of file diff --git a/gui/Dockerfile b/gui/Dockerfile index e6a63f58d..bdfe1cedb 100644 --- a/gui/Dockerfile +++ b/gui/Dockerfile @@ -1,13 +1,19 @@ -FROM node:lts AS deps - +FROM node:18-alpine AS deps +RUN apk add --no-cache libc6-compat WORKDIR /app - -COPY package*.json ./ +COPY package.json package-lock.json ./ RUN npm ci -FROM node:lts AS builder +# Rebuild the source code only when needed +FROM node:18-alpine AS builder + WORKDIR /app -COPY . . + COPY --from=deps /app/node_modules ./node_modules +COPY . . +ARG NEXT_PUBLIC_API_BASE_URL=/api +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +EXPOSE 3000 + CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/gui/DockerfileProd b/gui/DockerfileProd index e76962175..aa3186411 100644 --- a/gui/DockerfileProd +++ b/gui/DockerfileProd @@ -1,11 +1,43 @@ -FROM node:lts +FROM node:18-alpine AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --only=production + +# Rebuild the source code only when needed +FROM node:18-alpine AS builder WORKDIR /app -COPY package.json . -RUN npm install +COPY --from=deps /app/node_modules ./node_modules COPY . . +ARG NEXT_PUBLIC_API_BASE_URL=/api +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL RUN npm run build -CMD ["npm", "run", "start"] \ No newline at end of file + +# Production image, copy all the files and run next +FROM node:18-alpine AS runner +WORKDIR /app + +ENV NODE_ENV production + +RUN addgroup --system --gid 1001 supergroup +RUN adduser --system --uid 1001 superuser + +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=superuser:supergroup /app/.next/standalone ./ +COPY --from=builder --chown=superuser:supergroup /app/.next/static ./.next/static + +USER superuser + +EXPOSE 3000 + +ENV PORT 3000 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/gui/next.config.js b/gui/next.config.js index 74c156b97..bd31d6ce9 100644 --- a/gui/next.config.js +++ b/gui/next.config.js @@ -1,6 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { assetPrefix: process.env.NODE_ENV === "production" ? "/" : "./", + output: 'standalone' }; module.exports = nextConfig; From b638d37b15af091cfdbc92d003c1ad48d4ba1d89 Mon Sep 17 00:00:00 2001 From: Maverick-F35 <138012351+Maverick-F35@users.noreply.github.com> Date: Wed, 26 Jul 2023 10:55:36 +0530 Subject: [PATCH 40/77] handled resource path change (#861) * handled resource path change * readme changed * added new test case for stable diffusion * test cases refactored --- .../stable_diffusion_image_gen.py | 11 +++++++- superagi/tools/instagram_tool/README.MD | 2 +- .../test_stable_diffusion_image_gen.py | 25 ++++++++++++++----- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/superagi/tools/image_generation/stable_diffusion_image_gen.py b/superagi/tools/image_generation/stable_diffusion_image_gen.py index b329b3ebf..6831f5d98 100644 --- a/superagi/tools/image_generation/stable_diffusion_image_gen.py +++ b/superagi/tools/image_generation/stable_diffusion_image_gen.py @@ -8,6 +8,8 @@ from superagi.helper.resource_helper import ResourceHelper from superagi.resource_manager.file_manager import FileManager from superagi.tools.base_tool import BaseTool +from superagi.models.agent_execution import AgentExecution +from superagi.models.agent import Agent class StableDiffusionImageGenInput(BaseModel): @@ -35,6 +37,7 @@ class StableDiffusionImageGenTool(BaseTool): args_schema: Type[BaseModel] = StableDiffusionImageGenInput description: str = "Generate Images using Stable Diffusion" agent_id: int = None + agent_execution_id: int = None resource_manager: Optional[FileManager] = None class Config: @@ -70,7 +73,13 @@ def _execute(self, prompt: str, image_names: list, width: int = 512, height: int self.resource_manager.write_binary_file(image_names[i], img_byte_arr.getvalue()) for image in image_names: - image_paths.append(ResourceHelper.get_resource_path(image)) + final_path = ResourceHelper.get_agent_read_resource_path(image, agent=Agent.get_agent_from_id( + session=self.toolkit_config.session, agent_id=self.agent_id), agent_execution=AgentExecution + .get_agent_execution_from_id(session=self + .toolkit_config.session, + agent_execution_id=self + .agent_execution_id)) + image_paths.append(final_path) return f"Images downloaded and saved successfully at the following locations: {image_paths}" diff --git a/superagi/tools/instagram_tool/README.MD b/superagi/tools/instagram_tool/README.MD index fcaa034ac..293504c06 100644 --- a/superagi/tools/instagram_tool/README.MD +++ b/superagi/tools/instagram_tool/README.MD @@ -4,7 +4,7 @@ # SuperAGI Instagram Tool -The SuperAGI Instagram Tool works with the stable diffusion tool, generates an image & caption based on the goals defined by the user and posts it on their instagram business account. +The SuperAGI Instagram Tool works with the stable diffusion tool, generates an image & caption based on the goals defined by the user and posts it on their instagram business account.Currently will only work on the webapp ## ⚙️ Installation diff --git a/tests/unit_tests/tools/image_generation/test_stable_diffusion_image_gen.py b/tests/unit_tests/tools/image_generation/test_stable_diffusion_image_gen.py index dafd60b27..3f34cec6f 100644 --- a/tests/unit_tests/tools/image_generation/test_stable_diffusion_image_gen.py +++ b/tests/unit_tests/tools/image_generation/test_stable_diffusion_image_gen.py @@ -27,7 +27,13 @@ def create_sample_image_base64(): def stable_diffusion_tool(): with patch('superagi.tools.image_generation.stable_diffusion_image_gen.requests.post') as post_mock, \ patch( - 'superagi.tools.image_generation.stable_diffusion_image_gen.FileManager') as resource_manager_mock: + 'superagi.tools.image_generation.stable_diffusion_image_gen.FileManager') as resource_manager_mock, \ + patch( + 'superagi.tools.image_generation.stable_diffusion_image_gen.ResourceHelper') as resource_helper_mock, \ + patch( + 'superagi.tools.image_generation.stable_diffusion_image_gen.Agent') as agent_mock, \ + patch( + 'superagi.tools.image_generation.stable_diffusion_image_gen.AgentExecution') as agent_execution_mock: # Create a mock response object response_mock = Mock() @@ -39,16 +45,23 @@ def stable_diffusion_tool(): resource_manager_mock.write_binary_file.return_value = None + # Mock Agent and AgentExecution to return dummy values + agent_mock.get_agent_from_id.return_value = Mock() + agent_execution_mock.get_agent_execution_from_id.return_value = Mock() + yield + def test_execute(stable_diffusion_tool): tool = StableDiffusionImageGenTool() tool.resource_manager = Mock() - tool.toolkit_config.get_tool_config = mock_get_tool_config - - - result = tool._execute('prompt', ['img1.png', 'img2.png']) - assert result.startswith('Images downloaded and saved successfully') + tool.agent_id = 123 # Use a dummy agent_id for testing purposes + tool.toolkit_config.get_tool_config = lambda key: 'fake_api_key' if key == 'STABILITY_API_KEY' else 'engine_id_1' + prompt = 'Test prompt' + image_names = ['img1.png', 'img2.png'] + expected_result = 'Images downloaded and saved successfully' + result = tool._execute(prompt, image_names) + assert result.startswith(expected_result) tool.resource_manager.write_binary_file.assert_called() def test_call_stable_diffusion(stable_diffusion_tool): From fdadbf03eb0c5d8f985809f9f6d7650035005f47 Mon Sep 17 00:00:00 2001 From: abhijeet Date: Wed, 26 Jul 2023 11:06:13 +0530 Subject: [PATCH 41/77] Updated Expiry Time backward fix --- main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.py b/main.py index 0045c59a4..0b0464da7 100644 --- a/main.py +++ b/main.py @@ -129,6 +129,8 @@ def create_access_token(email, Authorize: AuthJWT = Depends()): expiry_time_hours = superagi.config.config.get_config("JWT_EXPIRY") if type(expiry_time_hours) == str: expiry_time_hours = int(expiry_time_hours) + if expiry_time_hours is None: + expiry_time_hours = 200 expires = timedelta(hours=expiry_time_hours) access_token = Authorize.create_access_token(subject=email, expires_time=expires) return access_token From e105b2e6d2b3ec02a2003ef22f0dc201e0a218e9 Mon Sep 17 00:00:00 2001 From: luciferlinx <129729795+luciferlinx101@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:52:03 +0530 Subject: [PATCH 42/77] readme toolkit fix (#867) --- superagi/helper/tool_helper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/superagi/helper/tool_helper.py b/superagi/helper/tool_helper.py index 3dc4d1587..b9eaf0865 100644 --- a/superagi/helper/tool_helper.py +++ b/superagi/helper/tool_helper.py @@ -230,6 +230,8 @@ def process_files(folder_paths, session, organisation, code_link=None): def get_readme_content_from_code_link(tool_code_link): + if tool_code_link is None: + return None parsed_url = urlparse(tool_code_link) path_parts = parsed_url.path.split("/") From d9c942f1b3eaac4e955ad063f3613f90e9ef3256 Mon Sep 17 00:00:00 2001 From: Nishant Borthakur <101320057+nborthy@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:54:01 +0530 Subject: [PATCH 43/77] Knowledge frontend (#860) * knowledge first cut * knowledge details * knowledge add form * agent create knowledge select * add database part 1 * add database part 2 * local storage fixes * database details * minor fix * comments resolution * knowledge details dropdown * knowledge form added * delete database * delete database modal * delete database modal 2 * minorf ix * marketplace for knowledge * update database changes * dropdown changes * uninstall knowledge in marketplace * minor fixes * minor fixws * api integration * bug fixes on db connect * bug fix on deleting database * knowledge dropdown fi * model fixes * minor fiox * remove tab fixes * minor fix * loading text fixes * index validation * uninstall knowledge api * minor fix * minor time fix * code refactoring * knowledge id changed to knowledge name * introduction and use cases added * uninstall marketplace template by id * minor fixes * Changes in API * Changea * bug fixes where adding loader while installing, icon alignment, selecting a knowledge made compulsory, knowledge sidebar issues are fixed, adding of loader in marketplace, connection of database, uninstalling knowledge resolved, dropdown bug by mayur resolved, knowledge id was not going with the agent configs in, resolved it * minor bug fixes * adding of installed state in the cards of knowledge marketplace * install template condtion * merging with dev of missed part * install dropdown bug fix * select index validation * delete db prompt change * time issue fix * connect db response handling * minor fix * final changes * minor fix --------- Co-authored-by: NishantBorthakur Co-authored-by: Tarraann Co-authored-by: namansleeps --- gui/pages/Content/Agents/AgentCreate.js | 158 +++++++- .../Content/Agents/AgentTemplatesList.js | 15 +- gui/pages/Content/Agents/AgentWorkspace.js | 16 +- gui/pages/Content/Agents/Details.js | 4 + gui/pages/Content/Knowledge/AddKnowledge.js | 46 +++ gui/pages/Content/Knowledge/Knowledge.js | 61 ++++ .../Content/Knowledge/Knowledge.module.css | 76 ++++ .../Content/Knowledge/KnowledgeDetails.js | 220 +++++++++++ gui/pages/Content/Knowledge/KnowledgeForm.js | 242 ++++++++++++ .../Content/Marketplace/KnowledgeTemplate.js | 328 +++++++++++++++++ gui/pages/Content/Marketplace/Market.js | 18 +- .../Content/Marketplace/MarketKnowledge.js | 83 +++++ gui/pages/Content/Marketplace/MarketTools.js | 2 +- gui/pages/Content/Toolkits/Tool.module.css | 58 +-- .../Content/Toolkits/ToolkitWorkspace.js | 5 +- gui/pages/Content/Toolkits/Toolkits.js | 2 - gui/pages/Dashboard/Content.js | 65 +++- gui/pages/Dashboard/Settings/AddDatabase.js | 344 ++++++++++++++++++ gui/pages/Dashboard/Settings/Database.js | 185 ++++++++++ .../Dashboard/Settings/DatabaseDetails.js | 211 +++++++++++ gui/pages/Dashboard/Settings/Model.js | 161 ++++++++ gui/pages/Dashboard/Settings/Settings.js | 177 ++------- gui/pages/Dashboard/SideBar.js | 10 +- gui/pages/_app.css | 126 ++++++- gui/pages/_app.js | 31 +- gui/pages/api/DashboardService.js | 70 +++- gui/public/images/books.svg | 8 + gui/public/images/close_light.svg | 8 - gui/public/images/database.svg | 8 + gui/public/images/knowledge.svg | 8 + gui/public/images/model_light.svg | 8 + gui/public/images/pinecone.svg | 9 + gui/public/images/plus_symbol.svg | 8 + gui/public/images/qdrant.svg | 22 ++ gui/public/images/stack.svg | 8 + gui/public/images/three_dots_vertical.svg | 8 + gui/utils/utils.js | 69 ++++ 37 files changed, 2639 insertions(+), 239 deletions(-) create mode 100644 gui/pages/Content/Knowledge/AddKnowledge.js create mode 100644 gui/pages/Content/Knowledge/Knowledge.js create mode 100644 gui/pages/Content/Knowledge/Knowledge.module.css create mode 100644 gui/pages/Content/Knowledge/KnowledgeDetails.js create mode 100644 gui/pages/Content/Knowledge/KnowledgeForm.js create mode 100644 gui/pages/Content/Marketplace/KnowledgeTemplate.js create mode 100644 gui/pages/Content/Marketplace/MarketKnowledge.js create mode 100644 gui/pages/Dashboard/Settings/AddDatabase.js create mode 100644 gui/pages/Dashboard/Settings/Database.js create mode 100644 gui/pages/Dashboard/Settings/DatabaseDetails.js create mode 100644 gui/pages/Dashboard/Settings/Model.js create mode 100644 gui/public/images/books.svg delete mode 100644 gui/public/images/close_light.svg create mode 100644 gui/public/images/database.svg create mode 100644 gui/public/images/knowledge.svg create mode 100644 gui/public/images/model_light.svg create mode 100644 gui/public/images/pinecone.svg create mode 100644 gui/public/images/plus_symbol.svg create mode 100644 gui/public/images/qdrant.svg create mode 100644 gui/public/images/stack.svg create mode 100644 gui/public/images/three_dots_vertical.svg diff --git a/gui/pages/Content/Agents/AgentCreate.js b/gui/pages/Content/Agents/AgentCreate.js index c01d945c0..fa7d0d4fe 100644 --- a/gui/pages/Content/Agents/AgentCreate.js +++ b/gui/pages/Content/Agents/AgentCreate.js @@ -2,7 +2,6 @@ import React, {useState, useEffect, useRef} from 'react'; import Image from "next/image"; import {ToastContainer, toast} from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import styles from './Agents.module.css'; import { createAgent, editAgentTemplate, @@ -20,10 +19,23 @@ import { setLocalStorageArray, returnResourceIcon, getUserTimezone, createInternalId,preventDefault,excludedToolkits } from "@/utils/utils"; import {EventBus} from "@/utils/eventBus"; +import styles from "@/pages/Content/Agents/Agents.module.css"; +import styles1 from "@/pages/Content/Knowledge/Knowledge.module.css"; import 'moment-timezone'; import AgentSchedule from "@/pages/Content/Agents/AgentSchedule"; -export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgents, toolkits, organisationId, template, internalId, env}) { +export default function AgentCreate({ + sendAgentData, + knowledge, + selectedProjectId, + fetchAgents, + toolkits, + organisationId, + template, + internalId, + sendKnowledgeData, + env + }) { const [advancedOptions, setAdvancedOptions] = useState(false); const [agentName, setAgentName] = useState(""); const [agentTemplateId, setAgentTemplateId] = useState(null); @@ -71,6 +83,11 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen const rollingRef = useRef(null); const [rollingDropdown, setRollingDropdown] = useState(false); + const [selectedKnowledge, setSelectedKnowledge] = useState(''); + const [selectedKnowledgeId, setSelectedKnowledgeId] = useState(null); + const knowledgeRef = useRef(null); + const [knowledgeDropdown, setKnowledgeDropdown] = useState(false); + const databases = ["Pinecone"] const [database, setDatabase] = useState(databases[0]); const databaseRef = useRef(null); @@ -185,6 +202,10 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen setRollingDropdown(false) } + if (knowledgeRef.current && !knowledgeRef.current.contains(event.target)) { + setKnowledgeDropdown(false) + } + if (databaseRef.current && !databaseRef.current.contains(event.target)) { setDatabaseDropdown(false) } @@ -252,6 +273,12 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen }; + const handleKnowledgeSelect = (index) => { + setLocalStorageValue("agent_knowledge_" + String(internalId), knowledge[index].name, setSelectedKnowledge); + setLocalStorageValue("agent_knowledge_id_" + String(internalId), knowledge[index].id, setSelectedKnowledgeId); + setKnowledgeDropdown(false); + }; + const handleStepChange = (event) => { setLocalStorageValue("agent_step_time_" + String(internalId), event.target.value, setStepTime); }; @@ -380,28 +407,40 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen toast.error("Agent name can't be blank", {autoClose: 1800}); return false; } + if (agentDescription?.replace(/\s/g, '') === '') { toast.error("Agent description can't be blank", {autoClose: 1800}); return false; } + const isEmptyGoal = goals.some((goal) => goal.replace(/\s/g, '') === ''); if (isEmptyGoal) { toast.error("Goal can't be empty", {autoClose: 1800}); return false; } + if (selectedTools.length <= 0) { toast.error("Add atleast one tool", {autoClose: 1800}); return false; } + if(!modelsArray.includes(model)) { toast.error("Your key does not have access to the selected model", {autoClose: 1800}); return false; } + + if (toolNames.includes('Knowledge Search') && !selectedKnowledge) { + toast.error("Add atleast one knowledge", {autoClose: 1800}); + return; + } + return true; } const handleAddAgent = () => { - if (!validateAgentData(true)) return; + if (!validateAgentData(true)) { + return; + } setCreateClickable(false); @@ -427,7 +466,9 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen "permission_type": permission_type, "LTM_DB": longTermMemory ? database : null, "user_timezone": getUserTimezone(), + "knowledge" : toolNames.includes('Knowledge Search') ? selectedKnowledgeId : null, }; + const scheduleAgentData = { "agent_config": agentData, "schedule": scheduleData, @@ -717,8 +758,18 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen setInput(JSON.parse(agent_files)); } } + + const agent_knowledge = localStorage.getItem("agent_knowledge_" + String(internalId)); + if (agent_knowledge) { + setSelectedKnowledge(agent_knowledge); + } }, [internalId]) + function openMarketplace() { + openNewTab(-4, "Marketplace", "Marketplace", false); + localStorage.setItem('marketplace_tab', 'market_knowledge'); + } + return (<>
    @@ -748,7 +799,7 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen {goals.length > 1 &&
    }
    ))} @@ -772,7 +823,7 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen {instructions.length > 1 &&
    }
    ))} @@ -871,6 +922,97 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen
    + {toolNames.includes("Knowledge Search") &&
    + +
    +
    setKnowledgeDropdown(!knowledgeDropdown)} + style={selectedKnowledge ? {width: '100%'} : {width: '100%', color: '#888888'}}> + {selectedKnowledge || 'Select knowledge'}expand-icon +
    +
    + {knowledgeDropdown && knowledge && knowledge.length > 0 && +
    + {knowledge.map((item, index) => ( +
    handleKnowledgeSelect(index)} + style={{padding: '12px 14px', maxWidth: '100%'}}> + {item.name} +
    ))} +
    +
    sendKnowledgeData({ + id: -6, + name: "new knowledge", + contentType: "Add_Knowledge", + internalId: createInternalId() + })}> + add-icon  Add + new knowledge +
    +
    +
    +
    + marketplace  Browse knowledge from marketplace +
    +
    +
    } + {knowledgeDropdown && knowledge && knowledge.length <= 0 && +
    +
    + no-permissions + No knowledge found +
    +
    +
    sendKnowledgeData({ + id: -6, + name: "new knowledge", + contentType: "Add_Knowledge", + internalId: createInternalId() + })}> + add-icon  Add + new knowledge +
    +
    +
    +
    + marketplace  Browse knowledge from marketplace +
    +
    +
    } +
    +
    +
    }
    removeFile(index)}>close-icon
    @@ -950,7 +1092,7 @@ export default function AgentCreate({sendAgentData, selectedProjectId, fetchAgen
    }
    -
    +
    {constraints.map((constraint, index) => (
    ))} diff --git a/gui/pages/Content/Agents/AgentTemplatesList.js b/gui/pages/Content/Agents/AgentTemplatesList.js index dfec926da..905f28dff 100644 --- a/gui/pages/Content/Agents/AgentTemplatesList.js +++ b/gui/pages/Content/Agents/AgentTemplatesList.js @@ -5,7 +5,17 @@ import {fetchAgentTemplateListLocal} from "@/pages/api/DashboardService"; import AgentCreate from "@/pages/Content/Agents/AgentCreate"; import {setLocalStorageValue, openNewTab} from "@/utils/utils"; -export default function AgentTemplatesList({sendAgentData, selectedProjectId, fetchAgents, toolkits, organisationId, internalId, env}) { +export default function AgentTemplatesList({ + sendAgentData, + knowledge, + selectedProjectId, + fetchAgents, + toolkits, + organisationId, + internalId, + sendKnowledgeData, + env + }) { const [agentTemplates, setAgentTemplates] = useState([]) const [createAgentClicked, setCreateAgentClicked] = useState(false) const [sendTemplate, setSendTemplate] = useState(null) @@ -103,7 +113,8 @@ export default function AgentTemplatesList({sendAgentData, selectedProjectId, fe
    }
    -
    : : }
    diff --git a/gui/pages/Content/Agents/AgentWorkspace.js b/gui/pages/Content/Agents/AgentWorkspace.js index 71c80bd00..02016580c 100644 --- a/gui/pages/Content/Agents/AgentWorkspace.js +++ b/gui/pages/Content/Agents/AgentWorkspace.js @@ -244,7 +244,7 @@ export default function AgentWorkspace({env, agentId, agentName, selectedView, a console.error('Error fetching agent data:', error); }); } - }; + } function fetchExecutions(agentId, currentRun = null) { getAgentExecutions(agentId) @@ -280,6 +280,8 @@ export default function AgentWorkspace({env, agentId, agentName, selectedView, a .catch((error) => { console.error('Error saving agent as template:', error); }); + + setDropdown(false); } useEffect(() => { @@ -349,10 +351,10 @@ export default function AgentWorkspace({env, agentId, agentName, selectedView, a run-icon New Run
    - {} + {dropdown &&
    setDropdown(true)} onMouseLeave={() => setDropdown(false)}>
    • saveAgentTemplate()}>Save as Template
    • @@ -492,7 +494,7 @@ export default function AgentWorkspace({env, agentId, agentName, selectedView, a {goals.length > 1 &&
      }
    ))} @@ -515,7 +517,7 @@ export default function AgentWorkspace({env, agentId, agentName, selectedView, a {instructions.length > 1 &&
    }
    ))} @@ -558,4 +560,4 @@ export default function AgentWorkspace({env, agentId, agentName, selectedView, a
    ); -} +} \ No newline at end of file diff --git a/gui/pages/Content/Agents/Details.js b/gui/pages/Content/Agents/Details.js index 2a5c66f48..0bbdb5db3 100644 --- a/gui/pages/Content/Agents/Details.js +++ b/gui/pages/Content/Agents/Details.js @@ -162,6 +162,10 @@ export default function Details({agentDetails, runCount, goals, instructions, ag
    queue-icon
    {agentDetails?.agent_type || ''}
    +
    +
    book-icon
    +
    knowledge name
    +
    model-icon
    {agentDetails?.model || ''}
    diff --git a/gui/pages/Content/Knowledge/AddKnowledge.js b/gui/pages/Content/Knowledge/AddKnowledge.js new file mode 100644 index 000000000..98a6abd90 --- /dev/null +++ b/gui/pages/Content/Knowledge/AddKnowledge.js @@ -0,0 +1,46 @@ +import React, {useState, useEffect} from 'react'; +import KnowledgeForm from "@/pages/Content/Knowledge/KnowledgeForm"; + +export default function AddKnowledge({internalId, sendKnowledgeData}) { + const [knowledgeName, setKnowledgeName] = useState(''); + const [knowledgeDescription, setKnowledgeDescription] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(null); + + useEffect(() => { + const knowledge_name = localStorage.getItem("knowledge_name_" + String(internalId)) + if (knowledge_name) { + setKnowledgeName(knowledge_name); + } + + const knowledge_description = localStorage.getItem("knowledge_description_" + String(internalId)) + if (knowledge_description) { + setKnowledgeDescription(knowledge_description); + } + + const knowledge_index = localStorage.getItem("knowledge_index_" + String(internalId)) + if (knowledge_index) { + setSelectedIndex(JSON.parse(knowledge_index)); + } + }, [internalId]) + + return (<> +
    +
    +
    + +
    +
    +
    + ) +} \ No newline at end of file diff --git a/gui/pages/Content/Knowledge/Knowledge.js b/gui/pages/Content/Knowledge/Knowledge.js new file mode 100644 index 000000000..4235a874c --- /dev/null +++ b/gui/pages/Content/Knowledge/Knowledge.js @@ -0,0 +1,61 @@ +import React from 'react'; +import Image from "next/image"; +import styles from '../Toolkits/Tool.module.css'; +import styles1 from '../Agents/Agents.module.css' +import {createInternalId} from "@/utils/utils"; + +export default function Knowledge({sendKnowledgeData, knowledge}) { + return ( + <> +
    +
    +

    Agents

    +
    +
    + +
    + + {knowledge && knowledge.length > 0 ? ( +
    +
    + {knowledge.map((item, index) => ( +
    sendKnowledgeData({ + id: item.id, + name: item.name, + contentType: "Knowledge", + internalId: createInternalId() + })}> +
    +
    +
    +
    +
    {item.name} {item.is_marketplace && + markteplace-icon}
    +
    by {item.contributed_by}
    +
    +
    +
    +
    +
    ) + )} +
    +
    + ) : ( +
    + No Knowledge found +
    + )} +
    + + ); +} \ No newline at end of file diff --git a/gui/pages/Content/Knowledge/Knowledge.module.css b/gui/pages/Content/Knowledge/Knowledge.module.css new file mode 100644 index 000000000..7768fb5c3 --- /dev/null +++ b/gui/pages/Content/Knowledge/Knowledge.module.css @@ -0,0 +1,76 @@ +.knowledge_label { + margin-bottom: 4px; + font-size: 12px; + color: #888888; +} + +.knowledge_info { + font-size: 12px; + color: white; +} + +.knowledge_info_box { + margin-bottom: 20px; +} + +.knowledge_wrapper { + margin-bottom: 20px; + display: flex; + justify-content: space-between; +} + +.knowledge_db { + font-size: 12px; + color: #888888; + font-weight: normal; + height: auto; + max-width: 240px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.knowledge_db_name { + padding:12px 14px; + border-top: 1px solid #888888; +} + +.knowledge_alert { + border-radius: 8px; + background-color: #423D52; + border-left: 4px solid #B3B2BB; + color: white; + font-size: 12px; + padding: 12px 14px; + display: flex; + justify-content: flex-start; +} + +.database_container { + background-color: rgb(39, 35, 53); + width: calc(33% - 10px); + padding: 10px; + color: white; + font-style: normal; + font-weight: 400; + font-size: 15px; + border-radius: 8px; + cursor: pointer; + text-align: center; +} + +.database_wrapper { + display: flex; + justify-content: flex-start; + gap: 10px; + flex-wrap: wrap; +} +.installed_knowledge_card_class { + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.14); + display: flex; + padding: 4px 8px; + align-items: center; + gap: 6px; +} \ No newline at end of file diff --git a/gui/pages/Content/Knowledge/KnowledgeDetails.js b/gui/pages/Content/Knowledge/KnowledgeDetails.js new file mode 100644 index 000000000..1d45b5fb3 --- /dev/null +++ b/gui/pages/Content/Knowledge/KnowledgeDetails.js @@ -0,0 +1,220 @@ +import React, {useEffect, useState} from 'react'; +import styles1 from './Knowledge.module.css' +import {ToastContainer, toast} from "react-toastify"; +import styles from "@/pages/Content/Toolkits/Tool.module.css"; +import Image from "next/image"; +import KnowledgeForm from "@/pages/Content/Knowledge/KnowledgeForm"; +import {deleteCustomKnowledge, deleteMarketplaceKnowledge, getKnowledgeDetails} from "@/pages/api/DashboardService"; +import {removeTab} from "@/utils/utils"; +import {EventBus} from "@/utils/eventBus"; + +export default function KnowledgeDetails({internalId, knowledgeId}) { + const [showDescription, setShowDescription] = useState(false); + const [dropdown, setDropdown] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [knowledgeName, setKnowledgeName] = useState(''); + const [knowledgeDescription, setKnowledgeDescription] = useState(''); + const [installationType, setInstallationType] = useState(''); + const [model, setModel] = useState(''); + const [tokenizer, setTokenizer] = useState(''); + const [chunkSize, setChunkSize] = useState(''); + const [vectorDatabase, setVectorDatabase] = useState(''); + const [knowledgeDatatype, setKnowledgeDatatype] = useState(''); + const [textSplitters, setTextSplitters] = useState(''); + const [chunkOverlap, setChunkOverlap] = useState(''); + const [dimension, setDimension] = useState(''); + const [vectorDBIndex, setVectorDBIndex] = useState(''); + + const uninstallKnowledge = () => { + setDropdown(false); + + if (installationType === 'Marketplace') { + deleteMarketplaceKnowledge(knowledgeName) + .then((response) => { + console.log(response) + if(response.data.success){ + toast.success("Knowledge uninstalled successfully", {autoClose: 1800}); + removeTab(knowledgeId, knowledgeName, "Knowledge", internalId); + EventBus.emit('reFetchKnowledge', {}); + } + else{ + toast.error("Unable to uninstall knowledge", {autoClose: 1800}); + } + }) + .catch((error) => { + toast.error("Unable to uninstall knowledge", {autoClose: 1800}); + console.error('Error uninstalling knowledge:', error); + }); + } else { + deleteCustomKnowledge(knowledgeId) + .then((response) => { + console.log(response) + if(response.data.success) { + toast.success("Knowledge uninstalled successfully", {autoClose: 1800}); + removeTab(knowledgeId, knowledgeName, "Knowledge", internalId); + EventBus.emit('reFetchKnowledge', {}); + } + else { + toast.error("Unable to uninstall knowledge", {autoClose: 1800}); + } + }) + .catch((error) => { + toast.error("Unable to uninstall knowledge", {autoClose: 1800}); + console.error('Error uninstalling knowledge:', error); + }); + } + } + + const viewKnowledge = () => { + setDropdown(false); + } + + const editKnowledge = () => { + setIsEditing(true); + setDropdown(false); + } + + useEffect(() => { + if (knowledgeId) { + getKnowledgeDetails(knowledgeId) + .then((response) => { + const data = response.data || []; + setKnowledgeName(data.name); + setKnowledgeDescription(data.description); + setInstallationType(data.installation_type); + setModel(data.model); + setTokenizer(data.tokenizer); + setChunkSize(data.chunk_size); + setVectorDatabase(data.vector_database); + setKnowledgeDatatype(data.data_type); + setTextSplitters(data.text_splitter); + setChunkOverlap(data.chunk_overlap); + setDimension(data.dimensions); + setVectorDBIndex(data.vector_database_index); + }) + .catch((error) => { + console.error('Error fetching knowledge details:', error); + }); + } + }, [internalId]); + + return (<> +
    +
    +
    + {isEditing ? + : +
    +
    +
    +
    +
    +
    {knowledgeName}
    +
    + {`${showDescription ? knowledgeDescription : knowledgeDescription.slice(0, 70)}`} + {knowledgeDescription.length > 70 && + setShowDescription(!showDescription)}> + {showDescription ? '...less' : '...more'} + } +
    +
    +
    +
    + + {dropdown &&
    setDropdown(true)} onMouseLeave={() => setDropdown(false)}> +
      + {installationType === 'Marketplace' ? +
    • View in marketplace
    • : +
    • Edit details
    • } +
    • Uninstall knowledge
    • +
    +
    } +
    +
    +
    + {installationType === 'Marketplace' &&
    +
    +
    + +
    {installationType}
    +
    +
    + +
    {model}
    +
    +
    + +
    {tokenizer}
    +
    +
    + +
    {chunkSize}
    +
    +
    + +
    {vectorDatabase}
    +
    +
    +
    +
    + +
    {knowledgeDatatype}
    +
    +
    + +
    {textSplitters}
    +
    +
    + +
    {chunkOverlap}
    +
    +
    + +
    {dimension}
    +
    +
    + +
    {vectorDBIndex?.name || ''}
    +
    +
    +
    } + {installationType === 'Custom' &&
    +
    +
    + +
    {installationType}
    +
    +
    + +
    {vectorDBIndex?.name || ''}
    +
    +
    +
    +
    + +
    {vectorDatabase}
    +
    +
    +
    } +
    } +
    +
    +
    + + ); +} \ No newline at end of file diff --git a/gui/pages/Content/Knowledge/KnowledgeForm.js b/gui/pages/Content/Knowledge/KnowledgeForm.js new file mode 100644 index 000000000..e500bfbc3 --- /dev/null +++ b/gui/pages/Content/Knowledge/KnowledgeForm.js @@ -0,0 +1,242 @@ +import React, {useState, useEffect, useRef} from 'react'; +import styles1 from '@/pages/Content/Knowledge/Knowledge.module.css' +import {removeTab, setLocalStorageValue, setLocalStorageArray, createInternalId} from "@/utils/utils"; +import styles from "@/pages/Content/Agents/Agents.module.css"; +import Image from "next/image"; +import {ToastContainer, toast} from "react-toastify"; +import {addUpdateKnowledge, getValidIndices} from "@/pages/api/DashboardService"; +import {EventBus} from "@/utils/eventBus"; + +export default function KnowledgeForm({ + internalId, + knowledgeId, + knowledgeName, + setKnowledgeName, + knowledgeDescription, + setKnowledgeDescription, + selectedIndex, + setSelectedIndex, + isEditing, + setIsEditing, + sendKnowledgeData + }) { + const [addClickable, setAddClickable] = useState(true); + const indexRef = useRef(null); + const [indexDropdown, setIndexDropdown] = useState(false); + const [pinconeIndices, setPineconeIndices] = useState([]); + const [qdrantIndices, setQdrantIndices] = useState([]); + + useEffect(() => { + getValidIndices() + .then((response) => { + const data = response.data || []; + if (data) { + setPineconeIndices(data.pinecone || []); + setQdrantIndices(data.qdrant || []); + } + }) + .catch((error) => { + console.error('Error fetching indices:', error); + }); + }, []); + + useEffect(() => { + function handleClickOutside(event) { + if (indexRef.current && !indexRef.current.contains(event.target)) { + setIndexDropdown(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const handleNameChange = (event) => { + setLocalStorageValue("knowledge_name_" + String(internalId), event.target.value, setKnowledgeName); + }; + + const handleDescriptionChange = (event) => { + setLocalStorageValue("knowledge_description_" + String(internalId), event.target.value, setKnowledgeDescription); + }; + + function validationCheck() { + let isValid = true; + + if (knowledgeName.replace(/\s/g, '') === '') { + toast.error("Knowledge name can't be blank", {autoClose: 1800}); + isValid = false; + } + + if (!selectedIndex) { + toast.error("Please select an index", {autoClose: 1800}); + isValid = false; + } + + return isValid; + } + + const handleAddKnowledge = () => { + if (!validationCheck()) { + return + } + + const knowledgeData = { + "id": 0, + "name": knowledgeName, + "description": knowledgeDescription, + "index_id": selectedIndex.id + } + + addUpdateKnowledge(knowledgeData) + .then((response) => { + toast.success("Knowledge added successfully", {autoClose: 1800}); + sendKnowledgeData({ + id: response.data.id, + name: knowledgeName, + contentType: "Knowledge", + internalId: createInternalId() + }); + EventBus.emit('reFetchKnowledge', {}); + }) + .catch((error) => { + toast.error("Unable to add knowledge", {autoClose: 1800}); + console.error('Error deleting knowledge:', error); + }); + + setAddClickable(false); + } + + const handleUpdateKnowledge = () => { + if (!validationCheck()) { + return + } + + const knowledgeData = { + "id": knowledgeId, + "name": knowledgeName, + "description": knowledgeDescription, + "index_id": selectedIndex.id + } + + addUpdateKnowledge(knowledgeData) + .then((response) => { + toast.success("Knowledge updated successfully", {autoClose: 1800}); + EventBus.emit('reFetchKnowledge', {}); + }) + .catch((error) => { + toast.error("Unable to update knowledge", {autoClose: 1800}); + console.error('Error deleting knowledge:', error); + }); + + setIsEditing(false); + setAddClickable(false); + } + + const handleIndexSelect = (index) => { + setLocalStorageArray("knowledge_index_" + String(internalId), index, setSelectedIndex); + setIndexDropdown(false); + } + + const checkIndexValidity = (validState) => { + let errorMessage = ""; + let isValid = true; + + if (!validState) { + isValid = false; + errorMessage = "The configured index is either empty or has marketplace knowledge"; + } + + return [isValid, errorMessage]; + } + + return (<> +
    +
    {isEditing ? 'Edit knowledge' : 'Add a new knowledge'}
    +
    +
    +
    +
    + info-icon +
    +
    + Currently we support Open AI “text-knowledge-ada-002” model knowledge only. Please make sure you add the same. +
    +
    +
    +
    +
    + + +
    +
    +
    + +