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.
This commit is contained in:
parent
6f93175a46
commit
bb7638f1e5
6 changed files with 296 additions and 60 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue