| """ |
| App Controller Agent β main_agent.py |
| ===================================== |
| Opens, closes, focuses, and lists applications via natural language. |
| |
| Usage: |
| run_agent("open VS Code") |
| run_agent("close Postman") |
| run_agent("switch to Chrome") |
| run_agent("what apps are open?") |
| run_agent("open VS Code in ~/Documents/AI OS") |
| run_agent("open Chrome at notion.so") |
| """ |
|
|
| import os |
| import json |
| from groq import Groq |
| from dotenv import load_dotenv |
|
|
| load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".env")) |
|
|
| from app_launcher import open_app, close_app, focus_app, list_open_apps, REGISTRY, get_vscode_recent_folders |
|
|
| |
|
|
| AGENT_META = { |
| "name": "app_controller", |
| "description": "Opens, closes, and switches focus between applications on the desktop.", |
| "capabilities": [ |
| "Open any installed application by name", |
| "Open VS Code or terminal in a specific folder", |
| "Open Chrome or Firefox at a specific URL", |
| "Close any running application", |
| "Switch focus to any open application", |
| "List all currently open applications", |
| ], |
| "call_when": [ |
| "User says open, launch, start followed by an app name", |
| "User says 'open vscode', 'open chrome', 'open terminal'", |
| "User says close, quit, kill followed by an app name", |
| "User says switch to, focus, bring up followed by an app name", |
| "User asks what apps are open, what's running", |
| "User says open VS Code in a folder", |
| "User says open Chrome at a URL", |
| ], |
| "does_not_own": [ |
| "Installing applications", |
| "System settings or configuration", |
| "File operations beyond opening in an app", |
| "Creating files or writing code inside VS Code" |
| ], |
| "chains_with": [], |
| "entry": "run_agent", |
| } |
|
|
| |
|
|
| PARSER_PROMPT = """You are a command parser for a Linux desktop app controller. |
| |
| Parse the user's message into a structured command. |
| |
| AVAILABLE ACTIONS: |
| - open β launch an application (optionally with path or url) |
| - close β close/quit an application |
| - focus β switch focus to an already-open application |
| - list β list all currently open applications |
| |
| KNOWN APPS (use exact key names): |
| {app_list} |
| |
| RULES: |
| - "open", "launch", "start", "run" β action: open |
| - "close", "quit", "kill", "exit", "shut down" β action: close |
| - "switch to", "focus", "bring up", "go to", "show" β action: focus |
| - "what's open", "what apps", "what's running", "list apps" β action: list |
| - If user says "open VS Code in ~/Documents/AI OS" β action: open, app: vscode, path: ~/Documents/AI OS |
| - If user says "open Chrome at notion.so" β action: open, app: chrome, url: https://notion.so |
| - If app is not in known list, use the raw name as-is in app field |
| - For list action, app field should be empty string |
| |
| Respond ONLY with valid JSON, no markdown: |
| { |
| "action": "open|close|focus|list", |
| "app": "<app_key_or_raw_name>", |
| "path": "<folder_path_or_empty>", |
| "url": "<url_or_empty>", |
| "confidence": 0.0-1.0, |
| "reasoning": "<one line>" |
| }""" |
|
|
|
|
| def _parse_command(query: str) -> dict: |
| """Parse directly β no LLM needed for simple app commands.""" |
| import re |
| q = query.lower().strip() |
|
|
| |
| if any(w in q for w in ["close", "quit", "kill", "exit", "shut"]): |
| action = "close" |
| elif any(w in q for w in ["switch to", "focus", "bring up", "show", "go to"]): |
| action = "focus" |
| elif any(w in q for w in ["list", "what's open", "what apps", "running", "what is open"]): |
| action = "list" |
| else: |
| action = "open" |
|
|
| |
| path = "" |
| path_match = re.search(r"\b(?:in|at)\s+([~/][\S]*)", query) |
| if path_match: |
| path = path_match.group(1) |
|
|
| |
| app = q |
| for phrase in ["open", "launch", "start", "close", "quit", "kill", |
| "switch to", "focus", "bring up", "please", "can you", "hey"]: |
| app = app.replace(phrase, " ") |
| if path: |
| app = app.replace(f"in {path.lower()}", "").replace(f"at {path.lower()}", "") |
| app = " ".join(app.split()) |
|
|
| |
| from app_launcher import resolve_app |
| key, _ = resolve_app(app) |
| if key: |
| app = key |
|
|
| return { |
| "action": action, |
| "app": app.strip(), |
| "path": path, |
| "url": "", |
| "confidence": 0.95, |
| "reasoning": "direct parse", |
| } |
|
|
| NEW_WINDOW_PHRASES = ["new window", "another window", "second window", "open again", "reopen"] |
|
|
| def _fallback_parse(query: str) -> dict: |
| q = query.lower() |
| action = "open" |
| if any(w in q for w in ["close", "quit", "kill", "exit", "shut"]): |
| action = "close" |
| elif any(w in q for w in ["switch", "focus", "bring", "show", "go to"]): |
| action = "focus" |
| elif any(w in q for w in ["list", "what's open", "what apps", "running"]): |
| action = "list" |
|
|
| wants_new = any(p in q for p in NEW_WINDOW_PHRASES) |
|
|
| import re as _re |
| path = "" |
| path_match = _re.search(r"\bin\s+([\S]+)", query) |
| if path_match: |
| path = path_match.group(1) |
| app = q |
| for word in ["open", "close", "launch", "start", "quit", "kill", |
| "switch to", "focus", "bring up", "please", "can you", |
| "hey", "in " + path]: |
| app = app.replace(word, "").replace(word.lower(), "").strip() |
|
|
| return {"action": action, "app": app.strip(), "path": "", "url": "", |
| "force_new": wants_new, |
| "confidence": 0.6, "reasoning": "fallback"} |
|
|
|
|
| |
|
|
| def _format_response(action: str, result: dict) -> str: |
| """Format the launcher result into a clean chat response.""" |
| if action == "list": |
| apps = result.get("apps", []) |
| if not apps: |
| return "No applications are currently open." |
| known = [a["name"] for a in apps if a["known"]] |
| unknown = [a["title"] for a in apps if not a["known"]] |
| lines = [] |
| if known: |
| lines.append("**Open apps:** " + ", ".join(known)) |
| if unknown: |
| lines.append("**Other windows:** " + ", ".join(unknown[:5])) |
| return "\n".join(lines) |
| if result.get("action") == "already_open": |
| app_key = result.get("app", "") |
| return f"__CONFIRM_NEW_WINDOW__{app_key}" |
|
|
| return result.get("message", "Done.") |
|
|
| |
|
|
| def run_agent(query: str = "", **kwargs) -> str: |
| """ |
| Parse natural language command and execute it. |
| |
| Args: |
| query: Natural language command e.g. "open VS Code", "close Postman" |
| |
| Returns: |
| Human-readable result string. |
| """ |
| force_new = query.startswith("__FORCE_NEW__") or kwargs.get("force_new", False) |
| pending_path = kwargs.get("pending_path", "") |
| create_folder = kwargs.get("create_folder", False) |
|
|
| if pending_path: |
| if not pending_path.startswith("~") and not pending_path.startswith("/"): |
| pending_path = "~/" + pending_path.lstrip("/") |
| |
| expanded = os.path.expanduser(pending_path) |
| print(f"[AppController] pending_path={pending_path} expanded={expanded}") |
| print(f"[AppController] exists={os.path.exists(expanded)} isdir={os.path.isdir(expanded)}") |
| |
| try: |
| os.makedirs(expanded, exist_ok=True) |
| print(f"[AppController] makedirs done, isdir now={os.path.isdir(expanded)}") |
| except Exception as e: |
| print(f"[AppController] makedirs FAILED: {e}") |
| |
| result = open_app("vscode", path=expanded) |
| return _format_response("open", result) |
| if query.startswith("__FORCE_NEW__"): |
| query = query.replace("__FORCE_NEW__", "", 1) |
|
|
| print(f"\n[AppController] Query: {query}, force_new: {force_new}") |
| if not query.strip(): |
| return "What would you like me to open, close, or switch to?" |
|
|
| command = _parse_command(query) |
| action = command.get("action", "open") |
| app = command.get("app", "") |
| path = command.get("path", "") |
| url = command.get("url", "") |
|
|
| print(f"[AppController] β {action} '{app}'" |
| + (f" path={path}" if path else "") |
| + (f" url={url}" if url else "")) |
|
|
| if action == "open": |
| VSCODE_KEYS = ["vscode", "vs code", "visual studio code", "code"] |
| is_vscode = any(k in app.lower() for k in VSCODE_KEYS) |
|
|
| |
| if is_vscode and not path: |
| recent = [] |
| try: |
| from app_launcher import get_vscode_recent_folders |
| recent = get_vscode_recent_folders() |
| except Exception: |
| pass |
| import json |
| return f"__ASK_VSCODE_PATH__{json.dumps(recent)}" |
|
|
| |
| if is_vscode and path: |
| |
| if path.startswith("/Documents") or path.startswith("/Downloads") or path.startswith("/Desktop"): |
| path = os.path.expanduser("~") + path |
| elif not path.startswith("~") and not path.startswith("/"): |
| path = "~/" + path.lstrip("/") |
| |
| expanded = os.path.expanduser(path) |
| |
| try: |
| os.makedirs(expanded, exist_ok=True) |
| print(f"[AppController] Ensured folder: {expanded}") |
| except PermissionError: |
| return f"Cannot create folder at {path} β permission denied. Try ~/Documents/test1234 instead." |
| except Exception as e: |
| return f"Could not create folder: {e}" |
| |
| path = expanded |
|
|
| result = open_app(app, path=path, url=url, force_new=force_new) |
|
|
| if result.get("action") == "already_open": |
| return f"__CONFIRM_NEW_WINDOW__{result.get('app', app)}" |
|
|
| elif action == "close": |
| result = close_app(app) |
| elif action == "focus": |
| result = focus_app(app) |
| elif action == "list": |
| result = list_open_apps() |
| else: |
| result = {"success": False, "message": f"Unknown action: {action}"} |
|
|
| return _format_response(action, result) |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| import sys |
| query = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "what apps are open" |
| print(run_agent(query)) |