Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/resources/ai_task.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ Use this resource to define Coder tasks.
<!-- schema generated by tfplugindocs -->
## Schema

### Required
### Optional

- `sidebar_app` (Block Set, Min: 1, Max: 1) The coder_app to display in the sidebar. Usually a chat interface with the AI agent running in the workspace, like https://github.com/coder/agentapi. (see [below for nested schema](#nestedblock--sidebar_app))
- `app_id` (String) The ID of the coder_app resource that provides the AI interface for this task.
- `sidebar_app` (Block Set, Max: 1, Deprecated) The coder_app to display in the sidebar. Usually a chat interface with the AI agent running in the workspace, like https://github.com/coder/agentapi. (see [below for nested schema](#nestedblock--sidebar_app))

### Read-Only

- `id` (String) A unique identifier for this resource.
- `prompt` (String) The prompt text provided to the task by Coder.

<a id="nestedblock--sidebar_app"></a>
### Nested Schema for `sidebar_app`
Expand Down
62 changes: 54 additions & 8 deletions provider/ai_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package provider

import (
"context"
"os"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand All @@ -21,13 +21,43 @@ type AITaskSidebarApp struct {
// TaskPromptParameterName is the name of the parameter which is *required* to be defined when a coder_ai_task is used.
const TaskPromptParameterName = "AI Prompt"

func aiTask() *schema.Resource {
func aiTaskResource() *schema.Resource {
return &schema.Resource{
SchemaVersion: 1,

Description: "Use this resource to define Coder tasks.",
CreateContext: func(c context.Context, resourceData *schema.ResourceData, i any) diag.Diagnostics {
resourceData.SetId(uuid.NewString())
if idStr := os.Getenv("CODER_TASK_ID"); idStr != "" {
resourceData.SetId(idStr)
} else {
return diag.Errorf("CODER_TASK_ID must be set")
}

if prompt := os.Getenv("CODER_TASK_PROMPT"); prompt != "" {
resourceData.Set("prompt", prompt)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, prompt vs input? I don't personally mind either way as both work. "You give your task an initial prompt and then send new input".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RFC laid out prompt so I'm tempted to keep it here, I also don't mind either way.

} else {
resourceData.Set("prompt", "default")
}

var (
appID = resourceData.Get("app_id").(string)
sidebarAppSet = resourceData.Get("sidebar_app").(*schema.Set)
)

if appID == "" && sidebarAppSet.Len() > 0 {
sidebarApps := sidebarAppSet.List()
sidebarApp := sidebarApps[0].(map[string]any)

if id, ok := sidebarApp["id"].(string); ok && id != "" {
appID = id
resourceData.Set("app_id", id)
}
}

if appID == "" {
return diag.Errorf("'app_id' must be set")
}

return nil
},
ReadContext: schema.NoopContext,
Expand All @@ -39,11 +69,13 @@ func aiTask() *schema.Resource {
Computed: true,
},
"sidebar_app": {
Type: schema.TypeSet,
Description: "The coder_app to display in the sidebar. Usually a chat interface with the AI agent running in the workspace, like https://github.com/coder/agentapi.",
ForceNew: true,
Required: true,
MaxItems: 1,
Type: schema.TypeSet,
Description: "The coder_app to display in the sidebar. Usually a chat interface with the AI agent running in the workspace, like https://github.com/coder/agentapi.",
Deprecated: "This field has been deprecated in favor of the `app_id` field.",
ForceNew: true,
Optional: true,
MaxItems: 1,
ConflictsWith: []string{"app_id"},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Expand All @@ -56,6 +88,20 @@ func aiTask() *schema.Resource {
},
},
},
"prompt": {
Type: schema.TypeString,
Description: "The prompt text provided to the task by Coder.",
Computed: true,
},
"app_id": {
Type: schema.TypeString,
Description: "The ID of the coder_app resource that provides the AI interface for this task.",
ForceNew: true,
Optional: true,
Computed: true,
ValidateFunc: validation.IsUUID,
ConflictsWith: []string{"sidebar_app"},
},
},
}
}
108 changes: 92 additions & 16 deletions provider/ai_task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (
)

func TestAITask(t *testing.T) {
t.Parallel()
t.Setenv("CODER_TASK_ID", "7d8d4c2e-fb57-44f9-a183-22509819c2e7")
t.Setenv("CODER_TASK_PROMPT", "some task prompt")

t.Run("OK", func(t *testing.T) {
t.Parallel()
Expand All @@ -22,44 +23,100 @@ func TestAITask(t *testing.T) {
Config: `
provider "coder" {
}
resource "coder_agent" "dev" {
os = "linux"
arch = "amd64"
resource "coder_ai_task" "test" {
app_id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc"
}
`,
Check: func(state *terraform.State) error {
require.Len(t, state.Modules, 1)
resource := state.Modules[0].Resources["coder_ai_task.test"]
require.NotNil(t, resource)
for _, key := range []string{
"id",
"prompt",
"app_id",
} {
value := resource.Primary.Attributes[key]
require.NotNil(t, value)
require.Greater(t, len(value), 0)
}

taskID := resource.Primary.Attributes["id"]
require.Equal(t, "7d8d4c2e-fb57-44f9-a183-22509819c2e7", taskID)

taskPrompt := resource.Primary.Attributes["prompt"]
require.Equal(t, "some task prompt", taskPrompt)

return nil
},
}},
})
})

