Skip to content

Commit 30b4008

Browse files
refactor: lift task creation to coderd
Instead of creating tasks with a specialised call to `CreateWorkspace` on the frontend, we instead lift this to the backend and allow the frontend to simply call `CreateAITask`.
1 parent dadeab8 commit 30b4008

File tree

7 files changed

+163
-7
lines changed

7 files changed

+163
-7
lines changed

coderd/aitasks.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import (
77

88
"github.com/google/uuid"
99

10+
"github.com/coder/coder/v2/coderd/audit"
11+
"github.com/coder/coder/v2/coderd/database"
1012
"github.com/coder/coder/v2/coderd/httpapi"
13+
"github.com/coder/coder/v2/coderd/httpmw"
1114
"github.com/coder/coder/v2/codersdk"
1215
)
1316

@@ -61,3 +64,59 @@ func (api *API) aiTasksPrompts(rw http.ResponseWriter, r *http.Request) {
6164
Prompts: promptsByBuildID,
6265
})
6366
}
67+
68+
// This endpoint is experimental and not guaranteed to be stable, so we're not
69+
// generating public-facing documentation for it.
70+
func (api *API) aiTasksCreate(rw http.ResponseWriter, r *http.Request) {
71+
var (
72+
ctx = r.Context()
73+
apiKey = httpmw.APIKey(r)
74+
auditor = api.Auditor.Load()
75+
)
76+
77+
var req codersdk.CreateAITasksRequest
78+
if !httpapi.Read(ctx, rw, r, &req) {
79+
return
80+
}
81+
82+
user, err := api.Database.GetUserByID(ctx, apiKey.UserID)
83+
if httpapi.Is404Error(err) {
84+
httpapi.ResourceNotFound(rw)
85+
return
86+
}
87+
if err != nil {
88+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
89+
Message: "Internal error fetching user.",
90+
Detail: err.Error(),
91+
})
92+
return
93+
}
94+
95+
createReq := codersdk.CreateWorkspaceRequest{
96+
Name: req.Name,
97+
TemplateVersionID: req.TemplateVersionID,
98+
TemplateVersionPresetID: req.TemplateVersionPresetID,
99+
RichParameterValues: []codersdk.WorkspaceBuildParameter{
100+
{Name: "AI Prompt", Value: req.Prompt},
101+
},
102+
}
103+
104+
owner := workspaceOwner{
105+
ID: user.ID,
106+
Username: user.Username,
107+
AvatarURL: user.AvatarURL,
108+
}
109+
110+
aReq, commitAudit := audit.InitRequest[database.WorkspaceTable](rw, &audit.RequestParams{
111+
Audit: *auditor,
112+
Log: api.Logger,
113+
Request: r,
114+
Action: database.AuditActionCreate,
115+
AdditionalFields: audit.AdditionalFields{
116+
WorkspaceOwner: owner.Username,
117+
},
118+
})
119+
120+
defer commitAudit()
121+
createWorkspace(ctx, aReq, apiKey.UserID, api, owner, createReq, rw, r)
122+
}

coderd/aitasks_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,58 @@ func TestAITasksPrompts(t *testing.T) {
139139
require.Empty(t, prompts.Prompts)
140140
})
141141
}
142+
143+
func TestAITasksCreate(t *testing.T) {
144+
t.Parallel()
145+
146+
makeEchoResponses := func(parameters []*proto.RichParameter) *echo.Responses {
147+
return &echo.Responses{
148+
Parse: echo.ParseComplete,
149+
ProvisionApply: echo.ApplyComplete,
150+
ProvisionPlan: []*proto.Response{
151+
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{Parameters: parameters}}},
152+
},
153+
}
154+
}
155+
156+
t.Run("OK", func(t *testing.T) {
157+
var (
158+
ctx = testutil.Context(t, testutil.WaitShort)
159+
160+
taskName = "task-foo-bar-baz"
161+
taskPrompt = "Some task prompt"
162+
)
163+
164+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
165+
user := coderdtest.CreateFirstUser(t, client)
166+
167+
// Given: A template with an "AI Prompt"
168+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, makeEchoResponses([]*proto.RichParameter{
169+
{Name: "AI Prompt", Type: "string"},
170+
}))
171+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
172+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
173+
174+
expClient := codersdk.NewExperimentalClient(client)
175+
176+
// When: We attempt to create a Task.
177+
workspace, err := expClient.AITasksCreate(ctx, codersdk.CreateAITasksRequest{
178+
Name: taskName,
179+
TemplateVersionID: template.ActiveVersionID,
180+
Prompt: taskPrompt,
181+
})
182+
require.NoError(t, err)
183+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
184+
185+
// Then: We expect a workspace to have been created.
186+
require.Equal(t, taskName, workspace.Name)
187+
require.Equal(t, template.ID, workspace.TemplateID)
188+
189+
// And: We expect it to have the "AI Prompt" parameter correctly set.
190+
parameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
191+
require.NoError(t, err)
192+
require.Len(t, parameters, 1)
193+
require.Equal(t, "AI Prompt", parameters[0].Name)
194+
require.Equal(t, taskPrompt, parameters[0].Value)
195+
})
196+
}

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,7 @@ func New(options *Options) *API {
994994
r.Use(apiKeyMiddleware)
995995
r.Route("/aitasks", func(r chi.Router) {
996996
r.Get("/prompts", api.aiTasksPrompts)
997+
r.Post("/", api.aiTasksCreate)
997998
})
998999
r.Route("/mcp", func(r chi.Router) {
9991000
r.Use(

codersdk/aitasks.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,29 @@ func (c *ExperimentalClient) AITaskPrompts(ctx context.Context, buildIDs []uuid.
4444
var prompts AITasksPromptsResponse
4545
return prompts, json.NewDecoder(res.Body).Decode(&prompts)
4646
}
47+
48+
type CreateAITasksRequest struct {
49+
Name string `json:"name"`
50+
TemplateVersionID uuid.UUID `json:"template_version_id"`
51+
TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty"`
52+
Prompt string `json:"prompt"`
53+
}
54+
55+
func (c *ExperimentalClient) AITasksCreate(ctx context.Context, request CreateAITasksRequest) (Workspace, error) {
56+
res, err := c.Request(ctx, http.MethodPost, "/api/experimental/aitasks/", request)
57+
if err != nil {
58+
return Workspace{}, err
59+
}
60+
defer res.Body.Close()
61+
62+
if res.StatusCode != http.StatusCreated {
63+
return Workspace{}, ReadBodyAsError(res)
64+
}
65+
66+
var workspace Workspace
67+
if err := json.NewDecoder(res.Body).Decode(&workspace); err != nil {
68+
return Workspace{}, err
69+
}
70+
71+
return workspace, nil
72+
}

site/src/api/api.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2665,6 +2665,17 @@ class ExperimentalApiMethods {
26652665

26662666
return response.data;
26672667
};
2668+
2669+
createAITask = async (
2670+
req: TypesGen.CreateAITasksRequest,
2671+
): Promise<TypesGen.Workspace> => {
2672+
const response = await this.axios.post<TypesGen.Workspace>(
2673+
"/api/experimental/aitasks",
2674+
req,
2675+
);
2676+
2677+
return response.data;
2678+
};
26682679
}
26692680

26702681
// This is a hard coded CSRF token/cookie pair for local development. In prod,

site/src/api/typesGenerated.ts

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/pages/TasksPage/TasksPage.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,6 @@ type TaskFormProps = {
232232
};
233233

234234
const TaskForm: FC<TaskFormProps> = ({ templates, onSuccess }) => {
235-
const { user } = useAuthenticated();
236235
const queryClient = useQueryClient();
237236
const [selectedTemplateId, setSelectedTemplateId] = useState<string>(
238237
templates[0].id,
@@ -293,7 +292,7 @@ const TaskForm: FC<TaskFormProps> = ({ templates, onSuccess }) => {
293292
templateVersionId,
294293
presetId,
295294
}: CreateTaskMutationFnProps) =>
296-
data.createTask(prompt, user.id, templateVersionId, presetId),
295+
data.createTask(prompt, templateVersionId, presetId),
297296
onSuccess: async (task) => {
298297
await queryClient.invalidateQueries({
299298
queryKey: ["tasks"],
@@ -727,7 +726,6 @@ export const data = {
727726

728727
async createTask(
729728
prompt: string,
730-
userId: string,
731729
templateVersionId: string,
732730
presetId: string | null = null,
733731
): Promise<Task> {
@@ -741,13 +739,11 @@ export const data = {
741739
}
742740
}
743741

744-
const workspace = await API.createWorkspace(userId, {
742+
const workspace = await API.experimental.createAITask({
745743
name: `task-${generateWorkspaceName()}`,
746744
template_version_id: templateVersionId,
747745
template_version_preset_id: preset_id || undefined,
748-
rich_parameter_values: [
749-
{ name: AI_PROMPT_PARAMETER_NAME, value: prompt },
750-
],
746+
prompt,
751747
});
752748

753749
return {

0 commit comments

Comments
 (0)