aboutsummaryrefslogtreecommitdiff
path: root/autogpts/autogpt/autogpt/core/workspace/simple.py
diff options
context:
space:
mode:
Diffstat (limited to 'autogpts/autogpt/autogpt/core/workspace/simple.py')
-rw-r--r--autogpts/autogpt/autogpt/core/workspace/simple.py194
1 files changed, 194 insertions, 0 deletions
diff --git a/autogpts/autogpt/autogpt/core/workspace/simple.py b/autogpts/autogpt/autogpt/core/workspace/simple.py
new file mode 100644
index 000000000..1c7a3f903
--- /dev/null
+++ b/autogpts/autogpt/autogpt/core/workspace/simple.py
@@ -0,0 +1,194 @@
+import json
+import logging
+import typing
+from pathlib import Path
+
+from pydantic import SecretField
+
+from autogpt.core.configuration import (
+ Configurable,
+ SystemConfiguration,
+ SystemSettings,
+ UserConfigurable,
+)
+from autogpt.core.workspace.base import Workspace
+
+if typing.TYPE_CHECKING:
+ # Cyclic import
+ from autogpt.core.agent.simple import AgentSettings
+
+
+class WorkspaceConfiguration(SystemConfiguration):
+ root: str
+ parent: str = UserConfigurable()
+ restrict_to_workspace: bool = UserConfigurable()
+
+
+class WorkspaceSettings(SystemSettings):
+ configuration: WorkspaceConfiguration
+
+
+class SimpleWorkspace(Configurable, Workspace):
+ default_settings = WorkspaceSettings(
+ name="workspace",
+ description="The workspace is the root directory for all agent activity.",
+ configuration=WorkspaceConfiguration(
+ root="",
+ parent="~/auto-gpt/agents",
+ restrict_to_workspace=True,
+ ),
+ )
+
+ NULL_BYTES = ["\0", "\000", "\x00", "\u0000", "%00"]
+
+ def __init__(
+ self,
+ settings: WorkspaceSettings,
+ logger: logging.Logger,
+ ):
+ self._configuration = settings.configuration
+ self._logger = logger.getChild("workspace")
+
+ @property
+ def root(self) -> Path:
+ return Path(self._configuration.root)
+
+ @property
+ def debug_log_path(self) -> Path:
+ return self.root / "logs" / "debug.log"
+
+ @property
+ def cycle_log_path(self) -> Path:
+ return self.root / "logs" / "cycle.log"
+
+ @property
+ def configuration_path(self) -> Path:
+ return self.root / "configuration.yml"
+
+ @property
+ def restrict_to_workspace(self) -> bool:
+ return self._configuration.restrict_to_workspace
+
+ def get_path(self, relative_path: str | Path) -> Path:
+ """Get the full path for an item in the workspace.
+
+ Parameters
+ ----------
+ relative_path
+ The relative path to resolve in the workspace.
+
+ Returns
+ -------
+ Path
+ The resolved path relative to the workspace.
+
+ """
+ return self._sanitize_path(
+ relative_path,
+ root=self.root,
+ restrict_to_root=self.restrict_to_workspace,
+ )
+
+ def _sanitize_path(
+ self,
+ relative_path: str | Path,
+ root: str | Path = None,
+ restrict_to_root: bool = True,
+ ) -> Path:
+ """Resolve the relative path within the given root if possible.
+
+ Parameters
+ ----------
+ relative_path
+ The relative path to resolve.
+ root
+ The root path to resolve the relative path within.
+ restrict_to_root
+ Whether to restrict the path to the root.
+
+ Returns
+ -------
+ Path
+ The resolved path.
+
+ Raises
+ ------
+ ValueError
+ If the path is absolute and a root is provided.
+ ValueError
+ If the path is outside the root and the root is restricted.
+
+ """
+
+ # Posix systems disallow null bytes in paths. Windows is agnostic about it.
+ # Do an explicit check here for all sorts of null byte representations.
+
+ for null_byte in self.NULL_BYTES:
+ if null_byte in str(relative_path) or null_byte in str(root):
+ raise ValueError("embedded null byte")
+
+ if root is None:
+ return Path(relative_path).resolve()
+
+ self._logger.debug(f"Resolving path '{relative_path}' in workspace '{root}'")
+ root, relative_path = Path(root).resolve(), Path(relative_path)
+ self._logger.debug(f"Resolved root as '{root}'")
+
+ if relative_path.is_absolute():
+ raise ValueError(
+ f"Attempted to access absolute path '{relative_path}' "
+ f"in workspace '{root}'."
+ )
+ full_path = root.joinpath(relative_path).resolve()
+
+ self._logger.debug(f"Joined paths as '{full_path}'")
+
+ if restrict_to_root and not full_path.is_relative_to(root):
+ raise ValueError(
+ f"Attempted to access path '{full_path}' outside of workspace '{root}'."
+ )
+
+ return full_path
+
+ ###################################
+ # Factory methods for agent setup #
+ ###################################
+
+ @staticmethod
+ def setup_workspace(settings: "AgentSettings", logger: logging.Logger) -> Path:
+ workspace_parent = settings.workspace.configuration.parent
+ workspace_parent = Path(workspace_parent).expanduser().resolve()
+ workspace_parent.mkdir(parents=True, exist_ok=True)
+
+ agent_name = settings.agent.name
+
+ workspace_root = workspace_parent / agent_name
+ workspace_root.mkdir(parents=True, exist_ok=True)
+
+ settings.workspace.configuration.root = str(workspace_root)
+
+ with (workspace_root / "agent_settings.json").open("w") as f:
+ settings_json = settings.json(
+ encoder=lambda x: x.get_secret_value()
+ if isinstance(x, SecretField)
+ else x,
+ )
+ f.write(settings_json)
+
+ # TODO: What are all the kinds of logs we want here?
+ log_path = workspace_root / "logs"
+ log_path.mkdir(parents=True, exist_ok=True)
+ (log_path / "debug.log").touch()
+ (log_path / "cycle.log").touch()
+
+ return workspace_root
+
+ @staticmethod
+ def load_agent_settings(workspace_root: Path) -> "AgentSettings":
+ # Cyclic import
+ from autogpt.core.agent.simple import AgentSettings
+
+ with (workspace_root / "agent_settings.json").open("r") as f:
+ agent_settings = json.load(f)
+
+ return AgentSettings.parse_obj(agent_settings)