t.Run("InvalidAppID", func(t *testing.T) {
t.Parallel()

resource.Test(t, resource.TestCase{
ProviderFactories: coderFactory(),
IsUnitTest: true,
Steps: []resource.TestStep{{
Config: `
provider "coder" {
}
resource "coder_ai_task" "test" {
app_id = "not-a-uuid"
}
resource "coder_app" "code-server" {
agent_id = coder_agent.dev.id
slug = "code-server"
display_name = "code-server"
icon = "builtin:vim"
url = "http://localhost:13337"
open_in = "slim-window"
`,
ExpectError: regexp.MustCompile(`expected "app_id" to be a valid UUID`),
}},
})
})

t.Run("DeprecatedSidebarApp", func(t *testing.T) {
t.Parallel()

resource.Test(t, resource.TestCase{
ProviderFactories: coderFactory(),
IsUnitTest: true,
Steps: []resource.TestStep{{
Config: `
provider "coder" {
}
resource "coder_ai_task" "test" {
sidebar_app {
id = coder_app.code-server.id
id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc"
}
}
`,
Check: func(state *terraform.State) error {
require.Len(t, state.Modules, 1)
resource := state.Modules[0].Resources["coder_ai_task.test"]
require.NotNil(t, resource)

for _, key := range []string{
"id",
"sidebar_app.#",
"prompt",
"app_id",
} {
value := resource.Primary.Attributes[key]
require.NotNil(t, value)
require.Greater(t, len(value), 0)
}

require.Equal(t, "1", resource.Primary.Attributes["sidebar_app.#"])
sidebarAppID := resource.Primary.Attributes["sidebar_app.0.id"]
require.Equal(t, "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc", sidebarAppID)

actualAppID := resource.Primary.Attributes["app_id"]
require.Equal(t, "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc", actualAppID)

return nil
},
}},
})
})

t.Run("InvalidSidebarAppID", func(t *testing.T) {
t.Run("ConflictingFields", func(t *testing.T) {
t.Parallel()

resource.Test(t, resource.TestCase{
Expand All @@ -70,12 +127,31 @@ func TestAITask(t *testing.T) {
provider "coder" {
}
resource "coder_ai_task" "test" {
app_id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc"
sidebar_app {
id = "not-a-uuid"
id = "9a3ff7b4-4b3f-48c6-8d3a-a8118ac921fc"
}
}
`,
ExpectError: regexp.MustCompile(`expected "sidebar_app.0.id" to be a valid UUID`),
ExpectError: regexp.MustCompile(`"app_id": conflicts with sidebar_app`),
}},
})
})

t.Run("NoAppID", func(t *testing.T) {
t.Parallel()

resource.Test(t, resource.TestCase{
ProviderFactories: coderFactory(),
IsUnitTest: true,
Steps: []resource.TestStep{{
Config: `
provider "coder" {
}
resource "coder_ai_task" "test" {
}
`,
ExpectError: regexp.MustCompile(`'app_id' must be set`),
}},
})
})
Expand Down
2 changes: 1 addition & 1 deletion provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func New() *schema.Provider {
ResourcesMap: map[string]*schema.Resource{
"coder_agent": agentResource(),
"coder_agent_instance": agentInstanceResource(),
"coder_ai_task": aiTask(),
"coder_ai_task": aiTaskResource(),
"coder_app": appResource(),
"coder_metadata": metadataResource(),
"coder_script": scriptResource(),
Expand Down