A-Mahla commited on
Commit
304e233
·
1 Parent(s): c9554cf

ADD CUA backbone (#1)

Browse files

* MOCK backend

* MOCK backend

* Fix pre-commit

* Fix pre-commit

* CHG Step models

* FIX pre-commit

* CHG README.md

* FIX pre-commit

* ADD backend route model

* CHG pre-commit

* FIX pre-commit

Files changed (47) hide show
  1. .github/actions/setup-uv/action.yml +16 -0
  2. .github/workflows/pre-commit.yml +34 -0
  3. .gitignore +2 -0
  4. .pre-commit-config.yaml +55 -0
  5. Makefile +32 -0
  6. README.md +224 -0
  7. assets/architecture.png +0 -0
  8. cua2-core/src/cua2_core/app.py +4 -5
  9. cua2-core/src/cua2_core/main.py +1 -1
  10. cua2-core/src/cua2_core/models/__init__.py +0 -1
  11. cua2-core/src/cua2_core/models/models.py +52 -22
  12. cua2-core/src/cua2_core/routes/__init__.py +0 -1
  13. cua2-core/src/cua2_core/routes/routes.py +13 -10
  14. cua2-core/src/cua2_core/routes/websocket.py +14 -11
  15. cua2-core/src/cua2_core/services/__init__.py +0 -1
  16. cua2-core/src/cua2_core/services/agent_service.py +44 -50
  17. cua2-core/src/cua2_core/services/simulation_metadata/simulated_trace.json +3 -4
  18. cua2-core/src/cua2_core/websocket/__init__.py +0 -1
  19. cua2-core/src/cua2_core/websocket/websocket_manager.py +5 -3
  20. cua2-front/eslint.config.js +26 -0
  21. cua2-front/index.html +0 -2
  22. cua2-front/package-lock.json +28 -5
  23. cua2-front/package.json +5 -4
  24. cua2-front/src/App.tsx +0 -1
  25. cua2-front/src/component/poc/ConnectionStatus.tsx +37 -0
  26. cua2-front/src/component/poc/Header.tsx +47 -0
  27. cua2-front/src/component/poc/Metadata.tsx +38 -0
  28. cua2-front/src/component/poc/ProcessingIndicator.tsx +34 -0
  29. cua2-front/src/component/poc/StackSteps.tsx +29 -0
  30. cua2-front/src/component/poc/StepCard.tsx +84 -0
  31. cua2-front/src/component/poc/TaskButton.tsx +76 -0
  32. cua2-front/src/component/poc/VNCStream.tsx +30 -0
  33. cua2-front/src/component/poc/index.ts +8 -0
  34. cua2-front/src/components/mock/ConnectionStatus.tsx +37 -0
  35. cua2-front/src/components/mock/Header.tsx +47 -0
  36. cua2-front/src/components/mock/Metadata.tsx +38 -0
  37. cua2-front/src/components/mock/ProcessingIndicator.tsx +34 -0
  38. cua2-front/src/components/mock/StackSteps.tsx +29 -0
  39. cua2-front/src/components/mock/StepCard.tsx +84 -0
  40. cua2-front/src/components/mock/TaskButton.tsx +76 -0
  41. cua2-front/src/components/mock/VNCStream.tsx +30 -0
  42. cua2-front/src/components/mock/index.ts +8 -0
  43. cua2-front/src/index.css +8 -3
  44. cua2-front/src/pages/Index.tsx +77 -60
  45. cua2-front/src/types/agent.ts +64 -16
  46. cua2-front/tsconfig.app.json +2 -2
  47. pyproject.toml +89 -0
.github/actions/setup-uv/action.yml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 'Setup UV'
2
+ description: 'Install UV and set up the virtual environment'
3
+
4
+ runs:
5
+ using: composite
6
+ steps:
7
+ - name: Install uv
8
+ shell: bash
9
+ run: |
10
+ curl -LsSf https://astral.sh/uv/install.sh | sh
11
+ echo "$HOME/.cargo/bin" >> $GITHUB_PATH
12
+
13
+ - name: Install dependencies
14
+ shell: bash
15
+ run: |
16
+ make sync
.github/workflows/pre-commit.yml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Pre-commit
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ pre-commit:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ submodules: true
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.11"
21
+
22
+ - name: Set up Node.js
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: "20"
26
+ cache: "npm"
27
+ cache-dependency-path: |
28
+ cua2-front/package-lock.json
29
+
30
+ - uses: ./.github/actions/setup-uv
31
+
32
+ - name: Run pre-commit
33
+ run: |
34
+ uv run pre-commit run --all-files --show-diff-on-failure
.gitignore CHANGED
@@ -224,3 +224,5 @@ dist-ssr
224
  *.njsproj
225
  *.sln
226
  *.sw?
 
 
 
224
  *.njsproj
225
  *.sln
226
  *.sw?
227
+
228
+ data/
.pre-commit-config.yaml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.5.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-added-large-files
9
+ - id: check-ast
10
+ - id: check-json
11
+ exclude: ^(frontend/.*\.json|cua2-front/tsconfig.*\.json)$
12
+ - id: check-merge-conflict
13
+ - id: detect-private-key
14
+
15
+ - repo: https://github.com/pycqa/isort
16
+ rev: 5.13.2
17
+ hooks:
18
+ - id: isort
19
+ args: ["--profile", "black"]
20
+
21
+ - repo: https://github.com/charliermarsh/ruff-pre-commit
22
+ rev: v0.9.0
23
+ hooks:
24
+ - id: ruff
25
+ args: [--fix]
26
+ - id: ruff-format
27
+
28
+ - repo: https://github.com/pre-commit/mirrors-mypy
29
+ rev: v1.14.1
30
+ hooks:
31
+ - id: mypy
32
+ additional_dependencies: [types-PyYAML, types-requests]
33
+ args: [--ignore-missing-imports]
34
+
35
+ - repo: https://github.com/codespell-project/codespell
36
+ rev: v2.3.0
37
+ hooks:
38
+ - id: codespell
39
+ args: ["--skip=*.json,*.jsonl,*.txt,*.md,*.ipynb"]
40
+
41
+ - repo: local
42
+ hooks:
43
+ - id: eslint-cua2-front
44
+ name: ESLint Frontend
45
+ entry: bash -c 'cd cua2-front && npx eslint src/ --config eslint.config.js'
46
+ language: system
47
+ files: ^cua2-front/.*\.(ts|tsx|js|jsx)$
48
+ pass_filenames: false
49
+
50
+ - id: typescript-check
51
+ name: TypeScript Type Check
52
+ entry: bash -c 'cd cua2-front && npx tsc --noEmit --project tsconfig.json'
53
+ language: system
54
+ files: ^cua2-front/.*\.(ts|tsx)$
55
+ pass_filenames: false
Makefile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: sync setup install dev-backend dev-frontend dev clean
2
+
3
+ # Sync all dependencies (Python + Node.js)
4
+ sync:
5
+ @echo "Syncing Python dependencies..."
6
+ uv sync --all-extras
7
+ @echo "Installing frontend dependencies..."
8
+ cd cua2-front && npm install
9
+ @echo "✓ All dependencies synced!"
10
+
11
+ setup: sync
12
+
13
+ install-frontend:
14
+ cd cua2-front && npm install
15
+
16
+ # Start backend development server
17
+ dev-backend:
18
+ cd cua2-core && uv run uvicorn cua2_core.main:app --reload --host 0.0.0.0 --port 8000
19
+
20
+ # Start frontend development server
21
+ dev-frontend:
22
+ cd cua2-front && npm run dev
23
+
24
+ pre-commit:
25
+ uv run pre-commit run --all-files --show-diff-on-failure
26
+
27
+ clean:
28
+ find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
29
+ find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
30
+ find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
31
+ cd cua2-front && rm -rf node_modules dist 2>/dev/null || true
32
+ @echo "✓ Cleaned!"
README.md ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CUA2 - Computer Use Agent 2
2
+
3
+ An AI-powered automation interface featuring real-time agent task processing, VNC streaming, and step-by-step execution visualization.
4
+
5
+ ## 🚀 Overview
6
+
7
+ CUA2 is a full-stack application that provides a modern web interface for AI agents to perform automated computer tasks. The system features real-time WebSocket communication between a FastAPI backend and React frontend, allowing users to monitor agent execution, view screenshots, track token usage, and stream VNC sessions.
8
+
9
+ ## 🏗️ Architecture
10
+
11
+ ![CUA2 Architecture](assets/architecture.png)
12
+
13
+ ## 🛠️ Tech Stack
14
+
15
+ ### Backend (`cua2-core`)
16
+ - **FastAPI**
17
+ - **Uvicorn**
18
+ - **smolagents** - AI agent framework with OpenAI/LiteLLM support
19
+
20
+ ### Frontend (`cua2-front`)
21
+ - **React TS**
22
+ - **Vite**
23
+
24
+ ## 📋 Prerequisites
25
+
26
+ - **Python** 3.10 or higher
27
+ - **Node.js** 18 or higher
28
+ - **npm**
29
+ - **uv** - Python package manager
30
+
31
+ ### Installing uv
32
+
33
+ **macOS/Linux:**
34
+ ```bash
35
+ curl -LsSf https://astral.sh/uv/install.sh | sh
36
+ ```
37
+
38
+ For more installation options, visit: https://docs.astral.sh/uv/getting-started/installation/
39
+
40
+
41
+
42
+ ## 🚀 Getting Started
43
+
44
+ ### 1. Clone the Repository
45
+
46
+ ```bash
47
+ git clone https://github.com/huggingface/CUA2.git
48
+ cd CUA2
49
+ ```
50
+
51
+ ### 2. Install Dependencies
52
+
53
+ Use the Makefile for quick setup:
54
+
55
+ ```bash
56
+ make sync
57
+ ```
58
+
59
+ This will:
60
+ - Install Python dependencies using `uv`
61
+ - Install Node.js dependencies for the frontend
62
+
63
+ Or install manually:
64
+
65
+ ```bash
66
+ # Backend dependencies
67
+ cd cua2-core
68
+ uv sync --all-extras
69
+
70
+ # Frontend dependencies
71
+ cd ../cua2-front
72
+ npm install
73
+ ```
74
+
75
+ ### 3. Environment Configuration
76
+
77
+ Copy the example environment file and configure your settings:
78
+
79
+ ```bash
80
+ cd cua2-core
81
+ cp env.example .env
82
+ ```
83
+
84
+ Edit `.env` with your configuration:
85
+ - API keys for OpenAI/LiteLLM
86
+ - Database connections (if applicable)
87
+ - Other service credentials
88
+
89
+ ### 4. Start Development Servers
90
+
91
+ #### Option 1: Using Makefile (Recommended)
92
+
93
+ Open two terminal windows:
94
+
95
+ **Terminal 1 - Backend:**
96
+ ```bash
97
+ make dev-backend
98
+ ```
99
+
100
+ **Terminal 2 - Frontend:**
101
+ ```bash
102
+ make dev-frontend
103
+ ```
104
+
105
+ #### Option 2: Manual Start
106
+
107
+ **Terminal 1 - Backend:**
108
+ ```bash
109
+ cd cua2-core
110
+ uv run uvicorn cua2_core.main:app --reload --host 0.0.0.0 --port 8000
111
+ ```
112
+
113
+ **Terminal 2 - Frontend:**
114
+ ```bash
115
+ cd cua2-front
116
+ npm run dev
117
+ ```
118
+
119
+ ### 5. Access the Application
120
+
121
+ - **Frontend**: http://localhost:5173
122
+ - **Backend API**: http://localhost:8000
123
+ - **API Documentation**: http://localhost:8000/docs
124
+ - **ReDoc**: http://localhost:8000/redoc
125
+
126
+ ## 📁 Project Structure
127
+
128
+ ```
129
+ CUA2/
130
+ ├── cua2-core/ # Backend application
131
+ │ ├── src/
132
+ │ │ └── cua2_core/
133
+ │ │ ├── app.py # FastAPI application setup
134
+ │ │ ├── main.py # Application entry point
135
+ │ │ ├── models/
136
+ │ │ │ └── models.py # Pydantic models
137
+ │ │ ├── routes/
138
+ │ │ │ ├── routes.py # REST API endpoints
139
+ │ │ │ └── websocket.py # WebSocket endpoint
140
+ │ │ ├── services/
141
+ │ │ │ ├── agent_service.py # Agent task processing
142
+ │ │ │ └── simulation_metadata/ # Demo data
143
+ │ │ └── websocket/
144
+ │ │ └── websocket_manager.py # WebSocket management
145
+ │ ├── pyproject.toml # Python dependencies
146
+ │ └── env.example # Environment variables template
147
+
148
+ ├── cua2-front/ # Frontend application
149
+ │ ├── src/
150
+ │ │ ├── App.tsx # Main application component
151
+ │ │ ├── pages/
152
+ │ │ │ └── Index.tsx # Main page
153
+ │ │ ├── components/
154
+ │ │ │ └── mock/ # UI components
155
+ │ │ ├── hooks/
156
+ │ │ │ └── useWebSocket.ts # WebSocket hook
157
+ │ │ └── types/
158
+ │ │ └── agent.ts # TypeScript type definitions
159
+ │ ├── package.json # Node dependencies
160
+ │ └── vite.config.ts # Vite configuration
161
+
162
+ ├── Makefile # Development commands
163
+ └── README.md # This file
164
+ ```
165
+
166
+ ## 🔌 API Endpoints
167
+
168
+ ### REST API
169
+
170
+ | Method | Endpoint | Description |
171
+ |--------|----------|-------------|
172
+ | GET | `/health` | Health check with WebSocket connection count |
173
+ | GET | `/tasks` | Get all active tasks |
174
+ | GET | `/tasks/{task_id}` | Get specific task status |
175
+ | GET | `/docs` | Interactive API documentation (Swagger) |
176
+ | GET | `/redoc` | Alternative API documentation (ReDoc) |
177
+
178
+ ### WebSocket
179
+
180
+
181
+ #### Client → Server Events
182
+
183
+ - `user_task` - New user task request
184
+
185
+ #### Server → Client Events
186
+
187
+ - `agent_start` - Agent begins processing
188
+ - `agent_progress` - New step completed with image and metadata
189
+ - `agent_complete` - Task finished successfully
190
+ - `agent_error` - Error occurred during processing
191
+ - `vnc_url_set` - VNC stream URL available
192
+ - `vnc_url_unset` - VNC stream ended
193
+ - `heartbeat` - Connection keep-alive
194
+
195
+ ## 🧪 Development
196
+
197
+ ### Available Make Commands
198
+
199
+ ```bash
200
+ make sync # Sync all dependencies (Python + Node.js)
201
+ make dev-backend # Start backend development server
202
+ make dev-frontend # Start frontend development server
203
+ make pre-commit # Run pre-commit hooks
204
+ make clean # Clean build artifacts and caches
205
+ ```
206
+
207
+ ### Code Quality
208
+
209
+ ```bash
210
+ # Backend
211
+ make pre-commit
212
+ ```
213
+
214
+ ### Build for Production
215
+
216
+ ```bash
217
+ # Frontend
218
+ cd cua2-front
219
+ npm run build
220
+
221
+ # The build output will be in cua2-front/dist/
222
+ ```
223
+
224
+ **Happy Coding! 🚀**
assets/architecture.png ADDED
cua2-core/src/cua2_core/app.py CHANGED
@@ -1,12 +1,11 @@
1
  from contextlib import asynccontextmanager
2
 
 
 
3
  from dotenv import load_dotenv
4
  from fastapi import FastAPI
5
  from fastapi.middleware.cors import CORSMiddleware
6
 
7
- from cua2_core.services.agent_service import AgentService
8
- from cua2_core.websocket.websocket_manager import WebSocketManager
9
-
10
  # Load environment variables
11
  load_dotenv()
12
 
@@ -39,8 +38,8 @@ async def lifespan(app: FastAPI):
39
 
40
  # Create FastAPI app with lifespan
41
  app = FastAPI(
42
- title="Computer Use Studio Backend",
43
- description="Backend API for Computer Use Studio - AI-powered automation interface",
44
  version="1.0.0",
45
  docs_url="/docs",
46
  redoc_url="/redoc",
 
1
  from contextlib import asynccontextmanager
2
 
3
+ from cua2_core.services.agent_service import AgentService
4
+ from cua2_core.websocket.websocket_manager import WebSocketManager
5
  from dotenv import load_dotenv
6
  from fastapi import FastAPI
7
  from fastapi.middleware.cors import CORSMiddleware
8
 
 
 
 
9
  # Load environment variables
10
  load_dotenv()
11
 
 
38
 
39
  # Create FastAPI app with lifespan
40
  app = FastAPI(
41
+ title="Computer Use Backend",
42
+ description="Backend API for Computer Use - AI-powered automation interface",
43
  version="1.0.0",
44
  docs_url="/docs",
45
  redoc_url="/redoc",
cua2-core/src/cua2_core/main.py CHANGED
@@ -22,7 +22,7 @@ if __name__ == "__main__":
22
  port = int(os.getenv("PORT", 8000))
23
  debug = os.getenv("DEBUG", "false").lower() == "true"
24
 
25
- print(f"Starting Computer Use Studio Backend on {host}:{port}")
26
  print(f"Debug mode: {debug}")
27
  print(f"API Documentation: http://{host}:{port}/docs")
28
  print(f"WebSocket endpoint: ws://{host}:{port}/ws")
 
22
  port = int(os.getenv("PORT", 8000))
23
  debug = os.getenv("DEBUG", "false").lower() == "true"
24
 
25
+ print(f"Starting Computer Use Backend on {host}:{port}")
26
  print(f"Debug mode: {debug}")
27
  print(f"API Documentation: http://{host}:{port}/docs")
28
  print(f"WebSocket endpoint: ws://{host}:{port}/ws")
cua2-core/src/cua2_core/models/__init__.py CHANGED
@@ -1,2 +1 @@
1
  """Models module for CUA2 Core"""
2
-
 
1
  """Models module for CUA2 Core"""
 
cua2-core/src/cua2_core/models/models.py CHANGED
@@ -1,57 +1,68 @@
1
  import json
2
  import os
3
  from datetime import datetime
4
- from enum import Enum
5
- from typing import Annotated, Literal, TypeAlias
6
 
7
  from pydantic import BaseModel, Field, field_serializer, model_validator
 
8
 
9
  #################### Backend -> Frontend ########################
10
 
 
11
  class AgentAction(BaseModel):
12
  """Agent action structure"""
13
 
14
- actionType: Literal["click", "write", "press", "scroll", "wait", "open", "launch_app", "refresh", "go_back"]
 
 
 
 
 
 
 
 
 
 
15
  actionArguments: dict
16
 
17
  def to_string(self) -> str:
18
  """Convert action to a human-readable string"""
19
  action_type = self.actionType
20
  args = self.actionArguments
21
-
22
  if action_type == "click":
23
  x = args.get("x", "?")
24
  y = args.get("y", "?")
25
  return f"Click at coordinates ({x}, {y})"
26
-
27
  elif action_type == "write":
28
  text = args.get("text", "")
29
  return f"Type text: '{text}'"
30
-
31
  elif action_type == "press":
32
  key = args.get("key", "")
33
  return f"Press key: {key}"
34
-
35
  elif action_type == "scroll":
36
  direction = args.get("direction", "down")
37
  amount = args.get("amount", 2)
38
  return f"Scroll {direction} by {amount}"
39
-
40
  elif action_type == "wait":
41
  seconds = args.get("seconds", 0)
42
  return f"Wait for {seconds} seconds"
43
-
44
  elif action_type == "open":
45
  file_or_url = args.get("file_or_url", "")
46
  return f"Open: {file_or_url}"
47
-
48
  elif action_type == "launch_app":
49
  app_name = args.get("app_name", "")
50
  return f"Launch app: {app_name}"
51
-
52
  elif action_type == "refresh":
53
  return "Refresh the current page"
54
-
55
  elif action_type == "go_back":
56
  return "Go back one page"
57
 
@@ -62,19 +73,19 @@ class AgentStep(BaseModel):
62
  traceId: str
63
  stepId: str
64
  image: str
65
- thought: str
66
- actions: list[AgentAction]
67
- timeTaken: float
68
  inputTokensUsed: int
69
  outputTokensUsed: int
70
- timestamp: datetime
71
- step_evaluation: Literal['like', 'dislike', 'neutral']
72
-
73
- @field_serializer('actions')
 
 
74
  def serialize_actions(self, actions: list[AgentAction], _info):
75
  """Convert actions to list of strings when dumping (controlled by context)"""
76
 
77
- if _info.context and _info.context.get('actions_as_json', False):
78
  return [action.model_dump(mode="json") for action in actions]
79
 
80
  return [action.to_string() for action in actions]
@@ -86,8 +97,9 @@ class AgentTraceMetadata(BaseModel):
86
  traceId: str = ""
87
  inputTokensUsed: int = 0
88
  outputTokensUsed: int = 0
89
- timeTaken: float = 0.0 # in seconds
90
  numberOfSteps: int = 0
 
91
 
92
 
93
  class AgentTrace(BaseModel):
@@ -208,7 +220,11 @@ class ActiveTask(BaseModel):
208
  self.traceMetadata.traceId = self.message_id
209
  os.makedirs(self.trace_path, exist_ok=True)
210
  with open(f"{self.trace_path}/tasks.json", "w") as f:
211
- json.dump(self.model_dump(mode="json", context={"actions_as_json": True}), f, indent=2)
 
 
 
 
212
 
213
  return self
214
 
@@ -219,3 +235,17 @@ class HealthResponse(BaseModel):
219
  status: str
220
  timestamp: datetime
221
  websocket_connections: int
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import json
2
  import os
3
  from datetime import datetime
4
+ from typing import Annotated, Literal, Optional
 
5
 
6
  from pydantic import BaseModel, Field, field_serializer, model_validator
7
+ from typing_extensions import TypeAlias
8
 
9
  #################### Backend -> Frontend ########################
10
 
11
+
12
  class AgentAction(BaseModel):
13
  """Agent action structure"""
14
 
15
+ actionType: Literal[
16
+ "click",
17
+ "write",
18
+ "press",
19
+ "scroll",
20
+ "wait",
21
+ "open",
22
+ "launch_app",
23
+ "refresh",
24
+ "go_back",
25
+ ]
26
  actionArguments: dict
27
 
28
  def to_string(self) -> str:
29
  """Convert action to a human-readable string"""
30
  action_type = self.actionType
31
  args = self.actionArguments
32
+
33
  if action_type == "click":
34
  x = args.get("x", "?")
35
  y = args.get("y", "?")
36
  return f"Click at coordinates ({x}, {y})"
37
+
38
  elif action_type == "write":
39
  text = args.get("text", "")
40
  return f"Type text: '{text}'"
41
+
42
  elif action_type == "press":
43
  key = args.get("key", "")
44
  return f"Press key: {key}"
45
+
46
  elif action_type == "scroll":
47
  direction = args.get("direction", "down")
48
  amount = args.get("amount", 2)
49
  return f"Scroll {direction} by {amount}"
50
+
51
  elif action_type == "wait":
52
  seconds = args.get("seconds", 0)
53
  return f"Wait for {seconds} seconds"
54
+
55
  elif action_type == "open":
56
  file_or_url = args.get("file_or_url", "")
57
  return f"Open: {file_or_url}"
58
+
59
  elif action_type == "launch_app":
60
  app_name = args.get("app_name", "")
61
  return f"Launch app: {app_name}"
62
+
63
  elif action_type == "refresh":
64
  return "Refresh the current page"
65
+
66
  elif action_type == "go_back":
67
  return "Go back one page"
68
 
 
73
  traceId: str
74
  stepId: str
75
  image: str
76
+ duration: float
 
 
77
  inputTokensUsed: int
78
  outputTokensUsed: int
79
+ step_evaluation: Literal["like", "dislike", "neutral"]
80
+ error: Optional[str] = None
81
+ thought: Optional[str] = None
82
+ actions: Optional[list[AgentAction]] = None
83
+
84
+ @field_serializer("actions")
85
  def serialize_actions(self, actions: list[AgentAction], _info):
86
  """Convert actions to list of strings when dumping (controlled by context)"""
87
 
88
+ if _info.context and _info.context.get("actions_as_json", False):
89
  return [action.model_dump(mode="json") for action in actions]
90
 
91
  return [action.to_string() for action in actions]
 
97
  traceId: str = ""
98
  inputTokensUsed: int = 0
99
  outputTokensUsed: int = 0
100
+ duration: float = 0.0 # in seconds
101
  numberOfSteps: int = 0
102
+ maxSteps: int = 0
103
 
104
 
105
  class AgentTrace(BaseModel):
 
220
  self.traceMetadata.traceId = self.message_id
221
  os.makedirs(self.trace_path, exist_ok=True)
222
  with open(f"{self.trace_path}/tasks.json", "w") as f:
223
+ json.dump(
224
+ self.model_dump(mode="json", context={"actions_as_json": True}),
225
+ f,
226
+ indent=2,
227
+ )
228
 
229
  return self
230
 
 
235
  status: str
236
  timestamp: datetime
237
  websocket_connections: int
238
+
239
+
240
+ class TaskStatusResponse(BaseModel):
241
+ """Response for a specific task status"""
242
+
243
+ task_id: str
244
+ status: ActiveTask
245
+
246
+
247
+ class ActiveTasksResponse(BaseModel):
248
+ """Response for active tasks"""
249
+
250
+ active_tasks: dict[str, ActiveTask]
251
+ total_connections: int
cua2-core/src/cua2_core/routes/__init__.py CHANGED
@@ -1,2 +1 @@
1
  """Routes module for CUA2 Core"""
2
-
 
1
  """Routes module for CUA2 Core"""
 
cua2-core/src/cua2_core/routes/routes.py CHANGED
@@ -1,11 +1,14 @@
1
  from datetime import datetime
2
 
3
- from fastapi import APIRouter, Depends, HTTPException, Request
4
-
5
  # Get services from app state
6
- from cua2_core.models.models import HealthResponse
 
 
 
 
7
  from cua2_core.services.agent_service import AgentService
8
  from cua2_core.websocket.websocket_manager import WebSocketManager
 
9
 
10
  # Create router
11
  router = APIRouter()
@@ -33,19 +36,19 @@ async def health_check(
33
  )
34
 
35
 
36
- @router.get("/tasks")
37
  async def get_active_tasks(
38
  agent_service: AgentService = Depends(get_agent_service),
39
  websocket_manager: WebSocketManager = Depends(get_websocket_manager),
40
  ):
41
  """Get currently active tasks"""
42
- return {
43
- "active_tasks": agent_service.get_active_tasks(),
44
- "total_connections": websocket_manager.get_connection_count(),
45
- }
46
 
47
 
48
- @router.get("/tasks/{task_id}")
49
  async def get_task_status(
50
  task_id: str, agent_service: AgentService = Depends(get_agent_service)
51
  ):
@@ -53,4 +56,4 @@ async def get_task_status(
53
  task_status = agent_service.get_task_status(task_id)
54
  if task_status is None:
55
  raise HTTPException(status_code=404, detail="Task not found")
56
- return {"task_id": task_id, "status": task_status}
 
1
  from datetime import datetime
2
 
 
 
3
  # Get services from app state
4
+ from cua2_core.models.models import (
5
+ ActiveTasksResponse,
6
+ HealthResponse,
7
+ TaskStatusResponse,
8
+ )
9
  from cua2_core.services.agent_service import AgentService
10
  from cua2_core.websocket.websocket_manager import WebSocketManager
11
+ from fastapi import APIRouter, Depends, HTTPException, Request
12
 
13
  # Create router
14
  router = APIRouter()
 
36
  )
37
 
38
 
39
+ @router.get("/tasks", response_model=ActiveTasksResponse)
40
  async def get_active_tasks(
41
  agent_service: AgentService = Depends(get_agent_service),
42
  websocket_manager: WebSocketManager = Depends(get_websocket_manager),
43
  ):
44
  """Get currently active tasks"""
45
+ return ActiveTasksResponse(
46
+ active_tasks=agent_service.get_active_tasks(),
47
+ total_connections=websocket_manager.get_connection_count(),
48
+ )
49
 
50
 
51
+ @router.get("/tasks/{task_id}", response_model=TaskStatusResponse)
52
  async def get_task_status(
53
  task_id: str, agent_service: AgentService = Depends(get_agent_service)
54
  ):
 
56
  task_status = agent_service.get_task_status(task_id)
57
  if task_status is None:
58
  raise HTTPException(status_code=404, detail="Task not found")
59
+ return TaskStatusResponse(task_id=task_id, status=task_status)
cua2-core/src/cua2_core/routes/websocket.py CHANGED
@@ -1,10 +1,9 @@
1
  import json
2
 
3
- from fastapi import APIRouter, WebSocket, WebSocketDisconnect
4
-
5
  # Get services from app state
6
  from cua2_core.app import app
7
- from cua2_core.models.models import UserTaskMessage, AgentTrace, HeartbeatEvent
 
8
 
9
  # Create router
10
  router = APIRouter()
@@ -34,7 +33,7 @@ async def websocket_endpoint(websocket: WebSocket):
34
  # Parse the message
35
  message_data = json.loads(data)
36
  print(f"Received message: {message_data}")
37
-
38
  # Check if it's a user task message
39
  if message_data.get("type") == "user_task":
40
  # Extract and parse the trace
@@ -43,10 +42,13 @@ async def websocket_endpoint(websocket: WebSocket):
43
  # Convert timestamp string to datetime if needed
44
  if isinstance(trace_data.get("timestamp"), str):
45
  from datetime import datetime
46
- trace_data["timestamp"] = datetime.fromisoformat(trace_data["timestamp"].replace("Z", "+00:00"))
47
-
 
 
 
48
  trace = AgentTrace(**trace_data)
49
-
50
  # Process the user task with the trace
51
  trace_id = await agent_service.process_user_task(trace)
52
  print(f"Started processing trace: {trace_id}")
@@ -56,9 +58,9 @@ async def websocket_endpoint(websocket: WebSocket):
56
  except json.JSONDecodeError as e:
57
  print(f"JSON decode error: {e}")
58
  from cua2_core.models.models import AgentErrorEvent
 
59
  error_response = AgentErrorEvent(
60
- type="agent_error",
61
- error="Invalid JSON format"
62
  )
63
  await websocket_manager.send_personal_message(
64
  error_response, websocket
@@ -67,11 +69,12 @@ async def websocket_endpoint(websocket: WebSocket):
67
  except Exception as e:
68
  print(f"Error processing message: {e}")
69
  import traceback
 
70
  traceback.print_exc()
71
  from cua2_core.models.models import AgentErrorEvent
 
72
  error_response = AgentErrorEvent(
73
- type="agent_error",
74
- error=f"Error processing message: {str(e)}"
75
  )
76
  await websocket_manager.send_personal_message(
77
  error_response, websocket
 
1
  import json
2
 
 
 
3
  # Get services from app state
4
  from cua2_core.app import app
5
+ from cua2_core.models.models import AgentTrace, HeartbeatEvent
6
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
7
 
8
  # Create router
9
  router = APIRouter()
 
33
  # Parse the message
34
  message_data = json.loads(data)
35
  print(f"Received message: {message_data}")
36
+
37
  # Check if it's a user task message
38
  if message_data.get("type") == "user_task":
39
  # Extract and parse the trace
 
42
  # Convert timestamp string to datetime if needed
43
  if isinstance(trace_data.get("timestamp"), str):
44
  from datetime import datetime
45
+
46
+ trace_data["timestamp"] = datetime.fromisoformat(
47
+ trace_data["timestamp"].replace("Z", "+00:00")
48
+ )
49
+
50
  trace = AgentTrace(**trace_data)
51
+
52
  # Process the user task with the trace
53
  trace_id = await agent_service.process_user_task(trace)
54
  print(f"Started processing trace: {trace_id}")
 
58
  except json.JSONDecodeError as e:
59
  print(f"JSON decode error: {e}")
60
  from cua2_core.models.models import AgentErrorEvent
61
+
62
  error_response = AgentErrorEvent(
63
+ type="agent_error", error="Invalid JSON format"
 
64
  )
65
  await websocket_manager.send_personal_message(
66
  error_response, websocket
 
69
  except Exception as e:
70
  print(f"Error processing message: {e}")
71
  import traceback
72
+
73
  traceback.print_exc()
74
  from cua2_core.models.models import AgentErrorEvent
75
+
76
  error_response = AgentErrorEvent(
77
+ type="agent_error", error=f"Error processing message: {str(e)}"
 
78
  )
79
  await websocket_manager.send_personal_message(
80
  error_response, websocket
cua2-core/src/cua2_core/services/__init__.py CHANGED
@@ -1,2 +1 @@
1
  """Services module for CUA2 Core"""
2
-
 
1
  """Services module for CUA2 Core"""
 
cua2-core/src/cua2_core/services/agent_service.py CHANGED
@@ -1,20 +1,19 @@
1
  import asyncio
2
- import json
3
  import base64
4
- from datetime import datetime
5
  from pathlib import Path
6
  from typing import Optional
7
 
8
  from cua2_core.models.models import (
9
  ActiveTask,
10
- AgentTrace,
11
- AgentStep,
12
  AgentAction,
13
- AgentTraceMetadata,
14
- AgentStartEvent,
15
- AgentProgressEvent,
16
  AgentCompleteEvent,
17
  AgentErrorEvent,
 
 
 
 
 
18
  VncUrlSetEvent,
19
  VncUrlUnsetEvent,
20
  )
@@ -27,8 +26,12 @@ class AgentService:
27
  def __init__(self, websocket_manager):
28
  self.active_tasks: dict[str, ActiveTask] = {}
29
  self.websocket_manager: WebSocketManager = websocket_manager
30
- self.simulation_data_path = Path(__file__).parent / "simulation_metadata" / "simulated_trace.json"
31
- self.simulation_images_path = Path(__file__).parent / "simulation_metadata" / "images"
 
 
 
 
32
 
33
  async def process_user_task(self, trace: AgentTrace) -> str:
34
  """Process a user task and return the trace ID"""
@@ -48,59 +51,52 @@ class AgentService:
48
  )
49
 
50
  # Start the agent processing in the background
51
- asyncio.create_task(
52
- self._simulate_agent_processing(trace)
53
- )
54
 
55
  return trace_id
56
 
57
-
58
  async def _simulate_agent_processing(self, trace: AgentTrace):
59
  """Simulate agent processing using simulated_trace.json data"""
60
  trace_id = trace.id
61
-
62
  try:
63
  # Load simulation data
64
- with open(self.simulation_data_path, 'r') as f:
65
  simulation_data = json.load(f)
66
-
67
  # Send agent start event with the initial trace
68
- start_event = AgentStartEvent(
69
- type="agent_start",
70
- agentTrace=trace
71
- )
72
  await self.websocket_manager.broadcast(start_event)
73
 
74
  # mock VNC URL
75
  vnc_url = "https://www.youtube.com/embed/VCutEsRSJ5A?si=PT0ETJ7zIJ9ywhGW"
76
- vnc_set_event = VncUrlSetEvent(
77
- type="vnc_url_set",
78
- vncUrl=vnc_url
79
- )
80
  await self.websocket_manager.broadcast(vnc_set_event)
81
 
82
- trace_metadata = AgentTraceMetadata(traceId=trace_id)
83
-
84
  # Process each step from the simulation data
85
  for step_data in simulation_data["steps"]:
86
  # Wait before sending the next step to simulate processing time
87
- await asyncio.sleep(step_data["timeTaken"])
88
-
89
  # Load and encode the image
90
- image_path = self.simulation_images_path / step_data["image"].split("/")[-1]
91
- with open(image_path, 'rb') as img_file:
 
 
92
  image_bytes = img_file.read()
93
  image_base64 = f"data:image/png;base64,{base64.b64encode(image_bytes).decode('utf-8')}"
94
-
95
  # Convert actions to AgentAction objects
96
  actions = [
97
  AgentAction(
98
  actionType=action["actionType"],
99
- actionArguments=action["actionArguments"]
100
  )
101
  for action in step_data["actions"]
102
  ]
103
-
104
  # Create agent step
105
  agent_step = AgentStep(
106
  traceId=trace_id,
@@ -108,57 +104,55 @@ class AgentService:
108
  image=image_base64,
109
  thought=step_data["thought"],
110
  actions=actions,
111
- timeTaken=step_data["timeTaken"],
 
112
  inputTokensUsed=step_data["inputTokensUsed"],
113
  outputTokensUsed=step_data["outputTokensUsed"],
114
- timestamp=datetime.fromisoformat(step_data["timestamp"].replace("Z", "+00:00")),
115
- step_evaluation=step_data["step_evaluation"]
116
  )
117
 
118
  trace_metadata.numberOfSteps += 1
119
- trace_metadata.timeTaken += step_data["timeTaken"]
120
  trace_metadata.inputTokensUsed += step_data["inputTokensUsed"]
121
  trace_metadata.outputTokensUsed += step_data["outputTokensUsed"]
122
-
123
  # Send progress event
124
  progress_event = AgentProgressEvent(
125
  type="agent_progress",
126
  agentStep=agent_step,
127
- traceMetadata=trace_metadata
128
  )
129
  await self.websocket_manager.broadcast(progress_event)
130
-
131
  # Update active task
132
  self.active_tasks[trace_id].steps.append(agent_step)
133
-
134
  # Unset VNC URL before completion
135
  vnc_unset_event = VncUrlUnsetEvent(type="vnc_url_unset")
136
  await self.websocket_manager.broadcast(vnc_unset_event)
137
-
138
  # Send completion event
139
  complete_event = AgentCompleteEvent(
140
- type="agent_complete",
141
- traceMetadata=trace_metadata
142
  )
143
  await self.websocket_manager.broadcast(complete_event)
144
-
145
  # Update active task with final metadata
146
  self.active_tasks[trace_id].traceMetadata = trace_metadata
147
-
148
  # Clean up after a delay
149
  await asyncio.sleep(1)
150
  if trace_id in self.active_tasks:
151
  del self.active_tasks[trace_id]
152
-
153
  except Exception as e:
154
  print(f"Error in agent simulation: {str(e)}")
155
  # Send error event
156
  error_event = AgentErrorEvent(
157
- type="agent_error",
158
- error=f"Error processing task: {str(e)}"
159
  )
160
  await self.websocket_manager.broadcast(error_event)
161
-
162
  # Clean up
163
  if trace_id in self.active_tasks:
164
  del self.active_tasks[trace_id]
 
1
  import asyncio
 
2
  import base64
3
+ import json
4
  from pathlib import Path
5
  from typing import Optional
6
 
7
  from cua2_core.models.models import (
8
  ActiveTask,
 
 
9
  AgentAction,
 
 
 
10
  AgentCompleteEvent,
11
  AgentErrorEvent,
12
+ AgentProgressEvent,
13
+ AgentStartEvent,
14
+ AgentStep,
15
+ AgentTrace,
16
+ AgentTraceMetadata,
17
  VncUrlSetEvent,
18
  VncUrlUnsetEvent,
19
  )
 
26
  def __init__(self, websocket_manager):
27
  self.active_tasks: dict[str, ActiveTask] = {}
28
  self.websocket_manager: WebSocketManager = websocket_manager
29
+ self.simulation_data_path = (
30
+ Path(__file__).parent / "simulation_metadata" / "simulated_trace.json"
31
+ )
32
+ self.simulation_images_path = (
33
+ Path(__file__).parent / "simulation_metadata" / "images"
34
+ )
35
 
36
  async def process_user_task(self, trace: AgentTrace) -> str:
37
  """Process a user task and return the trace ID"""
 
51
  )
52
 
53
  # Start the agent processing in the background
54
+ asyncio.create_task(self._simulate_agent_processing(trace))
 
 
55
 
56
  return trace_id
57
 
 
58
  async def _simulate_agent_processing(self, trace: AgentTrace):
59
  """Simulate agent processing using simulated_trace.json data"""
60
  trace_id = trace.id
61
+
62
  try:
63
  # Load simulation data
64
+ with open(self.simulation_data_path, "r") as f:
65
  simulation_data = json.load(f)
66
+
67
  # Send agent start event with the initial trace
68
+ start_event = AgentStartEvent(type="agent_start", agentTrace=trace)
 
 
 
69
  await self.websocket_manager.broadcast(start_event)
70
 
71
  # mock VNC URL
72
  vnc_url = "https://www.youtube.com/embed/VCutEsRSJ5A?si=PT0ETJ7zIJ9ywhGW"
73
+ vnc_set_event = VncUrlSetEvent(type="vnc_url_set", vncUrl=vnc_url)
 
 
 
74
  await self.websocket_manager.broadcast(vnc_set_event)
75
 
76
+ trace_metadata = AgentTraceMetadata(traceId=trace_id, maxSteps=20)
77
+
78
  # Process each step from the simulation data
79
  for step_data in simulation_data["steps"]:
80
  # Wait before sending the next step to simulate processing time
81
+ await asyncio.sleep(step_data["duration"])
82
+
83
  # Load and encode the image
84
+ image_path = (
85
+ self.simulation_images_path / step_data["image"].split("/")[-1]
86
+ )
87
+ with open(image_path, "rb") as img_file:
88
  image_bytes = img_file.read()
89
  image_base64 = f"data:image/png;base64,{base64.b64encode(image_bytes).decode('utf-8')}"
90
+
91
  # Convert actions to AgentAction objects
92
  actions = [
93
  AgentAction(
94
  actionType=action["actionType"],
95
+ actionArguments=action["actionArguments"],
96
  )
97
  for action in step_data["actions"]
98
  ]
99
+
100
  # Create agent step
101
  agent_step = AgentStep(
102
  traceId=trace_id,
 
104
  image=image_base64,
105
  thought=step_data["thought"],
106
  actions=actions,
107
+ error="",
108
+ duration=step_data["duration"],
109
  inputTokensUsed=step_data["inputTokensUsed"],
110
  outputTokensUsed=step_data["outputTokensUsed"],
111
+ step_evaluation=step_data["step_evaluation"],
 
112
  )
113
 
114
  trace_metadata.numberOfSteps += 1
115
+ trace_metadata.duration += step_data["duration"]
116
  trace_metadata.inputTokensUsed += step_data["inputTokensUsed"]
117
  trace_metadata.outputTokensUsed += step_data["outputTokensUsed"]
118
+
119
  # Send progress event
120
  progress_event = AgentProgressEvent(
121
  type="agent_progress",
122
  agentStep=agent_step,
123
+ traceMetadata=trace_metadata,
124
  )
125
  await self.websocket_manager.broadcast(progress_event)
126
+
127
  # Update active task
128
  self.active_tasks[trace_id].steps.append(agent_step)
129
+
130
  # Unset VNC URL before completion
131
  vnc_unset_event = VncUrlUnsetEvent(type="vnc_url_unset")
132
  await self.websocket_manager.broadcast(vnc_unset_event)
133
+
134
  # Send completion event
135
  complete_event = AgentCompleteEvent(
136
+ type="agent_complete", traceMetadata=trace_metadata
 
137
  )
138
  await self.websocket_manager.broadcast(complete_event)
139
+
140
  # Update active task with final metadata
141
  self.active_tasks[trace_id].traceMetadata = trace_metadata
142
+
143
  # Clean up after a delay
144
  await asyncio.sleep(1)
145
  if trace_id in self.active_tasks:
146
  del self.active_tasks[trace_id]
147
+
148
  except Exception as e:
149
  print(f"Error in agent simulation: {str(e)}")
150
  # Send error event
151
  error_event = AgentErrorEvent(
152
+ type="agent_error", error=f"Error processing task: {str(e)}"
 
153
  )
154
  await self.websocket_manager.broadcast(error_event)
155
+
156
  # Clean up
157
  if trace_id in self.active_tasks:
158
  del self.active_tasks[trace_id]
cua2-core/src/cua2_core/services/simulation_metadata/simulated_trace.json CHANGED
@@ -13,7 +13,7 @@
13
  }
14
  }
15
  ],
16
- "timeTaken": 2.3,
17
  "inputTokensUsed": 1250,
18
  "outputTokensUsed": 85,
19
  "timestamp": "2025-10-17T14:30:02.300Z",
@@ -32,7 +32,7 @@
32
  }
33
  }
