mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: deprecate codersdk.AITaskPromptParameterName and reduce usage (#20501)
Depends on coder/sqlc#1 Fixes coder/internal#979 Updates coder/internal#973
This commit is contained in:
committed by
GitHub
parent
50749d131b
commit
859e94d67a
@@ -2,7 +2,6 @@ package cli_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
@@ -19,10 +18,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/cli/clitest"
|
"github.com/coder/coder/v2/cli/clitest"
|
||||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
||||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
||||||
"github.com/coder/coder/v2/coderd/util/slice"
|
"github.com/coder/coder/v2/coderd/util/slice"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/pty/ptytest"
|
"github.com/coder/coder/v2/pty/ptytest"
|
||||||
@@ -43,76 +39,22 @@ func makeAITask(t *testing.T, db database.Store, orgID, adminID, ownerID uuid.UU
|
|||||||
},
|
},
|
||||||
}).Do()
|
}).Do()
|
||||||
|
|
||||||
ws := database.WorkspaceTable{
|
build := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
||||||
OrganizationID: orgID,
|
OrganizationID: orgID,
|
||||||
OwnerID: ownerID,
|
OwnerID: ownerID,
|
||||||
TemplateID: tv.Template.ID,
|
TemplateID: tv.Template.ID,
|
||||||
}
|
}).
|
||||||
build := dbfake.WorkspaceBuild(t, db, ws).
|
|
||||||
Seed(database.WorkspaceBuild{
|
Seed(database.WorkspaceBuild{
|
||||||
TemplateVersionID: tv.TemplateVersion.ID,
|
TemplateVersionID: tv.TemplateVersion.ID,
|
||||||
Transition: transition,
|
Transition: transition,
|
||||||
}).WithAgent().Do()
|
}).
|
||||||
dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{
|
WithAgent().
|
||||||
{
|
WithTask(database.TaskTable{
|
||||||
WorkspaceBuildID: build.Build.ID,
|
Prompt: prompt,
|
||||||
Name: codersdk.AITaskPromptParameterName,
|
}, nil).
|
||||||
Value: prompt,
|
Do()
|
||||||
},
|
|
||||||
})
|
|
||||||
agents, err := db.GetWorkspaceAgentsByWorkspaceAndBuildNumber(
|
|
||||||
dbauthz.AsSystemRestricted(context.Background()),
|
|
||||||
database.GetWorkspaceAgentsByWorkspaceAndBuildNumberParams{
|
|
||||||
WorkspaceID: build.Workspace.ID,
|
|
||||||
BuildNumber: build.Build.BuildNumber,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, agents)
|
|
||||||
agentID := agents[0].ID
|
|
||||||
|
|
||||||
// Create a workspace app and set it as the sidebar app.
|
return build.Task
|
||||||
app := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{
|
|
||||||
AgentID: agentID,
|
|
||||||
Slug: "task-sidebar",
|
|
||||||
DisplayName: "Task Sidebar",
|
|
||||||
External: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update build flags to reference the sidebar app and HasAITask=true.
|
|
||||||
err = db.UpdateWorkspaceBuildFlagsByID(
|
|
||||||
dbauthz.AsSystemRestricted(context.Background()),
|
|
||||||
database.UpdateWorkspaceBuildFlagsByIDParams{
|
|
||||||
ID: build.Build.ID,
|
|
||||||
HasAITask: sql.NullBool{Bool: true, Valid: true},
|
|
||||||
HasExternalAgent: sql.NullBool{Bool: false, Valid: false},
|
|
||||||
SidebarAppID: uuid.NullUUID{UUID: app.ID, Valid: true},
|
|
||||||
UpdatedAt: build.Build.UpdatedAt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Create a task record in the tasks table for the new data model.
|
|
||||||
task := dbgen.Task(t, db, database.TaskTable{
|
|
||||||
OrganizationID: orgID,
|
|
||||||
OwnerID: ownerID,
|
|
||||||
Name: build.Workspace.Name,
|
|
||||||
WorkspaceID: uuid.NullUUID{UUID: build.Workspace.ID, Valid: true},
|
|
||||||
TemplateVersionID: tv.TemplateVersion.ID,
|
|
||||||
TemplateParameters: []byte("{}"),
|
|
||||||
Prompt: prompt,
|
|
||||||
CreatedAt: dbtime.Now(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Link the task to the workspace app.
|
|
||||||
dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{
|
|
||||||
TaskID: task.ID,
|
|
||||||
WorkspaceBuildNumber: build.Build.BuildNumber,
|
|
||||||
WorkspaceAgentID: uuid.NullUUID{UUID: agentID, Valid: true},
|
|
||||||
WorkspaceAppID: uuid.NullUUID{UUID: app.ID, Valid: true},
|
|
||||||
})
|
|
||||||
|
|
||||||
return task
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExpTaskList(t *testing.T) {
|
func TestExpTaskList(t *testing.T) {
|
||||||
|
|||||||
@@ -293,7 +293,6 @@ func createAITaskTemplate(t *testing.T, client *codersdk.Client, orgID uuid.UUID
|
|||||||
{
|
{
|
||||||
Type: &proto.Response_Plan{
|
Type: &proto.Response_Plan{
|
||||||
Plan: &proto.PlanComplete{
|
Plan: &proto.PlanComplete{
|
||||||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
|
|
||||||
HasAiTasks: true,
|
HasAiTasks: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -328,9 +327,7 @@ func createAITaskTemplate(t *testing.T, client *codersdk.Client, orgID uuid.UUID
|
|||||||
},
|
},
|
||||||
AiTasks: []*proto.AITask{
|
AiTasks: []*proto.AITask{
|
||||||
{
|
{
|
||||||
SidebarApp: &proto.AITaskSidebarApp{
|
AppId: taskAppID.String(),
|
||||||
Id: taskAppID.String(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+22
-55
@@ -7,7 +7,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -24,62 +23,12 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||||
"github.com/coder/coder/v2/coderd/searchquery"
|
"github.com/coder/coder/v2/coderd/searchquery"
|
||||||
"github.com/coder/coder/v2/coderd/taskname"
|
"github.com/coder/coder/v2/coderd/taskname"
|
||||||
|
"github.com/coder/coder/v2/coderd/util/slice"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
|
||||||
aiagentapi "github.com/coder/agentapi-sdk-go"
|
aiagentapi "github.com/coder/agentapi-sdk-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This endpoint is experimental and not guaranteed to be stable, so we're not
|
|
||||||
// generating public-facing documentation for it.
|
|
||||||
func (api *API) aiTasksPrompts(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := r.Context()
|
|
||||||
|
|
||||||
buildIDsParam := r.URL.Query().Get("build_ids")
|
|
||||||
if buildIDsParam == "" {
|
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
||||||
Message: "build_ids query parameter is required",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse build IDs
|
|
||||||
buildIDStrings := strings.Split(buildIDsParam, ",")
|
|
||||||
buildIDs := make([]uuid.UUID, 0, len(buildIDStrings))
|
|
||||||
for _, idStr := range buildIDStrings {
|
|
||||||
id, err := uuid.Parse(strings.TrimSpace(idStr))
|
|
||||||
if err != nil {
|
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
|
||||||
Message: fmt.Sprintf("Invalid build ID format: %s", idStr),
|
|
||||||
Detail: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buildIDs = append(buildIDs, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters, err := api.Database.GetWorkspaceBuildParametersByBuildIDs(ctx, buildIDs)
|
|
||||||
if err != nil {
|
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
|
||||||
Message: "Internal error fetching workspace build parameters.",
|
|
||||||
Detail: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
promptsByBuildID := make(map[string]string, len(parameters))
|
|
||||||
for _, param := range parameters {
|
|
||||||
if param.Name != codersdk.AITaskPromptParameterName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
buildID := param.WorkspaceBuildID.String()
|
|
||||||
promptsByBuildID[buildID] = param.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.AITasksPromptsResponse{
|
|
||||||
Prompts: promptsByBuildID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Summary Create a new AI task
|
// @Summary Create a new AI task
|
||||||
// @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable.
|
// @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable.
|
||||||
// @ID create-task
|
// @ID create-task
|
||||||
@@ -174,13 +123,31 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the template defines the AI Prompt parameter.
|
||||||
|
templateParams, err := api.Database.GetTemplateVersionParameters(ctx, req.TemplateVersionID)
|
||||||
|
if err != nil {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
|
Message: "Internal error fetching template parameters.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var richParams []codersdk.WorkspaceBuildParameter
|
||||||
|
if _, hasAIPromptParam := slice.Find(templateParams, func(param database.TemplateVersionParameter) bool {
|
||||||
|
return param.Name == codersdk.AITaskPromptParameterName
|
||||||
|
}); hasAIPromptParam {
|
||||||
|
// Only add the AI Prompt parameter if the template defines it.
|
||||||
|
richParams = []codersdk.WorkspaceBuildParameter{
|
||||||
|
{Name: codersdk.AITaskPromptParameterName, Value: req.Input},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createReq := codersdk.CreateWorkspaceRequest{
|
createReq := codersdk.CreateWorkspaceRequest{
|
||||||
Name: taskName,
|
Name: taskName,
|
||||||
TemplateVersionID: req.TemplateVersionID,
|
TemplateVersionID: req.TemplateVersionID,
|
||||||
TemplateVersionPresetID: req.TemplateVersionPresetID,
|
TemplateVersionPresetID: req.TemplateVersionPresetID,
|
||||||
RichParameterValues: []codersdk.WorkspaceBuildParameter{
|
RichParameterValues: richParams,
|
||||||
{Name: codersdk.AITaskPromptParameterName, Value: req.Input},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var owner workspaceOwner
|
var owner workspaceOwner
|
||||||
|
|||||||
+45
-129
@@ -35,128 +35,6 @@ import (
|
|||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAITasksPrompts(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("EmptyBuildIDs", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
client := coderdtest.New(t, &coderdtest.Options{})
|
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
|
||||||
experimentalClient := codersdk.NewExperimentalClient(client)
|
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitShort)
|
|
||||||
|
|
||||||
// Test with empty build IDs
|
|
||||||
prompts, err := experimentalClient.AITaskPrompts(ctx, []uuid.UUID{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, prompts.Prompts)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("MultipleBuilds", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
||||||
first := coderdtest.CreateFirstUser(t, adminClient)
|
|
||||||
memberClient, _ := coderdtest.CreateAnotherUser(t, adminClient, first.OrganizationID)
|
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
|
||||||
|
|
||||||
// Create a template with parameters
|
|
||||||
version := coderdtest.CreateTemplateVersion(t, adminClient, first.OrganizationID, &echo.Responses{
|
|
||||||
Parse: echo.ParseComplete,
|
|
||||||
ProvisionPlan: []*proto.Response{{
|
|
||||||
Type: &proto.Response_Plan{
|
|
||||||
Plan: &proto.PlanComplete{
|
|
||||||
Parameters: []*proto.RichParameter{
|
|
||||||
{
|
|
||||||
Name: "param1",
|
|
||||||
Type: "string",
|
|
||||||
DefaultValue: "default1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: codersdk.AITaskPromptParameterName,
|
|
||||||
Type: "string",
|
|
||||||
DefaultValue: "default2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
ProvisionApply: echo.ApplyComplete,
|
|
||||||
})
|
|
||||||
template := coderdtest.CreateTemplate(t, adminClient, first.OrganizationID, version.ID)
|
|
||||||
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
|
|
||||||
|
|
||||||
// Create two workspaces with different parameters
|
|
||||||
workspace1 := coderdtest.CreateWorkspace(t, memberClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
|
|
||||||
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
||||||
{Name: "param1", Value: "value1a"},
|
|
||||||
{Name: codersdk.AITaskPromptParameterName, Value: "value2a"},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace1.LatestBuild.ID)
|
|
||||||
|
|
||||||
workspace2 := coderdtest.CreateWorkspace(t, memberClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
|
|
||||||
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
||||||
{Name: "param1", Value: "value1b"},
|
|
||||||
{Name: codersdk.AITaskPromptParameterName, Value: "value2b"},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace2.LatestBuild.ID)
|
|
||||||
|
|
||||||
workspace3 := coderdtest.CreateWorkspace(t, adminClient, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
|
|
||||||
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
||||||
{Name: "param1", Value: "value1c"},
|
|
||||||
{Name: codersdk.AITaskPromptParameterName, Value: "value2c"},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, adminClient, workspace3.LatestBuild.ID)
|
|
||||||
allBuildIDs := []uuid.UUID{workspace1.LatestBuild.ID, workspace2.LatestBuild.ID, workspace3.LatestBuild.ID}
|
|
||||||
|
|
||||||
experimentalMemberClient := codersdk.NewExperimentalClient(memberClient)
|
|
||||||
// Test parameters endpoint as member
|
|
||||||
prompts, err := experimentalMemberClient.AITaskPrompts(ctx, allBuildIDs)
|
|
||||||
require.NoError(t, err)
|
|
||||||
// we expect 2 prompts because the member client does not have access to workspace3
|
|
||||||
// since it was created by the admin client
|
|
||||||
require.Len(t, prompts.Prompts, 2)
|
|
||||||
|
|
||||||
// Check workspace1 parameters
|
|
||||||
build1Prompt := prompts.Prompts[workspace1.LatestBuild.ID.String()]
|
|
||||||
require.Equal(t, "value2a", build1Prompt)
|
|
||||||
|
|
||||||
// Check workspace2 parameters
|
|
||||||
build2Prompt := prompts.Prompts[workspace2.LatestBuild.ID.String()]
|
|
||||||
require.Equal(t, "value2b", build2Prompt)
|
|
||||||
|
|
||||||
experimentalAdminClient := codersdk.NewExperimentalClient(adminClient)
|
|
||||||
// Test parameters endpoint as admin
|
|
||||||
// we expect 3 prompts because the admin client has access to all workspaces
|
|
||||||
prompts, err = experimentalAdminClient.AITaskPrompts(ctx, allBuildIDs)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, prompts.Prompts, 3)
|
|
||||||
|
|
||||||
// Check workspace3 parameters
|
|
||||||
build3Prompt := prompts.Prompts[workspace3.LatestBuild.ID.String()]
|
|
||||||
require.Equal(t, "value2c", build3Prompt)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("NonExistentBuildIDs", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
client := coderdtest.New(t, &coderdtest.Options{})
|
|
||||||
_ = coderdtest.CreateFirstUser(t, client)
|
|
||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitShort)
|
|
||||||
|
|
||||||
// Test with non-existent build IDs
|
|
||||||
nonExistentID := uuid.New()
|
|
||||||
experimentalClient := codersdk.NewExperimentalClient(client)
|
|
||||||
prompts, err := experimentalClient.AITaskPrompts(ctx, []uuid.UUID{nonExistentID})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, prompts.Prompts)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTasks(t *testing.T) {
|
func TestTasks(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -188,7 +66,6 @@ func TestTasks(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Type: &proto.Response_Plan{
|
Type: &proto.Response_Plan{
|
||||||
Plan: &proto.PlanComplete{
|
Plan: &proto.PlanComplete{
|
||||||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
|
|
||||||
HasAiTasks: true,
|
HasAiTasks: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -817,6 +694,51 @@ func TestTasksCreate(t *testing.T) {
|
|||||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
user := coderdtest.CreateFirstUser(t, client)
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||||
|
Parse: echo.ParseComplete,
|
||||||
|
ProvisionApply: echo.ApplyComplete,
|
||||||
|
ProvisionPlan: []*proto.Response{
|
||||||
|
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
||||||
|
HasAiTasks: true,
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||||
|
|
||||||
|
expClient := codersdk.NewExperimentalClient(client)
|
||||||
|
|
||||||
|
task, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
|
||||||
|
TemplateVersionID: template.ActiveVersionID,
|
||||||
|
Input: taskPrompt,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, task.WorkspaceID.Valid)
|
||||||
|
|
||||||
|
ws, err := client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, task.Name)
|
||||||
|
assert.Equal(t, template.ID, task.TemplateID)
|
||||||
|
|
||||||
|
parameters, err := client.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, parameters, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OK AIPromptBackCompat", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctx = testutil.Context(t, testutil.WaitShort)
|
||||||
|
|
||||||
|
taskPrompt = "Some task prompt"
|
||||||
|
)
|
||||||
|
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||||
|
user := coderdtest.CreateFirstUser(t, client)
|
||||||
|
|
||||||
// Given: A template with an "AI Prompt" parameter
|
// Given: A template with an "AI Prompt" parameter
|
||||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||||
Parse: echo.ParseComplete,
|
Parse: echo.ParseComplete,
|
||||||
@@ -896,7 +818,6 @@ func TestTasksCreate(t *testing.T) {
|
|||||||
ProvisionApply: echo.ApplyComplete,
|
ProvisionApply: echo.ApplyComplete,
|
||||||
ProvisionPlan: []*proto.Response{
|
ProvisionPlan: []*proto.Response{
|
||||||
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
||||||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
|
|
||||||
HasAiTasks: true,
|
HasAiTasks: true,
|
||||||
}}},
|
}}},
|
||||||
},
|
},
|
||||||
@@ -1012,7 +933,6 @@ func TestTasksCreate(t *testing.T) {
|
|||||||
ProvisionApply: echo.ApplyComplete,
|
ProvisionApply: echo.ApplyComplete,
|
||||||
ProvisionPlan: []*proto.Response{
|
ProvisionPlan: []*proto.Response{
|
||||||
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
||||||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
|
|
||||||
HasAiTasks: true,
|
HasAiTasks: true,
|
||||||
}}},
|
}}},
|
||||||
},
|
},
|
||||||
@@ -1072,7 +992,6 @@ func TestTasksCreate(t *testing.T) {
|
|||||||
ProvisionApply: echo.ApplyComplete,
|
ProvisionApply: echo.ApplyComplete,
|
||||||
ProvisionPlan: []*proto.Response{
|
ProvisionPlan: []*proto.Response{
|
||||||
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
||||||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
|
|
||||||
HasAiTasks: true,
|
HasAiTasks: true,
|
||||||
}}},
|
}}},
|
||||||
},
|
},
|
||||||
@@ -1109,7 +1028,6 @@ func TestTasksCreate(t *testing.T) {
|
|||||||
ProvisionApply: echo.ApplyComplete,
|
ProvisionApply: echo.ApplyComplete,
|
||||||
ProvisionPlan: []*proto.Response{
|
ProvisionPlan: []*proto.Response{
|
||||||
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
||||||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
|
|
||||||
HasAiTasks: true,
|
HasAiTasks: true,
|
||||||
}}},
|
}}},
|
||||||
},
|
},
|
||||||
@@ -1162,7 +1080,6 @@ func TestTasksCreate(t *testing.T) {
|
|||||||
ProvisionApply: echo.ApplyComplete,
|
ProvisionApply: echo.ApplyComplete,
|
||||||
ProvisionPlan: []*proto.Response{
|
ProvisionPlan: []*proto.Response{
|
||||||
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
||||||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
|
|
||||||
HasAiTasks: true,
|
HasAiTasks: true,
|
||||||
}}},
|
}}},
|
||||||
},
|
},
|
||||||
@@ -1175,7 +1092,6 @@ func TestTasksCreate(t *testing.T) {
|
|||||||
ProvisionApply: echo.ApplyComplete,
|
ProvisionApply: echo.ApplyComplete,
|
||||||
ProvisionPlan: []*proto.Response{
|
ProvisionPlan: []*proto.Response{
|
||||||
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
|
||||||
Parameters: []*proto.RichParameter{{Name: codersdk.AITaskPromptParameterName, Type: "string"}},
|
|
||||||
HasAiTasks: true,
|
HasAiTasks: true,
|
||||||
}}},
|
}}},
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-4
@@ -1021,10 +1021,7 @@ func New(options *Options) *API {
|
|||||||
apiRateLimiter,
|
apiRateLimiter,
|
||||||
httpmw.ReportCLITelemetry(api.Logger, options.Telemetry),
|
httpmw.ReportCLITelemetry(api.Logger, options.Telemetry),
|
||||||
)
|
)
|
||||||
r.Route("/aitasks", func(r chi.Router) {
|
|
||||||
r.Use(apiKeyMiddleware)
|
|
||||||
r.Get("/prompts", api.aiTasksPrompts)
|
|
||||||
})
|
|
||||||
r.Route("/tasks", func(r chi.Router) {
|
r.Route("/tasks", func(r chi.Router) {
|
||||||
r.Use(apiKeyMiddleware)
|
r.Use(apiKeyMiddleware)
|
||||||
|
|
||||||
|
|||||||
@@ -329,7 +329,6 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa
|
|||||||
&i.LatestBuildError,
|
&i.LatestBuildError,
|
||||||
&i.LatestBuildTransition,
|
&i.LatestBuildTransition,
|
||||||
&i.LatestBuildStatus,
|
&i.LatestBuildStatus,
|
||||||
&i.LatestBuildHasAITask,
|
|
||||||
&i.LatestBuildHasExternalAgent,
|
&i.LatestBuildHasExternalAgent,
|
||||||
&i.Count,
|
&i.Count,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|||||||
@@ -22289,7 +22289,6 @@ SELECT
|
|||||||
latest_build.error as latest_build_error,
|
latest_build.error as latest_build_error,
|
||||||
latest_build.transition as latest_build_transition,
|
latest_build.transition as latest_build_transition,
|
||||||
latest_build.job_status as latest_build_status,
|
latest_build.job_status as latest_build_status,
|
||||||
latest_build.has_ai_task as latest_build_has_ai_task,
|
|
||||||
latest_build.has_external_agent as latest_build_has_external_agent
|
latest_build.has_external_agent as latest_build_has_external_agent
|
||||||
FROM
|
FROM
|
||||||
workspaces_expanded as workspaces
|
workspaces_expanded as workspaces
|
||||||
@@ -22523,25 +22522,19 @@ WHERE
|
|||||||
(latest_build.template_version_id = template.active_version_id) = $18 :: boolean
|
(latest_build.template_version_id = template.active_version_id) = $18 :: boolean
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by has_ai_task in latest build
|
-- Filter by has_ai_task, checks if this is a task workspace.
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN $19 :: boolean IS NOT NULL THEN
|
WHEN $19::boolean IS NOT NULL
|
||||||
(COALESCE(latest_build.has_ai_task, false) OR (
|
THEN $19::boolean = EXISTS (
|
||||||
-- If the build has no AI task, it means that the provisioner job is in progress
|
SELECT
|
||||||
-- and we don't know if it has an AI task yet. In this case, we optimistically
|
1
|
||||||
-- assume that it has an AI task if the AI Prompt parameter is not empty. This
|
FROM
|
||||||
-- lets the AI Task frontend spawn a task and see it immediately after instead of
|
tasks
|
||||||
-- having to wait for the build to complete.
|
WHERE
|
||||||
latest_build.has_ai_task IS NULL AND
|
-- Consider all tasks, deleting a task does not turn the
|
||||||
latest_build.completed_at IS NULL AND
|
-- workspace into a non-task workspace.
|
||||||
EXISTS (
|
tasks.workspace_id = workspaces.id
|
||||||
SELECT 1
|
)
|
||||||
FROM workspace_build_parameters
|
|
||||||
WHERE workspace_build_parameters.workspace_build_id = latest_build.id
|
|
||||||
AND workspace_build_parameters.name = 'AI Prompt'
|
|
||||||
AND workspace_build_parameters.value != ''
|
|
||||||
)
|
|
||||||
)) = ($19 :: boolean)
|
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by has_external_agent in latest build
|
-- Filter by has_external_agent in latest build
|
||||||
@@ -22572,7 +22565,7 @@ WHERE
|
|||||||
-- @authorize_filter
|
-- @authorize_filter
|
||||||
), filtered_workspaces_order AS (
|
), filtered_workspaces_order AS (
|
||||||
SELECT
|
SELECT
|
||||||
fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_ai_task, fw.latest_build_has_external_agent
|
fw.id, fw.created_at, fw.updated_at, fw.owner_id, fw.organization_id, fw.template_id, fw.deleted, fw.name, fw.autostart_schedule, fw.ttl, fw.last_used_at, fw.dormant_at, fw.deleting_at, fw.automatic_updates, fw.favorite, fw.next_start_at, fw.group_acl, fw.user_acl, fw.owner_avatar_url, fw.owner_username, fw.owner_name, fw.organization_name, fw.organization_display_name, fw.organization_icon, fw.organization_description, fw.template_name, fw.template_display_name, fw.template_icon, fw.template_description, fw.task_id, fw.template_version_id, fw.template_version_name, fw.latest_build_completed_at, fw.latest_build_canceled_at, fw.latest_build_error, fw.latest_build_transition, fw.latest_build_status, fw.latest_build_has_external_agent
|
||||||
FROM
|
FROM
|
||||||
filtered_workspaces fw
|
filtered_workspaces fw
|
||||||
ORDER BY
|
ORDER BY
|
||||||
@@ -22593,7 +22586,7 @@ WHERE
|
|||||||
$25
|
$25
|
||||||
), filtered_workspaces_order_with_summary AS (
|
), filtered_workspaces_order_with_summary AS (
|
||||||
SELECT
|
SELECT
|
||||||
fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_ai_task, fwo.latest_build_has_external_agent
|
fwo.id, fwo.created_at, fwo.updated_at, fwo.owner_id, fwo.organization_id, fwo.template_id, fwo.deleted, fwo.name, fwo.autostart_schedule, fwo.ttl, fwo.last_used_at, fwo.dormant_at, fwo.deleting_at, fwo.automatic_updates, fwo.favorite, fwo.next_start_at, fwo.group_acl, fwo.user_acl, fwo.owner_avatar_url, fwo.owner_username, fwo.owner_name, fwo.organization_name, fwo.organization_display_name, fwo.organization_icon, fwo.organization_description, fwo.template_name, fwo.template_display_name, fwo.template_icon, fwo.template_description, fwo.task_id, fwo.template_version_id, fwo.template_version_name, fwo.latest_build_completed_at, fwo.latest_build_canceled_at, fwo.latest_build_error, fwo.latest_build_transition, fwo.latest_build_status, fwo.latest_build_has_external_agent
|
||||||
FROM
|
FROM
|
||||||
filtered_workspaces_order fwo
|
filtered_workspaces_order fwo
|
||||||
-- Return a technical summary row with total count of workspaces.
|
-- Return a technical summary row with total count of workspaces.
|
||||||
@@ -22638,7 +22631,6 @@ WHERE
|
|||||||
'', -- latest_build_error
|
'', -- latest_build_error
|
||||||
'start'::workspace_transition, -- latest_build_transition
|
'start'::workspace_transition, -- latest_build_transition
|
||||||
'unknown'::provisioner_job_status, -- latest_build_status
|
'unknown'::provisioner_job_status, -- latest_build_status
|
||||||
false, -- latest_build_has_ai_task
|
|
||||||
false -- latest_build_has_external_agent
|
false -- latest_build_has_external_agent
|
||||||
WHERE
|
WHERE
|
||||||
$27 :: boolean = true
|
$27 :: boolean = true
|
||||||
@@ -22649,7 +22641,7 @@ WHERE
|
|||||||
filtered_workspaces
|
filtered_workspaces
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_ai_task, fwos.latest_build_has_external_agent,
|
fwos.id, fwos.created_at, fwos.updated_at, fwos.owner_id, fwos.organization_id, fwos.template_id, fwos.deleted, fwos.name, fwos.autostart_schedule, fwos.ttl, fwos.last_used_at, fwos.dormant_at, fwos.deleting_at, fwos.automatic_updates, fwos.favorite, fwos.next_start_at, fwos.group_acl, fwos.user_acl, fwos.owner_avatar_url, fwos.owner_username, fwos.owner_name, fwos.organization_name, fwos.organization_display_name, fwos.organization_icon, fwos.organization_description, fwos.template_name, fwos.template_display_name, fwos.template_icon, fwos.template_description, fwos.task_id, fwos.template_version_id, fwos.template_version_name, fwos.latest_build_completed_at, fwos.latest_build_canceled_at, fwos.latest_build_error, fwos.latest_build_transition, fwos.latest_build_status, fwos.latest_build_has_external_agent,
|
||||||
tc.count
|
tc.count
|
||||||
FROM
|
FROM
|
||||||
filtered_workspaces_order_with_summary fwos
|
filtered_workspaces_order_with_summary fwos
|
||||||
@@ -22725,7 +22717,6 @@ type GetWorkspacesRow struct {
|
|||||||
LatestBuildError sql.NullString `db:"latest_build_error" json:"latest_build_error"`
|
LatestBuildError sql.NullString `db:"latest_build_error" json:"latest_build_error"`
|
||||||
LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"`
|
LatestBuildTransition WorkspaceTransition `db:"latest_build_transition" json:"latest_build_transition"`
|
||||||
LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"`
|
LatestBuildStatus ProvisionerJobStatus `db:"latest_build_status" json:"latest_build_status"`
|
||||||
LatestBuildHasAITask sql.NullBool `db:"latest_build_has_ai_task" json:"latest_build_has_ai_task"`
|
|
||||||
LatestBuildHasExternalAgent sql.NullBool `db:"latest_build_has_external_agent" json:"latest_build_has_external_agent"`
|
LatestBuildHasExternalAgent sql.NullBool `db:"latest_build_has_external_agent" json:"latest_build_has_external_agent"`
|
||||||
Count int64 `db:"count" json:"count"`
|
Count int64 `db:"count" json:"count"`
|
||||||
}
|
}
|
||||||
@@ -22808,7 +22799,6 @@ func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams)
|
|||||||
&i.LatestBuildError,
|
&i.LatestBuildError,
|
||||||
&i.LatestBuildTransition,
|
&i.LatestBuildTransition,
|
||||||
&i.LatestBuildStatus,
|
&i.LatestBuildStatus,
|
||||||
&i.LatestBuildHasAITask,
|
|
||||||
&i.LatestBuildHasExternalAgent,
|
&i.LatestBuildHasExternalAgent,
|
||||||
&i.Count,
|
&i.Count,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ SELECT
|
|||||||
latest_build.error as latest_build_error,
|
latest_build.error as latest_build_error,
|
||||||
latest_build.transition as latest_build_transition,
|
latest_build.transition as latest_build_transition,
|
||||||
latest_build.job_status as latest_build_status,
|
latest_build.job_status as latest_build_status,
|
||||||
latest_build.has_ai_task as latest_build_has_ai_task,
|
|
||||||
latest_build.has_external_agent as latest_build_has_external_agent
|
latest_build.has_external_agent as latest_build_has_external_agent
|
||||||
FROM
|
FROM
|
||||||
workspaces_expanded as workspaces
|
workspaces_expanded as workspaces
|
||||||
@@ -351,25 +350,19 @@ WHERE
|
|||||||
(latest_build.template_version_id = template.active_version_id) = sqlc.narg('using_active') :: boolean
|
(latest_build.template_version_id = template.active_version_id) = sqlc.narg('using_active') :: boolean
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by has_ai_task in latest build
|
-- Filter by has_ai_task, checks if this is a task workspace.
|
||||||
AND CASE
|
AND CASE
|
||||||
WHEN sqlc.narg('has_ai_task') :: boolean IS NOT NULL THEN
|
WHEN sqlc.narg('has_ai_task')::boolean IS NOT NULL
|
||||||
(COALESCE(latest_build.has_ai_task, false) OR (
|
THEN sqlc.narg('has_ai_task')::boolean = EXISTS (
|
||||||
-- If the build has no AI task, it means that the provisioner job is in progress
|
SELECT
|
||||||
-- and we don't know if it has an AI task yet. In this case, we optimistically
|
1
|
||||||
-- assume that it has an AI task if the AI Prompt parameter is not empty. This
|
FROM
|
||||||
-- lets the AI Task frontend spawn a task and see it immediately after instead of
|
tasks
|
||||||
-- having to wait for the build to complete.
|
WHERE
|
||||||
latest_build.has_ai_task IS NULL AND
|
-- Consider all tasks, deleting a task does not turn the
|
||||||
latest_build.completed_at IS NULL AND
|
-- workspace into a non-task workspace.
|
||||||
EXISTS (
|
tasks.workspace_id = workspaces.id
|
||||||
SELECT 1
|
)
|
||||||
FROM workspace_build_parameters
|
|
||||||
WHERE workspace_build_parameters.workspace_build_id = latest_build.id
|
|
||||||
AND workspace_build_parameters.name = 'AI Prompt'
|
|
||||||
AND workspace_build_parameters.value != ''
|
|
||||||
)
|
|
||||||
)) = (sqlc.narg('has_ai_task') :: boolean)
|
|
||||||
ELSE true
|
ELSE true
|
||||||
END
|
END
|
||||||
-- Filter by has_external_agent in latest build
|
-- Filter by has_external_agent in latest build
|
||||||
@@ -466,7 +459,6 @@ WHERE
|
|||||||
'', -- latest_build_error
|
'', -- latest_build_error
|
||||||
'start'::workspace_transition, -- latest_build_transition
|
'start'::workspace_transition, -- latest_build_transition
|
||||||
'unknown'::provisioner_job_status, -- latest_build_status
|
'unknown'::provisioner_job_status, -- latest_build_status
|
||||||
false, -- latest_build_has_ai_task
|
|
||||||
false -- latest_build_has_external_agent
|
false -- latest_build_has_external_agent
|
||||||
WHERE
|
WHERE
|
||||||
@with_summary :: boolean = true
|
@with_summary :: boolean = true
|
||||||
|
|||||||
+40
-66
@@ -4700,11 +4700,16 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
|
|||||||
|
|
||||||
ctx := testutil.Context(t, testutil.WaitLong)
|
ctx := testutil.Context(t, testutil.WaitLong)
|
||||||
|
|
||||||
// Helper function to create workspace with AI task configuration
|
// Helper function to create workspace with optional task.
|
||||||
createWorkspaceWithAIConfig := func(hasAITask sql.NullBool, jobCompleted bool, aiTaskPrompt *string) database.WorkspaceTable {
|
createWorkspace := func(jobCompleted, createTask bool, prompt string) uuid.UUID {
|
||||||
|
// TODO(mafredri): The bellow comment is based on deprecated logic and
|
||||||
|
// kept only present to test that the old observable behavior works as
|
||||||
|
// intended.
|
||||||
|
//
|
||||||
// When a provisioner job uses these tags, no provisioner will match it.
|
// When a provisioner job uses these tags, no provisioner will match it.
|
||||||
// We do this so jobs will always be stuck in "pending", allowing us to exercise the intermediary state when
|
// We do this so jobs will always be stuck in "pending", allowing us to
|
||||||
// has_ai_task is nil and we compensate by looking at pending provisioning jobs.
|
// exercise the intermediary state when has_ai_task is nil and we
|
||||||
|
// compensate by looking at pending provisioning jobs.
|
||||||
// See GetWorkspaces clauses.
|
// See GetWorkspaces clauses.
|
||||||
unpickableTags := database.StringMap{"custom": "true"}
|
unpickableTags := database.StringMap{"custom": "true"}
|
||||||
|
|
||||||
@@ -4723,102 +4728,71 @@ func TestWorkspaceFilterHasAITask(t *testing.T) {
|
|||||||
jobConfig.CompletedAt = sql.NullTime{Time: time.Now(), Valid: true}
|
jobConfig.CompletedAt = sql.NullTime{Time: time.Now(), Valid: true}
|
||||||
}
|
}
|
||||||
job := dbgen.ProvisionerJob(t, db, pubsub, jobConfig)
|
job := dbgen.ProvisionerJob(t, db, pubsub, jobConfig)
|
||||||
|
|
||||||
res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: job.ID})
|
res := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: job.ID})
|
||||||
agnt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID})
|
agnt := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ResourceID: res.ID})
|
||||||
|
taskApp := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agnt.ID})
|
||||||
var sidebarAppID uuid.UUID
|
|
||||||
if hasAITask.Bool {
|
|
||||||
sidebarApp := dbgen.WorkspaceApp(t, db, database.WorkspaceApp{AgentID: agnt.ID})
|
|
||||||
sidebarAppID = sidebarApp.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||||||
WorkspaceID: ws.ID,
|
WorkspaceID: ws.ID,
|
||||||
TemplateVersionID: version.ID,
|
TemplateVersionID: version.ID,
|
||||||
InitiatorID: user.UserID,
|
InitiatorID: user.UserID,
|
||||||
JobID: job.ID,
|
JobID: job.ID,
|
||||||
BuildNumber: 1,
|
BuildNumber: 1,
|
||||||
HasAITask: hasAITask,
|
AITaskSidebarAppID: uuid.NullUUID{UUID: taskApp.ID, Valid: createTask},
|
||||||
AITaskSidebarAppID: uuid.NullUUID{UUID: sidebarAppID, Valid: sidebarAppID != uuid.Nil},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if aiTaskPrompt != nil {
|
if createTask {
|
||||||
err := db.InsertWorkspaceBuildParameters(dbauthz.AsSystemRestricted(ctx), database.InsertWorkspaceBuildParametersParams{
|
task := dbgen.Task(t, db, database.TaskTable{
|
||||||
WorkspaceBuildID: build.ID,
|
WorkspaceID: uuid.NullUUID{UUID: ws.ID, Valid: true},
|
||||||
Name: []string{provider.TaskPromptParameterName},
|
OrganizationID: user.OrganizationID,
|
||||||
Value: []string{*aiTaskPrompt},
|
OwnerID: user.UserID,
|
||||||
|
TemplateVersionID: version.ID,
|
||||||
|
Prompt: prompt,
|
||||||
|
})
|
||||||
|
dbgen.TaskWorkspaceApp(t, db, database.TaskWorkspaceApp{
|
||||||
|
TaskID: task.ID,
|
||||||
|
WorkspaceBuildNumber: build.BuildNumber,
|
||||||
|
WorkspaceAgentID: uuid.NullUUID{UUID: agnt.ID, Valid: true},
|
||||||
|
WorkspaceAppID: uuid.NullUUID{UUID: taskApp.ID, Valid: true},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ws
|
return ws.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create test workspaces with different AI task configurations
|
// Create workspaces with tasks.
|
||||||
wsWithAITask := createWorkspaceWithAIConfig(sql.NullBool{Bool: true, Valid: true}, true, nil)
|
wsWithTask1 := createWorkspace(true, true, "Build me a web app")
|
||||||
wsWithoutAITask := createWorkspaceWithAIConfig(sql.NullBool{Bool: false, Valid: true}, false, nil)
|
wsWithTask2 := createWorkspace(false, true, "Another task")
|
||||||
|
|
||||||
aiTaskPrompt := "Build me a web app"
|
// Create workspaces without tasks
|
||||||
wsWithAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, false, &aiTaskPrompt)
|
wsWithoutTask1 := createWorkspace(true, false, "")
|
||||||
|
wsWithoutTask2 := createWorkspace(false, false, "")
|
||||||
anotherTaskPrompt := "Another task"
|
|
||||||
wsCompletedWithAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, true, &anotherTaskPrompt)
|
|
||||||
|
|
||||||
emptyPrompt := ""
|
|
||||||
wsWithEmptyAITaskParam := createWorkspaceWithAIConfig(sql.NullBool{Valid: false}, false, &emptyPrompt)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Debug: Check all workspaces without filter first
|
|
||||||
allRes, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Logf("Total workspaces created: %d", len(allRes.Workspaces))
|
|
||||||
for i, ws := range allRes.Workspaces {
|
|
||||||
t.Logf("All Workspace %d: ID=%s, Name=%s, Build ID=%s, Job ID=%s", i, ws.ID, ws.Name, ws.LatestBuild.ID, ws.LatestBuild.Job.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test filtering for workspaces with AI tasks
|
// Test filtering for workspaces with AI tasks
|
||||||
// Should include: wsWithAITask (has_ai_task=true) and wsWithAITaskParam (null + incomplete + param)
|
// Should include: wsWithTask1 and wsWithTask2
|
||||||
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
FilterQuery: "has-ai-task:true",
|
FilterQuery: "has-ai-task:true",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Logf("Expected 2 workspaces for has-ai-task:true, got %d", len(res.Workspaces))
|
|
||||||
t.Logf("Expected workspaces: %s, %s", wsWithAITask.ID, wsWithAITaskParam.ID)
|
|
||||||
for i, ws := range res.Workspaces {
|
|
||||||
t.Logf("AI Task True Workspace %d: ID=%s, Name=%s", i, ws.ID, ws.Name)
|
|
||||||
}
|
|
||||||
require.Len(t, res.Workspaces, 2)
|
require.Len(t, res.Workspaces, 2)
|
||||||
workspaceIDs := []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
|
workspaceIDs := []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
|
||||||
require.Contains(t, workspaceIDs, wsWithAITask.ID)
|
require.Contains(t, workspaceIDs, wsWithTask1)
|
||||||
require.Contains(t, workspaceIDs, wsWithAITaskParam.ID)
|
require.Contains(t, workspaceIDs, wsWithTask2)
|
||||||
|
|
||||||
// Test filtering for workspaces without AI tasks
|
// Test filtering for workspaces without AI tasks
|
||||||
// Should include: wsWithoutAITask, wsCompletedWithAITaskParam, wsWithEmptyAITaskParam
|
// Should include: wsWithoutTask1, wsWithoutTask2, wsWithoutTask3
|
||||||
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
||||||
FilterQuery: "has-ai-task:false",
|
FilterQuery: "has-ai-task:false",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Len(t, res.Workspaces, 2)
|
||||||
// Debug: print what we got
|
workspaceIDs = []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID}
|
||||||
t.Logf("Expected 3 workspaces for has-ai-task:false, got %d", len(res.Workspaces))
|
require.Contains(t, workspaceIDs, wsWithoutTask1)
|
||||||
for i, ws := range res.Workspaces {
|
require.Contains(t, workspaceIDs, wsWithoutTask2)
|
||||||
t.Logf("Workspace %d: ID=%s, Name=%s", i, ws.ID, ws.Name)
|
|
||||||
}
|
|
||||||
t.Logf("Expected IDs: %s, %s, %s", wsWithoutAITask.ID, wsCompletedWithAITaskParam.ID, wsWithEmptyAITaskParam.ID)
|
|
||||||
|
|
||||||
require.Len(t, res.Workspaces, 3)
|
|
||||||
workspaceIDs = []uuid.UUID{res.Workspaces[0].ID, res.Workspaces[1].ID, res.Workspaces[2].ID}
|
|
||||||
require.Contains(t, workspaceIDs, wsWithoutAITask.ID)
|
|
||||||
require.Contains(t, workspaceIDs, wsCompletedWithAITaskParam.ID)
|
|
||||||
require.Contains(t, workspaceIDs, wsWithEmptyAITaskParam.ID)
|
|
||||||
|
|
||||||
// Test no filter returns all
|
// Test no filter returns all
|
||||||
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
res, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, res.Workspaces, 5)
|
require.Len(t, res.Workspaces, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWorkspaceAppUpsertRestart(t *testing.T) {
|
func TestWorkspaceAppUpsertRestart(t *testing.T) {
|
||||||
|
|||||||
+8
-38
@@ -17,46 +17,16 @@ import (
|
|||||||
// AITaskPromptParameterName is the name of the parameter used to pass prompts
|
// AITaskPromptParameterName is the name of the parameter used to pass prompts
|
||||||
// to AI tasks.
|
// to AI tasks.
|
||||||
//
|
//
|
||||||
// Experimental: This value is experimental and may change in the future.
|
// Deprecated: This constant is deprecated and maintained only for backwards
|
||||||
|
// compatibility with older templates. Task prompts are now stored directly
|
||||||
|
// in the tasks.prompt database column. New code should access prompts via
|
||||||
|
// the Task.InitialPrompt field returned from task endpoints.
|
||||||
|
//
|
||||||
|
// This constant will be removed in a future major version. Templates should
|
||||||
|
// not rely on this parameter name, as the backend will continue to create it
|
||||||
|
// automatically for compatibility but reads from tasks.prompt.
|
||||||
const AITaskPromptParameterName = provider.TaskPromptParameterName
|
const AITaskPromptParameterName = provider.TaskPromptParameterName
|
||||||
|
|
||||||
// AITasksPromptsResponse represents the response from the AITaskPrompts method.
|
|
||||||
//
|
|
||||||
// Experimental: This method is experimental and may change in the future.
|
|
||||||
type AITasksPromptsResponse struct {
|
|
||||||
// Prompts is a map of workspace build IDs to prompts.
|
|
||||||
Prompts map[string]string `json:"prompts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AITaskPrompts returns prompts for multiple workspace builds by their IDs.
|
|
||||||
//
|
|
||||||
// Experimental: This method is experimental and may change in the future.
|
|
||||||
func (c *ExperimentalClient) AITaskPrompts(ctx context.Context, buildIDs []uuid.UUID) (AITasksPromptsResponse, error) {
|
|
||||||
if len(buildIDs) == 0 {
|
|
||||||
return AITasksPromptsResponse{
|
|
||||||
Prompts: make(map[string]string),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert UUIDs to strings and join them
|
|
||||||
buildIDStrings := make([]string, len(buildIDs))
|
|
||||||
for i, id := range buildIDs {
|
|
||||||
buildIDStrings[i] = id.String()
|
|
||||||
}
|
|
||||||
buildIDsParam := strings.Join(buildIDStrings, ",")
|
|
||||||
|
|
||||||
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/aitasks/prompts", nil, WithQueryParam("build_ids", buildIDsParam))
|
|
||||||
if err != nil {
|
|
||||||
return AITasksPromptsResponse{}, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return AITasksPromptsResponse{}, ReadBodyAsError(res)
|
|
||||||
}
|
|
||||||
var prompts AITasksPromptsResponse
|
|
||||||
return prompts, json.NewDecoder(res.Body).Decode(&prompts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateTaskRequest represents the request to create a new task.
|
// CreateTaskRequest represents the request to create a new task.
|
||||||
//
|
//
|
||||||
// Experimental: This type is experimental and may change in the future.
|
// Experimental: This type is experimental and may change in the future.
|
||||||
|
|||||||
@@ -2661,27 +2661,6 @@ export type CreateTaskFeedbackRequest = {
|
|||||||
class ExperimentalApiMethods {
|
class ExperimentalApiMethods {
|
||||||
constructor(protected readonly axios: AxiosInstance) {}
|
constructor(protected readonly axios: AxiosInstance) {}
|
||||||
|
|
||||||
getAITasksPrompts = async (
|
|
||||||
buildIds: TypesGen.WorkspaceBuild["id"][],
|
|
||||||
): Promise<TypesGen.AITasksPromptsResponse> => {
|
|
||||||
if (buildIds.length === 0) {
|
|
||||||
return {
|
|
||||||
prompts: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.axios.get<TypesGen.AITasksPromptsResponse>(
|
|
||||||
"/api/experimental/aitasks/prompts",
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
build_ids: buildIds.join(","),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
createTask = async (
|
createTask = async (
|
||||||
user: string,
|
user: string,
|
||||||
req: TypesGen.CreateTaskRequest,
|
req: TypesGen.CreateTaskRequest,
|
||||||
|
|||||||
Generated
+8
-14
@@ -108,23 +108,17 @@ export interface AIConfig {
|
|||||||
* AITaskPromptParameterName is the name of the parameter used to pass prompts
|
* AITaskPromptParameterName is the name of the parameter used to pass prompts
|
||||||
* to AI tasks.
|
* to AI tasks.
|
||||||
*
|
*
|
||||||
* Experimental: This value is experimental and may change in the future.
|
* Deprecated: This constant is deprecated and maintained only for backwards
|
||||||
|
* compatibility with older templates. Task prompts are now stored directly
|
||||||
|
* in the tasks.prompt database column. New code should access prompts via
|
||||||
|
* the Task.InitialPrompt field returned from task endpoints.
|
||||||
|
*
|
||||||
|
* This constant will be removed in a future major version. Templates should
|
||||||
|
* not rely on this parameter name, as the backend will continue to create it
|
||||||
|
* automatically for compatibility but reads from tasks.prompt.
|
||||||
*/
|
*/
|
||||||
export const AITaskPromptParameterName = "AI Prompt";
|
export const AITaskPromptParameterName = "AI Prompt";
|
||||||
|
|
||||||
// From codersdk/aitasks.go
|
|
||||||
/**
|
|
||||||
* AITasksPromptsResponse represents the response from the AITaskPrompts method.
|
|
||||||
*
|
|
||||||
* Experimental: This method is experimental and may change in the future.
|
|
||||||
*/
|
|
||||||
export interface AITasksPromptsResponse {
|
|
||||||
/**
|
|
||||||
* Prompts is a map of workspace build IDs to prompts.
|
|
||||||
*/
|
|
||||||
readonly prompts: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// From codersdk/allowlist.go
|
// From codersdk/allowlist.go
|
||||||
/**
|
/**
|
||||||
* APIAllowListTarget represents a single allow-list entry using the canonical
|
* APIAllowListTarget represents a single allow-list entry using the canonical
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
Template,
|
Template,
|
||||||
TemplateVersionExternalAuth,
|
TemplateVersionExternalAuth,
|
||||||
} from "api/typesGenerated";
|
} from "api/typesGenerated";
|
||||||
|
import { AITaskPromptParameterName } from "api/typesGenerated";
|
||||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||||
import { Button } from "components/Button/Button";
|
import { Button } from "components/Button/Button";
|
||||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||||
@@ -38,8 +39,6 @@ import { docs } from "utils/docs";
|
|||||||
import { PromptSelectTrigger } from "./PromptSelectTrigger";
|
import { PromptSelectTrigger } from "./PromptSelectTrigger";
|
||||||
import { TemplateVersionSelect } from "./TemplateVersionSelect";
|
import { TemplateVersionSelect } from "./TemplateVersionSelect";
|
||||||
|
|
||||||
const AI_PROMPT_PARAMETER_NAME = "AI Prompt";
|
|
||||||
|
|
||||||
type TaskPromptProps = {
|
type TaskPromptProps = {
|
||||||
templates: Template[] | undefined;
|
templates: Template[] | undefined;
|
||||||
error: unknown;
|
error: unknown;
|
||||||
@@ -168,7 +167,7 @@ const CreateTaskForm: FC<CreateTaskFormProps> = ({ templates, onSuccess }) => {
|
|||||||
|
|
||||||
// Read-only prompt if defined in preset
|
// Read-only prompt if defined in preset
|
||||||
const presetPrompt = selectedPreset?.Parameters?.find(
|
const presetPrompt = selectedPreset?.Parameters?.find(
|
||||||
(param) => param.Name === AI_PROMPT_PARAMETER_NAME,
|
(param) => param.Name === AITaskPromptParameterName,
|
||||||
)?.Value;
|
)?.Value;
|
||||||
const isPromptReadOnly = !!presetPrompt;
|
const isPromptReadOnly = !!presetPrompt;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user