From bb7638f1e527dfa3b0771dcf03069de291e18cf2 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 22 Jan 2026 16:14:59 +0800 Subject: [PATCH] Implement Autonomous Mode Filtering in Robot API - Added support for filtering robots by `autonomous_mode` in the ListRobots API. - Enhanced ListQuery structure to include an optional `AutonomousMode` field. - Updated listRobotsFromDB function to apply the autonomous mode filter based on the query. - Created new test cases to validate the filtering functionality for both autonomous and on-demand robots. - Revised related OpenAPI endpoints and frontend integration to accommodate the new filtering options. --- agent/robot/api/api_test.go | 107 ++++++++++++++++++++ agent/robot/api/robot.go | 4 +- agent/robot/api/types.go | 15 +-- openapi/agent/robot/TODO.md | 159 ++++++++++++++++++++---------- openapi/agent/robot/list.go | 9 ++ openapi/tests/agent/robot_test.go | 62 ++++++++++++ 6 files changed, 296 insertions(+), 60 deletions(-) diff --git a/agent/robot/api/api_test.go b/agent/robot/api/api_test.go index 9ad65c7a..9dc66109 100644 --- a/agent/robot/api/api_test.go +++ b/agent/robot/api/api_test.go @@ -205,6 +205,75 @@ func TestAPIRobotQueryWithData(t *testing.T) { }) } +// TestListRobotsAutonomousModeFilter tests the autonomous_mode filter +func TestListRobotsAutonomousModeFilter(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test") + } + + testutils.Prepare(t) + defer testutils.Clean(t) + + cleanupAPITestRobots(t) + defer cleanupAPITestRobots(t) + + // Setup: Create robots with different autonomous_mode settings + setupAPITestRobotWithMode(t, "robot_api_auto_001", "team_api_mode", true) // autonomous + setupAPITestRobotWithMode(t, "robot_api_auto_002", "team_api_mode", true) // autonomous + setupAPITestRobotWithMode(t, "robot_api_demand_001", "team_api_mode", false) // on-demand + + ctx := types.NewContext(context.Background(), nil) + + t.Run("ListRobots returns all robots when autonomous_mode is nil", func(t *testing.T) { + result, err := api.ListRobots(ctx, &api.ListQuery{ + TeamID: "team_api_mode", + Page: 1, + PageSize: 10, + }) + require.NoError(t, err) + require.NotNil(t, result) + + // Should have all 3 robots + assert.Equal(t, 3, result.Total) + }) + + t.Run("ListRobots filters by autonomous_mode=true", func(t *testing.T) { + autonomousMode := true + result, err := api.ListRobots(ctx, &api.ListQuery{ + TeamID: "team_api_mode", + AutonomousMode: &autonomousMode, + Page: 1, + PageSize: 10, + }) + require.NoError(t, err) + require.NotNil(t, result) + + // Should have only 2 autonomous robots + assert.Equal(t, 2, result.Total) + for _, robot := range result.Data { + assert.True(t, robot.AutonomousMode, "All returned robots should be autonomous") + } + }) + + t.Run("ListRobots filters by autonomous_mode=false", func(t *testing.T) { + autonomousMode := false + result, err := api.ListRobots(ctx, &api.ListQuery{ + TeamID: "team_api_mode", + AutonomousMode: &autonomousMode, + Page: 1, + PageSize: 10, + }) + require.NoError(t, err) + require.NotNil(t, result) + + // Should have only 1 on-demand robot + assert.Equal(t, 1, result.Total) + for _, robot := range result.Data { + assert.False(t, robot.AutonomousMode, "All returned robots should be on-demand") + } + }) +} + // TestAPIExecutionQueryWithData tests execution query APIs with real data func TestAPIExecutionQueryWithData(t *testing.T) { if testing.Short() { @@ -377,6 +446,44 @@ func TestAPITriggerWithData(t *testing.T) { // ==================== Helper Functions ==================== +// setupAPITestRobotWithMode creates a test robot with specific autonomous_mode setting +func setupAPITestRobotWithMode(t *testing.T, memberID, teamID string, autonomousMode bool) { + m := model.Select("__yao.member") + tableName := m.MetaData.Table.Name + qb := capsule.Query() + + robotConfig := map[string]interface{}{ + "identity": map[string]interface{}{ + "role": "API Test Robot", + "duties": []string{"Testing API functions"}, + }, + "quota": map[string]interface{}{ + "max": 5, + "queue": 20, + "priority": 5, + }, + } + configJSON, _ := json.Marshal(robotConfig) + + err := qb.Table(tableName).Insert([]map[string]interface{}{ + { + "member_id": memberID, + "team_id": teamID, + "member_type": "robot", + "display_name": "API Test Robot " + memberID, + "system_prompt": "You are an API test robot.", + "status": "active", + "role_id": "member", + "autonomous_mode": autonomousMode, + "robot_status": "idle", + "robot_config": string(configJSON), + }, + }) + if err != nil { + t.Fatalf("Failed to insert robot %s: %v", memberID, err) + } +} + // setupAPITestRobot creates a test robot in the database func setupAPITestRobot(t *testing.T, memberID, teamID string) { m := model.Select("__yao.member") diff --git a/agent/robot/api/robot.go b/agent/robot/api/robot.go index 56893d01..2e4159d9 100644 --- a/agent/robot/api/robot.go +++ b/agent/robot/api/robot.go @@ -168,7 +168,6 @@ func listRobotsFromDB(query *ListQuery) (*ListResult, error) { // Build where conditions wheres := []model.QueryWhere{ {Column: "member_type", Value: "robot"}, - {Column: "autonomous_mode", Value: true}, {Column: "status", Value: "active"}, } @@ -185,6 +184,9 @@ func listRobotsFromDB(query *ListQuery) (*ListResult, error) { Value: "%" + query.Keywords + "%", }) } + if query.AutonomousMode != nil { + wheres = append(wheres, model.QueryWhere{Column: "autonomous_mode", Value: *query.AutonomousMode}) + } // Build order orders := []model.QueryOrder{} diff --git a/agent/robot/api/types.go b/agent/robot/api/types.go index bcaa5dfd..9858e57d 100644 --- a/agent/robot/api/types.go +++ b/agent/robot/api/types.go @@ -9,13 +9,14 @@ import ( // ListQuery - query options for List() type ListQuery struct { - TeamID string `json:"team_id,omitempty"` - Status types.RobotStatus `json:"status,omitempty"` - Keywords string `json:"keywords,omitempty"` - ClockMode types.ClockMode `json:"clock_mode,omitempty"` - Page int `json:"page,omitempty"` - PageSize int `json:"pagesize,omitempty"` - Order string `json:"order,omitempty"` + TeamID string `json:"team_id,omitempty"` + Status types.RobotStatus `json:"status,omitempty"` + Keywords string `json:"keywords,omitempty"` + ClockMode types.ClockMode `json:"clock_mode,omitempty"` + AutonomousMode *bool `json:"autonomous_mode,omitempty"` // nil=all, true=autonomous only, false=on-demand only + Page int `json:"page,omitempty"` + PageSize int `json:"pagesize,omitempty"` + Order string `json:"order,omitempty"` } // ListResult - result of List() diff --git a/openapi/agent/robot/TODO.md b/openapi/agent/robot/TODO.md index b41165cc..20a5e617 100644 --- a/openapi/agent/robot/TODO.md +++ b/openapi/agent/robot/TODO.md @@ -16,9 +16,10 @@ Backend → SDK → Page Integration └─ List, Get, Create, Update, Delete robots -🟡 Phase 1-FE: Frontend Integration ⬜ [Current] - └─ Implement SDK (openapi/robot.ts) - └─ Page Integration (Robot list, detail, create, edit, delete) +✅ Phase 1-FE: Frontend Integration ✅ [Completed] + └─ SDK (openapi/robot.ts) ✅ + └─ Page Integration (Robot list, detail, create, edit, delete) ✅ + └─ UI/UX (CreatureLoading, bubble animations) ✅ 🟢 Phase 2: Execution Management Backend → SDK → Page Integration @@ -177,53 +178,92 @@ --- -## 🟡 Phase 1-FE: Frontend Integration ⬜ [Current] +## ✅ Phase 1-FE: Frontend Integration ✅ [Completed] **Goal:** Implement frontend SDK and integrate pages to validate Phase 1 deliverables -**Status:** ⬜ Not Started +**Status:** ✅ Completed -### 1-FE.1 SDK Implementation ⬜ +### 1-FE.1 SDK Implementation ✅ -> Location: `cui/packages/cui/openapi/robot.ts` +> Location: `cui/packages/cui/openapi/agent/robot/` -- [ ] Create `robot.ts` - Robot API SDK - - [ ] `listRobots(params)` - GET /v1/agent/robots - - [ ] `getRobot(id)` - GET /v1/agent/robots/:id - - [ ] `getRobotStatus(id)` - GET /v1/agent/robots/:id/status - - [ ] `createRobot(data)` - POST /v1/agent/robots - - [ ] `updateRobot(id, data)` - PUT /v1/agent/robots/:id - - [ ] `deleteRobot(id)` - DELETE /v1/agent/robots/:id -- [ ] Add TypeScript types for request/response -- [ ] Export from `openapi/index.ts` +- [x] Create `robot/types.ts` - TypeScript types for Robot API + - [x] `RobotFilter` - filter options for listing (including `autonomous_mode`) + - [x] `Robot` - robot data structure + - [x] `RobotStatusResponse` - runtime status + - [x] `RobotCreateRequest` / `RobotUpdateRequest` - CRUD requests + - [x] `RobotDeleteResponse` - delete response +- [x] Create `robot/robots.ts` - Robot API SDK class (`AgentRobots`) + - [x] `List(filter)` - GET /v1/agent/robots + - [x] `Get(id)` - GET /v1/agent/robots/:id + - [x] `GetStatus(id)` - GET /v1/agent/robots/:id/status + - [x] `Create(data)` - POST /v1/agent/robots + - [x] `Update(id, data)` - PUT /v1/agent/robots/:id + - [x] `Delete(id)` - DELETE /v1/agent/robots/:id +- [x] Create `robot/index.ts` - exports +- [x] Update `agent/api.ts` - add `robots` property to Agent class +- [x] Update `agent/index.ts` - export robot module +- [x] Linter check passed -### 1-FE.2 Page Integration ⬜ +### 1-FE.2 Page Integration ✅ -> Location: `cui/packages/cui/pages/robot/` +> Location: `cui/packages/cui/pages/mission-control/` -- [ ] Robot List Page - - [ ] Replace mock data with `listRobots()` API - - [ ] Implement pagination - - [ ] Implement filters (status, keywords, team) -- [ ] Robot Detail Page - - [ ] Fetch robot via `getRobot(id)` - - [ ] Display robot status via `getRobotStatus(id)` -- [ ] Create Robot - - [ ] Form validation - - [ ] Call `createRobot()` API - - [ ] Handle success/error -- [ ] Edit Robot - - [ ] Pre-populate form with existing data - - [ ] Call `updateRobot()` API -- [ ] Delete Robot - - [ ] Confirmation dialog - - [ ] Call `deleteRobot()` API - - [ ] Handle running execution conflict (409) +- [x] Create `useRobots` hook for API calls + - [x] `listRobots(filter)` - list robots with pagination + - [x] `getRobot(id)` - get single robot + - [x] `getRobotStatus(id)` - get runtime status + - [x] `createRobot(data)` - create robot + - [x] `updateRobot(id, data)` - update robot + - [x] `deleteRobot(id)` - delete robot + - [x] Error handling and loading state +- [x] Robot List Page (`mission-control/index.tsx`) + - [x] Replace mock data with `listRobots()` API (fallback to mock) + - [x] Fetch status for each robot via `getRobotStatus()` + - [x] Refresh list after robot created/updated/deleted + - [x] Empty state with "Create Agent" button (with bubble animation) + - [ ] Implement pagination (TODO: Phase 2) + - [ ] Implement filters (status, keywords, team) (TODO: Phase 2) +- [x] Robot Detail Modal (`AgentModal`) + - [x] Real-time status refresh via `getRobotStatus(id)` + - [x] Auto-refresh every 10 seconds while modal open + - [x] Merge real-time status with robot data +- [x] Create Robot (`AddAgentModal`) + - [x] Call `createRobot()` API + - [x] Handle success/error messages + - [x] Form validation (existing) + - [x] Load email domains, managers, agents, MCP servers from API +- [x] Edit Robot (`ConfigTab` in `AgentModal`) + - [x] Load robot data from API (`getRobot()`) + - [x] Load email domains, managers, roles from Team API + - [x] Load agents and MCP servers from API + - [x] Pre-populate form with existing data + - [x] Call `updateRobot()` API with `robot_config.clock` for schedule + - [x] Handle success/error messages + - [x] Work Schedule panel saves correctly +- [x] Delete Robot (`AdvancedPanel` in `ConfigTab`) + - [x] Confirmation dialog with name input + - [x] Call `deleteRobot()` API + - [x] Handle running execution conflict (409) + - [x] Refresh list after deletion -### 1-FE.3 Verification ⬜ +### 1-FE.3 UI/UX Enhancements ✅ -- [ ] E2E test: Create → List → Get → Update → Delete -- [ ] Permission test: Personal user vs Team user -- [ ] Error handling: 400, 403, 404, 409, 500 +- [x] `CreatureLoading` component with organic animations + - [x] Breathing aura, floating creature, orbit ring, particles + - [x] Three sizes: small, medium, large + - [x] Used in ConfigTab, ResultsTab, HistoryTab +- [x] Empty state "Create Agent" button with bubble animation + - [x] Cyan, purple, pink glowing bubbles rising +- [x] CSS variable compliance (`--color_mission_button_text`) +- [x] Consistent loading animations across all tabs + +### 1-FE.4 Verification ✅ + +- [x] Manual test: Create → List → Get → Update → Delete +- [ ] E2E automated test (TODO: Phase 3) +- [x] Permission test: Personal user vs Team user (manual tested) +- [x] Error handling: 400, 403, 404, 409, 500 --- @@ -579,8 +619,8 @@ yao/openapi/tests/robot/ | Phase | Risk | Backend | Frontend | Description | |-------|------|---------|----------|-------------| -| 1. Core CRUD | 🟢 | ✅ | ⬜ | Robot CRUD endpoints | -| 1-FE Frontend Integration | 🟢 | - | 🟡 | **Current**: SDK + Page Integration | +| 1. Core CRUD | 🟢 | ✅ | ✅ | Robot CRUD endpoints | +| 1-FE Frontend Integration | 🟢 | - | ✅ | SDK ✅, Page Integration ✅, UI/UX ✅ | | 2. Execution | 🟢 | ⬜ | ⬜ | Execution listing, control, trigger | | 3. Results/Activities | 🟢 | ⬜ | ⬜ | Deliverables and activity feed | | 4. i18n | 🟢 | ⬜ | ⬜ | Locale parameter support | @@ -719,21 +759,36 @@ import ( **Execute immediately after each phase backend completion:** -1. **SDK Implementation** - `cui/packages/cui/openapi/robot.ts` +1. **SDK Implementation** - `cui/packages/cui/openapi/agent/robot/` 2. **Type Definitions** - TypeScript request/response types -3. **Page Integration** - Replace mock data, call real APIs -4. **E2E Verification** - Full flow testing +3. **Hook Implementation** - `cui/packages/cui/hooks/useRobots.ts` +4. **Page Integration** - Replace mock data, call real APIs +5. **E2E Verification** - Full flow testing **File Locations:** ``` cui/packages/cui/ ├── openapi/ -│ ├── robot.ts # Robot API SDK -│ └── index.ts # Export all APIs -└── pages/robot/ - ├── index.tsx # Robot list page - ├── [id].tsx # Robot detail page - └── components/ # Shared components +│ └── agent/ +│ └── robot/ +│ ├── types.ts # TypeScript types +│ ├── robots.ts # AgentRobots SDK class +│ └── index.ts # Exports +├── hooks/ +│ └── useRobots.ts # React hook for robot API calls +├── styles/ +│ └── preset/ +│ └── vars.less # CSS variables (--color_mission_button_text) +└── pages/ + └── mission-control/ + ├── index.tsx # Robot list (grid) page + ├── index.less # Styles with bubble animations + └── components/ + ├── AgentModal/ # Robot detail modal + ├── AddAgentModal/ # Create robot modal + └── CreatureLoading/ # Branded loading component + ├── index.tsx + └── index.less ``` ### Incremental Deployment @@ -742,7 +797,7 @@ Each phase independently deliverable: | Phase | Backend | Frontend | Verifiable Features | |-------|---------|----------|---------------------| -| 1 | ✅ | 🟡 Current | Robot CRUD basic management | +| 1 | ✅ | ✅ | Robot CRUD basic management | | 2 | ⬜ | ⬜ | Execution list/control/trigger | | 3 | ⬜ | ⬜ | Results/Activities viewing | | 4 | ⬜ | ⬜ | Multi-language support | diff --git a/openapi/agent/robot/list.go b/openapi/agent/robot/list.go index 25d82104..1a7fc6f0 100644 --- a/openapi/agent/robot/list.go +++ b/openapi/agent/robot/list.go @@ -37,6 +37,7 @@ func ListRobots(c *gin.Context) { requestedTeamID := strings.TrimSpace(c.Query("team_id")) status := strings.TrimSpace(c.Query("status")) keywords := strings.TrimSpace(c.Query("keywords")) + autonomousModeStr := strings.TrimSpace(c.Query("autonomous_mode")) // Apply permission-based filtering // This ensures users only see robots they have access to: @@ -55,6 +56,14 @@ func ListRobots(c *gin.Context) { if status != "" { query.Status = robottypes.RobotStatus(status) } + // Parse autonomous_mode filter: "true" or "false" to filter, empty/other to show all + if autonomousModeStr == "true" { + autonomousMode := true + query.AutonomousMode = &autonomousMode + } else if autonomousModeStr == "false" { + autonomousMode := false + query.AutonomousMode = &autonomousMode + } // Create robot context ctx := &robottypes.Context{} diff --git a/openapi/tests/agent/robot_test.go b/openapi/tests/agent/robot_test.go index 6cba401a..559c7252 100644 --- a/openapi/tests/agent/robot_test.go +++ b/openapi/tests/agent/robot_test.go @@ -74,6 +74,68 @@ func TestListRobots(t *testing.T) { assert.Equal(t, float64(5), response["pagesize"]) }) + t.Run("ListRobotsWithAutonomousModeFilter", func(t *testing.T) { + // Test with autonomous_mode=true + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots?autonomous_mode=true", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + // Verify response structure + assert.Contains(t, response, "data") + assert.Contains(t, response, "total") + + // If there are robots, verify they are all autonomous + if data, ok := response["data"].([]interface{}); ok && len(data) > 0 { + for _, item := range data { + if robot, ok := item.(map[string]interface{}); ok { + assert.True(t, robot["autonomous_mode"].(bool), "All robots should have autonomous_mode=true") + } + } + } + }) + + t.Run("ListRobotsWithAutonomousModeFalse", func(t *testing.T) { + // Test with autonomous_mode=false + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots?autonomous_mode=false", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + // Verify response structure + assert.Contains(t, response, "data") + assert.Contains(t, response, "total") + + // If there are robots, verify they are all on-demand (not autonomous) + if data, ok := response["data"].([]interface{}); ok && len(data) > 0 { + for _, item := range data { + if robot, ok := item.(map[string]interface{}); ok { + assert.False(t, robot["autonomous_mode"].(bool), "All robots should have autonomous_mode=false") + } + } + } + }) + t.Run("ListRobotsUnauthorized", func(t *testing.T) { req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots", nil) require.NoError(t, err)