34
  ],
35
- "timeTaken": 1.8,
36
  "inputTokensUsed": 1180,
37
  "outputTokensUsed": 72,
38
  "timestamp": "2025-10-17T14:30:04.100Z",
@@ -51,7 +51,7 @@
51
  }
52
  }
53
  ],
54
- "timeTaken": 1.5,
55
  "inputTokensUsed": 1100,
56
  "outputTokensUsed": 68,
57
  "timestamp": "2025-10-17T14:30:05.600Z",
@@ -59,4 +59,3 @@
59
  }
60
  ]
61
  }
62
-
 
13
  }
14
  }
15
  ],
16
+ "duration": 2.3,
17
  "inputTokensUsed": 1250,
18
  "outputTokensUsed": 85,
19
  "timestamp": "2025-10-17T14:30:02.300Z",
 
32
  }
33
  }
34
  ],
35
+ "duration": 1.8,
36
  "inputTokensUsed": 1180,
37
  "outputTokensUsed": 72,
38
  "timestamp": "2025-10-17T14:30:04.100Z",
 
51
  }
52
  }
53
  ],
54
+ "duration": 1.5,
55
  "inputTokensUsed": 1100,
56
  "outputTokensUsed": 68,
57
  "timestamp": "2025-10-17T14:30:05.600Z",
 
