diff --git a/backend/apps/settings/AppSettings.py b/backend/apps/settings/AppSettings.py new file mode 100644 index 00000000..382332e3 --- /dev/null +++ b/backend/apps/settings/AppSettings.py @@ -0,0 +1,22 @@ +from backend.apps.settings.DEFAULT_SYSTEM_PROMPT import DEFAULT_SYSTEM_PROMPT +from pydantic import BaseModel +from typing import Optional + +class AppSettings(BaseModel): + # Agent settings + default_system_prompt: str = DEFAULT_SYSTEM_PROMPT + default_folder: Optional[str] = None + default_model: str = "sonnet" + default_mode: str = "agent" + default_max_turns: Optional[int] = None + anthropic_api_key: Optional[str] = None + + # Dashboard / UI preferences + zoom_sensitivity: float = 50.0 + theme: str = "dark" + new_agent_shortcut: str = "Meta+l" + browser_homepage: str = "https://www.google.com" + auto_select_mode_on_new_agent: bool = False + expand_new_chats_in_dashboard: bool = True + auto_reveal_sub_agents: bool = True + dev_mode: bool = False \ No newline at end of file diff --git a/backend/apps/settings/DEFAULT_SYSTEM_PROMPT.py b/backend/apps/settings/DEFAULT_SYSTEM_PROMPT.py new file mode 100644 index 00000000..288eaa01 --- /dev/null +++ b/backend/apps/settings/DEFAULT_SYSTEM_PROMPT.py @@ -0,0 +1,17 @@ +DEFAULT_SYSTEM_PROMPT: str = ( + "You are a personal AI assistant running inside OpenSwarm.\n\n" + "## Tool Priority\n" + "When a dedicated MCP tool exists for a task, use it directly — do not use the browser for things MCP tools can handle.\n" + "Priority order:\n" + "1. MCP tools first (Reddit, Google Workspace, Twitter, etc.) — fastest and most reliable\n" + "2. WebSearch / WebFetch — for general web lookups without a dedicated MCP\n" + "3. BrowserAgent — only when you need to visually interact with a website, fill forms, or do something no other tool can handle\n\n" + "## Tool Call Style\n" + "Default: do not narrate routine tool calls — just call the tool.\n" + "Narrate only when it helps: multi-step work, complex problems, or when the user explicitly asks.\n" + "Keep narration brief. Use plain language.\n\n" + "## Interaction Style\n" + "Be direct and action-oriented. Do not ask clarifying questions unless genuinely ambiguous — " + "make reasonable assumptions and act. If you need to ask, use the AskUserQuestion tool.\n" + "Do not over-explain what you are about to do. Just do it and show the results.\n" +) diff --git a/backend/apps/settings/settings.py b/backend/apps/settings/settings.py new file mode 100644 index 00000000..444b5109 --- /dev/null +++ b/backend/apps/settings/settings.py @@ -0,0 +1,62 @@ +import json +import os +from contextlib import asynccontextmanager + +from backend.config.Apps import SubApp +from backend.apps.settings.AppSettings import AppSettings +from backend.apps.settings.DEFAULT_SYSTEM_PROMPT import DEFAULT_SYSTEM_PROMPT +from backend.config.paths import DB_ROOT + +SETTINGS_DIR = os.path.join(DB_ROOT, "settings") +SETTINGS_FILE = os.path.join(SETTINGS_DIR, "settings.json") + +@asynccontextmanager +async def settings_lifespan(): + os.makedirs(SETTINGS_DIR, exist_ok=True) + yield + + +settings = SubApp("settings", settings_lifespan) + + +def load_settings() -> AppSettings: + """Load settings from JSON file, returning defaults if not found.""" + if os.path.exists(SETTINGS_FILE): + with open(SETTINGS_FILE) as f: + settings = AppSettings(**json.load(f)) + if settings.default_system_prompt is None: + settings.default_system_prompt = DEFAULT_SYSTEM_PROMPT + return settings + return AppSettings() + + +@settings.router.get("") +async def get_settings(): + return load_settings().model_dump() + + +@settings.router.put("") +async def update_settings(body: AppSettings): + os.makedirs(SETTINGS_DIR, exist_ok=True) + with open(SETTINGS_FILE, "w") as f: + json.dump(body.model_dump(), f, indent=2) + return {"ok": True, "settings": body.model_dump()} + + +@settings.router.post("/reset-system-prompt") +async def reset_system_prompt(): + current = load_settings() + current.default_system_prompt = DEFAULT_SYSTEM_PROMPT + os.makedirs(SETTINGS_DIR, exist_ok=True) + with open(SETTINGS_FILE, "w") as f: + json.dump(current.model_dump(), f, indent=2) + return {"ok": True, "settings": current.model_dump()} + + +# TODO: I deleted the upload-files route bc we don't need to make temp files since we can +# just natively reference the files in a users computer via electron so temp dupes are redundant +# frontend and other rippling changes will need to be made to support this + +# TODO: I deleted the browse-directories route bc this is only used for the Working directory +# selection in settings. Hence, we should just use the native file system browser to select +# the working directory. Rippling changes will need to be made to support this \ No newline at end of file diff --git a/backend/config/paths.py b/backend/config/paths.py index 1d1ffc7f..0e5f7692 100644 --- a/backend/config/paths.py +++ b/backend/config/paths.py @@ -9,28 +9,32 @@ data is stored in a platform-appropriate location import os import sys -_BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +P_BACKEND_DIR: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" +p_is_packaged: bool = os.environ.get("OPENSWARM_PACKAGED") == "1" -if _is_packaged: +DB_ROOT: str = "bruh" + +if p_is_packaged: if sys.platform == "darwin": - _app_support = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "OpenSwarm") + p_app_support: str = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "OpenSwarm") elif sys.platform == "win32": - _app_support = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "OpenSwarm") + p_app_support: str = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "OpenSwarm") else: - _app_support = os.path.join(os.environ.get("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")), "OpenSwarm") - DATA_ROOT = os.path.join(_app_support, "data") + p_app_support: str = os.path.join(os.environ.get("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")), "OpenSwarm") + DB_ROOT: str = os.path.join(p_app_support, "data") else: - DATA_ROOT = os.path.join(_BACKEND_DIR, "data") + DB_ROOT: str = os.path.join(P_BACKEND_DIR, "data") -SESSIONS_DIR = os.path.join(DATA_ROOT, "sessions") -TOOLS_DIR = os.path.join(DATA_ROOT, "tools") -SETTINGS_DIR = os.path.join(DATA_ROOT, "settings") -MODES_DIR = os.path.join(DATA_ROOT, "modes") -DASHBOARDS_DIR = os.path.join(DATA_ROOT, "dashboards") -OUTPUTS_DIR = os.path.join(DATA_ROOT, "outputs") -OUTPUTS_WORKSPACE_DIR = os.path.join(DATA_ROOT, "outputs_workspace") -SKILLS_WORKSPACE_DIR = os.path.join(DATA_ROOT, "skills_workspace") -DASHBOARD_LAYOUT_DIR = os.path.join(DATA_ROOT, "dashboard_layout") -BUILTIN_PERMISSIONS_PATH = os.path.join(DATA_ROOT, "builtin_permissions.json") \ No newline at end of file +assert DB_ROOT != "bruh", "DB_ROOT is not set" + +# SESSIONS_DIR = os.path.join(DATA_ROOT, "sessions") +# TOOLS_DIR = os.path.join(DATA_ROOT, "tools") +# SETTINGS_DIR = os.path.join(DATA_ROOT, "settings") +# MODES_DIR = os.path.join(DATA_ROOT, "modes") +# DASHBOARDS_DIR = os.path.join(DATA_ROOT, "dashboards") +# OUTPUTS_DIR = os.path.join(DATA_ROOT, "outputs") +# OUTPUTS_WORKSPACE_DIR = os.path.join(DATA_ROOT, "outputs_workspace") +# SKILLS_WORKSPACE_DIR = os.path.join(DATA_ROOT, "skills_workspace") +# DASHBOARD_LAYOUT_DIR = os.path.join(DATA_ROOT, "dashboard_layout") +# BUILTIN_PERMISSIONS_PATH = os.path.join(DATA_ROOT, "builtin_permissions.json") \ No newline at end of file