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

ADD generate-instruction (#3)

Browse files

* ADD generate_instruction

* CHG update_trace_step exception handling

* ADD frontend objets

cua2-core/src/cua2_core/models/models.py CHANGED
@@ -315,3 +315,17 @@ class AvailableModelsResponse(BaseModel):
315
  """Response for available models"""
316
 
317
  models: list[str]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  """Response for available models"""
316
 
317
  models: list[str]
318
+
319
+
320
+ class GenerateInstructionRequest(BaseModel):
321
+ """Request model for generating task instruction"""
322
+
323
+ model_id: str
324
+ prompt: Optional[str] = None
325
+
326
+
327
+ class GenerateInstructionResponse(BaseModel):
328
+ """Response model for generated task instruction"""
329
+
330
+ instruction: str
331
+ model_id: str
cua2-core/src/cua2_core/routes/routes.py CHANGED
@@ -3,12 +3,15 @@ from datetime import datetime
3
  # Get services from app state
4
  from cua2_core.models.models import (
5
  AvailableModelsResponse,
 
 
6
  HealthResponse,
7
  UpdateStepRequest,
8
  UpdateStepResponse,
9
  )
10
  from cua2_core.services.agent_service import AgentService
11
  from cua2_core.services.agent_utils.get_model import AVAILABLE_MODELS
 
12
  from cua2_core.websocket.websocket_manager import WebSocketManager
13
  from fastapi import APIRouter, Depends, HTTPException, Request
14
 
@@ -44,6 +47,30 @@ async def get_available_models():
44
  return AvailableModelsResponse(models=AVAILABLE_MODELS)
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  @router.patch("/traces/{trace_id}/steps/{step_id}", response_model=UpdateStepResponse)
48
  async def update_trace_step(
49
  trace_id: str,
@@ -62,7 +89,7 @@ async def update_trace_step(
62
  success=True,
63
  message="Step updated successfully",
64
  )
65
- except ValueError as e:
66
- raise HTTPException(status_code=400, detail=str(e))
67
  except FileNotFoundError as e:
68
  raise HTTPException(status_code=404, detail=str(e))
 
 
 
3
  # Get services from app state
4
  from cua2_core.models.models import (
5
  AvailableModelsResponse,
6
+ GenerateInstructionRequest,
7
+ GenerateInstructionResponse,
8
  HealthResponse,
9
  UpdateStepRequest,
10
  UpdateStepResponse,
11
  )
12
  from cua2_core.services.agent_service import AgentService
13
  from cua2_core.services.agent_utils.get_model import AVAILABLE_MODELS
14
+ from cua2_core.services.instruction_service import InstructionService
15
  from cua2_core.websocket.websocket_manager import WebSocketManager
16
  from fastapi import APIRouter, Depends, HTTPException, Request
17
 
 
47
  return AvailableModelsResponse(models=AVAILABLE_MODELS)
48
 
49
 
50
+ @router.post("/generate-instruction", response_model=GenerateInstructionResponse)
51
+ async def generate_task_instruction(
52
+ request: GenerateInstructionRequest,
53
+ ):
54
+ """Generate a task instruction using a specified model"""
55
+ try:
56
+ instruction = InstructionService.generate_instruction(
57
+ model_id=request.model_id, prompt=request.prompt
58
+ )
59
+
60
+ return GenerateInstructionResponse(
61
+ instruction=instruction, model_id=request.model_id
62
+ )
63
+
64
+ except ValueError as e:
65
+ raise HTTPException(status_code=400, detail=str(e))
66
+
67
+ except Exception as e:
68
+ raise HTTPException(
69
+ status_code=500,
70
+ detail=f"Error generating instruction: {str(e)}",
71
+ )
72
+
73
+
74
  @router.patch("/traces/{trace_id}/steps/{step_id}", response_model=UpdateStepResponse)
75
  async def update_trace_step(
76
  trace_id: str,
 
89
  success=True,
90
  message="Step updated successfully",
91
  )
 
 
92
  except FileNotFoundError as e:
93
  raise HTTPException(status_code=404, detail=str(e))
94
+ except Exception as e:
95
+ raise HTTPException(status_code=400, detail=str(e))
cua2-core/src/cua2_core/services/agent_utils/desktop_agent.py CHANGED
@@ -206,12 +206,8 @@ class E2BVisionAgent(CodeAgent):
206
  Args:
207
  url: The URL to open
208
  """
209
- # Make sure URL has http/https prefix
210
- if not url.startswith(("http://", "https://")):
211
- url = "https://" + url
212
-
213
  self.desktop.open(url)
214
- # Give it time to load
215
  time.sleep(2)
216
  self.logger.log(f"Opening URL: {url}")
217
  return f"Opened URL: {url}"
 
206
  Args:
207
  url: The URL to open
208
  """
 
 
 
 
209
  self.desktop.open(url)
210
+
211
  time.sleep(2)
212
  self.logger.log(f"Opening URL: {url}")
213
  return f"Opened URL: {url}"
cua2-core/src/cua2_core/services/agent_utils/get_model.py CHANGED
@@ -2,10 +2,6 @@ from smolagents import InferenceClientModel, Model
2
 
3
  # Available model IDs
4
  AVAILABLE_MODELS = [
5
- "Qwen/Qwen3-VL-2B-Instruct",
6
- "Qwen/Qwen3-VL-2B-Thinking",
7
- "Qwen/Qwen3-VL-4B-Instruct",
8
- "Qwen/Qwen3-VL-4B-Thinking",
9
  "Qwen/Qwen3-VL-8B-Instruct",
10
  "Qwen/Qwen3-VL-8B-Thinking",
11
  "Qwen/Qwen3-VL-30B-A3B-Instruct",
 
2
 
3
  # Available model IDs
4
  AVAILABLE_MODELS = [
 
 
 
 
5
  "Qwen/Qwen3-VL-8B-Instruct",
6
  "Qwen/Qwen3-VL-8B-Thinking",
7
  "Qwen/Qwen3-VL-30B-A3B-Instruct",
cua2-core/src/cua2_core/services/instruction_service.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import random
3
+ import time
4
+
5
+ from cua2_core.services.agent_utils.get_model import AVAILABLE_MODELS, get_model
6
+ from smolagents import ChatMessage, Model
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class InstructionService:
12
+ """Service for generating task instructions using LLM models"""
13
+
14
+ available_models = AVAILABLE_MODELS
15
+ seed_topics = [
16
+ "web browsing",
17
+ "email management",
18
+ "calendar scheduling",
19
+ "file management",
20
+ "note-taking",
21
+ "system settings",
22
+ "text editing",
23
+ "terminal commands",
24
+ ]
25
+
26
+ prompt_templates = [
27
+ (
28
+ "Generate a clear and specific task instruction for a desktop automation agent. "
29
+ "The task should involve {topic} and be completable using a desktop computer. "
30
+ "Do not assume any pre-existing files, emails, or resources exist on the system. "
31
+ "Return only the task instruction, nothing else. Keep it simple and focused on a single action."
32
+ ),
33
+ (
34
+ "Create a practical task instruction for desktop automation related to {topic}. "
35
+ "The task should be straightforward and achievable in one application. "
36
+ "Do not reference specific files or resources that may not exist locally. "
37
+ "Provide only the task description without any additional explanation."
38
+ ),
39
+ (
40
+ "Generate a specific {topic} task that a desktop automation agent can perform. "
41
+ "The task should be concrete and not require multiple applications. "
42
+ "Avoid assuming pre-existing documents, files, or local resources. "
43
+ "Return just the task instruction."
44
+ ),
45
+ (
46
+ "Provide a single, clear task instruction involving {topic} for a desktop agent. "
47
+ "The task should be simple and focused. "
48
+ "Do not assume any specific files or resources already exist on the computer. "
49
+ "Output only the instruction."
50
+ ),
51
+ (
52
+ "Think of a realistic {topic} task suitable for desktop automation. "
53
+ "Keep it simple and achievable in one application. "
54
+ "The task should not depend on pre-existing local files or resources. "
55
+ "Return only the task."
56
+ ),
57
+ ]
58
+
59
+ web_browsing_templates = [
60
+ (
61
+ "Generate a clear and specific web browsing task instruction for a desktop automation agent. "
62
+ "The task should be goal-centric, focused on retrieving information or performing an action online. "
63
+ "You can specify a URL or website to visit. "
64
+ "Return only the task instruction, nothing else. Keep it simple and focused on a single goal."
65
+ ),
66
+ (
67
+ "Create a practical web browsing task for desktop automation. "
68
+ "The task should focus on finding specific information or completing an online action. "
69
+ "Include a specific URL or website name if relevant to the goal. "
70
+ "Provide only the task description without any additional explanation."
71
+ ),
72
+ (
73
+ "Generate a specific web browsing task that a desktop automation agent can perform. "
74
+ "The task should be about retrieving information or performing an action on a website. "
75
+ "You may specify URLs or web addresses. Keep it concrete and single-purpose. "
76
+ "Return just the task instruction."
77
+ ),
78
+ (
79
+ "Provide a goal-oriented web browsing task instruction for a desktop agent. "
80
+ "Focus on what information to find or what action to perform online. "
81
+ "Specify a URL or website if it helps achieve the goal. "
82
+ "Output only the instruction."
83
+ ),
84
+ (
85
+ "Think of a realistic web browsing task suitable for desktop automation. "
86
+ "The task should be about accessing online information or performing a web-based action. "
87
+ "Include specific URLs or websites as needed. Keep it simple and goal-focused. "
88
+ "Return only the task."
89
+ ),
90
+ ]
91
+
92
+ default_prompt = (
93
+ "Generate a clear and specific task instruction for a desktop automation agent. "
94
+ "The task should be something that can be completed using a desktop computer, "
95
+ "such as opening applications, browsing websites, or manipulating files. "
96
+ "Do not assume any pre-existing files, emails, or resources exist on the system. "
97
+ "Return only the task instruction, nothing else. the instruction must be not to complexe and not multi-app task. "
98
+ )
99
+
100
+ @staticmethod
101
+ def get_random_prompt() -> str:
102
+ """
103
+ Generate a random prompt by selecting a random topic and template.
104
+ Uses special templates for web browsing that allow URL specification.
105
+ """
106
+ random.seed(time.time_ns())
107
+
108
+ topic = random.choice(InstructionService.seed_topics)
109
+
110
+ if topic == "web browsing":
111
+ template = random.choice(InstructionService.web_browsing_templates)
112
+ return template
113
+
114
+ template = random.choice(InstructionService.prompt_templates)
115
+ return template.format(topic=topic)
116
+
117
+ @staticmethod
118
+ def generate_instruction(
119
+ model_id: str, prompt: str | None = None, use_random: bool = True
120
+ ) -> str:
121
+ """
122
+ Generate a task instruction using the specified model
123
+
124
+ Args:
125
+ model_id: The ID of the model to use
126
+ prompt: Optional custom prompt. If None, uses default or random prompt
127
+ use_random: If True, uses random prompts for variety. If False, uses default prompt
128
+ """
129
+
130
+ if model_id not in InstructionService.available_models:
131
+ available_models_str = ", ".join(InstructionService.available_models)
132
+ raise ValueError(
133
+ f"Invalid model_id '{model_id}'. Must be one of: {available_models_str}"
134
+ )
135
+
136
+ try:
137
+ logger.info(f"Generating instruction with model: {model_id}")
138
+
139
+ model: Model = get_model(model_id)
140
+
141
+ if prompt:
142
+ generation_prompt = prompt
143
+ elif use_random:
144
+ generation_prompt = InstructionService.get_random_prompt()
145
+ else:
146
+ generation_prompt = InstructionService.default_prompt
147
+
148
+ instruction = model([ChatMessage(role="user", content=generation_prompt)])
149
+ logger.info(
150
+ f"Successfully generated instruction with {model_id}: {instruction.content[:100]}..."
151
+ )
152
+ return instruction.content
153
+
154
+ except Exception as e:
155
+ logger.error(f"Error generating instruction with {model_id}: {str(e)}")
156
+ raise Exception(f"Failed to generate instruction: {str(e)}")
157
+
158
+ @staticmethod
159
+ def get_available_models() -> list[str]:
160
+ """Get the list of available model IDs"""
161
+ return InstructionService.available_models
162
+
163
+ @staticmethod
164
+ def get_random_topic() -> str:
165
+ """Get a random topic from the seed topics"""
166
+ return random.choice(InstructionService.seed_topics)
167
+
168
+
169
+ if __name__ == "__main__":
170
+ instruction = InstructionService.generate_instruction(
171
+ model_id="Qwen/Qwen3-VL-8B-Instruct"
172
+ )
173
+ print(instruction)
cua2-core/tests/test_routes.py CHANGED
@@ -1,4 +1,4 @@
1
- from unittest.mock import Mock
2
 
3
  import pytest
4
  from cua2_core.models.models import AvailableModelsResponse, UpdateStepResponse
@@ -95,7 +95,7 @@ class TestGetAvailableModels:
95
 
96
  # Check for some specific models
97
  expected_models = [
98
- "Qwen/Qwen3-VL-2B-Instruct",
99
  "Qwen/Qwen3-VL-30B-A3B-Instruct",
100
  ]
101
 
@@ -284,6 +284,53 @@ class TestUpdateTraceStep:
284
  assert update_response.message == "Step updated successfully"
285
 
286
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  class TestRoutesIntegration:
288
  """Integration tests for multiple routes"""
289
 
 
1
+ from unittest.mock import Mock, patch
2
 
3
  import pytest
4
  from cua2_core.models.models import AvailableModelsResponse, UpdateStepResponse
 
95
 
96
  # Check for some specific models
97
  expected_models = [
98
+ "Qwen/Qwen3-VL-8B-Instruct",
99
  "Qwen/Qwen3-VL-30B-A3B-Instruct",
100
  ]
101
 
 
284
  assert update_response.message == "Step updated successfully"
285
 
286
 
287
+ class TestGenerateInstruction:
288
+ """Test suite for POST /generate-instruction endpoint"""
289
+
290
+ @patch("cua2_core.routes.routes.InstructionService.generate_instruction")
291
+ def test_generate_instruction_success(self, mock_generate, client):
292
+ """Test successful instruction generation with mocked model"""
293
+ # Mock the instruction generation
294
+ mock_instruction = "Open Google Chrome and navigate to example.com"
295
+ mock_generate.return_value = mock_instruction
296
+
297
+ request_data = {
298
+ "model_id": "Qwen/Qwen3-VL-8B-Instruct",
299
+ "prompt": "Generate a web browsing task",
300
+ }
301
+
302
+ response = client.post("/generate-instruction", json=request_data)
303
+
304
+ assert response.status_code == 200
305
+ data = response.json()
306
+
307
+ assert data["instruction"] == mock_instruction
308
+ assert data["model_id"] == request_data["model_id"]
309
+
310
+ # Verify the service was called correctly
311
+ mock_generate.assert_called_once_with(
312
+ model_id=request_data["model_id"], prompt=request_data["prompt"]
313
+ )
314
+
315
+ @patch("cua2_core.routes.routes.InstructionService.generate_instruction")
316
+ def test_generate_instruction_invalid_model(self, mock_generate, client):
317
+ """Test instruction generation with invalid model_id"""
318
+ # Mock the service to raise ValueError for invalid model
319
+ mock_generate.side_effect = ValueError(
320
+ "Invalid model_id 'invalid-model'. Must be one of: Qwen/Qwen3-VL-2B-Instruct, ..."
321
+ )
322
+
323
+ request_data = {
324
+ "model_id": "invalid-model",
325
+ "prompt": "Generate a task",
326
+ }
327
+
328
+ response = client.post("/generate-instruction", json=request_data)
329
+
330
+ assert response.status_code == 400
331
+ assert "Invalid model_id" in response.json()["detail"]
332
+
333
+
334
  class TestRoutesIntegration:
335
  """Integration tests for multiple routes"""
336
 
cua2-front/src/types/agent.ts CHANGED
@@ -97,3 +97,13 @@ export interface UpdateStepResponse {
97
  success: boolean;
98
  message: string;
99
  }
 
 
 
 
 
 
 
 
 
 
 
97
  success: boolean;
98
  message: string;
99
  }
100
+
101
+ export interface GenerateInstructionRequest {
102
+ model_id: string;
103
+ prompt?: string;
104
+ }
105
+
106
+ export interface GenerateInstructionResponse {
107
+ instruction: string;
108
+ model_id: string;
109
+ }