59
  }
60
  ]
61
  }
 
cua2-core/src/cua2_core/websocket/__init__.py CHANGED
@@ -1,2 +1 @@
1
  """WebSocket module for CUA2 Core"""
2
-
 
1
  """WebSocket module for CUA2 Core"""
 
cua2-core/src/cua2_core/websocket/websocket_manager.py CHANGED
@@ -2,9 +2,8 @@ import asyncio
2
  import json
3
  from typing import Dict, Optional, Set
4
 
5
- from fastapi import WebSocket
6
-
7
  from cua2_core.models.models import AgentTraceMetadata, WebSocketEvent
 
8
 
9
 
10
  class WebSocketManager:
@@ -77,7 +76,10 @@ class WebSocketManager:
77
  await self.broadcast(event)
78
 
79
  async def send_agent_complete(
80
- self, content: str, message_id: str, metadata: Optional[AgentTraceMetadata] = None
 
 
 
81
  ):
82
  """Send agent complete event"""
83
  event = WebSocketEvent(
 
2
  import json
3
  from typing import Dict, Optional, Set
4
 
 
 
5
  from cua2_core.models.models import AgentTraceMetadata, WebSocketEvent
6
+ from fastapi import WebSocket
7
 
8
 
9
  class WebSocketManager:
 
76
  await self.broadcast(event)
77
 
78
  async def send_agent_complete(
79
+ self,
80
+ content: str,
81
+ message_id: str,
82
+ metadata: Optional[AgentTraceMetadata] = None,
83
  ):
84
  """Send agent complete event"""
85
  event = WebSocketEvent(
cua2-front/eslint.config.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "typescript-eslint";
6
+
7
+ export default tseslint.config(
8
+ { ignores: ["dist"] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ["**/*.{ts,tsx}"],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ "react-hooks": reactHooks,
18
+ "react-refresh": reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
23
+ "@typescript-eslint/no-unused-vars": "off",
24
+ },
25
+ },
26
+ );
cua2-front/index.html CHANGED
@@ -2,7 +2,6 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>CUA2</title>
8
  </head>
@@ -11,4 +10,3 @@
11
  <script type="module" src="/src/main.tsx"></script>
12
  </body>
13
  </html>
14
-
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>CUA2</title>
7
  </head>
 
10
  <script type="module" src="/src/main.tsx"></script>
11
  </body>
12
  </html>
 
cua2-front/package-lock.json CHANGED
@@ -10,10 +10,11 @@
10
  "dependencies": {
11
  "react": "^18.3.1",
12
  "react-dom": "^18.3.1",
13
- "react-router-dom": "^6.30.1"
 
14
  },
15
  "devDependencies": {
16
- "@eslint/js": "^9.32.0",
17
  "@types/node": "^22.16.5",
18
  "@types/react": "^18.3.23",
19
  "@types/react-dom": "^18.3.7",
@@ -539,9 +540,9 @@
539
  }
540
  },
