A-Mahla commited on
Commit
3f05344
·
1 Parent(s): ccd68a1

Dockerized App (#4)

Browse files

* ADD generate_instruction

* CHG update_trace_step exception handling

* ADD frontend objets

* ADD Dockerfile

* ADD getApiBaseUrl

* ADD dynamic nb of backend worker

* FIX pre-commit

.dockerignore ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ .pytest_cache/
23
+ .coverage
24
+ htmlcov/
25
+ .tox/
26
+ .venv
27
+ venv/
28
+ ENV/
29
+ env/
30
+
31
+ # Node
32
+ node_modules/
33
+ npm-debug.log*
34
+ yarn-debug.log*
35
+ yarn-error.log*
36
+ .pnpm-debug.log*
37
+ cua2-front/dist/
38
+ cua2-front/build/
39
+
40
+ # IDE
41
+ .vscode/
42
+ .idea/
43
+ *.swp
44
+ *.swo
45
+ *~
46
+ .DS_Store
47
+
48
+ # Git
49
+ .git/
50
+ .gitignore
51
+ .gitattributes
52
+
53
+ # Documentation
54
+ *.md
55
+ !README.md
56
+ docs/
57
+
58
+ # Data and logs
59
+ *.log
60
+ *.pid
61
+ data/
62
+ cua2-core/data/
63
+
64
+ # Environment
65
+ .env
66
+ .env.local
67
+ .env.*.local
68
+
69
+ # Test
70
+ tests/
71
+ test_*.py
72
+ *_test.py
Dockerfile ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Builder
2
+ FROM node:20-alpine AS frontend-builder
3
+
4
+ WORKDIR /app/frontend
5
+
6
+ COPY cua2-front/package*.json ./
7
+
8
+ RUN npm ci
9
+
10
+ COPY cua2-front/ ./
11
+
12
+ RUN npm run build
13
+
14
+ # Stage 2: Production image
15
+ FROM python:3.11-slim
16
+
17
+ RUN apt-get update && apt-get install -y \
18
+ nginx \
19
+ curl \
20
+ procps \
21
+ && rm -rf /var/lib/apt/lists/*
22
+
23
+ WORKDIR /app
24
+
25
+ COPY pyproject.toml uv.lock ./
26
+ COPY cua2-core/ ./cua2-core/
27
+
28
+ RUN pip install --no-cache-dir uv
29
+
30
+ RUN cd /app && uv sync --frozen
31
+
32
+ COPY --from=frontend-builder /app/frontend/dist /app/static
33
+
34
+ COPY nginx.conf /etc/nginx/nginx.conf
35
+
36
+ COPY entrypoint.sh /app/entrypoint.sh
37
+ RUN chmod +x /app/entrypoint.sh
38
+
39
+ EXPOSE 7860
40
+
41
+ ENV PYTHONUNBUFFERED=1
42
+ ENV HOST=0.0.0.0
43
+ ENV PORT=8000
44
+
45
+ # Use entrypoint script
46
+ ENTRYPOINT ["/app/entrypoint.sh"]
Makefile CHANGED
@@ -1,4 +1,4 @@
1
- .PHONY: sync setup install dev-backend dev-frontend dev clean
2
 
3
  # Sync all dependencies (Python + Node.js)
4
  sync:
@@ -38,3 +38,42 @@ clean:
38
  find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
39
  cd cua2-front && rm -rf node_modules dist 2>/dev/null || true
40
  @echo "✓ Cleaned!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: sync setup install dev-backend dev-frontend dev clean docker-build docker-run docker-stop docker-clean docker-logs
2
 
3
  # Sync all dependencies (Python + Node.js)
4
  sync:
 
38
  find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
39
  cd cua2-front && rm -rf node_modules dist 2>/dev/null || true
40
  @echo "✓ Cleaned!"
41
+
42
+ # Docker commands
43
+ docker-build:
44
+ @echo "Building Docker image..."
45
+ make docker-stop
46
+ docker build -t cua2:latest .
47
+ @echo "✓ Docker image built successfully!"
48
+
49
+ docker-run:
50
+ @echo "Starting CUA2 container..."
51
+ @if [ -z "$$E2B_API_KEY" ]; then \
52
+ echo "Error: E2B_API_KEY environment variable is not set"; \
53
+ echo "Please set it with: export E2B_API_KEY=your-key"; \
54
+ exit 1; \
55
+ fi
56
+ @if [ -z "$$HF_TOKEN" ]; then \
57
+ echo "Error: HF_TOKEN environment variable is not set"; \
58
+ echo "Please set it with: export HF_TOKEN=your-token"; \
59
+ exit 1; \
60
+ fi
61
+ docker run -d --name cua2-app -p 7860:7860 \
62
+ -e E2B_API_KEY="$$E2B_API_KEY" \
63
+ -e HF_TOKEN="$$HF_TOKEN" \
64
+ cua2:latest
65
+ @echo "✓ Container started! Access at http://localhost:7860"
66
+
67
+ docker-stop:
68
+ @echo "Stopping CUA2 container..."
69
+ docker stop cua2-app || true
70
+ docker rm cua2-app || true
71
+ @echo "✓ Container stopped!"
72
+
73
+ docker-clean:
74
+ @echo "Removing CUA2 Docker images..."
75
+ docker rmi cua2:latest || true
76
+ @echo "✓ Docker images removed!"
77
+
78
+ docker-logs:
79
+ docker logs -f cua2-app
cua2-core/src/cua2_core/app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from contextlib import asynccontextmanager
2
 
3
  from cua2_core.services.agent_service import AgentService
@@ -17,6 +18,9 @@ async def lifespan(app: FastAPI):
17
  # Startup: Initialize services
18
  print("Initializing services...")
19
 
 
 
 
20
  websocket_manager = WebSocketManager()
21
 
22
  sandbox_service = SandboxService()
 
1
+ import os
2
  from contextlib import asynccontextmanager
3
 
4
  from cua2_core.services.agent_service import AgentService
 
18
  # Startup: Initialize services
19
  print("Initializing services...")
20
 
21
+ if not os.getenv("HF_TOKEN"):
22
+ raise ValueError("HF_TOKEN is not set")
23
+
24
  websocket_manager = WebSocketManager()
25
 
26
  sandbox_service = SandboxService()
cua2-front/src/config/api.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ /**
3
+ * Get the WebSocket URL based on the environment
4
+ * In production (Docker), it uses the current host
5
+ * In development, it uses the configured URL or defaults to localhost:8000
6
+ */
7
+ export const getWebSocketUrl = (): string => {
8
+ // Check if we have a configured WebSocket URL from environment
9
+ const envWsUrl = import.meta.env.VITE_WS_URL;
10
+
11
+ if (envWsUrl) {
12
+ return envWsUrl;
13
+ }
14
+
15
+ // In production (when served from same origin), use relative WebSocket URL
16
+ if (import.meta.env.PROD) {
17
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
18
+ const host = window.location.host;
19
+ return `${protocol}//${host}/ws`;
20
+ }
21
+
22
+ // Development fallback
23
+ return 'ws://localhost:8000/ws';
24
+ };
25
+
26
+ /**
27
+ * Get the base API URL based on the environment
28
+ * In production (Docker), it uses the current host with /api prefix
29
+ * In development, it uses the configured URL or defaults to localhost:8000/api
30
+ */
31
+ export const getApiBaseUrl = (): string => {
32
+ // Check if we have a configured API URL from environment
33
+ const envApiUrl = import.meta.env.VITE_API_URL;
34
+
35
+ if (envApiUrl) {
36
+ return envApiUrl;
37
+ }
38
+
39
+ // In production (when served from same origin), use relative API URL
40
+ if (import.meta.env.PROD) {
41
+ const protocol = window.location.protocol;
42
+ const host = window.location.host;
43
+ return `${protocol}//${host}/api`;
44
+ }
45
+
46
+ // Development fallback
47
+ return 'http://localhost:8000/api';
48
+ };
cua2-front/src/pages/Index.tsx CHANGED
@@ -1,4 +1,5 @@
1
  import { Header, Metadata, StackSteps, VNCStream } from '@/components/mock';
 
2
  import { useWebSocket } from '@/hooks/useWebSocket';
3
  import { AgentStep, AgentTrace, WebSocketEvent } from '@/types/agent';
4
  import { useState } from 'react';
@@ -12,9 +13,8 @@ const Index = () => {
12
 
13
  // #################### WebSocket Connection ########################
14
 
15
- // WebSocket connection - Use environment variable
16
- // const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000/ws';
17
- const WS_URL = 'ws://localhost:8000/ws';
18
 
19
  const handleWebSocketMessage = (event: WebSocketEvent) => {
20
  console.log('WebSocket event received:', event);
 
1
  import { Header, Metadata, StackSteps, VNCStream } from '@/components/mock';
2
+ import { getWebSocketUrl } from '@/config/api';
3
  import { useWebSocket } from '@/hooks/useWebSocket';
4
  import { AgentStep, AgentTrace, WebSocketEvent } from '@/types/agent';
5
  import { useState } from 'react';
 
13
 
14
  // #################### WebSocket Connection ########################
15
 
16
+ // WebSocket connection - Automatically configured based on environment
17
+ const WS_URL = getWebSocketUrl();
 
18
 
19
  const handleWebSocketMessage = (event: WebSocketEvent) => {
20
  console.log('WebSocket event received:', event);
entrypoint.sh ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "Starting CUA2 Application..."
5
+
6
+ service nginx start
7
+
8
+ sleep 2
9
+
10
+ # Check if nginx is running
11
+ if ! pgrep nginx > /dev/null; then
12
+ echo "Error: nginx failed to start"
13
+ exit 1
14
+ fi
15
+
16
+
17
+ cd /app/cua2-core
18
+
19
+ # Set default number of workers if not specified
20
+ WORKERS=${WORKERS:-1}
21
+
22
+ echo "Starting backend with $WORKERS worker(s)..."
23
+
24
+ # Use uv to run the application
25
+ exec uv run uvicorn cua2_core.main:app --host 0.0.0.0 --port 8000 --workers $WORKERS --log-level info
nginx.conf ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ user www-data;
2
+ worker_processes auto;
3
+ pid /run/nginx.pid;
4
+
5
+ events {
6
+ worker_connections 1024;
7
+ }
8
+
9
+ http {
10
+ include /etc/nginx/mime.types;
11
+ default_type application/octet-stream;
12
+
13
+ access_log /var/log/nginx/access.log;
14
+ error_log /var/log/nginx/error.log;
15
+
16
+ sendfile on;
17
+ tcp_nopush on;
18
+ tcp_nodelay on;
19
+ keepalive_timeout 65;
20
+ types_hash_max_size 2048;
21
+
22
+ gzip on;
23
+ gzip_vary on;
24
+ gzip_proxied any;
25
+ gzip_comp_level 6;
26
+ gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
27
+
28
+ server {
29
+ listen 7860;
30
+ server_name _;
31
+
32
+ root /app/static;
33
+ index index.html;
34
+
35
+ location / {
36
+ try_files $uri $uri/ /index.html;
37
+
38
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
39
+ expires 1y;
40
+ add_header Cache-Control "public, immutable";
41
+ }
42
+ }
43
+
44
+ # Proxy API requests to backend
45
+ location /api/ {
46
+ proxy_pass http://127.0.0.1:8000;
47
+ proxy_http_version 1.1;
48
+ proxy_set_header Host $host;
49
+ proxy_set_header X-Real-IP $remote_addr;
50
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
51
+ proxy_set_header X-Forwarded-Proto $scheme;
52
+
53
+ proxy_connect_timeout 60s;
54
+ proxy_send_timeout 60s;
55
+ proxy_read_timeout 60s;
56
+ }
57
+
58
+ # Proxy WebSocket connections
59
+ location /ws {
60
+ proxy_pass http://127.0.0.1:8000;
61
+ proxy_http_version 1.1;
62
+ proxy_set_header Upgrade $http_upgrade;
63
+ proxy_set_header Connection "upgrade";
64
+ proxy_set_header Host $host;
65
+ proxy_set_header X-Real-IP $remote_addr;
66
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
67
+ proxy_set_header X-Forwarded-Proto $scheme;
68
+
69
+ proxy_connect_timeout 7d;
70
+ proxy_send_timeout 7d;
71
+ proxy_read_timeout 7d;
72
+ }
73
+
74
+ # Proxy health check
75
+ location /health {
76
+ proxy_pass http://127.0.0.1:8000;
77
+ proxy_http_version 1.1;
78
+ proxy_set_header Host $host;
79
+ }
80
+
81
+ location /docs {
82
+ proxy_pass http://127.0.0.1:8000;
83
+ proxy_http_version 1.1;
84
+ proxy_set_header Host $host;
85
+ }
86
+
87
+ location /redoc {
88
+ proxy_pass http://127.0.0.1:8000;
89
+ proxy_http_version 1.1;
90
+ proxy_set_header Host $host;
91
+ }
92
+
93
+ location /openapi.json {
94
+ proxy_pass http://127.0.0.1:8000;
95
+ proxy_http_version 1.1;
96
+ proxy_set_header Host $host;
97
+ }
98
+ }
99
+ }