diff --git a/commands/genai_agent.go b/commands/genai_agent.go index f1471bf8c..ccde568c7 100644 --- a/commands/genai_agent.go +++ b/commands/genai_agent.go @@ -2,6 +2,8 @@ package commands import ( "fmt" + "strings" + "time" "github.com/digitalocean/doctl" "github.com/digitalocean/doctl/commands/displayers" @@ -70,9 +72,34 @@ func AgentCmd() *Command { aliasOpt("ls"), displayerType(&displayers.Agent{}), ) + + // Existing filters AddStringFlag(cmdAgentList, doctl.ArgAgentRegion, "", "", "Retrieves a list of Agents in a specified region") AddStringFlag(cmdAgentList, doctl.ArgTag, "", "", "Retrieves a list of Agents with a specified tag") - cmdAgentList.Example = `The following example retrieves a list of all Agent in the ` + "`" + `tor1` + "`" + ` region: doctl genai agent list --region tor1` + + // New filtering options + AddStringFlag(cmdAgentList, doctl.ArgAgentName, "", "", "Filter agents by name (partial match)") + AddStringFlag(cmdAgentList, doctl.ArgModelId, "", "", "Filter agents by model ID") + AddStringFlag(cmdAgentList, doctl.ArgProjectID, "", "", "Filter agents by project ID") + AddStringFlag(cmdAgentList, "created-after", "", "", "Filter agents created after specified date (YYYY-MM-DD format)") + AddStringFlag(cmdAgentList, "created-before", "", "", "Filter agents created before specified date (YYYY-MM-DD format)") + + cmdAgentList.Example = `The following examples show various filtering options: + + # List all agents in tor1 region + doctl genai agent list --region tor1 + + # List agents with specific tag + doctl genai agent list --tag production + + # List agents by name (partial match) + doctl genai agent list --name "chatbot" + + # List agents in a specific project + doctl genai agent list --project-id "12345678-1234-1234-1234-123456789012" + + # List agents created after 2024-01-01 + doctl genai agent list --created-after "2024-01-01"` cmdAgentGet := CmdBuilder( cmd, @@ -165,11 +192,16 @@ func AgentCmd() *Command { return cmd } -// RunAgentList lists all agents. +// RunAgentList lists all agents with enhanced filtering capabilities. func RunAgentList(c *CmdConfig) error { + // Get filter parameters region, _ := c.Doit.GetString(c.NS, "region") projectId, _ := c.Doit.GetString(c.NS, "project-id") tag, _ := c.Doit.GetString(c.NS, "tag") + name, _ := c.Doit.GetString(c.NS, "name") + modelId, _ := c.Doit.GetString(c.NS, "model-id") + createdAfter, _ := c.Doit.GetString(c.NS, "created-after") + createdBefore, _ := c.Doit.GetString(c.NS, "created-before") agents, err := c.GenAI().ListAgents() if err != nil { @@ -178,27 +210,75 @@ func RunAgentList(c *CmdConfig) error { filtered := make(do.Agents, 0, len(agents)) for _, agent := range agents { - if region != "" && agent.Agent.Region != region { + // Apply filters + if !matchesFilters(agent, region, projectId, tag, name, modelId, createdAfter, createdBefore) { continue } - if projectId != "" && agent.Agent.ProjectId != projectId { - continue + filtered = append(filtered, agent) + } + + return c.Display(&displayers.Agent{Agents: filtered}) +} + +// matchesFilters applies all filtering logic to determine if an agent should be included +func matchesFilters(agent do.Agent, region, projectId, tag, name, modelId, createdAfter, createdBefore string) bool { + // Region filter + if region != "" && agent.Agent.Region != region { + return false + } + + // Project ID filter + if projectId != "" && agent.Agent.ProjectId != projectId { + return false + } + + // Tag filter (exact match) + if tag != "" { + found := false + for _, t := range agent.Agent.Tags { + if t == tag { + found = true + break + } + } + if !found { + return false } - if tag != "" { - found := false - for _, t := range agent.Agent.Tags { - if t == tag { - found = true - break + } + + // Name filter (partial match, case-insensitive) + if name != "" && !strings.Contains(strings.ToLower(agent.Agent.Name), strings.ToLower(name)) { + return false + } + + // Model ID filter + if modelId != "" && agent.Agent.Model != nil && agent.Agent.Model.Uuid != modelId { + return false + } + + // Note: Status and Visibility filters are not available in current godo.Agent struct + + // Date filters + if createdAfter != "" || createdBefore != "" { + createdAt := agent.Agent.CreatedAt + if createdAt != nil && !createdAt.Time.IsZero() { + // Parse the input dates + if createdAfter != "" { + afterDate, err := time.Parse("2006-01-02", createdAfter) + if err == nil && createdAt.Time.Before(afterDate) { + return false } } - if !found { - continue + if createdBefore != "" { + beforeDate, err := time.Parse("2006-01-02", createdBefore) + if err == nil && createdAt.Time.After(beforeDate) { + return false + } } } - filtered = append(filtered, agent) } - return c.Display(&displayers.Agent{Agents: filtered}) + + return true } // RunAgentCreate creates a new agent. diff --git a/commands/genai_agent_test.go b/commands/genai_agent_test.go index 493f34dca..5d3aedcec 100644 --- a/commands/genai_agent_test.go +++ b/commands/genai_agent_test.go @@ -15,6 +15,7 @@ package commands import ( "testing" + "time" "github.com/digitalocean/godo" @@ -91,6 +92,231 @@ func TestRunAgentList(t *testing.T) { assert.NoError(t, err) }) } + +func TestRunAgentListWithFilters(t *testing.T) { + // Create timestamps for testing + createdAt1 := &godo.Timestamp{Time: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)} + createdAt2 := &godo.Timestamp{Time: time.Date(2024, 2, 1, 15, 30, 0, 0, time.UTC)} + + testAgents := do.Agents{ + { + Agent: &godo.Agent{ + Uuid: "00000000-0000-4000-8000-000000000001", + Name: "ChatBot Agent", + Region: "tor1", + ProjectId: "00000000-0000-4000-8000-000000000000", + Tags: []string{"production", "chatbot"}, + Model: &godo.Model{ + Uuid: "00000000-0000-4000-8000-000000000000", + }, + CreatedAt: createdAt1, + }, + }, + { + Agent: &godo.Agent{ + Uuid: "00000000-0000-4000-8000-000000000002", + Name: "Support Agent", + Region: "nyc1", + ProjectId: "00000000-0000-4000-8000-000000000001", + Tags: []string{"support", "internal"}, + Model: &godo.Model{ + Uuid: "00000000-0000-4000-8000-000000000001", + }, + CreatedAt: createdAt2, + }, + }, + } + + tests := []struct { + name string + filters map[string]string + expectedCount int + description string + }{ + { + name: "Filter by region", + filters: map[string]string{"region": "tor1"}, + expectedCount: 1, + description: "Should return only agents in tor1 region", + }, + { + name: "Filter by project ID", + filters: map[string]string{"project-id": "00000000-0000-4000-8000-000000000000"}, + expectedCount: 1, + description: "Should return only agents in specific project", + }, + { + name: "Filter by name (partial match)", + filters: map[string]string{"name": "Chat"}, + expectedCount: 1, + description: "Should return agents with name containing 'Chat'", + }, + { + name: "Filter by tag", + filters: map[string]string{"tag": "production"}, + expectedCount: 1, + description: "Should return agents with 'production' tag", + }, + // Note: Status and Visibility filters are commented out as these fields + // may not exist in the current godo.Agent struct + // { + // name: "Filter by status", + // filters: map[string]string{"status": "active"}, + // expectedCount: 1, + // description: "Should return only active agents", + // }, + // { + // name: "Filter by visibility", + // filters: map[string]string{"visibility": "VISIBILITY_PUBLIC"}, + // expectedCount: 1, + // description: "Should return only public agents", + // }, + { + name: "Filter by model ID", + filters: map[string]string{"model-id": "00000000-0000-4000-8000-000000000000"}, + expectedCount: 1, + description: "Should return agents using specific model", + }, + { + name: "Filter by created after date", + filters: map[string]string{"created-after": "2024-01-20"}, + expectedCount: 1, + description: "Should return agents created after 2024-01-20", + }, + { + name: "Filter by created before date", + filters: map[string]string{"created-before": "2024-01-20"}, + expectedCount: 1, + description: "Should return agents created before 2024-01-20", + }, + { + name: "Multiple filters", + filters: map[string]string{"region": "tor1", "tag": "production"}, + expectedCount: 1, + description: "Should return agents matching multiple filters", + }, + { + name: "No matching filters", + filters: map[string]string{"region": "sfo1"}, + expectedCount: 0, + description: "Should return no agents when no matches", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + // Set up filters + for key, value := range tt.filters { + config.Doit.Set(config.NS, key, value) + } + + tm.genAI.EXPECT().ListAgents().Return(testAgents, nil) + + err := RunAgentList(config) + assert.NoError(t, err) + }) + }) + } +} + +func TestMatchesFilters(t *testing.T) { + createdAt := &godo.Timestamp{Time: time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)} + + agent := do.Agent{ + Agent: &godo.Agent{ + Uuid: "00000000-0000-4000-8000-000000000001", + Name: "Test Agent", + Region: "tor1", + ProjectId: "00000000-0000-4000-8000-000000000000", + Tags: []string{"test", "production"}, + Model: &godo.Model{ + Uuid: "00000000-0000-4000-8000-000000000000", + }, + CreatedAt: createdAt, + }, + } + + tests := []struct { + name string + filters map[string]string + expected bool + }{ + { + name: "Match region", + filters: map[string]string{"region": "tor1"}, + expected: true, + }, + { + name: "No match region", + filters: map[string]string{"region": "nyc1"}, + expected: false, + }, + { + name: "Match name (partial)", + filters: map[string]string{"name": "Test"}, + expected: true, + }, + { + name: "Match name (case insensitive)", + filters: map[string]string{"name": "test"}, + expected: true, + }, + { + name: "No match name", + filters: map[string]string{"name": "NonExistent"}, + expected: false, + }, + { + name: "Match tag", + filters: map[string]string{"tag": "test"}, + expected: true, + }, + { + name: "No match tag", + filters: map[string]string{"tag": "nonexistent"}, + expected: false, + }, + // Note: Status and Visibility filters are commented out as these fields + // may not exist in the current godo.Agent struct + // { + // name: "Match status", + // filters: map[string]string{"status": "active"}, + // expected: true, + // }, + // { + // name: "Match visibility", + // filters: map[string]string{"visibility": "VISIBILITY_PUBLIC"}, + // expected: true, + // }, + { + name: "Match created after", + filters: map[string]string{"created-after": "2024-01-01"}, + expected: true, + }, + { + name: "Match created before", + filters: map[string]string{"created-before": "2024-02-01"}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := matchesFilters( + agent, + tt.filters["region"], + tt.filters["project-id"], + tt.filters["tag"], + tt.filters["name"], + tt.filters["model-id"], + tt.filters["created-after"], + tt.filters["created-before"], + ) + assert.Equal(t, tt.expected, result) + }) + } +} func TestRunAgentUpdate(t *testing.T) { withTestClient(t, func(config *CmdConfig, tm *tcMocks) { agentID := "00000000-0000-4000-8000-000000000000" diff --git a/doctl b/doctl new file mode 100755 index 000000000..800464c04 Binary files /dev/null and b/doctl differ