541
  "node_modules/@eslint/js": {
542
- "version": "9.37.0",
543
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
544
- "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
545
  "dev": true,
546
  "license": "MIT",
547
  "engines": {
@@ -2020,6 +2021,19 @@
2020
  "url": "https://opencollective.com/eslint"
2021
  }
2022
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
2023
  "node_modules/espree": {
2024
  "version": "10.4.0",
2025
  "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
@@ -3027,6 +3041,15 @@
3027
  "typescript": ">=4.8.4 <6.0.0"
3028
  }
3029
  },
 
 
 
 
 
 
 
 
 
3030
  "node_modules/undici-types": {
3031
  "version": "6.21.0",
3032
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
 
10
  "dependencies": {
11
  "react": "^18.3.1",
12
  "react-dom": "^18.3.1",
13
+ "react-router-dom": "^6.30.1",
14
+ "ulid": "^3.0.1"
15
  },
16
  "devDependencies": {
17
+ "@eslint/js": "^9.38.0",
18
  "@types/node": "^22.16.5",
19
  "@types/react": "^18.3.23",
20
  "@types/react-dom": "^18.3.7",
 
540
  }
541
  },
542
  "node_modules/@eslint/js": {
543
+ "version": "9.38.0",
544
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
545
+ "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
546
  "dev": true,
547
  "license": "MIT",
548
  "engines": {
 
2021
  "url": "https://opencollective.com/eslint"
2022
  }
2023
  },
2024
+ "node_modules/eslint/node_modules/@eslint/js": {
2025
+ "version": "9.37.0",
2026
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
2027
+ "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
2028
+ "dev": true,
2029
+ "license": "MIT",
2030
+ "engines": {
2031
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2032
+ },
2033
+ "funding": {
2034
+ "url": "https://eslint.org/donate"
2035
+ }
2036
+ },
2037
  "node_modules/espree": {
2038
  "version": "10.4.0",
2039
  "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
 
3041
  "typescript": ">=4.8.4 <6.0.0"
3042
  }
3043
  },
3044
+ "node_modules/ulid": {
3045
+ "version": "3.0.1",
3046
+ "resolved": "https://registry.npmjs.org/ulid/-/ulid-3.0.1.tgz",
3047
+ "integrity": "sha512-dPJyqPzx8preQhqq24bBG1YNkvigm87K8kVEHCD+ruZg24t6IFEFv00xMWfxcC4djmFtiTLdFuADn4+DOz6R7Q==",
3048
+ "license": "MIT",
3049
+ "bin": {
3050
+ "ulid": "dist/cli.js"
3051
+ }
3052
+ },
3053
  "node_modules/undici-types": {
3054
  "version": "6.21.0",
3055
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
cua2-front/package.json CHANGED
@@ -7,17 +7,18 @@
7
  "dev": "vite",
8
  "build": "vite build",
9
  "build:dev": "vite build --mode development",
10
- "lint": "eslint src/ --config src/eslint.config.js",
11
- "type-check": "tsc --noEmit --project src/tsconfig.json",
12
  "preview": "vite preview"
13
  },
