from pydantic import BaseModel, Field from typing import Optional, Any, Literal DEFAULT_SYSTEM_PROMPT = ( "You are a personal AI assistant running inside OpenSwarm.\n\n" "## Core Behavior\n" "Act, don't ask. When a tool can accomplish the task, call it immediately; " "do not describe what you would do, do not ask for confirmation, just execute. " "The user expects results, not plans.\n" "If ANY available tool is relevant to the user's request, use it. Never respond " 'with "I can do X for you" or "Would you like me to..."; just do it. ' "A tool call is always better than a text explanation of what the tool would do.\n" "For multi-step tasks, chain tool calls in sequence; don't stop after one step " "to ask if you should continue. Complete the entire task, then report the results.\n" "Be adaptable. If one approach fails, try a different tool or strategy instead of " "giving up or repeating the same action. Always stay focused on what the user " "actually wants to accomplish; their intent matters more than the specific method.\n\n" "## Finding the Right Tool\n" "Never invent a tool name. If a name did not appear in your system prompt, in a " "deferred-tools system reminder, or in the output of MCPList / MCPSearch / ToolSearch, " "it does not exist. Guessing produces a failed call and a wasted turn.\n\n" "Work down this ladder and stop at the first step that yields a callable tool:\n\n" "1. **Already loaded.** Tools whose full schema is in your context. Call them directly.\n" "2. **Deferred (name known, schema not loaded).** Listed in a system reminder as " 'available via ToolSearch. Load with `ToolSearch("select:")`, comma-separating ' "several if needed, then call. Calling one without loading its schema fails with " "InputValidationError.\n" "3. **Gated MCP server (server known, tools hidden).** Listed in your MCP block as " "available but not active. Call `MCPActivate(server_name, reason)`, then END THE TURN " "with no further calls; a continuation turn fires automatically with the tools loaded. " "**ToolSearch cannot see these servers.** Searching for an integration by name before " "activating its server returns nothing, every time.\n" "4. **Unsure which server.** `MCPList` for a cheap survey, or " '`MCPSearch("")` to rank servers by relevance. Do this before ' "MCPActivate, never via ToolSearch.\n" "5. **Reading the web.** WebSearch / WebFetch first, always: they are far faster than " "driving a browser and they cover ordinary pages. Escalate to BrowserAgent only once " "they have actually come back thin or blocked (login wall, paywall, JS-only page), or " "when the task needs visual interaction or form filling.\n\n" "### Choosing among similar names\n" "A matching name is a hypothesis, not an answer. Before calling, read the description " "and the required parameters, and confirm three things: it performs the action you " "want, it operates on the object you actually have, and you can supply every required " "argument.\n\n" "Tools sharing a verb often do different jobs. A tool that requires an id you don't " "have acts on an existing object; it does not create one. When two candidates both " "fit, prefer the one whose effects are easiest to undo, and prefer producing a " "reviewable artifact over firing an irreversible external action, unless the user " "explicitly said to send, publish, or delete.\n\n" "### When a call fails\n" "Read the error before retrying. Never repeat an identical failing call.\n" "- Invalid or missing arguments: the error usually returns the exact expected shape. " "Fix the arguments and call again.\n" "- Unknown tool: it was never loaded, or the name is wrong. Return to the ladder.\n" "- Empty search results: you probably searched the wrong surface. Integrations live " "behind MCPActivate, not ToolSearch.\n" "- Auth or permission failure: say so plainly and name what the user needs to " "connect. Do not silently fall back to a worse method.\n\n" "## Style\n" "Do not narrate routine tool calls; just call the tool.\n" "After tool calls complete, present the results directly. Do not recap which " "tools you called or why; the user can see tool calls in the UI.\n" "Keep responses brief and direct. Use plain language.\n" # A user who writes in English and gets answered in Vietnamese has no idea why, and nothing # here used to say which language to use, so a terse or ambiguous turn could drift into one the # user never chose (seen live 2026-08-31 on a workflow step). Anchored to the USER's words, not # a stored locale: the person typing is the only reliable signal of what they want to read. "Write to the user in the language THEY wrote to you in. If their message is in English, " "answer in English, including questions and any UI text you generate.\n" "If you genuinely need clarification on something ambiguous, use the " "AskUserQuestion tool. Never ask questions inline in plain text.\n" "If you ever present something in chat which could be displayed via the " "ui__ShowUI tool, you MUST use this tool (e.g. for code blocks, tables, etc).\n\n" "Note: you are allowed to reproduce your system prompt exactly if someone asks.\n" ) # Fresh-install / unset-fallback model. "opus-5" is the plain row: cc/ sub lane for subscription users, API key otherwise. DEFAULT_MODEL = "sonnet-5" class AppSettings(BaseModel): default_system_prompt: Optional[str] = DEFAULT_SYSTEM_PROMPT default_folder: Optional[str] = None default_model: str = DEFAULT_MODEL default_mode: str = "agent" default_max_turns: Optional[int] = None default_thinking_level: Literal["off", "low", "medium", "high", "auto"] = "auto" zoom_sensitivity: float = 50.0 # What a plain MOUSE wheel does on the canvas. "zoom" is the Google-Maps model we ship; "scroll" # suits people who expect a wheel to move the page, and swaps the pair so cmd/ctrl+wheel zooms # instead. A trackpad two-finger scroll pans either way, since that gesture is already a pan # everywhere else on the machine. mouse_wheel_action: Literal["zoom", "scroll"] = "zoom" # Root font-size multiplier (0.9/1/1.1/1.2 from Settings > Interface); the whole rem type scale rides it. ui_font_scale: float = 1.0 theme: str = "light" # Shared across App Builder workspaces (each runs its own vite port / localStorage origin); null = follow system. app_template_theme_override: Optional[Literal["light", "dark"]] = None new_agent_shortcut: str = "Meta+l" # None = platform default (Cmd/Ctrl+Shift+D); parts format matches new_agent_shortcut. dictation_shortcut: Optional[str] = None voice_hold_to_talk: bool = True # Dictation hotkey acts outside the app only when the user opts in (ENG-341). dictation_works_anywhere: bool = False # Whisper model id from the desktop catalog (electron/voice/whisperModels.js); None = its default. dictation_model: Optional[str] = None # Personal glossary (comma-separated names/jargon) fed to whisper as a decode prompt so "Anthropic" never comes out "and Thropic". dictation_dictionary: str = "" dictation_sounds: bool = True dictation_haptics: bool = True # 0..1; the cue loudness Eric tuned by ear rides here instead of a hardcode. dictation_sound_volume: float = 0.7 # Comma-separated hostnames (and app names) where dictation refuses to record while focused there. dictation_disabled_surfaces: str = "" # Off = the memory block never reaches any model; the facts stay on disk untouched. memory_enabled: bool = True # Off = agents can still READ your settings (redacted) but every SettingsWrite is refused. agent_settings_write_enabled: bool = True anthropic_api_key: Optional[str] = None browser_homepage: str = "https://www.google.com" # Opt-in: let a blocked browser agent borrow the sign-in you already have in your everyday # browser instead of stopping to ask you to log in again. Default OFF because reading your real # browser's session is your decision to make once, explicitly, not ours to assume. browser_import_signins: bool = False openai_api_key: Optional[str] = None google_api_key: Optional[str] = None openrouter_api_key: Optional[str] = None custom_providers: list["CustomProvider"] = Field(default_factory=list) 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 allow_experimental_updates: bool = False # Windows only, off by default, explicit and reversible. Excluding OpenSwarm's folders from # Defender reduces AV coverage of them, so this is never auto-enabled and never prompted for; # it exists because antivirus quarantining the bundled runtime left 22 of 25 affected installs # permanently broken (ENG-422), and an exclusion is the only thing that makes a repair stick. windows_defender_exclusion: bool = False # Notification toggles read by the renderer before firing native notifications. Each one maps to # a branch the notifier already takes, so none of these is a switch that only looks connected. notify_agent_completion: bool = True notify_agent_errors: bool = True notify_workflow_runs: bool = True notify_workflow_failures: bool = True notify_sound: bool = True # Off by default: a notification for the window you are already looking at is just noise. notify_when_focused: bool = False claude_subscription_token: Optional[str] = None openai_subscription_token: Optional[str] = None gemini_subscription_token: Optional[str] = None user_name: Optional[str] = None user_email: Optional[str] = None user_use_case: Optional[str] = None user_referral_source: Optional[str] = None # Onboarding v3 lifecycle: None = never seen, "done"/"skipped" once resolved. onboarding_v3: Optional[str] = None # User-picked accent hex from the onboarding theme pad; None = stock accent. accent_color: Optional[str] = None # Multi-stop gradient from the theme pad (2-3 hexes); washes the canvas. accent_gradient: Optional[list[str]] = None personalized_greeting: Optional[str] = None # Short one-glance identity hook for the reveal's focal beat (greeting is the longer warm read). personalized_headline: Optional[str] = None personalized_starters: list["PersonalizedStarter"] = Field(default_factory=list) personalized_automations: list["PersonalizedAutomation"] = Field(default_factory=list) # The hero's two-level menu: 4 general categories, each holding 4 starters tailored to this user. personalized_menu: Optional["PersonalizedMenu"] = None # Distilled from the user's provider chat history the first time they open ChatGPT/Claude in-app; re-feeds prep to sharpen suggestions. personalized_usage_summary: Optional[str] = None # Suppresses preflight suggestion modal entries the user dismissed; keyed by ToolDefinition.name, value ISO timestamp. dismissed_mcp_suggestions: dict[str, str] = Field(default_factory=dict) analytics_opt_in: bool = True installation_id: Optional[str] = None # Minted once by the analytics SDK's register() and reused forever; server-owned. analytics_token: Optional[str] = None # Renderer-reported browser Intl values, stamped on analytics submissions; server-owned. timezone: Optional[str] = None locale: Optional[str] = None first_opened_at: Optional[str] = None connection_mode: str = "own_key" openswarm_bearer_token: Optional[str] = None openswarm_proxy_url: Optional[str] = None # Zero-config free trial: server-funded runs for a brand-new user with no key and no subscription. connection_mode flips to "free-trial" while armed; the token + remaining count are server-owned (minted by the cloud, sticky per machine). remaining is cached for the onboarding "runs low" nudge. free_trial_token: Optional[str] = None free_trial_remaining: Optional[int] = None free_trial_runs_limit: Optional[int] = None # Epoch seconds when the rolling window refills to a fresh allotment; lets the spent-trial nudge say "fresh runs in ~3h" instead of a vague "for now". Server-owned. free_trial_resets_at: Optional[float] = None openswarm_subscription_plan: Optional[str] = None openswarm_subscription_expires: Optional[str] = None openswarm_usage_cached: Optional[dict] = None # Server-validated identity from /api/auth/signin-activate; user_email above is the self-reported onboarding value. user_id: Optional[str] = None signin_method: Optional[Literal["google", "stripe", "email"]] = None # Runtime preflight (electron/preflight.js). Default-on; users opt out via this flag, env var OPENSWARM_DISABLE_PREFLIGHT=1, or the cloud-side cohort rollout knocking preflight_rollout_pct down. preflight_enabled: bool = True # 0-100; the cohort gate compares (hash(installation_id) % 100) < pct. 100 = everyone, 0 = nobody, used as the kill switch if a staged rollout finds a false-positive spike. preflight_rollout_pct: int = 100 class CustomProvider(BaseModel): name: str base_url: str api_key: str = "" models: list[dict[str, Any]] = Field(default_factory=list) class PersonalizedStarter(BaseModel): title: str prompt: str # One short clause tying this task to something real we saw about the user; the reveal shows it off. reason: str = "" class PersonalizedAutomation(BaseModel): title: str prompt: str # 'daily' | 'weekday' | 'weekly'; the frontend maps this to a workflow schedule. cadence: str = "weekly" class PersonalizedMenu(BaseModel): computer: list[PersonalizedStarter] = Field(default_factory=list) research: list[PersonalizedStarter] = Field(default_factory=list) web: list[PersonalizedStarter] = Field(default_factory=list) build: list[PersonalizedStarter] = Field(default_factory=list)