Spaces:
Runtime error
Runtime error
Commit ·
12bfabc
1
Parent(s): fee0a8b
update autogpt
Browse files- autogpt/agent/agent.py +132 -39
- autogpt/agent/agent_manager.py +50 -8
- autogpt/commands/analyze_code.py +9 -3
- autogpt/commands/command.py +156 -0
- autogpt/commands/execute_code.py +53 -28
- autogpt/commands/file_operations.py +44 -39
- plugins/Auto-GPT-Plugins.zip +0 -0
- plugins/__PUT_PLUGIN_ZIPS_HERE__ +0 -0
- scripts/check_requirements.py +2 -1
- scripts/install_plugin_deps.py +35 -0
autogpt/agent/agent.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
| 1 |
from colorama import Fore, Style
|
| 2 |
|
| 3 |
from autogpt.app import execute_command, get_command
|
| 4 |
-
from autogpt.chat import chat_with_ai, create_chat_message
|
| 5 |
from autogpt.config import Config
|
| 6 |
from autogpt.json_utils.json_fix_llm import fix_json_using_multiple_techniques
|
| 7 |
-
from autogpt.json_utils.utilities import validate_json
|
|
|
|
| 8 |
from autogpt.logs import logger, print_assistant_thoughts
|
| 9 |
from autogpt.speech import say_text
|
| 10 |
from autogpt.spinner import Spinner
|
| 11 |
from autogpt.utils import clean_input
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
class Agent:
|
|
@@ -19,18 +20,25 @@ class Agent:
|
|
| 19 |
memory: The memory object to use.
|
| 20 |
full_message_history: The full message history.
|
| 21 |
next_action_count: The number of actions to execute.
|
| 22 |
-
system_prompt: The system prompt is the initial prompt that defines everything
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
triggering_prompt: The last sentence the AI will see before answering.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
SYSTEM PROMPT
|
| 30 |
CONTEXTUAL INFORMATION (memory, previous conversations, anything relevant)
|
| 31 |
TRIGGERING PROMPT
|
| 32 |
|
| 33 |
-
The triggering prompt reminds the AI about its short term meta task
|
|
|
|
| 34 |
"""
|
| 35 |
|
| 36 |
def __init__(
|
|
@@ -39,15 +47,26 @@ class Agent:
|
|
| 39 |
memory,
|
| 40 |
full_message_history,
|
| 41 |
next_action_count,
|
|
|
|
|
|
|
| 42 |
system_prompt,
|
| 43 |
triggering_prompt,
|
|
|
|
| 44 |
):
|
|
|
|
| 45 |
self.ai_name = ai_name
|
| 46 |
self.memory = memory
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
self.full_message_history = full_message_history
|
| 48 |
self.next_action_count = next_action_count
|
|
|
|
|
|
|
| 49 |
self.system_prompt = system_prompt
|
| 50 |
self.triggering_prompt = triggering_prompt
|
|
|
|
| 51 |
|
| 52 |
def start_interaction_loop(self):
|
| 53 |
# Interaction Loop
|
|
@@ -69,10 +88,10 @@ class Agent:
|
|
| 69 |
"Continuous Limit Reached: ", Fore.YELLOW, f"{cfg.continuous_limit}"
|
| 70 |
)
|
| 71 |
break
|
| 72 |
-
|
| 73 |
# Send message to AI, get response
|
| 74 |
with Spinner("Thinking... "):
|
| 75 |
assistant_reply = chat_with_ai(
|
|
|
|
| 76 |
self.system_prompt,
|
| 77 |
self.triggering_prompt,
|
| 78 |
self.full_message_history,
|
|
@@ -81,60 +100,92 @@ class Agent:
|
|
| 81 |
) # TODO: This hardcodes the model to use GPT3.5. Make this an argument
|
| 82 |
|
| 83 |
assistant_reply_json = fix_json_using_multiple_techniques(assistant_reply)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
# Print Assistant thoughts
|
| 86 |
if assistant_reply_json != {}:
|
| 87 |
-
validate_json(assistant_reply_json,
|
| 88 |
# Get command name and arguments
|
| 89 |
try:
|
| 90 |
-
print_assistant_thoughts(
|
|
|
|
|
|
|
| 91 |
command_name, arguments = get_command(assistant_reply_json)
|
| 92 |
-
# command_name, arguments = assistant_reply_json_valid["command"]["name"], assistant_reply_json_valid["command"]["args"]
|
| 93 |
if cfg.speak_mode:
|
| 94 |
say_text(f"I want to execute {command_name}")
|
|
|
|
|
|
|
|
|
|
| 95 |
except Exception as e:
|
| 96 |
logger.error("Error: \n", str(e))
|
| 97 |
|
| 98 |
if not cfg.continuous_mode and self.next_action_count == 0:
|
| 99 |
-
### GET USER AUTHORIZATION TO EXECUTE COMMAND ###
|
| 100 |
# Get key press: Prompt the user to press enter to continue or escape
|
| 101 |
# to exit
|
|
|
|
| 102 |
logger.typewriter_log(
|
| 103 |
"NEXT ACTION: ",
|
| 104 |
Fore.CYAN,
|
| 105 |
f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} "
|
| 106 |
f"ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}",
|
| 107 |
)
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
"
|
| 111 |
-
|
| 112 |
-
|
| 113 |
)
|
| 114 |
while True:
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
| 119 |
user_input = "GENERATE NEXT COMMAND JSON"
|
| 120 |
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
elif console_input.lower().strip() == "":
|
| 122 |
-
|
| 123 |
continue
|
| 124 |
-
elif console_input.lower().startswith("
|
| 125 |
try:
|
| 126 |
self.next_action_count = abs(
|
| 127 |
int(console_input.split(" ")[1])
|
| 128 |
)
|
| 129 |
user_input = "GENERATE NEXT COMMAND JSON"
|
| 130 |
except ValueError:
|
| 131 |
-
|
| 132 |
"Invalid input format. Please enter 'y -n' where n is"
|
| 133 |
" the number of continuous tasks."
|
| 134 |
)
|
| 135 |
continue
|
| 136 |
break
|
| 137 |
-
elif console_input.lower() ==
|
| 138 |
user_input = "EXIT"
|
| 139 |
break
|
| 140 |
else:
|
|
@@ -149,7 +200,7 @@ class Agent:
|
|
| 149 |
"",
|
| 150 |
)
|
| 151 |
elif user_input == "EXIT":
|
| 152 |
-
|
| 153 |
break
|
| 154 |
else:
|
| 155 |
# Print command
|
|
@@ -168,21 +219,27 @@ class Agent:
|
|
| 168 |
elif command_name == "human_feedback":
|
| 169 |
result = f"Human feedback: {user_input}"
|
| 170 |
else:
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
if self.next_action_count > 0:
|
| 176 |
self.next_action_count -= 1
|
| 177 |
|
| 178 |
-
memory_to_add = (
|
| 179 |
-
f"Assistant Reply: {assistant_reply} "
|
| 180 |
-
f"\nResult: {result} "
|
| 181 |
-
f"\nHuman Feedback: {user_input} "
|
| 182 |
-
)
|
| 183 |
-
|
| 184 |
-
self.memory.add(memory_to_add)
|
| 185 |
-
|
| 186 |
# Check if there's a result from the command append it to the message
|
| 187 |
# history
|
| 188 |
if result is not None:
|
|
@@ -195,3 +252,39 @@ class Agent:
|
|
| 195 |
logger.typewriter_log(
|
| 196 |
"SYSTEM: ", Fore.YELLOW, "Unable to execute command"
|
| 197 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from colorama import Fore, Style
|
| 2 |
|
| 3 |
from autogpt.app import execute_command, get_command
|
|
|
|
| 4 |
from autogpt.config import Config
|
| 5 |
from autogpt.json_utils.json_fix_llm import fix_json_using_multiple_techniques
|
| 6 |
+
from autogpt.json_utils.utilities import LLM_DEFAULT_RESPONSE_FORMAT, validate_json
|
| 7 |
+
from autogpt.llm import chat_with_ai, create_chat_completion, create_chat_message
|
| 8 |
from autogpt.logs import logger, print_assistant_thoughts
|
| 9 |
from autogpt.speech import say_text
|
| 10 |
from autogpt.spinner import Spinner
|
| 11 |
from autogpt.utils import clean_input
|
| 12 |
+
from autogpt.workspace import Workspace
|
| 13 |
|
| 14 |
|
| 15 |
class Agent:
|
|
|
|
| 20 |
memory: The memory object to use.
|
| 21 |
full_message_history: The full message history.
|
| 22 |
next_action_count: The number of actions to execute.
|
| 23 |
+
system_prompt: The system prompt is the initial prompt that defines everything
|
| 24 |
+
the AI needs to know to achieve its task successfully.
|
| 25 |
+
Currently, the dynamic and customizable information in the system prompt are
|
| 26 |
+
ai_name, description and goals.
|
| 27 |
|
| 28 |
+
triggering_prompt: The last sentence the AI will see before answering.
|
| 29 |
+
For Auto-GPT, this prompt is:
|
| 30 |
+
Determine which next command to use, and respond using the format specified
|
| 31 |
+
above:
|
| 32 |
+
The triggering prompt is not part of the system prompt because between the
|
| 33 |
+
system prompt and the triggering
|
| 34 |
+
prompt we have contextual information that can distract the AI and make it
|
| 35 |
+
forget that its goal is to find the next task to achieve.
|
| 36 |
SYSTEM PROMPT
|
| 37 |
CONTEXTUAL INFORMATION (memory, previous conversations, anything relevant)
|
| 38 |
TRIGGERING PROMPT
|
| 39 |
|
| 40 |
+
The triggering prompt reminds the AI about its short term meta task
|
| 41 |
+
(defining the next task)
|
| 42 |
"""
|
| 43 |
|
| 44 |
def __init__(
|
|
|
|
| 47 |
memory,
|
| 48 |
full_message_history,
|
| 49 |
next_action_count,
|
| 50 |
+
command_registry,
|
| 51 |
+
config,
|
| 52 |
system_prompt,
|
| 53 |
triggering_prompt,
|
| 54 |
+
workspace_directory,
|
| 55 |
):
|
| 56 |
+
cfg = Config()
|
| 57 |
self.ai_name = ai_name
|
| 58 |
self.memory = memory
|
| 59 |
+
self.summary_memory = (
|
| 60 |
+
"I was created." # Initial memory necessary to avoid hilucination
|
| 61 |
+
)
|
| 62 |
+
self.last_memory_index = 0
|
| 63 |
self.full_message_history = full_message_history
|
| 64 |
self.next_action_count = next_action_count
|
| 65 |
+
self.command_registry = command_registry
|
| 66 |
+
self.config = config
|
| 67 |
self.system_prompt = system_prompt
|
| 68 |
self.triggering_prompt = triggering_prompt
|
| 69 |
+
self.workspace = Workspace(workspace_directory, cfg.restrict_to_workspace)
|
| 70 |
|
| 71 |
def start_interaction_loop(self):
|
| 72 |
# Interaction Loop
|
|
|
|
| 88 |
"Continuous Limit Reached: ", Fore.YELLOW, f"{cfg.continuous_limit}"
|
| 89 |
)
|
| 90 |
break
|
|
|
|
| 91 |
# Send message to AI, get response
|
| 92 |
with Spinner("Thinking... "):
|
| 93 |
assistant_reply = chat_with_ai(
|
| 94 |
+
self,
|
| 95 |
self.system_prompt,
|
| 96 |
self.triggering_prompt,
|
| 97 |
self.full_message_history,
|
|
|
|
| 100 |
) # TODO: This hardcodes the model to use GPT3.5. Make this an argument
|
| 101 |
|
| 102 |
assistant_reply_json = fix_json_using_multiple_techniques(assistant_reply)
|
| 103 |
+
for plugin in cfg.plugins:
|
| 104 |
+
if not plugin.can_handle_post_planning():
|
| 105 |
+
continue
|
| 106 |
+
assistant_reply_json = plugin.post_planning(self, assistant_reply_json)
|
| 107 |
|
| 108 |
# Print Assistant thoughts
|
| 109 |
if assistant_reply_json != {}:
|
| 110 |
+
validate_json(assistant_reply_json, LLM_DEFAULT_RESPONSE_FORMAT)
|
| 111 |
# Get command name and arguments
|
| 112 |
try:
|
| 113 |
+
print_assistant_thoughts(
|
| 114 |
+
self.ai_name, assistant_reply_json, cfg.speak_mode
|
| 115 |
+
)
|
| 116 |
command_name, arguments = get_command(assistant_reply_json)
|
|
|
|
| 117 |
if cfg.speak_mode:
|
| 118 |
say_text(f"I want to execute {command_name}")
|
| 119 |
+
|
| 120 |
+
arguments = self._resolve_pathlike_command_args(arguments)
|
| 121 |
+
|
| 122 |
except Exception as e:
|
| 123 |
logger.error("Error: \n", str(e))
|
| 124 |
|
| 125 |
if not cfg.continuous_mode and self.next_action_count == 0:
|
| 126 |
+
# ### GET USER AUTHORIZATION TO EXECUTE COMMAND ###
|
| 127 |
# Get key press: Prompt the user to press enter to continue or escape
|
| 128 |
# to exit
|
| 129 |
+
self.user_input = ""
|
| 130 |
logger.typewriter_log(
|
| 131 |
"NEXT ACTION: ",
|
| 132 |
Fore.CYAN,
|
| 133 |
f"COMMAND = {Fore.CYAN}{command_name}{Style.RESET_ALL} "
|
| 134 |
f"ARGUMENTS = {Fore.CYAN}{arguments}{Style.RESET_ALL}",
|
| 135 |
)
|
| 136 |
+
|
| 137 |
+
logger.info(
|
| 138 |
+
"Enter 'y' to authorise command, 'y -N' to run N continuous commands, 's' to run self-feedback commands"
|
| 139 |
+
"'n' to exit program, or enter feedback for "
|
| 140 |
+
f"{self.ai_name}..."
|
| 141 |
)
|
| 142 |
while True:
|
| 143 |
+
if cfg.chat_messages_enabled:
|
| 144 |
+
console_input = clean_input("Waiting for your response...")
|
| 145 |
+
else:
|
| 146 |
+
console_input = clean_input(
|
| 147 |
+
Fore.MAGENTA + "Input:" + Style.RESET_ALL
|
| 148 |
+
)
|
| 149 |
+
if console_input.lower().strip() == cfg.authorise_key:
|
| 150 |
user_input = "GENERATE NEXT COMMAND JSON"
|
| 151 |
break
|
| 152 |
+
elif console_input.lower().strip() == "s":
|
| 153 |
+
logger.typewriter_log(
|
| 154 |
+
"-=-=-=-=-=-=-= THOUGHTS, REASONING, PLAN AND CRITICISM WILL NOW BE VERIFIED BY AGENT -=-=-=-=-=-=-=",
|
| 155 |
+
Fore.GREEN,
|
| 156 |
+
"",
|
| 157 |
+
)
|
| 158 |
+
thoughts = assistant_reply_json.get("thoughts", {})
|
| 159 |
+
self_feedback_resp = self.get_self_feedback(
|
| 160 |
+
thoughts, cfg.fast_llm_model
|
| 161 |
+
)
|
| 162 |
+
logger.typewriter_log(
|
| 163 |
+
f"SELF FEEDBACK: {self_feedback_resp}",
|
| 164 |
+
Fore.YELLOW,
|
| 165 |
+
"",
|
| 166 |
+
)
|
| 167 |
+
if self_feedback_resp[0].lower().strip() == cfg.authorise_key:
|
| 168 |
+
user_input = "GENERATE NEXT COMMAND JSON"
|
| 169 |
+
else:
|
| 170 |
+
user_input = self_feedback_resp
|
| 171 |
+
break
|
| 172 |
elif console_input.lower().strip() == "":
|
| 173 |
+
logger.warn("Invalid input format.")
|
| 174 |
continue
|
| 175 |
+
elif console_input.lower().startswith(f"{cfg.authorise_key} -"):
|
| 176 |
try:
|
| 177 |
self.next_action_count = abs(
|
| 178 |
int(console_input.split(" ")[1])
|
| 179 |
)
|
| 180 |
user_input = "GENERATE NEXT COMMAND JSON"
|
| 181 |
except ValueError:
|
| 182 |
+
logger.warn(
|
| 183 |
"Invalid input format. Please enter 'y -n' where n is"
|
| 184 |
" the number of continuous tasks."
|
| 185 |
)
|
| 186 |
continue
|
| 187 |
break
|
| 188 |
+
elif console_input.lower() == cfg.exit_key:
|
| 189 |
user_input = "EXIT"
|
| 190 |
break
|
| 191 |
else:
|
|
|
|
| 200 |
"",
|
| 201 |
)
|
| 202 |
elif user_input == "EXIT":
|
| 203 |
+
logger.info("Exiting...")
|
| 204 |
break
|
| 205 |
else:
|
| 206 |
# Print command
|
|
|
|
| 219 |
elif command_name == "human_feedback":
|
| 220 |
result = f"Human feedback: {user_input}"
|
| 221 |
else:
|
| 222 |
+
for plugin in cfg.plugins:
|
| 223 |
+
if not plugin.can_handle_pre_command():
|
| 224 |
+
continue
|
| 225 |
+
command_name, arguments = plugin.pre_command(
|
| 226 |
+
command_name, arguments
|
| 227 |
+
)
|
| 228 |
+
command_result = execute_command(
|
| 229 |
+
self.command_registry,
|
| 230 |
+
command_name,
|
| 231 |
+
arguments,
|
| 232 |
+
self.config.prompt_generator,
|
| 233 |
)
|
| 234 |
+
result = f"Command {command_name} returned: " f"{command_result}"
|
| 235 |
+
|
| 236 |
+
for plugin in cfg.plugins:
|
| 237 |
+
if not plugin.can_handle_post_command():
|
| 238 |
+
continue
|
| 239 |
+
result = plugin.post_command(command_name, result)
|
| 240 |
if self.next_action_count > 0:
|
| 241 |
self.next_action_count -= 1
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
# Check if there's a result from the command append it to the message
|
| 244 |
# history
|
| 245 |
if result is not None:
|
|
|
|
| 252 |
logger.typewriter_log(
|
| 253 |
"SYSTEM: ", Fore.YELLOW, "Unable to execute command"
|
| 254 |
)
|
| 255 |
+
|
| 256 |
+
def _resolve_pathlike_command_args(self, command_args):
|
| 257 |
+
if "directory" in command_args and command_args["directory"] in {"", "/"}:
|
| 258 |
+
command_args["directory"] = str(self.workspace.root)
|
| 259 |
+
else:
|
| 260 |
+
for pathlike in ["filename", "directory", "clone_path"]:
|
| 261 |
+
if pathlike in command_args:
|
| 262 |
+
command_args[pathlike] = str(
|
| 263 |
+
self.workspace.get_path(command_args[pathlike])
|
| 264 |
+
)
|
| 265 |
+
return command_args
|
| 266 |
+
|
| 267 |
+
def get_self_feedback(self, thoughts: dict, llm_model: str) -> str:
|
| 268 |
+
"""Generates a feedback response based on the provided thoughts dictionary.
|
| 269 |
+
This method takes in a dictionary of thoughts containing keys such as 'reasoning',
|
| 270 |
+
'plan', 'thoughts', and 'criticism'. It combines these elements into a single
|
| 271 |
+
feedback message and uses the create_chat_completion() function to generate a
|
| 272 |
+
response based on the input message.
|
| 273 |
+
Args:
|
| 274 |
+
thoughts (dict): A dictionary containing thought elements like reasoning,
|
| 275 |
+
plan, thoughts, and criticism.
|
| 276 |
+
Returns:
|
| 277 |
+
str: A feedback response generated using the provided thoughts dictionary.
|
| 278 |
+
"""
|
| 279 |
+
ai_role = self.config.ai_role
|
| 280 |
+
|
| 281 |
+
feedback_prompt = f"Below is a message from an AI agent with the role of {ai_role}. Please review the provided Thought, Reasoning, Plan, and Criticism. If these elements accurately contribute to the successful execution of the assumed role, respond with the letter 'Y' followed by a space, and then explain why it is effective. If the provided information is not suitable for achieving the role's objectives, please provide one or more sentences addressing the issue and suggesting a resolution."
|
| 282 |
+
reasoning = thoughts.get("reasoning", "")
|
| 283 |
+
plan = thoughts.get("plan", "")
|
| 284 |
+
thought = thoughts.get("thoughts", "")
|
| 285 |
+
criticism = thoughts.get("criticism", "")
|
| 286 |
+
feedback_thoughts = thought + reasoning + plan + criticism
|
| 287 |
+
return create_chat_completion(
|
| 288 |
+
[{"role": "user", "content": feedback_prompt + feedback_thoughts}],
|
| 289 |
+
llm_model,
|
| 290 |
+
)
|
autogpt/agent/agent_manager.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
"""Agent manager for managing GPT agents"""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
-
from typing import
|
| 5 |
|
| 6 |
-
from autogpt.config.config import
|
| 7 |
-
from autogpt.
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
class AgentManager(metaclass=Singleton):
|
|
@@ -13,6 +14,7 @@ class AgentManager(metaclass=Singleton):
|
|
| 13 |
def __init__(self):
|
| 14 |
self.next_key = 0
|
| 15 |
self.agents = {} # key, (task, full_message_history, model)
|
|
|
|
| 16 |
|
| 17 |
# Create new GPT agent
|
| 18 |
# TODO: Centralise use of create_chat_completion() to globally enforce token limit
|
|
@@ -28,19 +30,32 @@ class AgentManager(metaclass=Singleton):
|
|
| 28 |
Returns:
|
| 29 |
The key of the new agent
|
| 30 |
"""
|
| 31 |
-
messages = [
|
| 32 |
{"role": "user", "content": prompt},
|
| 33 |
]
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
# Start GPT instance
|
| 36 |
agent_reply = create_chat_completion(
|
| 37 |
model=model,
|
| 38 |
messages=messages,
|
| 39 |
)
|
| 40 |
|
| 41 |
-
# Update full message history
|
| 42 |
messages.append({"role": "assistant", "content": agent_reply})
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
key = self.next_key
|
| 45 |
# This is done instead of len(agents) to make keys unique even if agents
|
| 46 |
# are deleted
|
|
@@ -48,6 +63,11 @@ class AgentManager(metaclass=Singleton):
|
|
| 48 |
|
| 49 |
self.agents[key] = (task, messages, model)
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
return key, agent_reply
|
| 52 |
|
| 53 |
def message_agent(self, key: str | int, message: str) -> str:
|
|
@@ -65,15 +85,37 @@ class AgentManager(metaclass=Singleton):
|
|
| 65 |
# Add user message to message history before sending to agent
|
| 66 |
messages.append({"role": "user", "content": message})
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
# Start GPT instance
|
| 69 |
agent_reply = create_chat_completion(
|
| 70 |
model=model,
|
| 71 |
messages=messages,
|
| 72 |
)
|
| 73 |
|
| 74 |
-
# Update full message history
|
| 75 |
messages.append({"role": "assistant", "content": agent_reply})
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
return agent_reply
|
| 78 |
|
| 79 |
def list_agents(self) -> list[tuple[str | int, str]]:
|
|
@@ -86,7 +128,7 @@ class AgentManager(metaclass=Singleton):
|
|
| 86 |
# Return a list of agent keys and their tasks
|
| 87 |
return [(key, task) for key, (task, _, _) in self.agents.items()]
|
| 88 |
|
| 89 |
-
def delete_agent(self, key:
|
| 90 |
"""Delete an agent from the agent manager
|
| 91 |
|
| 92 |
Args:
|
|
|
|
| 1 |
"""Agent manager for managing GPT agents"""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
+
from typing import List
|
| 5 |
|
| 6 |
+
from autogpt.config.config import Config
|
| 7 |
+
from autogpt.llm import Message, create_chat_completion
|
| 8 |
+
from autogpt.singleton import Singleton
|
| 9 |
|
| 10 |
|
| 11 |
class AgentManager(metaclass=Singleton):
|
|
|
|
| 14 |
def __init__(self):
|
| 15 |
self.next_key = 0
|
| 16 |
self.agents = {} # key, (task, full_message_history, model)
|
| 17 |
+
self.cfg = Config()
|
| 18 |
|
| 19 |
# Create new GPT agent
|
| 20 |
# TODO: Centralise use of create_chat_completion() to globally enforce token limit
|
|
|
|
| 30 |
Returns:
|
| 31 |
The key of the new agent
|
| 32 |
"""
|
| 33 |
+
messages: List[Message] = [
|
| 34 |
{"role": "user", "content": prompt},
|
| 35 |
]
|
| 36 |
+
for plugin in self.cfg.plugins:
|
| 37 |
+
if not plugin.can_handle_pre_instruction():
|
| 38 |
+
continue
|
| 39 |
+
if plugin_messages := plugin.pre_instruction(messages):
|
| 40 |
+
messages.extend(iter(plugin_messages))
|
| 41 |
# Start GPT instance
|
| 42 |
agent_reply = create_chat_completion(
|
| 43 |
model=model,
|
| 44 |
messages=messages,
|
| 45 |
)
|
| 46 |
|
|
|
|
| 47 |
messages.append({"role": "assistant", "content": agent_reply})
|
| 48 |
|
| 49 |
+
plugins_reply = ""
|
| 50 |
+
for i, plugin in enumerate(self.cfg.plugins):
|
| 51 |
+
if not plugin.can_handle_on_instruction():
|
| 52 |
+
continue
|
| 53 |
+
if plugin_result := plugin.on_instruction(messages):
|
| 54 |
+
sep = "\n" if i else ""
|
| 55 |
+
plugins_reply = f"{plugins_reply}{sep}{plugin_result}"
|
| 56 |
+
|
| 57 |
+
if plugins_reply and plugins_reply != "":
|
| 58 |
+
messages.append({"role": "assistant", "content": plugins_reply})
|
| 59 |
key = self.next_key
|
| 60 |
# This is done instead of len(agents) to make keys unique even if agents
|
| 61 |
# are deleted
|
|
|
|
| 63 |
|
| 64 |
self.agents[key] = (task, messages, model)
|
| 65 |
|
| 66 |
+
for plugin in self.cfg.plugins:
|
| 67 |
+
if not plugin.can_handle_post_instruction():
|
| 68 |
+
continue
|
| 69 |
+
agent_reply = plugin.post_instruction(agent_reply)
|
| 70 |
+
|
| 71 |
return key, agent_reply
|
| 72 |
|
| 73 |
def message_agent(self, key: str | int, message: str) -> str:
|
|
|
|
| 85 |
# Add user message to message history before sending to agent
|
| 86 |
messages.append({"role": "user", "content": message})
|
| 87 |
|
| 88 |
+
for plugin in self.cfg.plugins:
|
| 89 |
+
if not plugin.can_handle_pre_instruction():
|
| 90 |
+
continue
|
| 91 |
+
if plugin_messages := plugin.pre_instruction(messages):
|
| 92 |
+
for plugin_message in plugin_messages:
|
| 93 |
+
messages.append(plugin_message)
|
| 94 |
+
|
| 95 |
# Start GPT instance
|
| 96 |
agent_reply = create_chat_completion(
|
| 97 |
model=model,
|
| 98 |
messages=messages,
|
| 99 |
)
|
| 100 |
|
|
|
|
| 101 |
messages.append({"role": "assistant", "content": agent_reply})
|
| 102 |
|
| 103 |
+
plugins_reply = agent_reply
|
| 104 |
+
for i, plugin in enumerate(self.cfg.plugins):
|
| 105 |
+
if not plugin.can_handle_on_instruction():
|
| 106 |
+
continue
|
| 107 |
+
if plugin_result := plugin.on_instruction(messages):
|
| 108 |
+
sep = "\n" if i else ""
|
| 109 |
+
plugins_reply = f"{plugins_reply}{sep}{plugin_result}"
|
| 110 |
+
# Update full message history
|
| 111 |
+
if plugins_reply and plugins_reply != "":
|
| 112 |
+
messages.append({"role": "assistant", "content": plugins_reply})
|
| 113 |
+
|
| 114 |
+
for plugin in self.cfg.plugins:
|
| 115 |
+
if not plugin.can_handle_post_instruction():
|
| 116 |
+
continue
|
| 117 |
+
agent_reply = plugin.post_instruction(agent_reply)
|
| 118 |
+
|
| 119 |
return agent_reply
|
| 120 |
|
| 121 |
def list_agents(self) -> list[tuple[str | int, str]]:
|
|
|
|
| 128 |
# Return a list of agent keys and their tasks
|
| 129 |
return [(key, task) for key, (task, _, _) in self.agents.items()]
|
| 130 |
|
| 131 |
+
def delete_agent(self, key: str | int) -> bool:
|
| 132 |
"""Delete an agent from the agent manager
|
| 133 |
|
| 134 |
Args:
|
autogpt/commands/analyze_code.py
CHANGED
|
@@ -1,9 +1,15 @@
|
|
| 1 |
"""Code evaluation module."""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
-
from autogpt.
|
|
|
|
| 5 |
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
def analyze_code(code: str) -> list[str]:
|
| 8 |
"""
|
| 9 |
A function that takes in a string and returns a response from create chat
|
|
@@ -16,10 +22,10 @@ def analyze_code(code: str) -> list[str]:
|
|
| 16 |
improve the code.
|
| 17 |
"""
|
| 18 |
|
| 19 |
-
function_string = "def analyze_code(code: str) ->
|
| 20 |
args = [code]
|
| 21 |
description_string = (
|
| 22 |
-
"Analyzes the given code and returns a list of suggestions
|
| 23 |
)
|
| 24 |
|
| 25 |
return call_ai_function(function_string, args, description_string)
|
|
|
|
| 1 |
"""Code evaluation module."""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
+
from autogpt.commands.command import command
|
| 5 |
+
from autogpt.llm import call_ai_function
|
| 6 |
|
| 7 |
|
| 8 |
+
@command(
|
| 9 |
+
"analyze_code",
|
| 10 |
+
"Analyze Code",
|
| 11 |
+
'"code": "<full_code_string>"',
|
| 12 |
+
)
|
| 13 |
def analyze_code(code: str) -> list[str]:
|
| 14 |
"""
|
| 15 |
A function that takes in a string and returns a response from create chat
|
|
|
|
| 22 |
improve the code.
|
| 23 |
"""
|
| 24 |
|
| 25 |
+
function_string = "def analyze_code(code: str) -> list[str]:"
|
| 26 |
args = [code]
|
| 27 |
description_string = (
|
| 28 |
+
"Analyzes the given code and returns a list of suggestions for improvements."
|
| 29 |
)
|
| 30 |
|
| 31 |
return call_ai_function(function_string, args, description_string)
|
autogpt/commands/command.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import importlib
|
| 3 |
+
import inspect
|
| 4 |
+
from typing import Any, Callable, Optional
|
| 5 |
+
|
| 6 |
+
# Unique identifier for auto-gpt commands
|
| 7 |
+
AUTO_GPT_COMMAND_IDENTIFIER = "auto_gpt_command"
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Command:
|
| 11 |
+
"""A class representing a command.
|
| 12 |
+
|
| 13 |
+
Attributes:
|
| 14 |
+
name (str): The name of the command.
|
| 15 |
+
description (str): A brief description of what the command does.
|
| 16 |
+
signature (str): The signature of the function that the command executes. Defaults to None.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
def __init__(
|
| 20 |
+
self,
|
| 21 |
+
name: str,
|
| 22 |
+
description: str,
|
| 23 |
+
method: Callable[..., Any],
|
| 24 |
+
signature: str = "",
|
| 25 |
+
enabled: bool = True,
|
| 26 |
+
disabled_reason: Optional[str] = None,
|
| 27 |
+
):
|
| 28 |
+
self.name = name
|
| 29 |
+
self.description = description
|
| 30 |
+
self.method = method
|
| 31 |
+
self.signature = signature if signature else str(inspect.signature(self.method))
|
| 32 |
+
self.enabled = enabled
|
| 33 |
+
self.disabled_reason = disabled_reason
|
| 34 |
+
|
| 35 |
+
def __call__(self, *args, **kwargs) -> Any:
|
| 36 |
+
if not self.enabled:
|
| 37 |
+
return f"Command '{self.name}' is disabled: {self.disabled_reason}"
|
| 38 |
+
return self.method(*args, **kwargs)
|
| 39 |
+
|
| 40 |
+
def __str__(self) -> str:
|
| 41 |
+
return f"{self.name}: {self.description}, args: {self.signature}"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class CommandRegistry:
|
| 45 |
+
"""
|
| 46 |
+
The CommandRegistry class is a manager for a collection of Command objects.
|
| 47 |
+
It allows the registration, modification, and retrieval of Command objects,
|
| 48 |
+
as well as the scanning and loading of command plugins from a specified
|
| 49 |
+
directory.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
def __init__(self):
|
| 53 |
+
self.commands = {}
|
| 54 |
+
|
| 55 |
+
def _import_module(self, module_name: str) -> Any:
|
| 56 |
+
return importlib.import_module(module_name)
|
| 57 |
+
|
| 58 |
+
def _reload_module(self, module: Any) -> Any:
|
| 59 |
+
return importlib.reload(module)
|
| 60 |
+
|
| 61 |
+
def register(self, cmd: Command) -> None:
|
| 62 |
+
self.commands[cmd.name] = cmd
|
| 63 |
+
|
| 64 |
+
def unregister(self, command_name: str):
|
| 65 |
+
if command_name in self.commands:
|
| 66 |
+
del self.commands[command_name]
|
| 67 |
+
else:
|
| 68 |
+
raise KeyError(f"Command '{command_name}' not found in registry.")
|
| 69 |
+
|
| 70 |
+
def reload_commands(self) -> None:
|
| 71 |
+
"""Reloads all loaded command plugins."""
|
| 72 |
+
for cmd_name in self.commands:
|
| 73 |
+
cmd = self.commands[cmd_name]
|
| 74 |
+
module = self._import_module(cmd.__module__)
|
| 75 |
+
reloaded_module = self._reload_module(module)
|
| 76 |
+
if hasattr(reloaded_module, "register"):
|
| 77 |
+
reloaded_module.register(self)
|
| 78 |
+
|
| 79 |
+
def get_command(self, name: str) -> Callable[..., Any]:
|
| 80 |
+
return self.commands[name]
|
| 81 |
+
|
| 82 |
+
def call(self, command_name: str, **kwargs) -> Any:
|
| 83 |
+
if command_name not in self.commands:
|
| 84 |
+
raise KeyError(f"Command '{command_name}' not found in registry.")
|
| 85 |
+
command = self.commands[command_name]
|
| 86 |
+
return command(**kwargs)
|
| 87 |
+
|
| 88 |
+
def command_prompt(self) -> str:
|
| 89 |
+
"""
|
| 90 |
+
Returns a string representation of all registered `Command` objects for use in a prompt
|
| 91 |
+
"""
|
| 92 |
+
commands_list = [
|
| 93 |
+
f"{idx + 1}. {str(cmd)}" for idx, cmd in enumerate(self.commands.values())
|
| 94 |
+
]
|
| 95 |
+
return "\n".join(commands_list)
|
| 96 |
+
|
| 97 |
+
def import_commands(self, module_name: str) -> None:
|
| 98 |
+
"""
|
| 99 |
+
Imports the specified Python module containing command plugins.
|
| 100 |
+
|
| 101 |
+
This method imports the associated module and registers any functions or
|
| 102 |
+
classes that are decorated with the `AUTO_GPT_COMMAND_IDENTIFIER` attribute
|
| 103 |
+
as `Command` objects. The registered `Command` objects are then added to the
|
| 104 |
+
`commands` dictionary of the `CommandRegistry` object.
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
module_name (str): The name of the module to import for command plugins.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
module = importlib.import_module(module_name)
|
| 111 |
+
|
| 112 |
+
for attr_name in dir(module):
|
| 113 |
+
attr = getattr(module, attr_name)
|
| 114 |
+
# Register decorated functions
|
| 115 |
+
if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr(
|
| 116 |
+
attr, AUTO_GPT_COMMAND_IDENTIFIER
|
| 117 |
+
):
|
| 118 |
+
self.register(attr.command)
|
| 119 |
+
# Register command classes
|
| 120 |
+
elif (
|
| 121 |
+
inspect.isclass(attr) and issubclass(attr, Command) and attr != Command
|
| 122 |
+
):
|
| 123 |
+
cmd_instance = attr()
|
| 124 |
+
self.register(cmd_instance)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def command(
|
| 128 |
+
name: str,
|
| 129 |
+
description: str,
|
| 130 |
+
signature: str = "",
|
| 131 |
+
enabled: bool = True,
|
| 132 |
+
disabled_reason: Optional[str] = None,
|
| 133 |
+
) -> Callable[..., Any]:
|
| 134 |
+
"""The command decorator is used to create Command objects from ordinary functions."""
|
| 135 |
+
|
| 136 |
+
def decorator(func: Callable[..., Any]) -> Command:
|
| 137 |
+
cmd = Command(
|
| 138 |
+
name=name,
|
| 139 |
+
description=description,
|
| 140 |
+
method=func,
|
| 141 |
+
signature=signature,
|
| 142 |
+
enabled=enabled,
|
| 143 |
+
disabled_reason=disabled_reason,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
@functools.wraps(func)
|
| 147 |
+
def wrapper(*args, **kwargs) -> Any:
|
| 148 |
+
return func(*args, **kwargs)
|
| 149 |
+
|
| 150 |
+
wrapper.command = cmd
|
| 151 |
+
|
| 152 |
+
setattr(wrapper, AUTO_GPT_COMMAND_IDENTIFIER, True)
|
| 153 |
+
|
| 154 |
+
return wrapper
|
| 155 |
+
|
| 156 |
+
return decorator
|
autogpt/commands/execute_code.py
CHANGED
|
@@ -1,36 +1,38 @@
|
|
| 1 |
"""Execute code in a Docker container"""
|
| 2 |
import os
|
| 3 |
import subprocess
|
|
|
|
| 4 |
|
| 5 |
import docker
|
| 6 |
from docker.errors import ImageNotFound
|
| 7 |
|
| 8 |
-
from autogpt.
|
|
|
|
|
|
|
| 9 |
|
|
|
|
| 10 |
|
| 11 |
-
|
|
|
|
| 12 |
"""Execute a Python file in a Docker container and return the output
|
| 13 |
|
| 14 |
Args:
|
| 15 |
-
|
| 16 |
|
| 17 |
Returns:
|
| 18 |
str: The output of the file
|
| 19 |
"""
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
if not file.endswith(".py"):
|
| 24 |
return "Error: Invalid file type. Only .py files are allowed."
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
if not os.path.isfile(file_path):
|
| 29 |
-
return f"Error: File '{file}' does not exist."
|
| 30 |
|
| 31 |
if we_are_running_in_a_docker_container():
|
| 32 |
result = subprocess.run(
|
| 33 |
-
f"python {
|
| 34 |
)
|
| 35 |
if result.returncode == 0:
|
| 36 |
return result.stdout
|
|
@@ -39,16 +41,17 @@ def execute_python_file(file: str) -> str:
|
|
| 39 |
|
| 40 |
try:
|
| 41 |
client = docker.from_env()
|
| 42 |
-
|
| 43 |
# You can replace this with the desired Python image/version
|
| 44 |
# You can find available Python images on Docker Hub:
|
| 45 |
# https://hub.docker.com/_/python
|
| 46 |
image_name = "python:3-alpine"
|
| 47 |
try:
|
| 48 |
client.images.get(image_name)
|
| 49 |
-
|
| 50 |
except ImageNotFound:
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
# Use the low-level API to stream the pull response
|
| 53 |
low_level_client = docker.APIClient()
|
| 54 |
for line in low_level_client.pull(image_name, stream=True, decode=True):
|
|
@@ -56,15 +59,14 @@ def execute_python_file(file: str) -> str:
|
|
| 56 |
status = line.get("status")
|
| 57 |
progress = line.get("progress")
|
| 58 |
if status and progress:
|
| 59 |
-
|
| 60 |
elif status:
|
| 61 |
-
|
| 62 |
-
|
| 63 |
container = client.containers.run(
|
| 64 |
image_name,
|
| 65 |
-
f"python {
|
| 66 |
volumes={
|
| 67 |
-
|
| 68 |
"bind": "/workspace",
|
| 69 |
"mode": "ro",
|
| 70 |
}
|
|
@@ -85,7 +87,7 @@ def execute_python_file(file: str) -> str:
|
|
| 85 |
return logs
|
| 86 |
|
| 87 |
except docker.errors.DockerException as e:
|
| 88 |
-
|
| 89 |
"Could not run the script in a container. If you haven't already, please install Docker https://docs.docker.com/get-docker/"
|
| 90 |
)
|
| 91 |
return f"Error: {str(e)}"
|
|
@@ -94,6 +96,15 @@ def execute_python_file(file: str) -> str:
|
|
| 94 |
return f"Error: {str(e)}"
|
| 95 |
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
def execute_shell(command_line: str) -> str:
|
| 98 |
"""Execute a shell command and return the output
|
| 99 |
|
|
@@ -103,12 +114,15 @@ def execute_shell(command_line: str) -> str:
|
|
| 103 |
Returns:
|
| 104 |
str: The output of the command
|
| 105 |
"""
|
| 106 |
-
|
|
|
|
| 107 |
# Change dir into workspace if necessary
|
| 108 |
-
if
|
| 109 |
-
os.chdir(
|
| 110 |
|
| 111 |
-
|
|
|
|
|
|
|
| 112 |
|
| 113 |
result = subprocess.run(command_line, capture_output=True, shell=True)
|
| 114 |
output = f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
@@ -116,10 +130,18 @@ def execute_shell(command_line: str) -> str:
|
|
| 116 |
# Change back to whatever the prior working dir was
|
| 117 |
|
| 118 |
os.chdir(current_dir)
|
| 119 |
-
|
| 120 |
return output
|
| 121 |
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
def execute_shell_popen(command_line) -> str:
|
| 124 |
"""Execute a shell command with Popen and returns an english description
|
| 125 |
of the event and the process id
|
|
@@ -130,12 +152,15 @@ def execute_shell_popen(command_line) -> str:
|
|
| 130 |
Returns:
|
| 131 |
str: Description of the fact that the process started and its id
|
| 132 |
"""
|
|
|
|
| 133 |
current_dir = os.getcwd()
|
| 134 |
# Change dir into workspace if necessary
|
| 135 |
-
if
|
| 136 |
-
os.chdir(
|
| 137 |
|
| 138 |
-
|
|
|
|
|
|
|
| 139 |
|
| 140 |
do_not_show_output = subprocess.DEVNULL
|
| 141 |
process = subprocess.Popen(
|
|
|
|
| 1 |
"""Execute code in a Docker container"""
|
| 2 |
import os
|
| 3 |
import subprocess
|
| 4 |
+
from pathlib import Path
|
| 5 |
|
| 6 |
import docker
|
| 7 |
from docker.errors import ImageNotFound
|
| 8 |
|
| 9 |
+
from autogpt.commands.command import command
|
| 10 |
+
from autogpt.config import Config
|
| 11 |
+
from autogpt.logs import logger
|
| 12 |
|
| 13 |
+
CFG = Config()
|
| 14 |
|
| 15 |
+
@command("execute_python_file", "Execute Python File", '"filename": "<filename>"')
|
| 16 |
+
def execute_python_file(filename: str) -> str:
|
| 17 |
"""Execute a Python file in a Docker container and return the output
|
| 18 |
|
| 19 |
Args:
|
| 20 |
+
filename (str): The name of the file to execute
|
| 21 |
|
| 22 |
Returns:
|
| 23 |
str: The output of the file
|
| 24 |
"""
|
| 25 |
+
logger.info(f"Executing file '{filename}'")
|
| 26 |
|
| 27 |
+
if not filename.endswith(".py"):
|
|
|
|
|
|
|
| 28 |
return "Error: Invalid file type. Only .py files are allowed."
|
| 29 |
|
| 30 |
+
if not os.path.isfile(filename):
|
| 31 |
+
return f"Error: File '{filename}' does not exist."
|
|
|
|
|
|
|
| 32 |
|
| 33 |
if we_are_running_in_a_docker_container():
|
| 34 |
result = subprocess.run(
|
| 35 |
+
f"python {filename}", capture_output=True, encoding="utf8", shell=True
|
| 36 |
)
|
| 37 |
if result.returncode == 0:
|
| 38 |
return result.stdout
|
|
|
|
| 41 |
|
| 42 |
try:
|
| 43 |
client = docker.from_env()
|
|
|
|
| 44 |
# You can replace this with the desired Python image/version
|
| 45 |
# You can find available Python images on Docker Hub:
|
| 46 |
# https://hub.docker.com/_/python
|
| 47 |
image_name = "python:3-alpine"
|
| 48 |
try:
|
| 49 |
client.images.get(image_name)
|
| 50 |
+
logger.warn(f"Image '{image_name}' found locally")
|
| 51 |
except ImageNotFound:
|
| 52 |
+
logger.info(
|
| 53 |
+
f"Image '{image_name}' not found locally, pulling from Docker Hub"
|
| 54 |
+
)
|
| 55 |
# Use the low-level API to stream the pull response
|
| 56 |
low_level_client = docker.APIClient()
|
| 57 |
for line in low_level_client.pull(image_name, stream=True, decode=True):
|
|
|
|
| 59 |
status = line.get("status")
|
| 60 |
progress = line.get("progress")
|
| 61 |
if status and progress:
|
| 62 |
+
logger.info(f"{status}: {progress}")
|
| 63 |
elif status:
|
| 64 |
+
logger.info(status)
|
|
|
|
| 65 |
container = client.containers.run(
|
| 66 |
image_name,
|
| 67 |
+
f"python {Path(filename).relative_to(CFG.workspace_path)}",
|
| 68 |
volumes={
|
| 69 |
+
CFG.workspace_path: {
|
| 70 |
"bind": "/workspace",
|
| 71 |
"mode": "ro",
|
| 72 |
}
|
|
|
|
| 87 |
return logs
|
| 88 |
|
| 89 |
except docker.errors.DockerException as e:
|
| 90 |
+
logger.warn(
|
| 91 |
"Could not run the script in a container. If you haven't already, please install Docker https://docs.docker.com/get-docker/"
|
| 92 |
)
|
| 93 |
return f"Error: {str(e)}"
|
|
|
|
| 96 |
return f"Error: {str(e)}"
|
| 97 |
|
| 98 |
|
| 99 |
+
@command(
|
| 100 |
+
"execute_shell",
|
| 101 |
+
"Execute Shell Command, non-interactive commands only",
|
| 102 |
+
'"command_line": "<command_line>"',
|
| 103 |
+
CFG.execute_local_commands,
|
| 104 |
+
"You are not allowed to run local shell commands. To execute"
|
| 105 |
+
" shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' "
|
| 106 |
+
"in your config. Do not attempt to bypass the restriction.",
|
| 107 |
+
)
|
| 108 |
def execute_shell(command_line: str) -> str:
|
| 109 |
"""Execute a shell command and return the output
|
| 110 |
|
|
|
|
| 114 |
Returns:
|
| 115 |
str: The output of the command
|
| 116 |
"""
|
| 117 |
+
|
| 118 |
+
current_dir = Path.cwd()
|
| 119 |
# Change dir into workspace if necessary
|
| 120 |
+
if not current_dir.is_relative_to(CFG.workspace_path):
|
| 121 |
+
os.chdir(CFG.workspace_path)
|
| 122 |
|
| 123 |
+
logger.info(
|
| 124 |
+
f"Executing command '{command_line}' in working directory '{os.getcwd()}'"
|
| 125 |
+
)
|
| 126 |
|
| 127 |
result = subprocess.run(command_line, capture_output=True, shell=True)
|
| 128 |
output = f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
|
|
|
| 130 |
# Change back to whatever the prior working dir was
|
| 131 |
|
| 132 |
os.chdir(current_dir)
|
|
|
|
| 133 |
return output
|
| 134 |
|
| 135 |
|
| 136 |
+
@command(
|
| 137 |
+
"execute_shell_popen",
|
| 138 |
+
"Execute Shell Command, non-interactive commands only",
|
| 139 |
+
'"command_line": "<command_line>"',
|
| 140 |
+
CFG.execute_local_commands,
|
| 141 |
+
"You are not allowed to run local shell commands. To execute"
|
| 142 |
+
" shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' "
|
| 143 |
+
"in your config. Do not attempt to bypass the restriction.",
|
| 144 |
+
)
|
| 145 |
def execute_shell_popen(command_line) -> str:
|
| 146 |
"""Execute a shell command with Popen and returns an english description
|
| 147 |
of the event and the process id
|
|
|
|
| 152 |
Returns:
|
| 153 |
str: Description of the fact that the process started and its id
|
| 154 |
"""
|
| 155 |
+
|
| 156 |
current_dir = os.getcwd()
|
| 157 |
# Change dir into workspace if necessary
|
| 158 |
+
if CFG.workspace_path not in current_dir:
|
| 159 |
+
os.chdir(CFG.workspace_path)
|
| 160 |
|
| 161 |
+
logger.info(
|
| 162 |
+
f"Executing command '{command_line}' in working directory '{os.getcwd()}'"
|
| 163 |
+
)
|
| 164 |
|
| 165 |
do_not_show_output = subprocess.DEVNULL
|
| 166 |
process = subprocess.Popen(
|
autogpt/commands/file_operations.py
CHANGED
|
@@ -9,12 +9,13 @@ import requests
|
|
| 9 |
from colorama import Back, Fore
|
| 10 |
from requests.adapters import HTTPAdapter, Retry
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
from autogpt.spinner import Spinner
|
| 13 |
from autogpt.utils import readable_file_size
|
| 14 |
-
from autogpt.workspace import WORKSPACE_PATH, path_in_workspace
|
| 15 |
|
| 16 |
-
|
| 17 |
-
LOG_FILE_PATH = WORKSPACE_PATH / LOG_FILE
|
| 18 |
|
| 19 |
|
| 20 |
def check_duplicate_operation(operation: str, filename: str) -> bool:
|
|
@@ -27,7 +28,7 @@ def check_duplicate_operation(operation: str, filename: str) -> bool:
|
|
| 27 |
Returns:
|
| 28 |
bool: True if the operation has already been performed on the file
|
| 29 |
"""
|
| 30 |
-
log_content = read_file(
|
| 31 |
log_entry = f"{operation}: {filename}\n"
|
| 32 |
return log_entry in log_content
|
| 33 |
|
|
@@ -40,13 +41,7 @@ def log_operation(operation: str, filename: str) -> None:
|
|
| 40 |
filename (str): The name of the file the operation was performed on
|
| 41 |
"""
|
| 42 |
log_entry = f"{operation}: {filename}\n"
|
| 43 |
-
|
| 44 |
-
# Create the log file if it doesn't exist
|
| 45 |
-
if not os.path.exists(LOG_FILE_PATH):
|
| 46 |
-
with open(LOG_FILE_PATH, "w", encoding="utf-8") as f:
|
| 47 |
-
f.write("File Operation Logger ")
|
| 48 |
-
|
| 49 |
-
append_to_file(LOG_FILE, log_entry, shouldLog=False)
|
| 50 |
|
| 51 |
|
| 52 |
def split_file(
|
|
@@ -81,6 +76,7 @@ def split_file(
|
|
| 81 |
start += max_length - overlap
|
| 82 |
|
| 83 |
|
|
|
|
| 84 |
def read_file(filename: str) -> str:
|
| 85 |
"""Read a file and return the contents
|
| 86 |
|
|
@@ -91,8 +87,7 @@ def read_file(filename: str) -> str:
|
|
| 91 |
str: The contents of the file
|
| 92 |
"""
|
| 93 |
try:
|
| 94 |
-
|
| 95 |
-
with open(filepath, "r", encoding="utf-8") as f:
|
| 96 |
content = f.read()
|
| 97 |
return content
|
| 98 |
except Exception as e:
|
|
@@ -112,27 +107,28 @@ def ingest_file(
|
|
| 112 |
:param overlap: The number of overlapping characters between chunks, default is 200
|
| 113 |
"""
|
| 114 |
try:
|
| 115 |
-
|
| 116 |
content = read_file(filename)
|
| 117 |
content_length = len(content)
|
| 118 |
-
|
| 119 |
|
| 120 |
chunks = list(split_file(content, max_length=max_length, overlap=overlap))
|
| 121 |
|
| 122 |
num_chunks = len(chunks)
|
| 123 |
for i, chunk in enumerate(chunks):
|
| 124 |
-
|
| 125 |
memory_to_add = (
|
| 126 |
f"Filename: {filename}\n" f"Content part#{i + 1}/{num_chunks}: {chunk}"
|
| 127 |
)
|
| 128 |
|
| 129 |
memory.add(memory_to_add)
|
| 130 |
|
| 131 |
-
|
| 132 |
except Exception as e:
|
| 133 |
-
|
| 134 |
|
| 135 |
|
|
|
|
| 136 |
def write_to_file(filename: str, text: str) -> str:
|
| 137 |
"""Write text to a file
|
| 138 |
|
|
@@ -146,11 +142,9 @@ def write_to_file(filename: str, text: str) -> str:
|
|
| 146 |
if check_duplicate_operation("write", filename):
|
| 147 |
return "Error: File has already been updated."
|
| 148 |
try:
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
os.makedirs(directory)
|
| 153 |
-
with open(filepath, "w", encoding="utf-8") as f:
|
| 154 |
f.write(text)
|
| 155 |
log_operation("write", filename)
|
| 156 |
return "File written to successfully."
|
|
@@ -158,22 +152,27 @@ def write_to_file(filename: str, text: str) -> str:
|
|
| 158 |
return f"Error: {str(e)}"
|
| 159 |
|
| 160 |
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
| 162 |
"""Append text to a file
|
| 163 |
|
| 164 |
Args:
|
| 165 |
filename (str): The name of the file to append to
|
| 166 |
text (str): The text to append to the file
|
|
|
|
| 167 |
|
| 168 |
Returns:
|
| 169 |
str: A message indicating success or failure
|
| 170 |
"""
|
| 171 |
try:
|
| 172 |
-
|
| 173 |
-
|
|
|
|
| 174 |
f.write(text)
|
| 175 |
|
| 176 |
-
if
|
| 177 |
log_operation("append", filename)
|
| 178 |
|
| 179 |
return "Text appended successfully."
|
|
@@ -181,6 +180,7 @@ def append_to_file(filename: str, text: str, shouldLog: bool = True) -> str:
|
|
| 181 |
return f"Error: {str(e)}"
|
| 182 |
|
| 183 |
|
|
|
|
| 184 |
def delete_file(filename: str) -> str:
|
| 185 |
"""Delete a file
|
| 186 |
|
|
@@ -193,14 +193,14 @@ def delete_file(filename: str) -> str:
|
|
| 193 |
if check_duplicate_operation("delete", filename):
|
| 194 |
return "Error: File has already been deleted."
|
| 195 |
try:
|
| 196 |
-
|
| 197 |
-
os.remove(filepath)
|
| 198 |
log_operation("delete", filename)
|
| 199 |
return "File deleted successfully."
|
| 200 |
except Exception as e:
|
| 201 |
return f"Error: {str(e)}"
|
| 202 |
|
| 203 |
|
|
|
|
| 204 |
def search_files(directory: str) -> list[str]:
|
| 205 |
"""Search for files in a directory
|
| 206 |
|
|
@@ -212,29 +212,34 @@ def search_files(directory: str) -> list[str]:
|
|
| 212 |
"""
|
| 213 |
found_files = []
|
| 214 |
|
| 215 |
-
|
| 216 |
-
search_directory = WORKSPACE_PATH
|
| 217 |
-
else:
|
| 218 |
-
search_directory = path_in_workspace(directory)
|
| 219 |
-
|
| 220 |
-
for root, _, files in os.walk(search_directory):
|
| 221 |
for file in files:
|
| 222 |
if file.startswith("."):
|
| 223 |
continue
|
| 224 |
-
relative_path = os.path.relpath(
|
|
|
|
|
|
|
| 225 |
found_files.append(relative_path)
|
| 226 |
|
| 227 |
return found_files
|
| 228 |
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
def download_file(url, filename):
|
| 231 |
"""Downloads a file
|
| 232 |
Args:
|
| 233 |
url (str): URL of the file to download
|
| 234 |
filename (str): Filename to save the file as
|
| 235 |
"""
|
| 236 |
-
safe_filename = path_in_workspace(filename)
|
| 237 |
try:
|
|
|
|
|
|
|
| 238 |
message = f"{Fore.YELLOW}Downloading file from {Back.LIGHTBLUE_EX}{url}{Back.RESET}{Fore.RESET}"
|
| 239 |
with Spinner(message) as spinner:
|
| 240 |
session = requests.Session()
|
|
@@ -251,7 +256,7 @@ def download_file(url, filename):
|
|
| 251 |
total_size = int(r.headers.get("Content-Length", 0))
|
| 252 |
downloaded_size = 0
|
| 253 |
|
| 254 |
-
with open(
|
| 255 |
for chunk in r.iter_content(chunk_size=8192):
|
| 256 |
f.write(chunk)
|
| 257 |
downloaded_size += len(chunk)
|
|
@@ -260,7 +265,7 @@ def download_file(url, filename):
|
|
| 260 |
progress = f"{readable_file_size(downloaded_size)} / {readable_file_size(total_size)}"
|
| 261 |
spinner.update_message(f"{message} {progress}")
|
| 262 |
|
| 263 |
-
return f'Successfully downloaded and locally stored file: "{filename}"! (Size: {readable_file_size(
|
| 264 |
except requests.HTTPError as e:
|
| 265 |
return f"Got an HTTP Error whilst trying to download file: {e}"
|
| 266 |
except Exception as e:
|
|
|
|
| 9 |
from colorama import Back, Fore
|
| 10 |
from requests.adapters import HTTPAdapter, Retry
|
| 11 |
|
| 12 |
+
from autogpt.commands.command import command
|
| 13 |
+
from autogpt.config import Config
|
| 14 |
+
from autogpt.logs import logger
|
| 15 |
from autogpt.spinner import Spinner
|
| 16 |
from autogpt.utils import readable_file_size
|
|
|
|
| 17 |
|
| 18 |
+
CFG = Config()
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
def check_duplicate_operation(operation: str, filename: str) -> bool:
|
|
|
|
| 28 |
Returns:
|
| 29 |
bool: True if the operation has already been performed on the file
|
| 30 |
"""
|
| 31 |
+
log_content = read_file(CFG.file_logger_path)
|
| 32 |
log_entry = f"{operation}: {filename}\n"
|
| 33 |
return log_entry in log_content
|
| 34 |
|
|
|
|
| 41 |
filename (str): The name of the file the operation was performed on
|
| 42 |
"""
|
| 43 |
log_entry = f"{operation}: {filename}\n"
|
| 44 |
+
append_to_file(CFG.file_logger_path, log_entry, should_log=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
def split_file(
|
|
|
|
| 76 |
start += max_length - overlap
|
| 77 |
|
| 78 |
|
| 79 |
+
@command("read_file", "Read file", '"filename": "<filename>"')
|
| 80 |
def read_file(filename: str) -> str:
|
| 81 |
"""Read a file and return the contents
|
| 82 |
|
|
|
|
| 87 |
str: The contents of the file
|
| 88 |
"""
|
| 89 |
try:
|
| 90 |
+
with open(filename, "r", encoding="utf-8") as f:
|
|
|
|
| 91 |
content = f.read()
|
| 92 |
return content
|
| 93 |
except Exception as e:
|
|
|
|
| 107 |
:param overlap: The number of overlapping characters between chunks, default is 200
|
| 108 |
"""
|
| 109 |
try:
|
| 110 |
+
logger.info(f"Working with file {filename}")
|
| 111 |
content = read_file(filename)
|
| 112 |
content_length = len(content)
|
| 113 |
+
logger.info(f"File length: {content_length} characters")
|
| 114 |
|
| 115 |
chunks = list(split_file(content, max_length=max_length, overlap=overlap))
|
| 116 |
|
| 117 |
num_chunks = len(chunks)
|
| 118 |
for i, chunk in enumerate(chunks):
|
| 119 |
+
logger.info(f"Ingesting chunk {i + 1} / {num_chunks} into memory")
|
| 120 |
memory_to_add = (
|
| 121 |
f"Filename: {filename}\n" f"Content part#{i + 1}/{num_chunks}: {chunk}"
|
| 122 |
)
|
| 123 |
|
| 124 |
memory.add(memory_to_add)
|
| 125 |
|
| 126 |
+
logger.info(f"Done ingesting {num_chunks} chunks from {filename}.")
|
| 127 |
except Exception as e:
|
| 128 |
+
logger.info(f"Error while ingesting file '{filename}': {str(e)}")
|
| 129 |
|
| 130 |
|
| 131 |
+
@command("write_to_file", "Write to file", '"filename": "<filename>", "text": "<text>"')
|
| 132 |
def write_to_file(filename: str, text: str) -> str:
|
| 133 |
"""Write text to a file
|
| 134 |
|
|
|
|
| 142 |
if check_duplicate_operation("write", filename):
|
| 143 |
return "Error: File has already been updated."
|
| 144 |
try:
|
| 145 |
+
directory = os.path.dirname(filename)
|
| 146 |
+
os.makedirs(directory, exist_ok=True)
|
| 147 |
+
with open(filename, "w", encoding="utf-8") as f:
|
|
|
|
|
|
|
| 148 |
f.write(text)
|
| 149 |
log_operation("write", filename)
|
| 150 |
return "File written to successfully."
|
|
|
|
| 152 |
return f"Error: {str(e)}"
|
| 153 |
|
| 154 |
|
| 155 |
+
@command(
|
| 156 |
+
"append_to_file", "Append to file", '"filename": "<filename>", "text": "<text>"'
|
| 157 |
+
)
|
| 158 |
+
def append_to_file(filename: str, text: str, should_log: bool = True) -> str:
|
| 159 |
"""Append text to a file
|
| 160 |
|
| 161 |
Args:
|
| 162 |
filename (str): The name of the file to append to
|
| 163 |
text (str): The text to append to the file
|
| 164 |
+
should_log (bool): Should log output
|
| 165 |
|
| 166 |
Returns:
|
| 167 |
str: A message indicating success or failure
|
| 168 |
"""
|
| 169 |
try:
|
| 170 |
+
directory = os.path.dirname(filename)
|
| 171 |
+
os.makedirs(directory, exist_ok=True)
|
| 172 |
+
with open(filename, "a") as f:
|
| 173 |
f.write(text)
|
| 174 |
|
| 175 |
+
if should_log:
|
| 176 |
log_operation("append", filename)
|
| 177 |
|
| 178 |
return "Text appended successfully."
|
|
|
|
| 180 |
return f"Error: {str(e)}"
|
| 181 |
|
| 182 |
|
| 183 |
+
@command("delete_file", "Delete file", '"filename": "<filename>"')
|
| 184 |
def delete_file(filename: str) -> str:
|
| 185 |
"""Delete a file
|
| 186 |
|
|
|
|
| 193 |
if check_duplicate_operation("delete", filename):
|
| 194 |
return "Error: File has already been deleted."
|
| 195 |
try:
|
| 196 |
+
os.remove(filename)
|
|
|
|
| 197 |
log_operation("delete", filename)
|
| 198 |
return "File deleted successfully."
|
| 199 |
except Exception as e:
|
| 200 |
return f"Error: {str(e)}"
|
| 201 |
|
| 202 |
|
| 203 |
+
@command("search_files", "Search Files", '"directory": "<directory>"')
|
| 204 |
def search_files(directory: str) -> list[str]:
|
| 205 |
"""Search for files in a directory
|
| 206 |
|
|
|
|
| 212 |
"""
|
| 213 |
found_files = []
|
| 214 |
|
| 215 |
+
for root, _, files in os.walk(directory):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
for file in files:
|
| 217 |
if file.startswith("."):
|
| 218 |
continue
|
| 219 |
+
relative_path = os.path.relpath(
|
| 220 |
+
os.path.join(root, file), CFG.workspace_path
|
| 221 |
+
)
|
| 222 |
found_files.append(relative_path)
|
| 223 |
|
| 224 |
return found_files
|
| 225 |
|
| 226 |
|
| 227 |
+
@command(
|
| 228 |
+
"download_file",
|
| 229 |
+
"Download File",
|
| 230 |
+
'"url": "<url>", "filename": "<filename>"',
|
| 231 |
+
CFG.allow_downloads,
|
| 232 |
+
"Error: You do not have user authorization to download files locally.",
|
| 233 |
+
)
|
| 234 |
def download_file(url, filename):
|
| 235 |
"""Downloads a file
|
| 236 |
Args:
|
| 237 |
url (str): URL of the file to download
|
| 238 |
filename (str): Filename to save the file as
|
| 239 |
"""
|
|
|
|
| 240 |
try:
|
| 241 |
+
directory = os.path.dirname(filename)
|
| 242 |
+
os.makedirs(directory, exist_ok=True)
|
| 243 |
message = f"{Fore.YELLOW}Downloading file from {Back.LIGHTBLUE_EX}{url}{Back.RESET}{Fore.RESET}"
|
| 244 |
with Spinner(message) as spinner:
|
| 245 |
session = requests.Session()
|
|
|
|
| 256 |
total_size = int(r.headers.get("Content-Length", 0))
|
| 257 |
downloaded_size = 0
|
| 258 |
|
| 259 |
+
with open(filename, "wb") as f:
|
| 260 |
for chunk in r.iter_content(chunk_size=8192):
|
| 261 |
f.write(chunk)
|
| 262 |
downloaded_size += len(chunk)
|
|
|
|
| 265 |
progress = f"{readable_file_size(downloaded_size)} / {readable_file_size(total_size)}"
|
| 266 |
spinner.update_message(f"{message} {progress}")
|
| 267 |
|
| 268 |
+
return f'Successfully downloaded and locally stored file: "{filename}"! (Size: {readable_file_size(downloaded_size)})'
|
| 269 |
except requests.HTTPError as e:
|
| 270 |
return f"Got an HTTP Error whilst trying to download file: {e}"
|
| 271 |
except Exception as e:
|
plugins/Auto-GPT-Plugins.zip
ADDED
|
Binary file (255 kB). View file
|
|
|
plugins/__PUT_PLUGIN_ZIPS_HERE__
ADDED
|
File without changes
|
scripts/check_requirements.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import sys
|
| 2 |
|
| 3 |
import pkg_resources
|
|
@@ -16,7 +17,7 @@ def main():
|
|
| 16 |
for package in required_packages:
|
| 17 |
if not package: # Skip empty lines
|
| 18 |
continue
|
| 19 |
-
package_name =
|
| 20 |
if package_name.lower() not in installed_packages:
|
| 21 |
missing_packages.append(package_name)
|
| 22 |
|
|
|
|
| 1 |
+
import re
|
| 2 |
import sys
|
| 3 |
|
| 4 |
import pkg_resources
|
|
|
|
| 17 |
for package in required_packages:
|
| 18 |
if not package: # Skip empty lines
|
| 19 |
continue
|
| 20 |
+
package_name = re.split("[<>=@ ]+", package.strip())[0]
|
| 21 |
if package_name.lower() not in installed_packages:
|
| 22 |
missing_packages.append(package_name)
|
| 23 |
|
scripts/install_plugin_deps.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import subprocess
|
| 3 |
+
import sys
|
| 4 |
+
import zipfile
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def install_plugin_dependencies():
|
| 9 |
+
"""
|
| 10 |
+
Installs dependencies for all plugins in the plugins dir.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
None
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
None
|
| 17 |
+
"""
|
| 18 |
+
plugins_dir = Path(os.getenv("PLUGINS_DIR", "plugins"))
|
| 19 |
+
for plugin in plugins_dir.glob("*.zip"):
|
| 20 |
+
with zipfile.ZipFile(str(plugin), "r") as zfile:
|
| 21 |
+
try:
|
| 22 |
+
basedir = zfile.namelist()[0]
|
| 23 |
+
basereqs = os.path.join(basedir, "requirements.txt")
|
| 24 |
+
extracted = zfile.extract(basereqs, path=plugins_dir)
|
| 25 |
+
subprocess.check_call(
|
| 26 |
+
[sys.executable, "-m", "pip", "install", "-r", extracted]
|
| 27 |
+
)
|
| 28 |
+
os.remove(extracted)
|
| 29 |
+
os.rmdir(os.path.join(plugins_dir, basedir))
|
| 30 |
+
except KeyError:
|
| 31 |
+
continue
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
install_plugin_dependencies()
|