14
  "dependencies": {
15
  "react": "^18.3.1",
16
  "react-router-dom": "^6.30.1",
17
- "react-dom": "^18.3.1"
 
18
  },
19
  "devDependencies": {
20
- "@eslint/js": "^9.32.0",
21
  "@types/node": "^22.16.5",
22
  "@types/react": "^18.3.23",
23
  "@types/react-dom": "^18.3.7",
 
7
  "dev": "vite",
8
  "build": "vite build",
9
  "build:dev": "vite build --mode development",
10
+ "lint": "eslint src/ --config eslint.config.js",
11
+ "type-check": "tsc --noEmit --project tsconfig.json",
12
  "preview": "vite preview"
13
  },
14
  "dependencies": {
15
  "react": "^18.3.1",
16
  "react-router-dom": "^6.30.1",
17
+ "react-dom": "^18.3.1",
18
+ "ulid": "^3.0.1"
19
  },
20
  "devDependencies": {
21
+ "@eslint/js": "^9.38.0",
22
  "@types/node": "^22.16.5",
23
  "@types/react": "^18.3.23",
24
  "@types/react-dom": "^18.3.7",
cua2-front/src/App.tsx CHANGED
@@ -9,7 +9,6 @@ const App = () => (
9
  {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
10
  </Routes>
11
  </BrowserRouter>
12
-
13
  );
14
 
15
  export default App;
 
9
  {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
10
  </Routes>
11
  </BrowserRouter>
 
12
  );
13
 
14
  export default App;
cua2-front/src/component/poc/ConnectionStatus.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface ConnectionStatusProps {
4
+ isConnected: boolean;
5
+ }
6
+
7
+ export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ isConnected }) => {
8
+ return (
9
+ <div style={{
10
+ display: 'flex',
11
+ alignItems: 'center',
12
+ gap: '8px',
13
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
14
+ padding: '8px 16px',
15
+ borderRadius: '20px',
16
+ backdropFilter: 'blur(10px)',
17
+ border: '1px solid rgba(255, 255, 255, 0.3)'
18
+ }}>
19
+ <div style={{
20
+ width: '8px',
21
+ height: '8px',
22
+ borderRadius: '50%',
23
+ backgroundColor: isConnected ? '#10b981' : '#ef4444',
24
+ boxShadow: isConnected ? '0 0 8px #10b981' : '0 0 8px #ef4444',
25
+ animation: isConnected ? 'pulse 2s infinite' : 'none'
26
+ }}></div>
27
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
28
+ <span className="text-xs font-semibold text-white" style={{ lineHeight: '1.2' }}>
29
+ {isConnected ? 'Connected' : 'Disconnected'}
30
+ </span>
31
+ <span className="text-xs text-white" style={{ opacity: 0.7, fontSize: '10px', lineHeight: '1.2' }}>
32
+ WebSocket
33
+ </span>
34
+ </div>
35
+ </div>
36
+ );
37
+ };
cua2-front/src/component/poc/Header.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { ConnectionStatus } from './ConnectionStatus';
3
+ import { ProcessingIndicator } from './ProcessingIndicator';
4
+ import { TaskButton } from './TaskButton';
5
+
6
+ interface HeaderProps {
7
+ isConnected: boolean;
8
+ isAgentProcessing: boolean;
9
+ onSendTask: (content: string, modelId: string) => void;
10
+ }
11
+
12
+ export const Header: React.FC<HeaderProps> = ({ isConnected, isAgentProcessing, onSendTask }) => {
13
+ return (
14
+ <>
15
+ <div style={{
16
+ flexShrink: 0,
17
+ }}>
18
+ <div style={{ maxWidth: '1400px', margin: '0 auto', padding: '20px 32px' }}>
19
+ <div className="flex items-center justify-between">
20
+ <div className="flex items-center gap-6">
21
+ <ConnectionStatus isConnected={isConnected} />
22
+ <h1 className="text-3xl font-bold text-white" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
23
+ CUA2 Agent
24
+ </h1>
25
+ </div>
26
+ <ProcessingIndicator isAgentProcessing={isAgentProcessing} />
27
+ </div>
28
+ <TaskButton
29
+ isAgentProcessing={isAgentProcessing}
30
+ isConnected={isConnected}
31
+ onSendTask={onSendTask}
32
+ />
33
+ </div>
34
+ </div>
35
+
36
+ <style>{`
37
+ @keyframes spin {
38
+ to { transform: rotate(360deg); }
39
+ }
40
+ @keyframes pulse {
41
+ 0%, 100% { opacity: 1; }
42
+ 50% { opacity: 0.5; }
43
+ }
44
+ `}</style>
45
+ </>
46
+ );
47
+ };
cua2-front/src/component/poc/Metadata.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentTrace } from '@/types/agent';
2
+ import React from 'react';
3
+
4
+ interface MetadataProps {
5
+ trace?: AgentTrace;
6
+ }
7
+
8
+ export const Metadata: React.FC<MetadataProps> = ({ trace }) => {
9
+ return (
10
+ <div style={{ flexShrink: 0 }} className="bg-white rounded-lg shadow-md border border-gray-200 p-5">
11
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Metadata</h3>
12
+ {trace?.metadata ? (
13
+ <div style={{ display: 'flex', gap: '12px' }}>
14
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '8px', padding: '12px', border: '1px solid #bfdbfe', boxShadow: '0 2px 4px rgba(59, 130, 246, 0.1)' }}>
15
+ <span style={{ fontSize: '10px', fontWeight: 600, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Time</span>
16
+ <span style={{ fontSize: '24px', fontWeight: 700, color: '#1e40af' }}>{trace.metadata.duration.toFixed(2)}s</span>
17
+ </div>
18
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '8px', padding: '12px', border: '1px solid #bbf7d0', boxShadow: '0 2px 4px rgba(16, 185, 129, 0.1)' }}>
19
+ <span style={{ fontSize: '10px', fontWeight: 600, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>In Tokens</span>
20
+ <span style={{ fontSize: '24px', fontWeight: 700, color: '#166534' }}>{trace.metadata.inputTokensUsed.toLocaleString()}</span>
21
+ </div>
22
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '8px', padding: '12px', border: '1px solid #e9d5ff', boxShadow: '0 2px 4px rgba(139, 92, 246, 0.1)' }}>
23
+ <span style={{ fontSize: '10px', fontWeight: 600, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Out Tokens</span>
24
+ <span style={{ fontSize: '24px', fontWeight: 700, color: '#6b21a8' }}>{trace.metadata.outputTokensUsed.toLocaleString()}</span>
25
+ </div>
26
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff7ed', borderRadius: '8px', padding: '12px', border: '1px solid #fed7aa', boxShadow: '0 2px 4px rgba(249, 115, 22, 0.1)' }}>
27
+ <span style={{ fontSize: '10px', fontWeight: 600, color: '#ea580c', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Steps</span>
28
+ <span style={{ fontSize: '24px', fontWeight: 700, color: '#c2410c' }}>{trace.metadata.numberOfSteps}</span>
29
+ </div>
30
+ </div>
31
+ ) : (
32
+ <div className="text-gray-400 text-sm py-2">
33
+ {trace ? 'Waiting for completion...' : 'No task started yet'}
34
+ </div>
35
+ )}
36
+ </div>
37
+ );
38
+ };
cua2-front/src/component/poc/ProcessingIndicator.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface ProcessingIndicatorProps {
4
+ isAgentProcessing: boolean;
5
+ }
6
+
7
+ export const ProcessingIndicator: React.FC<ProcessingIndicatorProps> = ({ isAgentProcessing }) => {
8
+ if (!isAgentProcessing) return null;
9
+
10
+ return (
11
+ <div style={{
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ gap: '10px',
15
+ padding: '10px 20px',
16
+ backgroundColor: 'rgba(251, 191, 36, 0.2)',
17
+ borderRadius: '10px',
18
+ border: '1px solid rgba(251, 191, 36, 0.4)'
19
+ }}>
20
+ <span style={{
21
+ width: '16px',
22
+ height: '16px',
23
+ border: '2px solid #fbbf24',
24
+ borderTopColor: 'transparent',
25
+ borderRadius: '50%',
26
+ animation: 'spin 1s linear infinite',
27
+ display: 'inline-block'
28
+ }}></span>
29
+ <span style={{ fontSize: '14px', fontWeight: 600, color: '#fbbf24', letterSpacing: '0.5px' }}>
30
+ PROCESSING...
31
+ </span>
32
+ </div>
33
+ );
34
+ };
cua2-front/src/component/poc/StackSteps.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentTrace } from '@/types/agent';
2
+ import React from 'react';
3
+ import { StepCard } from './StepCard';
4
+
5
+ interface StackStepsProps {
6
+ trace?: AgentTrace;
7
+ }
8
+
9
+ export const StackSteps: React.FC<StackStepsProps> = ({ trace }) => {
10
+ return (
11
+ <div style={{ width: '360px', flexShrink: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', marginLeft: '12px', marginTop: '20px', marginBottom: '20px', boxShadow: '0 2px 12px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb' }}>
12
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Stack Steps</h3>
13
+ <div style={{ flex: 1, overflowY: 'auto', minHeight: 0, padding: '16px' }}>
14
+ {trace?.steps && trace.steps.length > 0 ? (
15
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
16
+ {trace.steps.map((step, index) => (
17
+ <StepCard key={step.stepId} step={step} index={index} />
18
+ ))}
19
+ </div>
20
+ ) : (
21
+ <div className="flex flex-col items-center justify-center h-full text-gray-400 p-6 text-center">
22
+ <p className="font-medium">No steps yet</p>
23
+ <p className="text-xs mt-1">Steps will appear as agent progresses</p>
24
+ </div>
25
+ )}
26
+ </div>
27
+ </div>
28
+ );
29
+ };
cua2-front/src/component/poc/StepCard.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentStep } from '@/types/agent';
2
+ import React from 'react';
3
+
4
+ interface StepCardProps {
5
+ step: AgentStep;
6
+ index: number;
7
+ }
8
+
9
+ export const StepCard: React.FC<StepCardProps> = ({ step, index }) => {
10
+ return (
11
+ <div
12
+ key={step.stepId}
13
+ style={{ backgroundColor: '#f9fafb', borderRadius: '8px', border: '1px solid #d1d5db', padding: '12px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)' }}
14
+ className="hover:border-blue-400 transition-all"
15
+ >
16
+ {/* Step Header */}
17
+ <div className="mb-6">
18
+ <span className="text-xs font-bold text-blue-600 uppercase tracking-wide">Step {index + 1}</span>
19
+ <hr style={{ margin: '12px 0', border: 'none', borderTop: '2px solid #d1d5db' }} />
20
+ </div>
21
+
22
+ {/* Step Image */}
23
+ {step.image && (
24
+ <div className="mb-6">
25
+ <div className="rounded-md overflow-hidden border border-gray-300 bg-white" style={{ maxHeight: '140px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
26
+ <img
27
+ src={step.image}
28
+ alt={`Step ${index + 1}`}
29
+ style={{ width: '100%', height: 'auto', maxHeight: '140px', objectFit: 'contain' }}
30
+ />
31
+ </div>
32
+ <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
33
+ </div>
34
+ )}
35
+
36
+ {/* Thought */}
37
+ <div className="mb-6">
38
+ <div className="bg-white rounded-md p-2.5 border border-gray-200">
39
+ <h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
40
+ <span>💭</span>
41
+ <span>Thought</span>
42
+ </h4>
43
+ <p className="text-xs text-gray-600 leading-relaxed">{step.thought}</p>
44
+ </div>
45
+ <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
46
+ </div>
47
+
48
+ {/* Actions */}
49
+ <div className="mb-6">
50
+ <div className="bg-white rounded-md p-2.5 border border-gray-200">
51
+ <h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
52
+ <span>⚡</span>
53
+ <span>Actions</span>
54
+ </h4>
55
+ <ul className="space-y-1" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
56
+ {step.actions.map((action, actionIndex) => (
57
+ <li key={actionIndex} className="text-xs text-gray-600 flex items-start leading-snug">
58
+ <span className="mr-1.5 text-blue-500 flex-shrink-0">→</span>
59
+ <span className="break-words">{action}</span>
60
+ </li>
61
+ ))}
62
+ </ul>
63
+ </div>
64
+ <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
65
+ </div>
66
+
67
+ {/* Step Metadata Footer */}
68
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
69
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bfdbfe' }}>
70
+ <span style={{ fontSize: '9px', fontWeight: 500, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Time</span>
71
+ <span style={{ fontSize: '12px', fontWeight: 700, color: '#1e40af' }}>{step.duration.toFixed(2)}s</span>
72
+ </div>
73
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bbf7d0' }}>
74
+ <span style={{ fontSize: '9px', fontWeight: 500, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.5px' }}>In Tokens</span>
75
+ <span style={{ fontSize: '12px', fontWeight: 700, color: '#166534' }}>{step.inputTokensUsed}</span>
76
+ </div>
77
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #e9d5ff' }}>
78
+ <span style={{ fontSize: '9px', fontWeight: 500, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Out Tokens</span>
79
+ <span style={{ fontSize: '12px', fontWeight: 700, color: '#6b21a8' }}>{step.outputTokensUsed}</span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ );
84
+ };
cua2-front/src/component/poc/TaskButton.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface TaskButtonProps {
4
+ isAgentProcessing: boolean;
5
+ isConnected: boolean;
6
+ onSendTask: (content: string, modelId: string) => void;
7
+ }
8
+
9
+ export const TaskButton: React.FC<TaskButtonProps> = ({ isAgentProcessing, isConnected, onSendTask }) => {
10
+ return (
11
+ <div
12
+ onClick={() => {
13
+ if (!isAgentProcessing && isConnected) {
14
+ onSendTask(
15
+ "Complete the online form by clicking through the required fields",
16
+ "anthropic/claude-sonnet-4-5-20250929"
17
+ );
18
+ }
19
+ }}
20
+ style={{
21
+ marginTop: '16px',
22
+ padding: '14px 18px',
23
+ background: isAgentProcessing || !isConnected
24
+ ? 'rgba(255, 255, 255, 0.1)'
25
+ : 'rgba(255, 255, 255, 0.15)',
26
+ borderRadius: '10px',
27
+ backdropFilter: 'blur(10px)',
28
+ border: '2px solid rgba(0, 0, 0, 0.3)',
29
+ cursor: isAgentProcessing || !isConnected ? 'not-allowed' : 'pointer',
30
+ transition: 'all 0.3s ease',
31
+ opacity: isAgentProcessing || !isConnected ? 0.6 : 1,
32
+ }}
33
+ onMouseEnter={(e) => {
34
+ if (!isAgentProcessing && isConnected) {
35
+ e.currentTarget.style.background = 'rgba(200, 200, 200, 0.3)';
36
+ e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.5)';
37
+ e.currentTarget.style.transform = 'translateY(-2px)';
38
+ e.currentTarget.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.2)';
39
+ }
40
+ }}
41
+ onMouseLeave={(e) => {
42
+ e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
43
+ e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.3)';
44
+ e.currentTarget.style.transform = 'translateY(0)';
45
+ e.currentTarget.style.boxShadow = 'none';
46
+ }}
47
+ >
48
+ <div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>
49
+ <div style={{ flex: 1 }}>
50
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
51
+ <span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.7)', textTransform: 'uppercase', letterSpacing: '1px' }}>Task</span>
52
+ {!isAgentProcessing && isConnected && (
53
+ <span style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.5)', fontStyle: 'italic' }}>
54
+ (click to run)
55
+ </span>
56
+ )}
57
+ </div>
58
+ <p style={{ fontSize: '15px', fontWeight: 500, color: '#1f2937' }}>
59
+ Complete the online form by clicking through the required fields
60
+ </p>
61
+ </div>
62
+ <div style={{
63
+ padding: '8px 16px',
64
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
65
+ borderRadius: '6px',
66
+ border: '1px solid rgba(0, 0, 0, 0.2)'
67
+ }}>
68
+ <span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.6)', textTransform: 'uppercase', letterSpacing: '1px' }}>Model</span>
69
+ <p style={{ fontSize: '12px', fontWeight: 600, color: '#1f2937', marginTop: '2px', whiteSpace: 'nowrap' }}>
70
+ claude-sonnet-4-5-20250929
71
+ </p>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ );
76
+ };
cua2-front/src/component/poc/VNCStream.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface VNCStreamProps {
4
+ vncUrl: string;
5
+ }
6
+
7
+ export const VNCStream: React.FC<VNCStreamProps> = ({ vncUrl }) => {
8
+ return (
9
+ <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb', overflow: 'hidden', padding: '20px' }}>
10
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">VNC Stream</h3>
11
+ <div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
12
+ {vncUrl ? (
13
+ <iframe
14
+ src={vncUrl}
15
+ style={{ width: '100%', height: '100%', border: 'none' }}
16
+ title="VNC Stream"
17
+ />
18
+ ) : (
19
+ <div className="text-gray-400 text-center p-8">
20
+ <svg className="w-16 h-16 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
21
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
22
+ </svg>
23
+ <p className="font-medium">No VNC stream available</p>
24
+ <p className="text-sm mt-1 text-gray-500">Stream will appear when agent starts</p>
25
+ </div>
26
+ )}
27
+ </div>
28
+ </div>
29
+ );
30
+ };
cua2-front/src/component/poc/index.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export { ConnectionStatus } from './ConnectionStatus';
2
+ export { Header } from './Header';
3
+ export { Metadata } from './Metadata';
4
+ export { ProcessingIndicator } from './ProcessingIndicator';
5
+ export { StackSteps } from './StackSteps';
6
+ export { StepCard } from './StepCard';
7
+ export { TaskButton } from './TaskButton';
8
+ export { VNCStream } from './VNCStream';
cua2-front/src/components/mock/ConnectionStatus.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface ConnectionStatusProps {
4
+ isConnected: boolean;
5
+ }
6
+
7
+ export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({ isConnected }) => {
8
+ return (
9
+ <div style={{
10
+ display: 'flex',
11
+ alignItems: 'center',
12
+ gap: '8px',
13
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
14
+ padding: '8px 16px',
15
+ borderRadius: '20px',
16
+ backdropFilter: 'blur(10px)',
17
+ border: '1px solid rgba(255, 255, 255, 0.3)'
18
+ }}>
19
+ <div style={{
20
+ width: '8px',
21
+ height: '8px',
22
+ borderRadius: '50%',
23
+ backgroundColor: isConnected ? '#10b981' : '#ef4444',
24
+ boxShadow: isConnected ? '0 0 8px #10b981' : '0 0 8px #ef4444',
25
+ animation: isConnected ? 'pulse 2s infinite' : 'none'
26
+ }}></div>
27
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
28
+ <span className="text-xs font-semibold text-white" style={{ lineHeight: '1.2' }}>
29
+ {isConnected ? 'Connected' : 'Disconnected'}
30
+ </span>
31
+ <span className="text-xs text-white" style={{ opacity: 0.7, fontSize: '10px', lineHeight: '1.2' }}>
32
+ WebSocket
33
+ </span>
34
+ </div>
35
+ </div>
36
+ );
37
+ };
cua2-front/src/components/mock/Header.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { ConnectionStatus } from './ConnectionStatus';
3
+ import { ProcessingIndicator } from './ProcessingIndicator';
4
+ import { TaskButton } from './TaskButton';
5
+
6
+ interface HeaderProps {
7
+ isConnected: boolean;
8
+ isAgentProcessing: boolean;
9
+ onSendTask: (content: string, modelId: string) => void;
10
+ }
11
+
12
+ export const Header: React.FC<HeaderProps> = ({ isConnected, isAgentProcessing, onSendTask }) => {
13
+ return (
14
+ <>
15
+ <div style={{
16
+ flexShrink: 0,
17
+ }}>
18
+ <div style={{ maxWidth: '1400px', margin: '0 auto', padding: '20px 32px' }}>
19
+ <div className="flex items-center justify-between">
20
+ <div className="flex items-center gap-6">
21
+ <ConnectionStatus isConnected={isConnected} />
22
+ <h1 className="text-3xl font-bold text-white" style={{ textShadow: '0 2px 4px rgba(0, 0, 0, 0.2)' }}>
23
+ CUA2 Agent
24
+ </h1>
25
+ </div>
26
+ <ProcessingIndicator isAgentProcessing={isAgentProcessing} />
27
+ </div>
28
+ <TaskButton
29
+ isAgentProcessing={isAgentProcessing}
30
+ isConnected={isConnected}
31
+ onSendTask={onSendTask}
32
+ />
33
+ </div>
34
+ </div>
35
+
36
+ <style>{`
37
+ @keyframes spin {
38
+ to { transform: rotate(360deg); }
39
+ }
40
+ @keyframes pulse {
41
+ 0%, 100% { opacity: 1; }
42
+ 50% { opacity: 0.5; }
43
+ }
44
+ `}</style>
45
+ </>
46
+ );
47
+ };
cua2-front/src/components/mock/Metadata.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentTrace } from '@/types/agent';
2
+ import React from 'react';
3
+
4
+ interface MetadataProps {
5
+ trace?: AgentTrace;
6
+ }
7
+
8
+ export const Metadata: React.FC<MetadataProps> = ({ trace }) => {
9
+ return (
10
+ <div style={{ flexShrink: 0 }} className="bg-white rounded-lg shadow-md border border-gray-200 p-5">
11
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Metadata</h3>
12
+ {trace?.metadata ? (
13
+ <div style={{ display: 'flex', gap: '12px' }}>
14
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '8px', padding: '12px', border: '1px solid #bfdbfe', boxShadow: '0 2px 4px rgba(59, 130, 246, 0.1)' }}>
15
+ <span style={{ fontSize: '10px', fontWeight: 600, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Time</span>
16
+ <span style={{ fontSize: '24px', fontWeight: 700, color: '#1e40af' }}>{trace.metadata.duration.toFixed(2)}s</span>
17
+ </div>
18
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '8px', padding: '12px', border: '1px solid #bbf7d0', boxShadow: '0 2px 4px rgba(16, 185, 129, 0.1)' }}>
19
+ <span style={{ fontSize: '10px', fontWeight: 600, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>In Tokens</span>
20
+ <span style={{ fontSize: '24px', fontWeight: 700, color: '#166534' }}>{trace.metadata.inputTokensUsed.toLocaleString()}</span>
21
+ </div>
22
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '8px', padding: '12px', border: '1px solid #e9d5ff', boxShadow: '0 2px 4px rgba(139, 92, 246, 0.1)' }}>
23
+ <span style={{ fontSize: '10px', fontWeight: 600, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Out Tokens</span>
24
+ <span style={{ fontSize: '24px', fontWeight: 700, color: '#6b21a8' }}>{trace.metadata.outputTokensUsed.toLocaleString()}</span>
25
+ </div>
26
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff7ed', borderRadius: '8px', padding: '12px', border: '1px solid #fed7aa', boxShadow: '0 2px 4px rgba(249, 115, 22, 0.1)' }}>
27
+ <span style={{ fontSize: '10px', fontWeight: 600, color: '#ea580c', textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: '6px' }}>Total Steps</span>
28
+ <span style={{ fontSize: '24px', fontWeight: 700, color: '#c2410c' }}>{trace.metadata.numberOfSteps}</span>
29
+ </div>
30
+ </div>
31
+ ) : (
32
+ <div className="text-gray-400 text-sm py-2">
33
+ {trace ? 'Waiting for completion...' : 'No task started yet'}
34
+ </div>
35
+ )}
36
+ </div>
37
+ );
38
+ };
cua2-front/src/components/mock/ProcessingIndicator.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface ProcessingIndicatorProps {
4
+ isAgentProcessing: boolean;
5
+ }
6
+
7
+ export const ProcessingIndicator: React.FC<ProcessingIndicatorProps> = ({ isAgentProcessing }) => {
8
+ if (!isAgentProcessing) return null;
9
+
10
+ return (
11
+ <div style={{
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ gap: '10px',
15
+ padding: '10px 20px',
16
+ backgroundColor: 'rgba(251, 191, 36, 0.2)',
17
+ borderRadius: '10px',
18
+ border: '1px solid rgba(251, 191, 36, 0.4)'
19
+ }}>
20
+ <span style={{
21
+ width: '16px',
22
+ height: '16px',
23
+ border: '2px solid #fbbf24',
24
+ borderTopColor: 'transparent',
25
+ borderRadius: '50%',
26
+ animation: 'spin 1s linear infinite',
27
+ display: 'inline-block'
28
+ }}></span>
29
+ <span style={{ fontSize: '14px', fontWeight: 600, color: '#fbbf24', letterSpacing: '0.5px' }}>
30
+ PROCESSING...
31
+ </span>
32
+ </div>
33
+ );
34
+ };
cua2-front/src/components/mock/StackSteps.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { AgentTrace } from '@/types/agent';
3
+ import { StepCard } from './StepCard';
4
+
5
+ interface StackStepsProps {
6
+ trace?: AgentTrace;
7
+ }
8
+
9
+ export const StackSteps: React.FC<StackStepsProps> = ({ trace }) => {
10
+ return (
11
+ <div style={{ width: '360px', flexShrink: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', marginLeft: '12px', marginTop: '20px', marginBottom: '20px', boxShadow: '0 2px 12px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb' }}>
12
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">Stack Steps</h3>
13
+ <div style={{ flex: 1, overflowY: 'auto', minHeight: 0, padding: '16px' }}>
14
+ {trace?.steps && trace.steps.length > 0 ? (
15
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
16
+ {trace.steps.map((step, index) => (
17
+ <StepCard key={step.stepId} step={step} index={index} />
18
+ ))}
19
+ </div>
20
+ ) : (
21
+ <div className="flex flex-col items-center justify-center h-full text-gray-400 p-6 text-center">
22
+ <p className="font-medium">No steps yet</p>
23
+ <p className="text-xs mt-1">Steps will appear as agent progresses</p>
24
+ </div>
25
+ )}
26
+ </div>
27
+ </div>
28
+ );
29
+ };
cua2-front/src/components/mock/StepCard.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AgentStep } from '@/types/agent';
2
+ import React from 'react';
3
+
4
+ interface StepCardProps {
5
+ step: AgentStep;
6
+ index: number;
7
+ }
8
+
9
+ export const StepCard: React.FC<StepCardProps> = ({ step, index }) => {
10
+ return (
11
+ <div
12
+ key={step.stepId}
13
+ style={{ backgroundColor: '#f9fafb', borderRadius: '8px', border: '1px solid #d1d5db', padding: '12px', boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)' }}
14
+ className="hover:border-blue-400 transition-all"
15
+ >
16
+ {/* Step Header */}
17
+ <div className="mb-6">
18
+ <span className="text-xs font-bold text-blue-600 uppercase tracking-wide">Step {index + 1}</span>
19
+ <hr style={{ margin: '12px 0', border: 'none', borderTop: '2px solid #d1d5db' }} />
20
+ </div>
21
+
22
+ {/* Step Image */}
23
+ {step.image && (
24
+ <div className="mb-6">
25
+ <div className="rounded-md overflow-hidden border border-gray-300 bg-white" style={{ maxHeight: '140px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
26
+ <img
27
+ src={step.image}
28
+ alt={`Step ${index + 1}`}
29
+ style={{ width: '100%', height: 'auto', maxHeight: '140px', objectFit: 'contain' }}
30
+ />
31
+ </div>
32
+ <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
33
+ </div>
34
+ )}
35
+
36
+ {/* Thought */}
37
+ <div className="mb-6">
38
+ <div className="bg-white rounded-md p-2.5 border border-gray-200">
39
+ <h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
40
+ <span>💭</span>
41
+ <span>Thought</span>
42
+ </h4>
43
+ <p className="text-xs text-gray-600 leading-relaxed">{step.thought}</p>
44
+ </div>
45
+ <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
46
+ </div>
47
+
48
+ {/* Actions */}
49
+ <div className="mb-6">
50
+ <div className="bg-white rounded-md p-2.5 border border-gray-200">
51
+ <h4 className="text-xs font-semibold text-gray-700 mb-1.5 flex items-center gap-1">
52
+ <span>⚡</span>
53
+ <span>Actions</span>
54
+ </h4>
55
+ <ul className="space-y-1" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
56
+ {step.actions.map((action, actionIndex) => (
57
+ <li key={actionIndex} className="text-xs text-gray-600 flex items-start leading-snug">
58
+ <span className="mr-1.5 text-blue-500 flex-shrink-0">→</span>
59
+ <span className="break-words">{action}</span>
60
+ </li>
61
+ ))}
62
+ </ul>
63
+ </div>
64
+ <hr style={{ margin: '20px 0', border: 'none', borderTop: '1px solid #e5e7eb' }} />
65
+ </div>
66
+
67
+ {/* Step Metadata Footer */}
68
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
69
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#eff6ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bfdbfe' }}>
70
+ <span style={{ fontSize: '9px', fontWeight: 500, color: '#2563eb', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Time</span>
71
+ <span style={{ fontSize: '12px', fontWeight: 700, color: '#1e40af' }}>{step.duration.toFixed(2)}s</span>
72
+ </div>
73
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#f0fdf4', borderRadius: '6px', padding: '6px 8px', border: '1px solid #bbf7d0' }}>
74
+ <span style={{ fontSize: '9px', fontWeight: 500, color: '#059669', textTransform: 'uppercase', letterSpacing: '0.5px' }}>In Tokens</span>
75
+ <span style={{ fontSize: '12px', fontWeight: 700, color: '#166534' }}>{step.inputTokensUsed}</span>
76
+ </div>
77
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', backgroundColor: '#faf5ff', borderRadius: '6px', padding: '6px 8px', border: '1px solid #e9d5ff' }}>
78
+ <span style={{ fontSize: '9px', fontWeight: 500, color: '#9333ea', textTransform: 'uppercase', letterSpacing: '0.5px' }}>Out Tokens</span>
79
+ <span style={{ fontSize: '12px', fontWeight: 700, color: '#6b21a8' }}>{step.outputTokensUsed}</span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ );
84
+ };
cua2-front/src/components/mock/TaskButton.tsx ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface TaskButtonProps {
4
+ isAgentProcessing: boolean;
5
+ isConnected: boolean;
6
+ onSendTask: (content: string, modelId: string) => void;
7
+ }
8
+
9
+ export const TaskButton: React.FC<TaskButtonProps> = ({ isAgentProcessing, isConnected, onSendTask }) => {
10
+ return (
11
+ <div
12
+ onClick={() => {
13
+ if (!isAgentProcessing && isConnected) {
14
+ onSendTask(
15
+ "Complete the online form by clicking through the required fields",
16
+ "anthropic/claude-sonnet-4-5-20250929"
17
+ );
18
+ }
19
+ }}
20
+ style={{
21
+ marginTop: '16px',
22
+ padding: '14px 18px',
23
+ background: isAgentProcessing || !isConnected
24
+ ? 'rgba(255, 255, 255, 0.1)'
25
+ : 'rgba(255, 255, 255, 0.15)',
26
+ borderRadius: '10px',
27
+ backdropFilter: 'blur(10px)',
28
+ border: '2px solid rgba(0, 0, 0, 0.3)',
29
+ cursor: isAgentProcessing || !isConnected ? 'not-allowed' : 'pointer',
30
+ transition: 'all 0.3s ease',
31
+ opacity: isAgentProcessing || !isConnected ? 0.6 : 1,
32
+ }}
33
+ onMouseEnter={(e) => {
34
+ if (!isAgentProcessing && isConnected) {
35
+ e.currentTarget.style.background = 'rgba(200, 200, 200, 0.3)';
36
+ e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.5)';
37
+ e.currentTarget.style.transform = 'translateY(-2px)';
38
+ e.currentTarget.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.2)';
39
+ }
40
+ }}
41
+ onMouseLeave={(e) => {
42
+ e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
43
+ e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.3)';
44
+ e.currentTarget.style.transform = 'translateY(0)';
45
+ e.currentTarget.style.boxShadow = 'none';
46
+ }}
47
+ >
48
+ <div style={{ display: 'flex', gap: '24px', alignItems: 'center' }}>
49
+ <div style={{ flex: 1 }}>
50
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
51
+ <span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.7)', textTransform: 'uppercase', letterSpacing: '1px' }}>Task</span>
52
+ {!isAgentProcessing && isConnected && (
53
+ <span style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.5)', fontStyle: 'italic' }}>
54
+ (click to run)
55
+ </span>
56
+ )}
57
+ </div>
58
+ <p style={{ fontSize: '15px', fontWeight: 500, color: '#1f2937' }}>
59
+ Complete the online form by clicking through the required fields
60
+ </p>
61
+ </div>
62
+ <div style={{
63
+ padding: '8px 16px',
64
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
65
+ borderRadius: '6px',
66
+ border: '1px solid rgba(0, 0, 0, 0.2)'
67
+ }}>
68
+ <span style={{ fontSize: '11px', fontWeight: 600, color: 'rgba(0, 0, 0, 0.6)', textTransform: 'uppercase', letterSpacing: '1px' }}>Model</span>
69
+ <p style={{ fontSize: '12px', fontWeight: 600, color: '#1f2937', marginTop: '2px', whiteSpace: 'nowrap' }}>
70
+ claude-sonnet-4-5-20250929
71
+ </p>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ );
76
+ };
cua2-front/src/components/mock/VNCStream.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface VNCStreamProps {
4
+ vncUrl: string;
5
+ }
6
+
7
+ export const VNCStream: React.FC<VNCStreamProps> = ({ vncUrl }) => {
8
+ return (
9
+ <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', backgroundColor: 'white', borderRadius: '10px', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)', border: '1px solid #e5e7eb', overflow: 'hidden', padding: '20px' }}>
10
+ <h3 className="text-lg font-semibold text-gray-800 mb-4">VNC Stream</h3>
11
+ <div style={{ flex: 1, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
12
+ {vncUrl ? (
13
+ <iframe
14
+ src={vncUrl}
15
+ style={{ width: '100%', height: '100%', border: 'none' }}
16
+ title="VNC Stream"
17
+ />
18
+ ) : (
19
+ <div className="text-gray-400 text-center p-8">
20
+ <svg className="w-16 h-16 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
21
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
22
+ </svg>
23
+ <p className="font-medium">No VNC stream available</p>
24
+ <p className="text-sm mt-1 text-gray-500">Stream will appear when agent starts</p>
25
+ </div>
26
+ )}
27
+ </div>
28
+ </div>
29
+ );
30
+ };
cua2-front/src/components/mock/index.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ export { ConnectionStatus } from './ConnectionStatus';
2
+ export { ProcessingIndicator } from './ProcessingIndicator';
3
+ export { TaskButton } from './TaskButton';
4
+ export { Header } from './Header';
5
+ export { VNCStream } from './VNCStream';
6
+ export { Metadata } from './Metadata';
7
+ export { StepCard } from './StepCard';
8
+ export { StackSteps } from './StackSteps';
cua2-front/src/index.css CHANGED
@@ -4,8 +4,12 @@
4
  box-sizing: border-box;
5
  }
6
 
7
- body {
8
  margin: 0;
 
 
 
 
9
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
10
  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
11
  sans-serif;
@@ -15,6 +19,7 @@ body {
15
 
16
  #root {
17
  width: 100%;
18
- height: 100vh;
 
 
19
  }
20
-
 
4
  box-sizing: border-box;
5
  }
6
 
7
+ html, body {
8
  margin: 0;
9
+ padding: 0;
10
+ height: 100%;
11
+ width: 100%;
12
+ overflow: hidden;
13
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
14
  'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
15
  sans-serif;
 
19
 
20
  #root {
21
  width: 100%;
22
+ height: 100%;
23
+ display: flex;
24
+ flex-direction: column;
25
  }
 
cua2-front/src/pages/Index.tsx CHANGED
@@ -1,14 +1,20 @@
1
  import React from 'react';
2
  import { useWebSocket } from '@/hooks/useWebSocket';
3
- import { AgentMessage, WebSocketEvent } from '@/types/agent';
4
- import { useEffect, useState } from 'react';
 
 
 
5
 
6
  const Index = () => {
7
- const [messages, setMessages] = useState<AgentMessage[]>([]);
8
  const [isAgentProcessing, setIsAgentProcessing] = useState(false);
9
  const [vncUrl, setVncUrl] = useState<string>('');
 
10
 
11
- // WebSocket connection - Use environment variable for flexibility across environments
 
 
12
  // const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000/ws';
13
  const WS_URL = 'ws://localhost:8000/ws';
14
 
@@ -18,71 +24,59 @@ const Index = () => {
18
  switch (event.type) {
19
  case 'agent_start':
20
  setIsAgentProcessing(true);
21
- if (event.content) {
22
- const newMessage: AgentMessage = {
23
- id: event.messageId,
24
- type: 'agent',
25
- instructions: event.instructions,
26
- modelId: event.modelId,
27
- timestamp: new Date(),
28
- isLoading: true,
29
- };
30
- setMessages(prev => [...prev, newMessage]);
31
- }
32
  break;
33
 
34
  case 'agent_progress':
35
- if (event.messageId && event.agentStep) {
36
- // Add new step from a agent trace run with image, generated text, actions, tokens and timestamp
37
- setMessages(prev =>
38
- prev.map(msg => {
39
- if (msg.id === event.agentStep.messageId) {
40
- const existingSteps = msg.steps || [];
41
- const stepExists = existingSteps.some(step => step.stepId === event.agentStep.stepId);
42
-
43
- if (!stepExists) {
44
- return { ...msg, steps: [...existingSteps, event.agentStep], isLoading: true };
45
- }
46
- return msg;
47
- }
48
- return msg;
49
- })
50
- );
51
- }
52
  break;
53
 
54
  case 'agent_complete':
55
  setIsAgentProcessing(false);
56
- if (event.messageId && event.metadata) {
57
- setMessages(prev =>
58
- prev.map(msg =>
59
- msg.id === event.metadata.messageId
60
- ? {
61
- ...msg,
62
- isLoading: false,
63
- metadata: event.metadata,
64
- }
65
- : msg
66
- )
67
- );
68
- }
69
  break;
70
 
71
  case 'agent_error':
72
  setIsAgentProcessing(false);
73
  // TODO: Handle agent error
 
74
  break;
75
 
76
  case 'vnc_url_set':
77
- if (event.vncUrl) {
78
- setVncUrl(event.vncUrl);
79
- }
80
  // TODO: Handle VNC URL set
 
81
  break;
82
 
83
  case 'vnc_url_unset':
84
  setVncUrl('');
85
  // TODO: Handle VNC URL unset
 
86
  break;
87
 
88
  case 'heartbeat':
@@ -92,7 +86,7 @@ const Index = () => {
92
  };
93
 
94
  const handleWebSocketError = () => {
95
- // Error handling is now throttled in the WebSocket hook
96
 
97
  };
98
 
@@ -102,29 +96,52 @@ const Index = () => {
102
  onError: handleWebSocketError,
103
  });
104
 
105
- const handleSendMessage = (content: string) => {
106
- const userMessage: AgentMessage = {
107
- id: Date.now().toString(),
108
- type: 'user',
109
- content,
 
 
 
 
 
 
110
  timestamp: new Date(),
 
111
  };
112
 
113
- setMessages(prev => [...prev, userMessage]);
114
 
115
  // Send message to Python backend via WebSocket
116
  sendMessage({
117
  type: 'user_task',
118
- content,
119
- model_id: "anthropic/claude-sonnet-4-5-20250929",
120
- timestamp: new Date().toISOString(),
121
  });
122
  };
123
 
 
124
 
125
  return (
126
- <div>
127
- <h1>Hello World</h1>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  </div>
129
  );
130
  };
 
1
  import React from 'react';
2
  import { useWebSocket } from '@/hooks/useWebSocket';
3
+ import { WebSocketEvent } from '@/types/agent';
4
+ import { useState } from 'react';
5
+ import { AgentTrace, AgentStep } from '@/types/agent';
6
+ import { ulid } from 'ulid';
7
+ import { Header, VNCStream, Metadata, StackSteps } from '@/components/mock';
8
 
9
  const Index = () => {
10
+ const [trace, setTrace] = useState<AgentTrace>();
11
  const [isAgentProcessing, setIsAgentProcessing] = useState(false);
12
  const [vncUrl, setVncUrl] = useState<string>('');
13
+ const [selectedModelId, setSelectedModelId] = useState<string>("claude-sonnet-4-5-20250929");
14
 
15
+ // #################### WebSocket Connection ########################
16
+
17
+ // WebSocket connection - Use environment variable
18
  // const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000/ws';
19
  const WS_URL = 'ws://localhost:8000/ws';
20
 
 
24
  switch (event.type) {
25
  case 'agent_start':
26
  setIsAgentProcessing(true);
27
+ setTrace(event.agentTrace);
28
+ console.log('Agent start received:', event.agentTrace);
 
 
 
 
 
 
 
 
 
29
  break;
30
 
31
  case 'agent_progress':
32
+ // Add new step from a agent trace run with image, generated text, actions, tokens and timestamp
33
+ setTrace(prev => {
34
+ const existingSteps = prev?.steps || [] as AgentStep[];
35
+ const stepExists = existingSteps.some(step => step.stepId === event.agentStep.stepId);
36
+
37
+ if (!stepExists) {
38
+ return {
39
+ ...prev,
40
+ steps: [...existingSteps, event.agentStep],
41
+ traceMetadata: event.traceMetadata,
42
+ isRunning: true
43
+ };
44
+ }
45
+ return prev;
46
+ });
47
+ console.log('Agent progress received:', event.agentStep);
 
48
  break;
49
 
50
  case 'agent_complete':
51
  setIsAgentProcessing(false);
52
+ setTrace(trace => {
53
+ return trace.id === event.traceMetadata.traceId
54
+ ? {
55
+ ...trace,
56
+ isRunning: false,
57
+ metadata: event.traceMetadata,
58
+ }
59
+ : trace;
60
+ });
61
+ console.log('Agent complete received:', event.traceMetadata);
 
 
 
62
  break;
63
 
64
  case 'agent_error':
65
  setIsAgentProcessing(false);
66
  // TODO: Handle agent error
67
+ console.log('Agent error received:', event.error);
68
  break;
69
 
70
  case 'vnc_url_set':
71
+ setVncUrl(event.vncUrl);
 
 
72
  // TODO: Handle VNC URL set
73
+ console.log('VNC URL set received:', event.vncUrl);
74
  break;
75
 
76
  case 'vnc_url_unset':
77
  setVncUrl('');
78
  // TODO: Handle VNC URL unset
79
+ console.log('VNC URL unset received:');
80
  break;
81
 
82
  case 'heartbeat':
 
86
  };
87
 
88
  const handleWebSocketError = () => {
89
+ // WebSocket Frontend Error handling
90
 
91
  };
92
 
 
96
  onError: handleWebSocketError,
97
  });
98
 
99
+ // #################### Frontend Functionality ########################
100
+
101
+ const handleModelId = (modelId: string) => {
102
+ setSelectedModelId(modelId);
103
+ };
104
+
105
+ const handleSendNewTask = (content: string, modelId: string) => {
106
+ const trace: AgentTrace = {
107
+ id: ulid(),
108
+ instruction: content,
109
+ modelId: selectedModelId,
110
  timestamp: new Date(),
111
+ isRunning: true,
112
  };
113
 
114
+ setTrace(trace);
115
 
116
  // Send message to Python backend via WebSocket
117
  sendMessage({
118
  type: 'user_task',
119
+ trace: trace,
 
 
120
  });
121
  };
122
 
123
+ // #################### Mock Frontend Rendering ########################
124
 
125
  return (
126
+ <div style={{ height: '100%', width: '100%', display: 'flex', flexDirection: 'column', backgroundColor: '#f3f4f6' }}>
127
+ <Header
128
+ isConnected={isConnected}
129
+ isAgentProcessing={isAgentProcessing}
130
+ onSendTask={handleSendNewTask}
131
+ />
132
+
133
+ <div style={{ flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', overflow: 'hidden', minHeight: 0, padding: '32px' }}>
134
+ <div style={{ width: '100%', height: '100%', maxWidth: '1400px', maxHeight: '900px', display: 'flex', flexDirection: 'row', overflow: 'hidden' }}>
135
+ {/* Left Side: VNC Stream + Metadata */}
136
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', padding: '20px 12px', gap: '20px', minWidth: 0 }}>
137
+ <VNCStream vncUrl={vncUrl} />
138
+ <Metadata trace={trace} />
139
+ </div>
140
+
141
+ {/* Right Side: Stack Steps */}
142
+ <StackSteps trace={trace} />
143
+ </div>
144
+ </div>
145
  </div>
146
  );
147
  };
cua2-front/src/types/agent.ts CHANGED
@@ -1,36 +1,84 @@
1
- export interface AgentMessage {
 
2
  id: string;
3
- type: 'user' | 'agent';
4
  timestamp: Date;
5
- instructions: string;
6
  modelId: string;
 
7
  steps?: AgentStep[];
8
- metadata?: AgentMetadata;
9
- isLoading?: boolean;
10
  }
11
 
12
  export interface AgentStep {
13
- messageId: string;
14
  stepId: string;
 
15
  image: string;
16
- generatedText: string;
17
  actions: string[];
 
18
  inputTokensUsed: number;
19
  outputTokensUsed: number;
20
- timestamp: Date;
21
  }
22
 
23
- export interface AgentMetadata {
24
- messageId: string;
25
  inputTokensUsed: number;
26
  outputTokensUsed: number;
27
- timeTaken: number;
28
  numberOfSteps: number;
29
  }
30
 
31
- export interface WebSocketEvent {
32
- type: 'agent_start' | 'agent_progress' | 'agent_complete' | 'agent_error' | 'vnc_url_set' | 'vnc_url_unset' | 'heartbeat';
33
- agentStep?: AgentStep;
34
- metadata?: AgentMetadata;
35
- vncUrl?: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
 
1
+
2
+ export interface AgentTrace {
3
  id: string;
 
4
  timestamp: Date;
5
+ instruction: string;
6
  modelId: string;
7
+ isRunning: boolean;
8
  steps?: AgentStep[];
9
+ metadata?: AgentTraceMetadata;
 
10
  }
11
 
12
  export interface AgentStep {
13
+ traceId: string;
14
  stepId: string;
15
+ error: string;
16
  image: string;
17
+ thought: string;
18
  actions: string[];
19
+ duration: number;
20
  inputTokensUsed: number;
21
  outputTokensUsed: number;
22
+ step_evaluation: 'like' | 'dislike' | 'neutral';
23
  }
24
 
25
+ export interface AgentTraceMetadata {
26
+ traceId: string;
27
  inputTokensUsed: number;
28
  outputTokensUsed: number;
29
+ duration: number;
30
  numberOfSteps: number;
31
  }
32
 
33
+ // #################### WebSocket Events Types - Server to Client ########################
34
+
35
+ interface AgentStartEvent {
36
+ type: 'agent_start';
37
+ agentTrace: AgentTrace;
38
+ }
39
+
40
+ interface AgentProgressEvent {
41
+ type: 'agent_progress';
42
+ agentStep: AgentStep;
43
+ traceMetadata: AgentTraceMetadata;
44
+ }
45
+
46
+ interface AgentCompleteEvent {
47
+ type: 'agent_complete';
48
+ traceMetadata: AgentTraceMetadata;
49
+ }
50
+
51
+ interface AgentErrorEvent {
52
+ type: 'agent_error';
53
+ error: string;
54
+ }
55
+
56
+ interface VncUrlSetEvent {
57
+ type: 'vnc_url_set';
58
+ vncUrl: string;
59
+ }
60
+
61
+ interface VncUrlUnsetEvent {
62
+ type: 'vnc_url_unset';
63
+ }
64
+
65
+ interface HeartbeatEvent {
66
+ type: 'heartbeat';
67
+ }
68
+
69
+ export type WebSocketEvent =
70
+ | AgentStartEvent
71
+ | AgentProgressEvent
72
+ | AgentCompleteEvent
73
+ | AgentErrorEvent
74
+ | VncUrlSetEvent
75
+ | VncUrlUnsetEvent
76
+ | HeartbeatEvent;
77
+
78
+ // #################### User Task Message Type (Through WebSocket) - Client to Server ########################
79
+
80
+
81
+ export interface UserTaskMessage {
82
+ type: 'user_task';
83
+ trace: AgentTrace;
84
  }
cua2-front/tsconfig.app.json CHANGED
@@ -30,6 +30,6 @@
30
  }
31
  },
32
  "include": [
33
- "src",
34
  ]
35
- }
 
30
  }
31
  },
