mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat(coderd): add task prompt modification endpoint (#20811)
This PR adds the backend implementation for modifying task prompts. Part of https://github.com/coder/internal/issues/1084 ## Changes - New `UpdateTaskPrompt` database query to update task prompts - New PATCH `/api/v2/tasks/{task}/prompt` endpoint ## Notes This is part 1 of a 2-part PR stack. The frontend UI will be added in a follow-up PR based on this branch (https://github.com/coder/coder/pull/20812). --- 🤖 PR was written by Claude Sonnet 4.5 Thinking using [Coder Mux](https://github.com/coder/cmux) and reviewed by a human 👩
This commit is contained in:
+88
-4
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -500,7 +501,7 @@ func (api *API) convertTasks(ctx context.Context, requesterID uuid.UUID, dbTasks
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Experimental
|
||||
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
|
||||
// @Param task path string true "Task ID" format(uuid)
|
||||
// @Param task path string true "Task ID, or task name"
|
||||
// @Success 200 {object} codersdk.Task
|
||||
// @Router /api/experimental/tasks/{user}/{task} [get]
|
||||
//
|
||||
@@ -578,7 +579,7 @@ func (api *API) taskGet(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Experimental
|
||||
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
|
||||
// @Param task path string true "Task ID" format(uuid)
|
||||
// @Param task path string true "Task ID, or task name"
|
||||
// @Success 202 "Task deletion initiated"
|
||||
// @Router /api/experimental/tasks/{user}/{task} [delete]
|
||||
//
|
||||
@@ -646,13 +647,96 @@ func (api *API) taskDelete(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusAccepted)
|
||||
}
|
||||
|
||||
// @Summary Update AI task input
|
||||
// @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable.
|
||||
// @ID update-task-input
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Experimental
|
||||
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
|
||||
// @Param task path string true "Task ID, or task name"
|
||||
// @Param request body codersdk.UpdateTaskInputRequest true "Update task input request"
|
||||
// @Success 204
|
||||
// @Router /api/experimental/tasks/{user}/{task}/input [patch]
|
||||
//
|
||||
// EXPERIMENTAL: This endpoint is experimental and not guaranteed to be stable.
|
||||
// taskUpdateInput allows modifying a task's prompt before the agent executes it.
|
||||
func (api *API) taskUpdateInput(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
task = httpmw.TaskParam(r)
|
||||
auditor = api.Auditor.Load()
|
||||
taskResourceInfo = audit.AdditionalFields{}
|
||||
)
|
||||
|
||||
aReq, commitAudit := audit.InitRequest[database.TaskTable](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
AdditionalFields: taskResourceInfo,
|
||||
})
|
||||
defer commitAudit()
|
||||
aReq.Old = task.TaskTable()
|
||||
aReq.UpdateOrganizationID(task.OrganizationID)
|
||||
|
||||
var req codersdk.UpdateTaskInputRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Input) == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Task input is required.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var updatedTask database.TaskTable
|
||||
if err := api.Database.InTx(func(tx database.Store) error {
|
||||
task, err := tx.GetTaskByID(ctx, task.ID)
|
||||
if err != nil {
|
||||
return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to fetch task.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if task.Status != database.TaskStatusPaused {
|
||||
return httperror.NewResponseError(http.StatusConflict, codersdk.Response{
|
||||
Message: "Unable to update task input, task must be paused.",
|
||||
Detail: "Please stop the task's workspace before updating the input.",
|
||||
})
|
||||
}
|
||||
|
||||
updatedTask, err = tx.UpdateTaskPrompt(ctx, database.UpdateTaskPromptParams{
|
||||
ID: task.ID,
|
||||
Prompt: req.Input,
|
||||
})
|
||||
if err != nil {
|
||||
return httperror.NewResponseError(http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to update task input.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}, nil); err != nil {
|
||||
httperror.WriteResponseError(ctx, rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
aReq.New = updatedTask
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// @Summary Send input to AI task
|
||||
// @Description: EXPERIMENTAL: this endpoint is experimental and not guaranteed to be stable.
|
||||
// @ID send-task-input
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Experimental
|
||||
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
|
||||
// @Param task path string true "Task ID" format(uuid)
|
||||
// @Param task path string true "Task ID, or task name"
|
||||
// @Param request body codersdk.TaskSendRequest true "Task input request"
|
||||
// @Success 204 "Input sent successfully"
|
||||
// @Router /api/experimental/tasks/{user}/{task}/send [post]
|
||||
@@ -726,7 +810,7 @@ func (api *API) taskSend(rw http.ResponseWriter, r *http.Request) {
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Experimental
|
||||
// @Param user path string true "Username, user ID, or 'me' for the authenticated user"
|
||||
// @Param task path string true "Task ID" format(uuid)
|
||||
// @Param task path string true "Task ID, or task name"
|
||||
// @Success 200 {object} codersdk.TaskLogsResponse
|
||||
// @Router /api/experimental/tasks/{user}/{task}/logs [get]
|
||||
//
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"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/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/notifications"
|
||||
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
@@ -738,6 +739,210 @@ func TestTasks(t *testing.T) {
|
||||
require.Equal(t, http.StatusBadGateway, sdkErr.StatusCode())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateInput", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
disableProvisioner bool
|
||||
transition database.WorkspaceTransition
|
||||
cancelTransition bool
|
||||
deleteTask bool
|
||||
taskInput string
|
||||
wantStatus codersdk.TaskStatus
|
||||
wantErr string
|
||||
wantErrStatusCode int
|
||||
}{
|
||||
{
|
||||
name: "TaskStatusInitializing",
|
||||
// We want to disable the provisioner so that the task
|
||||
// never gets provisioned (ensuring it stays in Initializing).
|
||||
disableProvisioner: true,
|
||||
taskInput: "Valid prompt",
|
||||
wantStatus: codersdk.TaskStatusInitializing,
|
||||
wantErr: "Unable to update",
|
||||
wantErrStatusCode: http.StatusConflict,
|
||||
},
|
||||
{
|
||||
name: "TaskStatusPaused",
|
||||
transition: database.WorkspaceTransitionStop,
|
||||
taskInput: "Valid prompt",
|
||||
wantStatus: codersdk.TaskStatusPaused,
|
||||
},
|
||||
{
|
||||
name: "TaskStatusError",
|
||||
transition: database.WorkspaceTransitionStart,
|
||||
cancelTransition: true,
|
||||
taskInput: "Valid prompt",
|
||||
wantStatus: codersdk.TaskStatusError,
|
||||
wantErr: "Unable to update",
|
||||
wantErrStatusCode: http.StatusConflict,
|
||||
},
|
||||
{
|
||||
name: "EmptyPrompt",
|
||||
transition: database.WorkspaceTransitionStop,
|
||||
// We want to ensure an empty prompt is rejected.
|
||||
taskInput: "",
|
||||
wantStatus: codersdk.TaskStatusPaused,
|
||||
wantErr: "Task input is required.",
|
||||
wantErrStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "TaskDeleted",
|
||||
transition: database.WorkspaceTransitionStop,
|
||||
deleteTask: true,
|
||||
taskInput: "Valid prompt",
|
||||
wantErr: httpapi.ResourceNotFoundResponse.Message,
|
||||
wantErrStatusCode: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, provisioner := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
template := createAITemplate(t, client, user)
|
||||
|
||||
if tt.disableProvisioner {
|
||||
provisioner.Close()
|
||||
}
|
||||
|
||||
// Given: We create a task
|
||||
exp := codersdk.NewExperimentalClient(client)
|
||||
task, err := exp.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Input: "initial prompt",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, task.WorkspaceID.Valid, "task should have a workspace ID")
|
||||
|
||||
if !tt.disableProvisioner {
|
||||
// Given: The Task is running
|
||||
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Given: We transition the task's workspace
|
||||
build := coderdtest.CreateWorkspaceBuild(t, client, workspace, tt.transition)
|
||||
if tt.cancelTransition {
|
||||
// Given: We cancel the workspace build
|
||||
err := client.CancelWorkspaceBuild(ctx, build.ID, codersdk.CancelWorkspaceBuildParams{})
|
||||
require.NoError(t, err)
|
||||
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
|
||||
// Then: We expect it to be canceled
|
||||
build, err = client.WorkspaceBuild(ctx, build.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, codersdk.WorkspaceStatusCanceled, build.Status)
|
||||
} else {
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if tt.deleteTask {
|
||||
err = exp.DeleteTask(ctx, codersdk.Me, task.ID)
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
// Given: Task has expected status
|
||||
task, err = exp.TaskByID(ctx, task.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantStatus, task.Status)
|
||||
}
|
||||
|
||||
// When: We attempt to update the task input
|
||||
err = exp.UpdateTaskInput(ctx, task.OwnerName, task.ID, codersdk.UpdateTaskInputRequest{
|
||||
Input: tt.taskInput,
|
||||
})
|
||||
if tt.wantErr != "" {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
|
||||
if tt.wantErrStatusCode != 0 {
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, tt.wantErrStatusCode, apiErr.StatusCode())
|
||||
}
|
||||
|
||||
if !tt.deleteTask {
|
||||
// Then: We expect the input to **not** be updated
|
||||
task, err = exp.TaskByID(ctx, task.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, tt.taskInput, task.InitialPrompt)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
if !tt.deleteTask {
|
||||
// Then: We expect the input to be updated
|
||||
task, err = exp.TaskByID(ctx, task.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.taskInput, task.InitialPrompt)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("NonExistentTask", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
|
||||
exp := codersdk.NewExperimentalClient(client)
|
||||
|
||||
// Attempt to update prompt for non-existent task
|
||||
err := exp.UpdateTaskInput(ctx, user.UserID.String(), uuid.New(), codersdk.UpdateTaskInputRequest{
|
||||
Input: "Should fail",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("UnauthorizedUser", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
anotherUser, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
template := createAITemplate(t, client, user)
|
||||
|
||||
// Create a task as the first user
|
||||
exp := codersdk.NewExperimentalClient(client)
|
||||
task, err := exp.CreateTask(ctx, codersdk.Me, codersdk.CreateTaskRequest{
|
||||
TemplateVersionID: template.ActiveVersionID,
|
||||
Input: "initial prompt",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, task.WorkspaceID.Valid)
|
||||
|
||||
// Wait for workspace to complete
|
||||
workspace, err := client.Workspace(ctx, task.WorkspaceID.UUID)
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Stop the workspace
|
||||
build := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
|
||||
// Attempt to update prompt as another user should fail with 404 Not Found
|
||||
otherExp := codersdk.NewExperimentalClient(anotherUser)
|
||||
err = otherExp.UpdateTaskInput(ctx, task.OwnerName, task.ID, codersdk.UpdateTaskInputRequest{
|
||||
Input: "Should fail - unauthorized",
|
||||
})
|
||||
require.Error(t, err)
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTasksCreate(t *testing.T) {
|
||||
|
||||
Generated
+56
-8
@@ -228,8 +228,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Task ID",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -265,8 +264,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Task ID",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -279,6 +277,50 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/experimental/tasks/{user}/{task}/input": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Experimental"
|
||||
],
|
||||
"summary": "Update AI task input",
|
||||
"operationId": "update-task-input",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Username, user ID, or 'me' for the authenticated user",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update task input request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateTaskInputRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/experimental/tasks/{user}/{task}/logs": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -301,8 +343,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Task ID",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -340,8 +381,7 @@ const docTemplate = `{
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Task ID",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -19025,6 +19065,14 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateTaskInputRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateTemplateACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+54
-8
@@ -198,8 +198,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Task ID",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -233,8 +232,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Task ID",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -247,6 +245,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/experimental/tasks/{user}/{task}/input": {
|
||||
"patch": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": ["Experimental"],
|
||||
"summary": "Update AI task input",
|
||||
"operationId": "update-task-input",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Username, user ID, or 'me' for the authenticated user",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update task input request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateTaskInputRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/experimental/tasks/{user}/{task}/logs": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -267,8 +307,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Task ID",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -304,8 +343,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Task ID",
|
||||
"description": "Task ID, or task name",
|
||||
"name": "task",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -17452,6 +17490,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateTaskInputRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateTemplateACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1036,6 +1036,7 @@ func New(options *Options) *API {
|
||||
r.Use(httpmw.ExtractTaskParam(options.Database))
|
||||
r.Get("/", api.taskGet)
|
||||
r.Delete("/", api.taskDelete)
|
||||
r.Patch("/input", api.taskUpdateInput)
|
||||
r.Post("/send", api.taskSend)
|
||||
r.Get("/logs", api.taskLogs)
|
||||
})
|
||||
|
||||
@@ -5130,6 +5130,21 @@ func (q *querier) UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg
|
||||
return q.db.UpdateTailnetPeerStatusByCoordinator(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
|
||||
// An actor is allowed to update the prompt of a task if they have
|
||||
// permission to update the task (same as UpdateTaskWorkspaceID).
|
||||
task, err := q.db.GetTaskByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.TaskTable{}, err
|
||||
}
|
||||
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, task.RBACObject()); err != nil {
|
||||
return database.TaskTable{}, err
|
||||
}
|
||||
|
||||
return q.db.UpdateTaskPrompt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
|
||||
// An actor is allowed to update the workspace ID of a task if they are the
|
||||
// owner of the task and workspace or have the appropriate permissions.
|
||||
|
||||
@@ -2457,6 +2457,22 @@ func (s *MethodTestSuite) TestTasks() {
|
||||
|
||||
check.Args(arg).Asserts(task, policy.ActionUpdate, ws, policy.ActionUpdate).Returns(database.TaskTable{})
|
||||
}))
|
||||
s.Run("UpdateTaskPrompt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
task := testutil.Fake(s.T(), faker, database.Task{})
|
||||
arg := database.UpdateTaskPromptParams{
|
||||
ID: task.ID,
|
||||
Prompt: "Updated prompt text",
|
||||
}
|
||||
|
||||
// Create a copy of the task with the updated prompt
|
||||
updatedTask := task
|
||||
updatedTask.Prompt = arg.Prompt
|
||||
|
||||
dbm.EXPECT().GetTaskByID(gomock.Any(), task.ID).Return(task, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateTaskPrompt(gomock.Any(), arg).Return(updatedTask.TaskTable(), nil).AnyTimes()
|
||||
|
||||
check.Args(arg).Asserts(task, policy.ActionUpdate).Returns(updatedTask.TaskTable())
|
||||
}))
|
||||
s.Run("GetTaskByWorkspaceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
task := testutil.Fake(s.T(), faker, database.Task{})
|
||||
task.WorkspaceID = uuid.NullUUID{UUID: uuid.New(), Valid: true}
|
||||
|
||||
@@ -3154,6 +3154,13 @@ func (m queryMetricsStore) UpdateTailnetPeerStatusByCoordinator(ctx context.Cont
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateTaskPrompt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateTaskPrompt").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateTaskWorkspaceID(ctx, arg)
|
||||
|
||||
@@ -6770,6 +6770,21 @@ func (mr *MockStoreMockRecorder) UpdateTailnetPeerStatusByCoordinator(ctx, arg a
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTailnetPeerStatusByCoordinator", reflect.TypeOf((*MockStore)(nil).UpdateTailnetPeerStatusByCoordinator), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateTaskPrompt mocks base method.
|
||||
func (m *MockStore) UpdateTaskPrompt(ctx context.Context, arg database.UpdateTaskPromptParams) (database.TaskTable, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateTaskPrompt", ctx, arg)
|
||||
ret0, _ := ret[0].(database.TaskTable)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateTaskPrompt indicates an expected call of UpdateTaskPrompt.
|
||||
func (mr *MockStoreMockRecorder) UpdateTaskPrompt(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTaskPrompt", reflect.TypeOf((*MockStore)(nil).UpdateTaskPrompt), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateTaskWorkspaceID mocks base method.
|
||||
func (m *MockStore) UpdateTaskWorkspaceID(ctx context.Context, arg database.UpdateTaskWorkspaceIDParams) (database.TaskTable, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -132,11 +132,28 @@ func (w ConnectionLog) RBACObject() rbac.Object {
|
||||
return obj
|
||||
}
|
||||
|
||||
// TaskTable converts a Task to it's reduced version.
|
||||
// A more generalized solution is to use json marshaling to
|
||||
// consistently keep these two structs in sync.
|
||||
// That would be a lot of overhead, and a more costly unit test is
|
||||
// written to make sure these match up.
|
||||
func (t Task) TaskTable() TaskTable {
|
||||
return TaskTable{
|
||||
ID: t.ID,
|
||||
OrganizationID: t.OrganizationID,
|
||||
OwnerID: t.OwnerID,
|
||||
Name: t.Name,
|
||||
WorkspaceID: t.WorkspaceID,
|
||||
TemplateVersionID: t.TemplateVersionID,
|
||||
TemplateParameters: t.TemplateParameters,
|
||||
Prompt: t.Prompt,
|
||||
CreatedAt: t.CreatedAt,
|
||||
DeletedAt: t.DeletedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Task) RBACObject() rbac.Object {
|
||||
return rbac.ResourceTask.
|
||||
WithID(t.ID).
|
||||
WithOwner(t.OwnerID.String()).
|
||||
InOrg(t.OrganizationID)
|
||||
return t.TaskTable().RBACObject()
|
||||
}
|
||||
|
||||
func (t TaskTable) RBACObject() rbac.Object {
|
||||
|
||||
@@ -58,6 +58,45 @@ func TestWorkspaceTableConvert(t *testing.T) {
|
||||
"To resolve this, go to the 'func (w Workspace) WorkspaceTable()' and ensure all fields are converted.")
|
||||
}
|
||||
|
||||
// TestTaskTableConvert verifies all task fields are converted
|
||||
// when reducing a `Task` to a `TaskTable`.
|
||||
// This test is a guard rail to prevent developer oversight mistakes.
|
||||
func TestTaskTableConvert(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
staticRandoms := &testutil.Random{
|
||||
String: func() string { return "foo" },
|
||||
Bool: func() bool { return true },
|
||||
Int: func() int64 { return 500 },
|
||||
Uint: func() uint64 { return 126 },
|
||||
Float: func() float64 { return 3.14 },
|
||||
Complex: func() complex128 { return 6.24 },
|
||||
Time: func() time.Time {
|
||||
return time.Date(2020, 5, 2, 5, 19, 21, 30, time.UTC)
|
||||
},
|
||||
}
|
||||
|
||||
// Copies the approach taken by TestWorkspaceTableConvert.
|
||||
//
|
||||
// If you use 'PopulateStruct' to create 2 tasks, using the same
|
||||
// "random" values for each type. Then they should be identical.
|
||||
//
|
||||
// So if 'task.TaskTable()' was missing any fields in its
|
||||
// conversion, the comparison would fail.
|
||||
|
||||
var task Task
|
||||
err := testutil.PopulateStruct(&task, staticRandoms)
|
||||
require.NoError(t, err)
|
||||
|
||||
var subset TaskTable
|
||||
err = testutil.PopulateStruct(&subset, staticRandoms)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, task.TaskTable(), subset,
|
||||
"'task.TaskTable()' is not missing at least 1 field when converting to 'TaskTable'. "+
|
||||
"To resolve this, go to the 'func (t Task) TaskTable()' and ensure all fields are converted.")
|
||||
}
|
||||
|
||||
// TestAuditLogsQueryConsistency ensures that GetAuditLogsOffset and CountAuditLogs
|
||||
// have identical WHERE clauses to prevent filtering inconsistencies.
|
||||
// This test is a guard rail to prevent developer oversight mistakes.
|
||||
|
||||
@@ -686,6 +686,7 @@ type sqlcQuerier interface {
|
||||
UpdateProvisionerJobWithCompleteWithStartedAtByID(ctx context.Context, arg UpdateProvisionerJobWithCompleteWithStartedAtByIDParams) error
|
||||
UpdateReplica(ctx context.Context, arg UpdateReplicaParams) (Replica, error)
|
||||
UpdateTailnetPeerStatusByCoordinator(ctx context.Context, arg UpdateTailnetPeerStatusByCoordinatorParams) error
|
||||
UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error)
|
||||
UpdateTaskWorkspaceID(ctx context.Context, arg UpdateTaskWorkspaceIDParams) (TaskTable, error)
|
||||
UpdateTemplateACLByID(ctx context.Context, arg UpdateTemplateACLByIDParams) error
|
||||
UpdateTemplateAccessControlByID(ctx context.Context, arg UpdateTemplateAccessControlByIDParams) error
|
||||
|
||||
@@ -13425,6 +13425,40 @@ func (q *sqlQuerier) ListTasks(ctx context.Context, arg ListTasksParams) ([]Task
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateTaskPrompt = `-- name: UpdateTaskPrompt :one
|
||||
UPDATE
|
||||
tasks
|
||||
SET
|
||||
prompt = $1::text
|
||||
WHERE
|
||||
id = $2::uuid
|
||||
AND deleted_at IS NULL
|
||||
RETURNING id, organization_id, owner_id, name, workspace_id, template_version_id, template_parameters, prompt, created_at, deleted_at
|
||||
`
|
||||
|
||||
type UpdateTaskPromptParams struct {
|
||||
Prompt string `db:"prompt" json:"prompt"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateTaskPrompt(ctx context.Context, arg UpdateTaskPromptParams) (TaskTable, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateTaskPrompt, arg.Prompt, arg.ID)
|
||||
var i TaskTable
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.OrganizationID,
|
||||
&i.OwnerID,
|
||||
&i.Name,
|
||||
&i.WorkspaceID,
|
||||
&i.TemplateVersionID,
|
||||
&i.TemplateParameters,
|
||||
&i.Prompt,
|
||||
&i.CreatedAt,
|
||||
&i.DeletedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateTaskWorkspaceID = `-- name: UpdateTaskWorkspaceID :one
|
||||
UPDATE
|
||||
tasks
|
||||
|
||||
@@ -64,3 +64,14 @@ WHERE
|
||||
id = @id::uuid
|
||||
AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
|
||||
-- name: UpdateTaskPrompt :one
|
||||
UPDATE
|
||||
tasks
|
||||
SET
|
||||
prompt = @prompt::text
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
@@ -352,6 +352,28 @@ func (c *ExperimentalClient) TaskSend(ctx context.Context, user string, id uuid.
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTaskInputRequest is used to update a task's input.
|
||||
//
|
||||
// Experimental: This type is experimental and may change in the future.
|
||||
type UpdateTaskInputRequest struct {
|
||||
Input string `json:"input"`
|
||||
}
|
||||
|
||||
// UpdateTaskInput updates the task's input.
|
||||
//
|
||||
// Experimental: This method is experimental and may change in the future.
|
||||
func (c *ExperimentalClient) UpdateTaskInput(ctx context.Context, user string, id uuid.UUID, req UpdateTaskInputRequest) error {
|
||||
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/tasks/%s/%s/input", user, id.String()), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaskLogType indicates the source of a task log entry.
|
||||
//
|
||||
// Experimental: This type is experimental and may change in the future.
|
||||
|
||||
Generated
+50
-13
@@ -90,10 +90,10 @@ curl -X GET http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task}
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------------|----------|-------------------------------------------------------|
|
||||
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
|
||||
| `task` | path | string(uuid) | true | Task ID |
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------|----------|-------------------------------------------------------|
|
||||
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
|
||||
| `task` | path | string | true | Task ID, or task name |
|
||||
|
||||
### Example responses
|
||||
|
||||
@@ -121,10 +121,10 @@ curl -X DELETE http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{ta
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------------|----------|-------------------------------------------------------|
|
||||
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
|
||||
| `task` | path | string(uuid) | true | Task ID |
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------|----------|-------------------------------------------------------|
|
||||
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
|
||||
| `task` | path | string | true | Task ID, or task name |
|
||||
|
||||
### Responses
|
||||
|
||||
@@ -134,6 +134,43 @@ curl -X DELETE http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{ta
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Update AI task input
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X PATCH http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task}/input \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`PATCH /api/experimental/tasks/{user}/{task}/input`
|
||||
|
||||
> Body parameter
|
||||
|
||||
```json
|
||||
{
|
||||
"input": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|------------------------------------------------------------------------------|----------|-------------------------------------------------------|
|
||||
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
|
||||
| `task` | path | string | true | Task ID, or task name |
|
||||
| `body` | body | [codersdk.UpdateTaskInputRequest](schemas.md#codersdkupdatetaskinputrequest) | true | Update task input request |
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|-----------------------------------------------------------------|-------------|--------|
|
||||
| 204 | [No Content](https://tools.ietf.org/html/rfc7231#section-6.3.5) | No Content | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get AI task logs
|
||||
|
||||
### Code samples
|
||||
@@ -149,10 +186,10 @@ curl -X GET http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task}
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------------|----------|-------------------------------------------------------|
|
||||
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
|
||||
| `task` | path | string(uuid) | true | Task ID |
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|--------|----------|-------------------------------------------------------|
|
||||
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
|
||||
| `task` | path | string | true | Task ID, or task name |
|
||||
|
||||
### Example responses
|
||||
|
||||
@@ -192,7 +229,7 @@ curl -X POST http://coder-server:8080/api/v2/api/experimental/tasks/{user}/{task
|
||||
| Name | In | Type | Required | Description |
|
||||
|--------|------|----------------------------------------------------------------|----------|-------------------------------------------------------|
|
||||
| `user` | path | string | true | Username, user ID, or 'me' for the authenticated user |
|
||||
| `task` | path | string(uuid) | true | Task ID |
|
||||
| `task` | path | string | true | Task ID, or task name |
|
||||
| `body` | body | [codersdk.TaskSendRequest](schemas.md#codersdktasksendrequest) | true | Task input request |
|
||||
|
||||
### Responses
|
||||
|
||||
Generated
+14
@@ -9198,6 +9198,20 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
|---------|-----------------|----------|--------------|-------------|
|
||||
| `roles` | array of string | false | | |
|
||||
|
||||
## codersdk.UpdateTaskInputRequest
|
||||
|
||||
```json
|
||||
{
|
||||
"input": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|---------|--------|----------|--------------|-------------|
|
||||
| `input` | string | false | | |
|
||||
|
||||
## codersdk.UpdateTemplateACL
|
||||
|
||||
```json
|
||||
|
||||
Generated
+10
@@ -5371,6 +5371,16 @@ export interface UpdateRoles {
|
||||
readonly roles: readonly string[];
|
||||
}
|
||||
|
||||
// From codersdk/aitasks.go
|
||||
/**
|
||||
* UpdateTaskInputRequest is used to update a task's input.
|
||||
*
|
||||
* Experimental: This type is experimental and may change in the future.
|
||||
*/
|
||||
export interface UpdateTaskInputRequest {
|
||||
readonly input: string;
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
export interface UpdateTemplateACL {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user