mirror of
https://github.com/trycua/computer.git
synced 2026-01-08 14:30:25 -06:00
Merge branch 'main' into feature/computer/typescript
This commit is contained in:
86
COMPATIBILITY.md
Normal file
86
COMPATIBILITY.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# C/ua Compatibility Matrix
|
||||
|
||||
## Table of Contents
|
||||
- [Host OS Compatibility](#host-os-compatibility)
|
||||
- [macOS Host](#macos-host)
|
||||
- [Ubuntu/Linux Host](#ubuntulinux-host)
|
||||
- [Windows Host](#windows-host)
|
||||
- [VM Emulation Support](#vm-emulation-support)
|
||||
- [Model Provider Compatibility](#model-provider-compatibility)
|
||||
|
||||
---
|
||||
|
||||
## Host OS Compatibility
|
||||
|
||||
*This section shows compatibility based on your **host operating system** (the OS you're running C/ua on).*
|
||||
|
||||
### macOS Host
|
||||
|
||||
| Installation Method | Requirements | Lume | Cloud | Notes |
|
||||
|-------------------|-------------|------|-------|-------|
|
||||
| **playground-docker.sh** | Docker Desktop | ✅ Full | ✅ Full | Recommended for quick setup |
|
||||
| **Dev Container** | VS Code/WindSurf + Docker | ✅ Full | ✅ Full | Best for development |
|
||||
| **PyPI packages** | Python 3.12+ | ✅ Full | ✅ Full | Most flexible |
|
||||
|
||||
**macOS Host Requirements:**
|
||||
- macOS 15+ (Sequoia) for local VM support
|
||||
- Apple Silicon (M1/M2/M3/M4) recommended for best performance
|
||||
- Docker Desktop for containerized installations
|
||||
|
||||
---
|
||||
|
||||
### Ubuntu/Linux Host
|
||||
|
||||
| Installation Method | Requirements | Lume | Cloud | Notes |
|
||||
|-------------------|-------------|------|-------|-------|
|
||||
| **playground-docker.sh** | Docker Engine | ✅ Full | ✅ Full | Recommended for quick setup |
|
||||
| **Dev Container** | VS Code/WindSurf + Docker | ✅ Full | ✅ Full | Best for development |
|
||||
| **PyPI packages** | Python 3.12+ | ✅ Full | ✅ Full | Most flexible |
|
||||
|
||||
**Ubuntu/Linux Host Requirements:**
|
||||
- Ubuntu 20.04+ or equivalent Linux distribution
|
||||
- Docker Engine or Docker Desktop
|
||||
- Python 3.12+ for PyPI installation
|
||||
|
||||
---
|
||||
|
||||
### Windows Host
|
||||
|
||||
| Installation Method | Requirements | Lume | Winsandbox | Cloud | Notes |
|
||||
|-------------------|-------------|------|------------|-------|-------|
|
||||
| **playground-docker.sh** | Docker Desktop + WSL2 | ❌ Not supported | ❌ Not supported | ✅ Full | Requires WSL2 |
|
||||
| **Dev Container** | VS Code/WindSurf + Docker + WSL2 | ❌ Not supported | ❌ Not supported | ✅ Full | Requires WSL2 |
|
||||
| **PyPI packages** | Python 3.12+ | ❌ Not supported | ✅ Limited | ✅ Full | WSL for .sh scripts |
|
||||
|
||||
**Windows Host Requirements:**
|
||||
- Windows 10/11 with WSL2 enabled for shell script execution
|
||||
- Docker Desktop with WSL2 backend
|
||||
- Windows Sandbox feature enabled (for Winsandbox support)
|
||||
- Python 3.12+ installed in WSL2 or Windows
|
||||
- **Note**: Lume CLI is not available on Windows - use Cloud or Winsandbox providers
|
||||
|
||||
---
|
||||
|
||||
## VM Emulation Support
|
||||
|
||||
*This section shows which **virtual machine operating systems** each provider can emulate.*
|
||||
|
||||
| Provider | macOS VM | Ubuntu/Linux VM | Windows VM | Notes |
|
||||
|----------|----------|-----------------|------------|-------|
|
||||
| **Lume** | ✅ Full support | ⚠️ Limited support | ⚠️ Limited support | macOS: native; Ubuntu/Linux/Windows: need custom image |
|
||||
| **Cloud** | 🚧 Coming soon | ✅ Full support | 🚧 Coming soon | Currently Ubuntu only, macOS/Windows in development |
|
||||
| **Winsandbox** | ❌ Not supported | ❌ Not supported | ✅ Windows only | Windows Sandbox environments only |
|
||||
|
||||
---
|
||||
|
||||
## Model Provider Compatibility
|
||||
|
||||
*This section shows which **AI model providers** are supported on each host operating system.*
|
||||
|
||||
| Provider | macOS Host | Ubuntu/Linux Host | Windows Host | Notes |
|
||||
|----------|------------|-------------------|--------------|-------|
|
||||
| **Anthropic** | ✅ Full support | ✅ Full support | ✅ Full support | Cloud-based API |
|
||||
| **OpenAI** | ✅ Full support | ✅ Full support | ✅ Full support | Cloud-based API |
|
||||
| **Ollama** | ✅ Full support | ✅ Full support | ✅ Full support | Local model serving |
|
||||
| **OpenAI Compatible** | ✅ Full support | ✅ Full support | ✅ Full support | Any OpenAI-compatible API endpoint |
|
||||
| **MLX VLM** | ✅ macOS only | ❌ Not supported | ❌ Not supported | Apple Silicon required. PyPI installation only. |
|
||||
47
README.md
47
README.md
@@ -53,15 +53,15 @@
|
||||
|
||||
|
||||
|
||||
### Option 1: Fully-managed install (recommended)
|
||||
*Guided install for quick use*
|
||||
### Option 1: Fully-managed install with Docker (recommended)
|
||||
*Docker-based guided install for quick use*
|
||||
|
||||
**macOS/Linux/Windows (via WSL):**
|
||||
```bash
|
||||
# Requires Python 3.11+
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/scripts/playground.sh)"
|
||||
# Requires Docker
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/scripts/playground-docker.sh)"
|
||||
```
|
||||
This script will guide you through setup and launch the Computer-Use Agent UI.
|
||||
This script will guide you through setup using Docker containers and launch the Computer-Use Agent UI.
|
||||
|
||||
---
|
||||
|
||||
@@ -72,26 +72,47 @@ This repository includes a [Dev Container](./.devcontainer/README.md) configurat
|
||||
|
||||
1. **Install the Dev Containers extension ([VS Code](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) or [WindSurf](https://docs.windsurf.com/windsurf/advanced#dev-containers-beta))**
|
||||
2. **Open the repository in the Dev Container:**
|
||||
- Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS)
|
||||
- Select `Dev Containers: Clone Repository in Container Volume...` and paste the repository URL: `https://github.com/trycua/cua.git` (if not cloned) or `Dev Containers: Open Folder in Container...` (if already cloned). **Note**: On WindSurf, the post install hook might not run automatically. If so, run `/bin/bash .devcontainer/post-install.sh` manually.
|
||||
3. **Run the Agent UI example:** Click 
|
||||
- Press `Ctrl+Shift+P` (or `⌘+Shift+P` on macOS)
|
||||
- Select `Dev Containers: Clone Repository in Container Volume...` and paste the repository URL: `https://github.com/trycua/cua.git` (if not cloned) or `Dev Containers: Open Folder in Container...` (if git cloned).
|
||||
> **Note**: On WindSurf, the post install hook might not run automatically. If so, run `/bin/bash .devcontainer/post-install.sh` manually.
|
||||
3. **Open the VS Code workspace:** Once the post-install.sh is done running, open the `.vscode/py.code-workspace` workspace and press 
|
||||
.
|
||||
4. **Run the Agent UI example:** Click 
|
||||
to start the Gradio UI. If prompted to install **debugpy (Python Debugger)** to enable remote debugging, select 'Yes' to proceed.
|
||||
4. **Access the Gradio UI:** The Gradio UI will be available at `http://localhost:7860` and will automatically forward to your host machine.
|
||||
5. **Access the Gradio UI:** The Gradio UI will be available at `http://localhost:7860` and will automatically forward to your host machine.
|
||||
|
||||
---
|
||||
|
||||
*How it works: Computer module provides secure desktops (Lume CLI locally, [C/ua Cloud Containers](https://trycua.com) remotely), Agent module provides local/API agents with OpenAI AgentResponse format and [trajectory tracing](https://trycua.com/trajectory-viewer).*
|
||||
### Supported [Agent Loops](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops)
|
||||
### Option 3: PyPI
|
||||
*Direct Python package installation*
|
||||
|
||||
```bash
|
||||
# conda create -yn cua python==3.12
|
||||
|
||||
pip install -U "cua-computer[all]" "cua-agent[all]"
|
||||
python -m agent.ui # Start the agent UI
|
||||
```
|
||||
|
||||
Or check out the [Usage Guide](#-usage-guide) to learn how to use our Python SDK in your own code.
|
||||
|
||||
---
|
||||
|
||||
## Supported [Agent Loops](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops)
|
||||
- [UITARS-1.5](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Run locally on Apple Silicon with MLX, or use cloud providers
|
||||
- [OpenAI CUA](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Use OpenAI's Computer-Use Preview model
|
||||
- [Anthropic CUA](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Use Anthropic's Computer-Use capabilities
|
||||
- [OmniParser-v2.0](https://github.com/trycua/cua/blob/main/libs/agent/README.md#agent-loops) - Control UI with [Set-of-Marks prompting](https://som-gpt4v.github.io/) using any vision model
|
||||
|
||||
## 🖥️ Compatibility
|
||||
|
||||
For detailed compatibility information including host OS support, VM emulation capabilities, and model provider compatibility, see the [Compatibility Matrix](./COMPATIBILITY.md).
|
||||
|
||||
# 💻 Developer Guide
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
Follow these steps to use C/ua in your own code. See [Developer Guide](./docs/Developer-Guide.md) for building from source.
|
||||
# 🐍 Usage Guide
|
||||
|
||||
Follow these steps to use C/ua in your own Python code. See [Developer Guide](./docs/Developer-Guide.md) for building from source.
|
||||
|
||||
### Step 1: Install Lume CLI
|
||||
|
||||
|
||||
@@ -34,10 +34,7 @@ pip install "cua-agent[anthropic]" # Anthropic Cua Loop
|
||||
pip install "cua-agent[uitars]" # UI-Tars support
|
||||
pip install "cua-agent[omni]" # Cua Loop based on OmniParser (includes Ollama for local models)
|
||||
pip install "cua-agent[ui]" # Gradio UI for the agent
|
||||
|
||||
# For local UI-TARS with MLX support, you need to manually install mlx-vlm:
|
||||
pip install "cua-agent[uitars-mlx]"
|
||||
pip install git+https://github.com/ddupont808/mlx-vlm.git@stable/fix/qwen2-position-id # PR: https://github.com/Blaizzy/mlx-vlm/pull/349
|
||||
pip install "cua-agent[uitars-mlx]" # MLX UI-Tars support
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
__version__ = "0.1.0"
|
||||
|
||||
# Initialize logging
|
||||
logger = logging.getLogger("cua.agent")
|
||||
logger = logging.getLogger("agent")
|
||||
|
||||
# Initialize telemetry when the package is imported
|
||||
try:
|
||||
|
||||
@@ -11,10 +11,8 @@ from .types import AgentResponse
|
||||
from .factory import LoopFactory
|
||||
from .provider_config import DEFAULT_MODELS, ENV_VARS
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComputerAgent:
|
||||
"""A computer agent that can perform automated tasks using natural language instructions."""
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ flush = _default_flush
|
||||
is_telemetry_enabled = _default_is_telemetry_enabled
|
||||
is_telemetry_globally_disabled = _default_is_telemetry_globally_disabled
|
||||
|
||||
logger = logging.getLogger("cua.agent.telemetry")
|
||||
logger = logging.getLogger("agent.telemetry")
|
||||
|
||||
try:
|
||||
# Import from core telemetry
|
||||
|
||||
@@ -26,10 +26,8 @@ from .api_handler import OmniAPIHandler
|
||||
from .tools.manager import ToolManager
|
||||
from .tools import ToolResult
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_data(input_string: str, data_type: str) -> str:
|
||||
"""Extract content from code blocks."""
|
||||
pattern = f"```{data_type}" + r"(.*?)(```|$)"
|
||||
|
||||
@@ -25,10 +25,8 @@ from .prompts import COMPUTER_USE, SYSTEM_PROMPT, MAC_SPECIFIC_NOTES
|
||||
from .clients.oaicompat import OAICompatClient
|
||||
from .clients.mlxvlm import MLXVLMUITarsClient
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UITARSLoop(BaseLoop):
|
||||
"""UI-TARS-specific implementation of the agent loop.
|
||||
|
||||
|
||||
@@ -132,6 +132,13 @@ class GradioChatScreenshotHandler(DefaultCallbackHandler):
|
||||
# Detect if current device is MacOS
|
||||
is_mac = platform.system().lower() == "darwin"
|
||||
|
||||
# Detect if lume is available (host device is macOS)
|
||||
is_lume_available = is_mac or (os.environ.get("PYLUME_HOST", "localhost") != "localhost")
|
||||
|
||||
print("PYLUME_HOST: ", os.environ.get("PYLUME_HOST", "localhost"))
|
||||
print("is_mac: ", is_mac)
|
||||
print("Lume available: ", is_lume_available)
|
||||
|
||||
# Map model names to specific provider model names
|
||||
MODEL_MAPPINGS = {
|
||||
"openai": {
|
||||
@@ -733,9 +740,9 @@ if __name__ == "__main__":
|
||||
is_mac = platform.system().lower() == "darwin"
|
||||
|
||||
providers = ["cloud"]
|
||||
if is_mac:
|
||||
if is_lume_available:
|
||||
providers += ["lume"]
|
||||
elif is_windows:
|
||||
if is_windows:
|
||||
providers += ["winsandbox"]
|
||||
|
||||
computer_provider = gr.Radio(
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "pdm.backend"
|
||||
|
||||
[project]
|
||||
name = "cua-agent"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "CUA (Computer Use) Agent for AI-driven computer interaction"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
@@ -38,8 +38,7 @@ uitars = [
|
||||
"httpx>=0.27.0,<0.29.0",
|
||||
]
|
||||
uitars-mlx = [
|
||||
# The mlx-vlm package needs to be installed manually with:
|
||||
# pip install git+https://github.com/ddupont808/mlx-vlm.git@stable/fix/qwen2-position-id
|
||||
"mlx-vlm>=0.1.27; sys_platform == 'darwin'"
|
||||
]
|
||||
ui = [
|
||||
"gradio>=5.23.3,<6.0.0",
|
||||
@@ -88,9 +87,8 @@ all = [
|
||||
"requests>=2.31.0,<3.0.0",
|
||||
"ollama>=0.4.7,<0.5.0",
|
||||
"gradio>=5.23.3,<6.0.0",
|
||||
"python-dotenv>=1.0.1,<2.0.0"
|
||||
# mlx-vlm needs to be installed manually with:
|
||||
# pip install git+https://github.com/ddupont808/mlx-vlm.git@stable/fix/qwen2-position-id
|
||||
"python-dotenv>=1.0.1,<2.0.0",
|
||||
"mlx-vlm>=0.1.27; sys_platform == 'darwin'"
|
||||
]
|
||||
|
||||
[tool.pdm]
|
||||
|
||||
@@ -15,13 +15,7 @@ from computer_server.diorama.diorama_computer import DioramaComputer
|
||||
from computer_server.handlers.macos import *
|
||||
|
||||
# simple, nicely formatted logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[%(asctime)s] [%(levelname)s] %(message)s',
|
||||
datefmt='%H:%M:%S',
|
||||
stream=sys.stdout
|
||||
)
|
||||
logger = logging.getLogger("diorama.virtual_desktop")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
automation_handler = MacOSAutomationHandler()
|
||||
|
||||
|
||||
@@ -28,13 +28,7 @@ import functools
|
||||
import logging
|
||||
|
||||
# simple, nicely formatted logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[%(asctime)s] [%(levelname)s] %(message)s',
|
||||
datefmt='%H:%M:%S',
|
||||
stream=sys.stdout
|
||||
)
|
||||
logger = logging.getLogger("diorama.draw")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from computer_server.diorama.safezone import (
|
||||
get_menubar_bounds,
|
||||
|
||||
@@ -12,8 +12,8 @@ import os
|
||||
import aiohttp
|
||||
|
||||
# Set up logging with more detail
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Configure WebSocket with larger message size
|
||||
WEBSOCKET_MAX_SIZE = 1024 * 1024 * 10 # 10MB limit
|
||||
|
||||
@@ -6,14 +6,14 @@ import sys
|
||||
__version__ = "0.1.0"
|
||||
|
||||
# Initialize logging
|
||||
logger = logging.getLogger("cua.computer")
|
||||
logger = logging.getLogger("computer")
|
||||
|
||||
# Initialize telemetry when the package is imported
|
||||
try:
|
||||
# Import from core telemetry
|
||||
from core.telemetry import (
|
||||
is_telemetry_enabled,
|
||||
flush,
|
||||
is_telemetry_enabled,
|
||||
record_event,
|
||||
)
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class Computer:
|
||||
experiments: Optional list of experimental features to enable (e.g. ["app-use"])
|
||||
"""
|
||||
|
||||
self.logger = Logger("cua.computer", verbosity)
|
||||
self.logger = Logger("computer", verbosity)
|
||||
self.logger.info("Initializing Computer...")
|
||||
|
||||
# Store original parameters
|
||||
@@ -132,11 +132,11 @@ class Computer:
|
||||
|
||||
# Configure root logger
|
||||
self.verbosity = verbosity
|
||||
self.logger = Logger("cua", verbosity)
|
||||
self.logger = Logger("computer", verbosity)
|
||||
|
||||
# Configure component loggers with proper hierarchy
|
||||
self.vm_logger = Logger("cua.vm", verbosity)
|
||||
self.interface_logger = Logger("cua.interface", verbosity)
|
||||
self.vm_logger = Logger("computer.vm", verbosity)
|
||||
self.interface_logger = Logger("computer.interface", verbosity)
|
||||
|
||||
if not use_host_computer_server:
|
||||
if ":" not in image or len(image.split(":")) != 2:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Helper functions and decorators for the Computer module.
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Optional, TypeVar, cast
|
||||
@@ -8,6 +9,8 @@ from typing import Any, Callable, Optional, TypeVar, cast
|
||||
# Global reference to the default computer instance
|
||||
_default_computer = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def set_default_computer(computer):
|
||||
"""
|
||||
Set the default computer instance to be used by the remote decorator.
|
||||
@@ -41,7 +44,7 @@ def sandboxed(venv_name: str = "default", computer: str = "default", max_retries
|
||||
try:
|
||||
return await comp.venv_exec(venv_name, func, *args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f"Attempt {i+1} failed: {e}")
|
||||
logger.error(f"Attempt {i+1} failed: {e}")
|
||||
await asyncio.sleep(1)
|
||||
if i == max_retries - 1:
|
||||
raise e
|
||||
|
||||
@@ -30,7 +30,7 @@ class LinuxComputerInterface(BaseComputerInterface):
|
||||
self._command_lock = asyncio.Lock() # Lock to ensure only one command at a time
|
||||
|
||||
# Set logger name for Linux interface
|
||||
self.logger = Logger("cua.interface.linux", LogLevel.NORMAL)
|
||||
self.logger = Logger("computer.interface.linux", LogLevel.NORMAL)
|
||||
|
||||
@property
|
||||
def ws_uri(self) -> str:
|
||||
|
||||
@@ -29,7 +29,7 @@ class MacOSComputerInterface(BaseComputerInterface):
|
||||
self._command_lock = asyncio.Lock() # Lock to ensure only one command at a time
|
||||
|
||||
# Set logger name for macOS interface
|
||||
self.logger = Logger("cua.interface.macos", LogLevel.NORMAL)
|
||||
self.logger = Logger("computer.interface.macos", LogLevel.NORMAL)
|
||||
|
||||
@property
|
||||
def ws_uri(self) -> str:
|
||||
|
||||
@@ -30,7 +30,7 @@ class WindowsComputerInterface(BaseComputerInterface):
|
||||
self._command_lock = asyncio.Lock() # Lock to ensure only one command at a time
|
||||
|
||||
# Set logger name for Windows interface
|
||||
self.logger = Logger("cua.interface.windows", LogLevel.NORMAL)
|
||||
self.logger = Logger("computer.interface.windows", LogLevel.NORMAL)
|
||||
|
||||
@property
|
||||
def ws_uri(self) -> str:
|
||||
|
||||
@@ -66,8 +66,6 @@ def lume_api_get(
|
||||
|
||||
# Only print the curl command when debug is enabled
|
||||
display_curl_string = ' '.join(display_cmd)
|
||||
if debug or verbose:
|
||||
print(f"DEBUG: Executing curl API call: {display_curl_string}")
|
||||
logger.debug(f"Executing API request: {display_curl_string}")
|
||||
|
||||
# Execute the command - for execution we need to use shell=True to handle URLs with special characters
|
||||
@@ -172,8 +170,6 @@ def lume_api_run(
|
||||
payload["sharedDirectories"] = run_opts["shared_directories"]
|
||||
|
||||
# Log the payload for debugging
|
||||
if debug or verbose:
|
||||
print(f"DEBUG: Payload for {vm_name} run request: {json.dumps(payload, indent=2)}")
|
||||
logger.debug(f"API payload: {json.dumps(payload, indent=2)}")
|
||||
|
||||
# Construct the curl command
|
||||
@@ -184,11 +180,6 @@ def lume_api_run(
|
||||
api_url
|
||||
]
|
||||
|
||||
# Always print the command for debugging
|
||||
if debug or verbose:
|
||||
print(f"DEBUG: Executing curl run API call: {' '.join(cmd)}")
|
||||
print(f"Run payload: {json.dumps(payload, indent=2)}")
|
||||
|
||||
# Execute the command
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
@@ -405,8 +396,6 @@ def lume_api_pull(
|
||||
f"http://{host}:{port}/lume/pull"
|
||||
])
|
||||
|
||||
if debug or verbose:
|
||||
print(f"DEBUG: Executing curl API call: {' '.join(pull_cmd)}")
|
||||
logger.debug(f"Executing API request: {' '.join(pull_cmd)}")
|
||||
|
||||
try:
|
||||
@@ -474,8 +463,6 @@ def lume_api_delete(
|
||||
|
||||
# Only print the curl command when debug is enabled
|
||||
display_curl_string = ' '.join(display_cmd)
|
||||
if debug or verbose:
|
||||
print(f"DEBUG: Executing curl API call: {display_curl_string}")
|
||||
logger.debug(f"Executing API request: {display_curl_string}")
|
||||
|
||||
# Execute the command - for execution we need to use shell=True to handle URLs with special characters
|
||||
|
||||
@@ -305,7 +305,7 @@ class LumierProvider(BaseVMProvider):
|
||||
cmd = ["docker", "run", "-d", "--name", self.container_name]
|
||||
|
||||
cmd.extend(["-p", f"{self.vnc_port}:8006"])
|
||||
print(f"Using specified noVNC_port: {self.vnc_port}")
|
||||
logger.debug(f"Using specified noVNC_port: {self.vnc_port}")
|
||||
|
||||
# Set API URL using the API port
|
||||
self._api_url = f"http://{self.host}:{self.api_port}"
|
||||
@@ -324,7 +324,7 @@ class LumierProvider(BaseVMProvider):
|
||||
"-v", f"{storage_dir}:/storage",
|
||||
"-e", f"HOST_STORAGE_PATH={storage_dir}"
|
||||
])
|
||||
print(f"Using persistent storage at: {storage_dir}")
|
||||
logger.debug(f"Using persistent storage at: {storage_dir}")
|
||||
|
||||
# Add shared folder volume mount if shared_path is specified
|
||||
if self.shared_path:
|
||||
@@ -337,12 +337,12 @@ class LumierProvider(BaseVMProvider):
|
||||
"-v", f"{shared_dir}:/shared",
|
||||
"-e", f"HOST_SHARED_PATH={shared_dir}"
|
||||
])
|
||||
print(f"Using shared folder at: {shared_dir}")
|
||||
logger.debug(f"Using shared folder at: {shared_dir}")
|
||||
|
||||
# Add environment variables
|
||||
# Always use the container_name as the VM_NAME for consistency
|
||||
# Use the VM image passed from the Computer class
|
||||
print(f"Using VM image: {self.image}")
|
||||
logger.debug(f"Using VM image: {self.image}")
|
||||
|
||||
# If ghcr.io is in the image, use the full image name
|
||||
if "ghcr.io" in self.image:
|
||||
@@ -362,22 +362,22 @@ class LumierProvider(BaseVMProvider):
|
||||
|
||||
# First check if the image exists locally
|
||||
try:
|
||||
print(f"Checking if Docker image {lumier_image} exists locally...")
|
||||
logger.debug(f"Checking if Docker image {lumier_image} exists locally...")
|
||||
check_image_cmd = ["docker", "image", "inspect", lumier_image]
|
||||
subprocess.run(check_image_cmd, capture_output=True, check=True)
|
||||
print(f"Docker image {lumier_image} found locally.")
|
||||
logger.debug(f"Docker image {lumier_image} found locally.")
|
||||
except subprocess.CalledProcessError:
|
||||
# Image doesn't exist locally
|
||||
print(f"\nWARNING: Docker image {lumier_image} not found locally.")
|
||||
print("The system will attempt to pull it from Docker Hub, which may fail if you have network connectivity issues.")
|
||||
print("If the Docker pull fails, you may need to manually pull the image first with:")
|
||||
print(f" docker pull {lumier_image}\n")
|
||||
logger.warning(f"\nWARNING: Docker image {lumier_image} not found locally.")
|
||||
logger.warning("The system will attempt to pull it from Docker Hub, which may fail if you have network connectivity issues.")
|
||||
logger.warning("If the Docker pull fails, you may need to manually pull the image first with:")
|
||||
logger.warning(f" docker pull {lumier_image}\n")
|
||||
|
||||
# Add the image to the command
|
||||
cmd.append(lumier_image)
|
||||
|
||||
# Print the Docker command for debugging
|
||||
print(f"DOCKER COMMAND: {' '.join(cmd)}")
|
||||
logger.debug(f"DOCKER COMMAND: {' '.join(cmd)}")
|
||||
|
||||
# Run the container with improved error handling
|
||||
try:
|
||||
@@ -395,8 +395,8 @@ class LumierProvider(BaseVMProvider):
|
||||
raise
|
||||
|
||||
# Container started, now check VM status with polling
|
||||
print("Container started, checking VM status...")
|
||||
print("NOTE: This may take some time while the VM image is being pulled and initialized")
|
||||
logger.debug("Container started, checking VM status...")
|
||||
logger.debug("NOTE: This may take some time while the VM image is being pulled and initialized")
|
||||
|
||||
# Start a background thread to show container logs in real-time
|
||||
import threading
|
||||
@@ -404,8 +404,8 @@ class LumierProvider(BaseVMProvider):
|
||||
def show_container_logs():
|
||||
# Give the container a moment to start generating logs
|
||||
time.sleep(1)
|
||||
print(f"\n---- CONTAINER LOGS FOR '{name}' (LIVE) ----")
|
||||
print("Showing logs as they are generated. Press Ctrl+C to stop viewing logs...\n")
|
||||
logger.debug(f"\n---- CONTAINER LOGS FOR '{name}' (LIVE) ----")
|
||||
logger.debug("Showing logs as they are generated. Press Ctrl+C to stop viewing logs...\n")
|
||||
|
||||
try:
|
||||
# Use docker logs with follow option
|
||||
@@ -415,17 +415,17 @@ class LumierProvider(BaseVMProvider):
|
||||
|
||||
# Read and print logs line by line
|
||||
for line in process.stdout:
|
||||
print(line, end='')
|
||||
logger.debug(line, end='')
|
||||
|
||||
# Break if process has exited
|
||||
if process.poll() is not None:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"\nError showing container logs: {e}")
|
||||
logger.error(f"\nError showing container logs: {e}")
|
||||
if self.verbose:
|
||||
logger.error(f"Error in log streaming thread: {e}")
|
||||
finally:
|
||||
print("\n---- LOG STREAMING ENDED ----")
|
||||
logger.debug("\n---- LOG STREAMING ENDED ----")
|
||||
# Make sure process is terminated
|
||||
if 'process' in locals() and process.poll() is None:
|
||||
process.terminate()
|
||||
@@ -452,11 +452,11 @@ class LumierProvider(BaseVMProvider):
|
||||
else:
|
||||
wait_time = min(30, 5 + (attempt * 2))
|
||||
|
||||
print(f"Waiting {wait_time}s before retry #{attempt+1}...")
|
||||
logger.debug(f"Waiting {wait_time}s before retry #{attempt+1}...")
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
# Try to get VM status
|
||||
print(f"Checking VM status (attempt {attempt+1})...")
|
||||
logger.debug(f"Checking VM status (attempt {attempt+1})...")
|
||||
vm_status = await self.get_vm(name)
|
||||
|
||||
# Check for API errors
|
||||
@@ -468,20 +468,20 @@ class LumierProvider(BaseVMProvider):
|
||||
# since _lume_api_get already logged the technical details
|
||||
if consecutive_errors == 1 or attempt % 5 == 0:
|
||||
if 'Empty reply from server' in error_msg:
|
||||
print("API server is starting up - container is running, but API isn't fully initialized yet.")
|
||||
print("This is expected during the initial VM setup - will continue polling...")
|
||||
logger.info("API server is starting up - container is running, but API isn't fully initialized yet.")
|
||||
logger.info("This is expected during the initial VM setup - will continue polling...")
|
||||
else:
|
||||
# Don't repeat the exact same error message each time
|
||||
logger.debug(f"API request error (attempt {attempt+1}): {error_msg}")
|
||||
logger.warning(f"API request error (attempt {attempt+1}): {error_msg}")
|
||||
# Just log that we're still working on it
|
||||
if attempt > 3:
|
||||
print("Still waiting for the API server to become available...")
|
||||
logger.debug("Still waiting for the API server to become available...")
|
||||
|
||||
# If we're getting errors but container is running, that's normal during startup
|
||||
if vm_status.get('status') == 'running':
|
||||
if not vm_running:
|
||||
print("Container is running, waiting for the VM within it to become fully ready...")
|
||||
print("This might take a minute while the VM initializes...")
|
||||
logger.info("Container is running, waiting for the VM within it to become fully ready...")
|
||||
logger.info("This might take a minute while the VM initializes...")
|
||||
vm_running = True
|
||||
|
||||
# Increase counter and continue
|
||||
@@ -497,35 +497,35 @@ class LumierProvider(BaseVMProvider):
|
||||
|
||||
# Check if we have an IP address, which means the VM is fully ready
|
||||
if 'ip_address' in vm_status and vm_status['ip_address']:
|
||||
print(f"VM is now fully running with IP: {vm_status.get('ip_address')}")
|
||||
logger.info(f"VM is now fully running with IP: {vm_status.get('ip_address')}")
|
||||
if 'vnc_url' in vm_status and vm_status['vnc_url']:
|
||||
print(f"VNC URL: {vm_status.get('vnc_url')}")
|
||||
logger.info(f"VNC URL: {vm_status.get('vnc_url')}")
|
||||
return vm_status
|
||||
else:
|
||||
print("VM is running but still initializing network interfaces...")
|
||||
print("Waiting for IP address to be assigned...")
|
||||
logger.debug("VM is running but still initializing network interfaces...")
|
||||
logger.debug("Waiting for IP address to be assigned...")
|
||||
else:
|
||||
# VM exists but might still be starting up
|
||||
status = vm_status.get('status', 'unknown')
|
||||
print(f"VM found but status is: {status}. Continuing to poll...")
|
||||
logger.debug(f"VM found but status is: {status}. Continuing to poll...")
|
||||
|
||||
# Increase counter for next iteration's delay calculation
|
||||
attempt += 1
|
||||
|
||||
# If we reach a very large number of attempts, give a reassuring message but continue
|
||||
if attempt % 10 == 0:
|
||||
print(f"Still waiting after {attempt} attempts. This might take several minutes for first-time setup.")
|
||||
logger.debug(f"Still waiting after {attempt} attempts. This might take several minutes for first-time setup.")
|
||||
if not vm_running and attempt >= 20:
|
||||
print("\nNOTE: First-time VM initialization can be slow as images are downloaded.")
|
||||
print("If this continues for more than 10 minutes, you may want to check:")
|
||||
print(" 1. Docker logs with: docker logs " + name)
|
||||
print(" 2. If your network can access container registries")
|
||||
print("Press Ctrl+C to abort if needed.\n")
|
||||
logger.warning("\nNOTE: First-time VM initialization can be slow as images are downloaded.")
|
||||
logger.warning("If this continues for more than 10 minutes, you may want to check:")
|
||||
logger.warning(" 1. Docker logs with: docker logs " + name)
|
||||
logger.warning(" 2. If your network can access container registries")
|
||||
logger.warning("Press Ctrl+C to abort if needed.\n")
|
||||
|
||||
# After 150 attempts (likely over 30-40 minutes), return current status
|
||||
if attempt >= 150:
|
||||
print(f"Reached 150 polling attempts. VM status is: {vm_status.get('status', 'unknown')}")
|
||||
print("Returning current VM status, but please check Docker logs if there are issues.")
|
||||
logger.debug(f"Reached 150 polling attempts. VM status is: {vm_status.get('status', 'unknown')}")
|
||||
logger.debug("Returning current VM status, but please check Docker logs if there are issues.")
|
||||
return vm_status
|
||||
|
||||
except Exception as e:
|
||||
@@ -535,9 +535,9 @@ class LumierProvider(BaseVMProvider):
|
||||
|
||||
# If we've had too many consecutive errors, might be a deeper problem
|
||||
if consecutive_errors >= 10:
|
||||
print(f"\nWARNING: Encountered {consecutive_errors} consecutive errors while checking VM status.")
|
||||
print("You may need to check the Docker container logs or restart the process.")
|
||||
print(f"Error details: {str(e)}\n")
|
||||
logger.warning(f"\nWARNING: Encountered {consecutive_errors} consecutive errors while checking VM status.")
|
||||
logger.warning("You may need to check the Docker container logs or restart the process.")
|
||||
logger.warning(f"Error details: {str(e)}\n")
|
||||
|
||||
# Increase attempt counter for next iteration
|
||||
attempt += 1
|
||||
@@ -545,7 +545,7 @@ class LumierProvider(BaseVMProvider):
|
||||
# After many consecutive errors, add a delay to avoid hammering the system
|
||||
if attempt > 5:
|
||||
error_delay = min(30, 10 + attempt)
|
||||
print(f"Multiple connection errors, waiting {error_delay}s before next attempt...")
|
||||
logger.warning(f"Multiple connection errors, waiting {error_delay}s before next attempt...")
|
||||
await asyncio.sleep(error_delay)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
@@ -568,7 +568,7 @@ class LumierProvider(BaseVMProvider):
|
||||
api_ready = False
|
||||
container_running = False
|
||||
|
||||
print(f"Waiting for container {container_name} to be ready (timeout: {timeout}s)...")
|
||||
logger.debug(f"Waiting for container {container_name} to be ready (timeout: {timeout}s)...")
|
||||
|
||||
while time.time() - start_time < timeout:
|
||||
# Check if container is running
|
||||
@@ -579,7 +579,6 @@ class LumierProvider(BaseVMProvider):
|
||||
|
||||
if container_status and container_status.startswith("Up"):
|
||||
container_running = True
|
||||
print(f"Container {container_name} is running")
|
||||
logger.info(f"Container {container_name} is running with status: {container_status}")
|
||||
else:
|
||||
logger.warning(f"Container {container_name} not yet running, status: {container_status}")
|
||||
@@ -603,7 +602,6 @@ class LumierProvider(BaseVMProvider):
|
||||
|
||||
if result.returncode == 0 and "ok" in result.stdout.lower():
|
||||
api_ready = True
|
||||
print(f"API is ready at {api_url}")
|
||||
logger.info(f"API is ready at {api_url}")
|
||||
break
|
||||
else:
|
||||
@@ -621,7 +619,6 @@ class LumierProvider(BaseVMProvider):
|
||||
if vm_result.returncode == 0 and vm_result.stdout.strip():
|
||||
# VM API responded with something - consider the API ready
|
||||
api_ready = True
|
||||
print(f"VM API is ready at {vm_api_url}")
|
||||
logger.info(f"VM API is ready at {vm_api_url}")
|
||||
break
|
||||
else:
|
||||
@@ -643,7 +640,6 @@ class LumierProvider(BaseVMProvider):
|
||||
else:
|
||||
curl_error = f"Unknown curl error code: {curl_code}"
|
||||
|
||||
print(f"API not ready yet: {curl_error}")
|
||||
logger.info(f"API not ready yet: {curl_error}")
|
||||
except subprocess.SubprocessError as e:
|
||||
logger.warning(f"Error checking API status: {e}")
|
||||
@@ -652,22 +648,19 @@ class LumierProvider(BaseVMProvider):
|
||||
# a bit longer before checking again, as the container may still be initializing
|
||||
elapsed_seconds = time.time() - start_time
|
||||
if int(elapsed_seconds) % 5 == 0: # Only print status every 5 seconds to reduce verbosity
|
||||
print(f"Waiting for API to initialize... ({elapsed_seconds:.1f}s / {timeout}s)")
|
||||
logger.debug(f"Waiting for API to initialize... ({elapsed_seconds:.1f}s / {timeout}s)")
|
||||
|
||||
await asyncio.sleep(3) # Longer sleep between API checks
|
||||
|
||||
# Handle timeout - if the container is running but API is not ready, that's not
|
||||
# necessarily an error - the API might just need more time to start up
|
||||
if not container_running:
|
||||
print(f"Timed out waiting for container {container_name} to start")
|
||||
logger.warning(f"Timed out waiting for container {container_name} to start")
|
||||
return False
|
||||
|
||||
if not api_ready:
|
||||
print(f"Container {container_name} is running, but API is not fully ready yet.")
|
||||
print("Proceeding with operations. API will become available shortly.")
|
||||
print("NOTE: You may see some 'API request failed' messages while the API initializes.")
|
||||
logger.warning(f"Container {container_name} is running, but API is not fully ready yet.")
|
||||
logger.warning(f"NOTE: You may see some 'API request failed' messages while the API initializes.")
|
||||
|
||||
# Return True if container is running, even if API isn't ready yet
|
||||
# This allows VM operations to proceed, with appropriate retries for API calls
|
||||
@@ -777,8 +770,8 @@ class LumierProvider(BaseVMProvider):
|
||||
# For follow mode with timeout, we'll run the command and handle the timeout
|
||||
log_cmd.append(container_name)
|
||||
logger.info(f"Following logs for container '{container_name}' with timeout {timeout}s")
|
||||
print(f"\n---- CONTAINER LOGS FOR '{container_name}' (LIVE) ----")
|
||||
print(f"Press Ctrl+C to stop following logs\n")
|
||||
logger.info(f"\n---- CONTAINER LOGS FOR '{container_name}' (LIVE) ----")
|
||||
logger.info(f"Press Ctrl+C to stop following logs\n")
|
||||
|
||||
try:
|
||||
# Run with timeout
|
||||
@@ -790,7 +783,7 @@ class LumierProvider(BaseVMProvider):
|
||||
process.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.terminate() # Stop after timeout
|
||||
print(f"\n---- LOG FOLLOWING STOPPED (timeout {timeout}s reached) ----")
|
||||
logger.info(f"\n---- LOG FOLLOWING STOPPED (timeout {timeout}s reached) ----")
|
||||
else:
|
||||
# Without timeout, wait for user interruption
|
||||
process.wait()
|
||||
@@ -798,14 +791,14 @@ class LumierProvider(BaseVMProvider):
|
||||
return "Logs were displayed to console in follow mode"
|
||||
except KeyboardInterrupt:
|
||||
process.terminate()
|
||||
print("\n---- LOG FOLLOWING STOPPED (user interrupted) ----")
|
||||
logger.info("\n---- LOG FOLLOWING STOPPED (user interrupted) ----")
|
||||
return "Logs were displayed to console in follow mode (interrupted)"
|
||||
else:
|
||||
# For follow mode without timeout, we'll print a helpful message
|
||||
log_cmd.append(container_name)
|
||||
logger.info(f"Following logs for container '{container_name}' indefinitely")
|
||||
print(f"\n---- CONTAINER LOGS FOR '{container_name}' (LIVE) ----")
|
||||
print(f"Press Ctrl+C to stop following logs\n")
|
||||
logger.info(f"\n---- CONTAINER LOGS FOR '{container_name}' (LIVE) ----")
|
||||
logger.info(f"Press Ctrl+C to stop following logs\n")
|
||||
|
||||
try:
|
||||
# Run the command and let it run until interrupted
|
||||
@@ -814,7 +807,7 @@ class LumierProvider(BaseVMProvider):
|
||||
return "Logs were displayed to console in follow mode"
|
||||
except KeyboardInterrupt:
|
||||
process.terminate()
|
||||
print("\n---- LOG FOLLOWING STOPPED (user interrupted) ----")
|
||||
logger.info("\n---- LOG FOLLOWING STOPPED (user interrupted) ----")
|
||||
return "Logs were displayed to console in follow mode (interrupted)"
|
||||
else:
|
||||
# For non-follow mode, capture and return the logs as a string
|
||||
@@ -827,11 +820,11 @@ class LumierProvider(BaseVMProvider):
|
||||
|
||||
# Only print header and logs if there's content
|
||||
if logs.strip():
|
||||
print(f"\n---- CONTAINER LOGS FOR '{container_name}' (LAST {num_lines} LINES) ----\n")
|
||||
print(logs)
|
||||
print(f"\n---- END OF LOGS ----")
|
||||
logger.info(f"\n---- CONTAINER LOGS FOR '{container_name}' (LAST {num_lines} LINES) ----\n")
|
||||
logger.info(logs)
|
||||
logger.info(f"\n---- END OF LOGS ----")
|
||||
else:
|
||||
print(f"\nNo logs available for container '{container_name}'")
|
||||
logger.info(f"\nNo logs available for container '{container_name}'")
|
||||
|
||||
return logs
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
||||
@@ -9,10 +9,10 @@ TELEMETRY_AVAILABLE = False
|
||||
|
||||
try:
|
||||
from core.telemetry import (
|
||||
record_event,
|
||||
increment,
|
||||
is_telemetry_enabled,
|
||||
is_telemetry_globally_disabled,
|
||||
record_event,
|
||||
)
|
||||
|
||||
def increment_counter(counter_name: str, value: int = 1) -> None:
|
||||
@@ -22,14 +22,14 @@ try:
|
||||
|
||||
def set_dimension(name: str, value: Any) -> None:
|
||||
"""Set a dimension that will be attached to all events."""
|
||||
logger = logging.getLogger("cua.computer.telemetry")
|
||||
logger = logging.getLogger("computer.telemetry")
|
||||
logger.debug(f"Setting dimension {name}={value}")
|
||||
|
||||
TELEMETRY_AVAILABLE = True
|
||||
logger = logging.getLogger("cua.computer.telemetry")
|
||||
logger = logging.getLogger("computer.telemetry")
|
||||
logger.info("Successfully imported telemetry")
|
||||
except ImportError as e:
|
||||
logger = logging.getLogger("cua.computer.telemetry")
|
||||
logger = logging.getLogger("computer.telemetry")
|
||||
logger.warning(f"Could not import telemetry: {e}")
|
||||
TELEMETRY_AVAILABLE = False
|
||||
|
||||
@@ -40,7 +40,7 @@ def _noop(*args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
logger = logging.getLogger("cua.computer.telemetry")
|
||||
logger = logging.getLogger("computer.telemetry")
|
||||
|
||||
# If telemetry isn't available, use no-op functions
|
||||
if not TELEMETRY_AVAILABLE:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "pdm.backend"
|
||||
|
||||
[project]
|
||||
name = "cua-computer"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "Computer-Use Interface (CUI) framework powering Cua"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
@@ -15,7 +15,7 @@ from typing import Any, Dict, List, Optional
|
||||
from core import __version__
|
||||
from core.telemetry.sender import send_telemetry
|
||||
|
||||
logger = logging.getLogger("cua.telemetry")
|
||||
logger = logging.getLogger("core.telemetry")
|
||||
|
||||
# Controls how frequently telemetry will be sent (percentage)
|
||||
TELEMETRY_SAMPLE_RATE = 5 # 5% sampling rate
|
||||
|
||||
@@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional
|
||||
import posthog
|
||||
from core import __version__
|
||||
|
||||
logger = logging.getLogger("cua.telemetry")
|
||||
logger = logging.getLogger("core.telemetry")
|
||||
|
||||
# Controls how frequently telemetry will be sent (percentage)
|
||||
TELEMETRY_SAMPLE_RATE = 100 # 100% sampling rate (was 5%)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
logger = logging.getLogger("cua.telemetry")
|
||||
logger = logging.getLogger("core.telemetry")
|
||||
|
||||
|
||||
def send_telemetry(payload: Dict[str, Any]) -> bool:
|
||||
|
||||
@@ -30,7 +30,7 @@ def _configure_telemetry_logging() -> None:
|
||||
level = logging.ERROR
|
||||
|
||||
# Configure the main telemetry logger
|
||||
telemetry_logger = logging.getLogger("cua.telemetry")
|
||||
telemetry_logger = logging.getLogger("core.telemetry")
|
||||
telemetry_logger.setLevel(level)
|
||||
|
||||
|
||||
@@ -46,11 +46,11 @@ try:
|
||||
|
||||
POSTHOG_AVAILABLE = True
|
||||
except ImportError:
|
||||
logger = logging.getLogger("cua.telemetry")
|
||||
logger = logging.getLogger("core.telemetry")
|
||||
logger.info("PostHog not available. Install with: pdm add posthog")
|
||||
POSTHOG_AVAILABLE = False
|
||||
|
||||
logger = logging.getLogger("cua.telemetry")
|
||||
logger = logging.getLogger("core.telemetry")
|
||||
|
||||
|
||||
# Check environment variables for global telemetry opt-out
|
||||
@@ -292,10 +292,9 @@ def set_telemetry_log_level(level: Optional[int] = None) -> None:
|
||||
|
||||
# Set the level for all telemetry-related loggers
|
||||
telemetry_loggers = [
|
||||
"cua.telemetry",
|
||||
"core.telemetry",
|
||||
"cua.agent.telemetry",
|
||||
"cua.computer.telemetry",
|
||||
"agent.telemetry",
|
||||
"computer.telemetry",
|
||||
"posthog",
|
||||
]
|
||||
|
||||
|
||||
323
scripts/playground-docker.sh
Normal file
323
scripts/playground-docker.sh
Normal file
@@ -0,0 +1,323 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Print with color
|
||||
print_info() {
|
||||
echo -e "${BLUE}==> $1${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}==> $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}==> $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}==> $1${NC}"
|
||||
}
|
||||
|
||||
echo "🚀 Launching C/ua Computer-Use Agent UI..."
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker is not installed!"
|
||||
echo ""
|
||||
echo "To use C/ua with Docker containers, you need to install Docker first:"
|
||||
echo ""
|
||||
echo "📦 Install Docker:"
|
||||
echo " • macOS: Download Docker Desktop from https://docker.com/products/docker-desktop"
|
||||
echo " • Windows: Download Docker Desktop from https://docker.com/products/docker-desktop"
|
||||
echo " • Linux: Follow instructions at https://docs.docker.com/engine/install/"
|
||||
echo ""
|
||||
echo "After installing Docker, run this script again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Docker daemon is running
|
||||
if ! docker info &> /dev/null; then
|
||||
print_error "Docker is installed but not running!"
|
||||
echo ""
|
||||
echo "Please start Docker Desktop and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "Docker is installed and running!"
|
||||
|
||||
# Save the original working directory
|
||||
ORIGINAL_DIR="$(pwd)"
|
||||
|
||||
DEMO_DIR="$HOME/.cua"
|
||||
mkdir -p "$DEMO_DIR"
|
||||
|
||||
|
||||
# Check if we're already in the cua repository
|
||||
# Look for the specific trycua identifier in pyproject.toml
|
||||
if [[ -f "pyproject.toml" ]] && grep -q "gh@trycua.com" "pyproject.toml"; then
|
||||
print_success "Already in C/ua repository - using current directory"
|
||||
REPO_DIR="$ORIGINAL_DIR"
|
||||
USE_EXISTING_REPO=true
|
||||
else
|
||||
# Directories used by the script when not in repo
|
||||
REPO_DIR="$DEMO_DIR/cua"
|
||||
USE_EXISTING_REPO=false
|
||||
fi
|
||||
|
||||
# Function to clean up on exit
|
||||
cleanup() {
|
||||
cd "$ORIGINAL_DIR" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "Choose your C/ua setup:"
|
||||
echo "1) ☁️ C/ua Cloud Containers (works on any system)"
|
||||
echo "2) 🖥️ Local macOS VMs (requires Apple Silicon Mac + macOS 15+)"
|
||||
echo "3) 🖥️ Local Windows VMs (requires Windows 10 / 11)"
|
||||
echo ""
|
||||
read -p "Enter your choice (1, 2, or 3): " CHOICE
|
||||
|
||||
if [[ "$CHOICE" == "1" ]]; then
|
||||
# C/ua Cloud Container setup
|
||||
echo ""
|
||||
print_info "Setting up C/ua Cloud Containers..."
|
||||
echo ""
|
||||
|
||||
# Check if existing .env.local already has CUA_API_KEY
|
||||
REPO_ENV_FILE="$REPO_DIR/.env.local"
|
||||
CURRENT_ENV_FILE="$ORIGINAL_DIR/.env.local"
|
||||
|
||||
CUA_API_KEY=""
|
||||
|
||||
# First check current directory
|
||||
if [[ -f "$CURRENT_ENV_FILE" ]] && grep -q "CUA_API_KEY=" "$CURRENT_ENV_FILE"; then
|
||||
EXISTING_CUA_KEY=$(grep "CUA_API_KEY=" "$CURRENT_ENV_FILE" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs)
|
||||
if [[ -n "$EXISTING_CUA_KEY" && "$EXISTING_CUA_KEY" != "your_cua_api_key_here" && "$EXISTING_CUA_KEY" != "" ]]; then
|
||||
CUA_API_KEY="$EXISTING_CUA_KEY"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Then check repo directory if not found in current dir
|
||||
if [[ -z "$CUA_API_KEY" ]] && [[ -f "$REPO_ENV_FILE" ]] && grep -q "CUA_API_KEY=" "$REPO_ENV_FILE"; then
|
||||
EXISTING_CUA_KEY=$(grep "CUA_API_KEY=" "$REPO_ENV_FILE" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs)
|
||||
if [[ -n "$EXISTING_CUA_KEY" && "$EXISTING_CUA_KEY" != "your_cua_api_key_here" && "$EXISTING_CUA_KEY" != "" ]]; then
|
||||
CUA_API_KEY="$EXISTING_CUA_KEY"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If no valid API key found, prompt for one
|
||||
if [[ -z "$CUA_API_KEY" ]]; then
|
||||
echo "To use C/ua Cloud Containers, you need to:"
|
||||
echo "1. Sign up at https://trycua.com"
|
||||
echo "2. Create a Cloud Container"
|
||||
echo "3. Generate an Api Key"
|
||||
echo ""
|
||||
read -p "Enter your C/ua Api Key: " CUA_API_KEY
|
||||
|
||||
if [[ -z "$CUA_API_KEY" ]]; then
|
||||
print_error "C/ua Api Key is required for Cloud Containers."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_success "Found existing CUA API key"
|
||||
fi
|
||||
|
||||
USE_CLOUD=true
|
||||
COMPUTER_TYPE="cloud"
|
||||
|
||||
elif [[ "$CHOICE" == "2" ]]; then
|
||||
# Local macOS VM setup
|
||||
echo ""
|
||||
print_info "Setting up local macOS VMs..."
|
||||
|
||||
# Check for Apple Silicon Mac
|
||||
if [[ $(uname -s) != "Darwin" || $(uname -m) != "arm64" ]]; then
|
||||
print_error "Local macOS VMs require an Apple Silicon Mac (M1/M2/M3/M4)."
|
||||
echo "💡 Consider using C/ua Cloud Containers instead (option 1)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for macOS 15 (Sequoia) or newer
|
||||
OSVERSION=$(sw_vers -productVersion)
|
||||
if [[ $(echo "$OSVERSION 15.0" | tr " " "\n" | sort -V | head -n 1) != "15.0" ]]; then
|
||||
print_error "Local macOS VMs require macOS 15 (Sequoia) or newer. You have $OSVERSION."
|
||||
echo "💡 Consider using C/ua Cloud Containers instead (option 1)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USE_CLOUD=false
|
||||
COMPUTER_TYPE="macos"
|
||||
|
||||
elif [[ "$CHOICE" == "3" ]]; then
|
||||
# Local Windows VM setup
|
||||
echo ""
|
||||
print_info "Setting up local Windows VMs..."
|
||||
|
||||
# Check if we're on Windows
|
||||
if [[ $(uname -s) != MINGW* && $(uname -s) != CYGWIN* && $(uname -s) != MSYS* ]]; then
|
||||
print_error "Local Windows VMs require Windows 10 or 11."
|
||||
echo "💡 Consider using C/ua Cloud Containers instead (option 1)."
|
||||
echo ""
|
||||
echo "🔗 If you are using WSL, refer to the blog post to get started: https://www.trycua.com/blog/windows-sandbox"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USE_CLOUD=false
|
||||
COMPUTER_TYPE="windows"
|
||||
|
||||
else
|
||||
print_error "Invalid choice. Please run the script again and choose 1, 2, or 3."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_success "All checks passed! 🎉"
|
||||
|
||||
# Create demo directory and handle repository
|
||||
if [[ "$USE_EXISTING_REPO" == "true" ]]; then
|
||||
print_info "Using existing repository in current directory"
|
||||
cd "$REPO_DIR"
|
||||
else
|
||||
# Clone or update the repository
|
||||
if [[ ! -d "$REPO_DIR" ]]; then
|
||||
print_info "Cloning C/ua repository..."
|
||||
cd "$DEMO_DIR"
|
||||
git clone https://github.com/trycua/cua.git
|
||||
else
|
||||
print_info "Updating C/ua repository..."
|
||||
cd "$REPO_DIR"
|
||||
git pull origin main
|
||||
fi
|
||||
|
||||
cd "$REPO_DIR"
|
||||
fi
|
||||
|
||||
# Create .env.local file with API keys
|
||||
ENV_FILE="$REPO_DIR/.env.local"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
cat > "$ENV_FILE" << EOF
|
||||
# Uncomment and add your API keys here
|
||||
# OPENAI_API_KEY=your_openai_api_key_here
|
||||
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
CUA_API_KEY=your_cua_api_key_here
|
||||
EOF
|
||||
print_success "Created .env.local file with API key placeholders"
|
||||
else
|
||||
print_success "Found existing .env.local file - keeping your current settings"
|
||||
fi
|
||||
|
||||
if [[ "$USE_CLOUD" == "true" ]]; then
|
||||
# Add CUA API key to .env.local if not already present
|
||||
if ! grep -q "CUA_API_KEY" "$ENV_FILE"; then
|
||||
echo "CUA_API_KEY=$CUA_API_KEY" >> "$ENV_FILE"
|
||||
print_success "Added CUA_API_KEY to .env.local"
|
||||
elif grep -q "CUA_API_KEY=your_cua_api_key_here" "$ENV_FILE"; then
|
||||
# Update placeholder with actual key
|
||||
sed -i.bak "s/CUA_API_KEY=your_cua_api_key_here/CUA_API_KEY=$CUA_API_KEY/" "$ENV_FILE"
|
||||
print_success "Updated CUA_API_KEY in .env.local"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build the Docker image if it doesn't exist
|
||||
print_info "Checking Docker image..."
|
||||
if ! docker image inspect cua-dev-image &> /dev/null; then
|
||||
print_info "Building Docker image (this may take a while)..."
|
||||
./scripts/run-docker-dev.sh build
|
||||
else
|
||||
print_success "Docker image already exists"
|
||||
fi
|
||||
|
||||
# Install Lume if needed for local VMs
|
||||
if [[ "$USE_CLOUD" == "false" && "$COMPUTER_TYPE" == "macos" ]]; then
|
||||
if ! command -v lume &> /dev/null; then
|
||||
print_info "Installing Lume CLI..."
|
||||
curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh | bash
|
||||
|
||||
# Add lume to PATH for this session if it's not already there
|
||||
if ! command -v lume &> /dev/null; then
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Pull the macOS CUA image if not already present
|
||||
if ! lume ls | grep -q "macos-sequoia-cua"; then
|
||||
# Check available disk space
|
||||
IMAGE_SIZE_GB=30
|
||||
AVAILABLE_SPACE_KB=$(df -k $HOME | tail -1 | awk '{print $4}')
|
||||
AVAILABLE_SPACE_GB=$(($AVAILABLE_SPACE_KB / 1024 / 1024))
|
||||
|
||||
echo "📊 The macOS CUA image will use approximately ${IMAGE_SIZE_GB}GB of disk space."
|
||||
echo " You currently have ${AVAILABLE_SPACE_GB}GB available on your system."
|
||||
|
||||
# Prompt for confirmation
|
||||
read -p " Continue? [y]/n: " CONTINUE
|
||||
CONTINUE=${CONTINUE:-y}
|
||||
|
||||
if [[ $CONTINUE =~ ^[Yy]$ ]]; then
|
||||
print_info "Pulling macOS CUA image (this may take a while)..."
|
||||
|
||||
# Use caffeinate on macOS to prevent system sleep during the pull
|
||||
if command -v caffeinate &> /dev/null; then
|
||||
print_info "Using caffeinate to prevent system sleep during download..."
|
||||
caffeinate -i lume pull macos-sequoia-cua:latest
|
||||
else
|
||||
lume pull macos-sequoia-cua:latest
|
||||
fi
|
||||
else
|
||||
print_error "Installation cancelled."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if the VM is running
|
||||
print_info "Checking if the macOS CUA VM is running..."
|
||||
VM_RUNNING=$(lume ls | grep "macos-sequoia-cua" | grep "running" || echo "")
|
||||
|
||||
if [ -z "$VM_RUNNING" ]; then
|
||||
print_info "Starting the macOS CUA VM in the background..."
|
||||
lume run macos-sequoia-cua:latest &
|
||||
# Wait a moment for the VM to initialize
|
||||
sleep 5
|
||||
print_success "VM started successfully."
|
||||
else
|
||||
print_success "macOS CUA VM is already running."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create a convenience script to run the demo
|
||||
cat > "$DEMO_DIR/start_ui.sh" << EOF
|
||||
#!/bin/bash
|
||||
cd "$REPO_DIR"
|
||||
./scripts/run-docker-dev.sh run agent_ui_examples.py
|
||||
EOF
|
||||
chmod +x "$DEMO_DIR/start_ui.sh"
|
||||
|
||||
print_success "Setup complete!"
|
||||
|
||||
if [[ "$USE_CLOUD" == "true" ]]; then
|
||||
echo "☁️ C/ua Cloud Container setup complete!"
|
||||
else
|
||||
echo "🖥️ C/ua Local VM setup complete!"
|
||||
fi
|
||||
|
||||
echo "📝 Edit $ENV_FILE to update your API keys"
|
||||
echo "🖥️ Start the playground by running: $DEMO_DIR/start_ui.sh"
|
||||
|
||||
# Start the demo automatically
|
||||
echo
|
||||
print_info "Starting the C/ua Computer-Use Agent UI..."
|
||||
echo ""
|
||||
|
||||
print_success "C/ua Computer-Use Agent UI is now running at http://localhost:7860/"
|
||||
echo
|
||||
echo "🌐 Open your browser and go to: http://localhost:7860/"
|
||||
echo
|
||||
"$DEMO_DIR/start_ui.sh"
|
||||
@@ -209,13 +209,6 @@ echo "📦 Updating C/ua packages..."
|
||||
pip install -U pip setuptools wheel Cmake
|
||||
pip install -U cua-computer "cua-agent[all]"
|
||||
|
||||
# Install mlx-vlm on Apple Silicon Macs
|
||||
if [[ $(uname -m) == 'arm64' ]]; then
|
||||
echo "Installing mlx-vlm for Apple Silicon Macs..."
|
||||
pip install git+https://github.com/Blaizzy/mlx-vlm.git
|
||||
# pip install git+https://github.com/ddupont808/mlx-vlm.git@stable/fix/qwen2-position-id
|
||||
fi
|
||||
|
||||
# Create a simple demo script
|
||||
mkdir -p "$DEMO_DIR"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user