32
  "include": [
33
+ "src"
34
  ]
35
+ }
pyproject.toml ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cua2-workspace"
7
+ version = "0.0.0-dev.0"
8
+ description = "CUA2 Workspace - A comprehensive platform for AI automation with desktop environments"
9
+ # readme = "README.md"
10
+ authors = [{ name = "Amir Mahla", email = "amir.mahla@icloud.com" }]
11
+ keywords = ["docker", "automation", "gui", "sandbox", "desktop", "playwright", "ai", "workspace"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Testing",
22
+ "Topic :: System :: Emulators",
23
+ "Topic :: Desktop Environment",
24
+ ]
25
+ requires-python = ">=3.10"
26
+
27
+ dependencies = [
28
+ "cua2-core"
29
+ ]
30
+
31
+
32
+ [tool.uv.workspace]
33
+ members = [
34
+ "cua2-core",
35
+ ]
36
+
37
+ [tool.uv.sources]
38
+ cua2-core = { workspace = true }
39
+
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "pytest>=7.0.0",
44
+ "pytest-asyncio>=0.21.0",
45
+ "pytest-cov>=4.0.0",
46
+ "black>=23.0.0",
47
+ "isort>=5.12.0",
48
+ "flake8>=6.0.0",
49
+ "mypy>=1.0.0",
50
+ "pre-commit>=3.0.0",
51
+ ]
52
+ test = [
53
+ "pytest>=7.0.0",
54
+ "pytest-asyncio>=0.21.0",
55
+ "pytest-cov>=4.0.0",
56
+ ]
57
+
58
+ [tool.hatch.build.targets.wheel]
59
+ packages = ["cua2-core/src/cua2-core"]
60
+
61
+ [tool.hatch.build.targets.sdist]
62
+ include = [
63
+ "/cua2-core",
64
+ "/README.md",
65
+ "/LICENSE",
66
+ ]
67
+
68
+ [tool.coverage.run]
69
+ source = ["cua2-core"]
70
+ omit = [
71
+ "*/tests/*",
72
+ "*/test_*",
73
+ "*/__pycache__/*",
74
+ "*/migrations/*",
75
+ ]
76
+
77
+ [tool.coverage.report]
78
+ exclude_lines = [
79
+ "pragma: no cover",
80
+ "def __repr__",
81
+ "if self.debug:",
82
+ "if settings.DEBUG",
83
+ "raise AssertionError",
84
+ "raise NotImplementedError",
85
+ "if 0:",
86
+ "if __name__ == .__main__.:",
87
+ "class .*\\bProtocol\\):",
88
+ "@(abc\\.)?abstractmethod",
89
+ ]