feat: add the /aitasks/prompts endpoint (#18464)

Add an endpoint to fetch AI task prompts for multiple workspace builds
at the same time. A prompt is the value of the "AI Prompt" workspace
build parameter. On main, the only way our API allows fetching workspace
build parameters is by using the `/workspacebuilds/$build_id/parameters`
endpoint, requiring a separate API call for every build.

The Tasks dashboard fetches Task workspaces in order to show them in a
list, and then needs to fetch the value of the `AI Prompt` parameter for
every task workspace (using its latest build id), requiring an
additional API call for each list item. This endpoint will allow the
dashboard to make just 2 calls to render the list: one to fetch task
workspaces, the other to fetch prompts.

<img width="1512" alt="Screenshot 2025-06-20 at 11 33 11"
src="https://github.com/user-attachments/assets/92899999-e922-44c5-8325-b4b23a0d2bff"
/>

Related to https://github.com/coder/internal/issues/660.
This commit is contained in:
Hugo Dutka
2025-06-24 13:06:02 +02:00
committed by GitHub
parent 0238f2926d
commit 670fa4a3cc
16 changed files with 493 additions and 1 deletions
+63
View File
@@ -0,0 +1,63 @@
package coderd
import (
"fmt"
"net/http"
"strings"
"github.com/google/uuid"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
)
// 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,
})
}
+141
View File
@@ -0,0 +1,141 @@
package coderd_test
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"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()
if !dbtestutil.WillUsePostgres() {
t.Skip("This test checks RBAC, which is not supported in the in-memory database")
}
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)
})
}
+8
View File
@@ -939,6 +939,14 @@ func New(options *Options) *API {
}) })
}) })
// Experimental routes are not guaranteed to be stable and may change at any time.
r.Route("/api/experimental", func(r chi.Router) {
r.Use(apiKeyMiddleware)
r.Route("/aitasks", func(r chi.Router) {
r.Get("/prompts", api.aiTasksPrompts)
})
})
r.Route("/api/v2", func(r chi.Router) { r.Route("/api/v2", func(r chi.Router) {
api.APIHandler = r api.APIHandler = r
+13
View File
@@ -3281,6 +3281,15 @@ func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuil
return q.db.GetWorkspaceBuildParameters(ctx, workspaceBuildID) return q.db.GetWorkspaceBuildParameters(ctx, workspaceBuildID)
} }
func (q *querier) GetWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIDs []uuid.UUID) ([]database.WorkspaceBuildParameter, error) {
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceWorkspace.Type)
if err != nil {
return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
}
return q.db.GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx, workspaceBuildIDs, prep)
}
func (q *querier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { func (q *querier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil { if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err return nil, err
@@ -5266,6 +5275,10 @@ func (q *querier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context,
return q.GetWorkspacesAndAgentsByOwnerID(ctx, ownerID) return q.GetWorkspacesAndAgentsByOwnerID(ctx, ownerID)
} }
func (q *querier) GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIDs []uuid.UUID, _ rbac.PreparedAuthorized) ([]database.WorkspaceBuildParameter, error) {
return q.GetWorkspaceBuildParametersByBuildIDs(ctx, workspaceBuildIDs)
}
// GetAuthorizedUsers is not required for dbauthz since GetUsers is already // GetAuthorizedUsers is not required for dbauthz since GetUsers is already
// authenticated. // authenticated.
func (q *querier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, _ rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { func (q *querier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, _ rbac.PreparedAuthorized) ([]database.GetUsersRow, error) {
+8
View File
@@ -2012,6 +2012,14 @@ func (s *MethodTestSuite) TestWorkspace() {
// No asserts here because SQLFilter. // No asserts here because SQLFilter.
check.Args(ws.OwnerID, emptyPreparedAuthorized{}).Asserts() check.Args(ws.OwnerID, emptyPreparedAuthorized{}).Asserts()
})) }))
s.Run("GetWorkspaceBuildParametersByBuildIDs", s.Subtest(func(db database.Store, check *expects) {
// no asserts here because SQLFilter
check.Args([]uuid.UUID{}).Asserts()
}))
s.Run("GetAuthorizedWorkspaceBuildParametersByBuildIDs", s.Subtest(func(db database.Store, check *expects) {
// no asserts here because SQLFilter
check.Args([]uuid.UUID{}, emptyPreparedAuthorized{}).Asserts()
}))
s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Subtest(func(db database.Store, check *expects) { s.Run("GetLatestWorkspaceBuildByWorkspaceID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{}) u := dbgen.User(s.T(), db, database.User{})
o := dbgen.Organization(s.T(), db, database.Organization{}) o := dbgen.Organization(s.T(), db, database.Organization{})
+29
View File
@@ -7960,6 +7960,11 @@ func (q *FakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu
return q.getWorkspaceBuildParametersNoLock(workspaceBuildID) return q.getWorkspaceBuildParametersNoLock(workspaceBuildID)
} }
func (q *FakeQuerier) GetWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIDs []uuid.UUID) ([]database.WorkspaceBuildParameter, error) {
// No auth filter.
return q.GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx, workspaceBuildIDs, nil)
}
func (q *FakeQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { func (q *FakeQuerier) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
q.mutex.RLock() q.mutex.RLock()
defer q.mutex.RUnlock() defer q.mutex.RUnlock()
@@ -13901,6 +13906,30 @@ func (q *FakeQuerier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Cont
return out, nil return out, nil
} }
func (q *FakeQuerier) GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIDs []uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.WorkspaceBuildParameter, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
if prepared != nil {
// Call this to match the same function calls as the SQL implementation.
_, err := prepared.CompileToSQL(ctx, rbac.ConfigWithoutACL())
if err != nil {
return nil, err
}
}
filteredParameters := make([]database.WorkspaceBuildParameter, 0)
for _, buildID := range workspaceBuildIDs {
parameters, err := q.GetWorkspaceBuildParameters(ctx, buildID)
if err != nil {
return nil, err
}
filteredParameters = append(filteredParameters, parameters...)
}
return filteredParameters, nil
}
func (q *FakeQuerier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { func (q *FakeQuerier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) {
if err := validateDatabaseType(arg); err != nil { if err := validateDatabaseType(arg); err != nil {
return nil, err return nil, err
+14
View File
@@ -1873,6 +1873,13 @@ func (m queryMetricsStore) GetWorkspaceBuildParameters(ctx context.Context, work
return params, err return params, err
} }
func (m queryMetricsStore) GetWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIds []uuid.UUID) ([]database.WorkspaceBuildParameter, error) {
start := time.Now()
r0, r1 := m.s.GetWorkspaceBuildParametersByBuildIDs(ctx, workspaceBuildIds)
m.queryLatencies.WithLabelValues("GetWorkspaceBuildParametersByBuildIDs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { func (m queryMetricsStore) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
start := time.Now() start := time.Now()
r0, r1 := m.s.GetWorkspaceBuildStatsByTemplates(ctx, since) r0, r1 := m.s.GetWorkspaceBuildStatsByTemplates(ctx, since)
@@ -3343,6 +3350,13 @@ func (m queryMetricsStore) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context
return r0, r1 return r0, r1
} }
func (m queryMetricsStore) GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIDs []uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.WorkspaceBuildParameter, error) {
start := time.Now()
r0, r1 := m.s.GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx, workspaceBuildIDs, prepared)
m.queryLatencies.WithLabelValues("GetAuthorizedWorkspaceBuildParametersByBuildIDs").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) { func (m queryMetricsStore) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, prepared rbac.PreparedAuthorized) ([]database.GetUsersRow, error) {
start := time.Now() start := time.Now()
r0, r1 := m.s.GetAuthorizedUsers(ctx, arg, prepared) r0, r1 := m.s.GetAuthorizedUsers(ctx, arg, prepared)
+30
View File
@@ -1247,6 +1247,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedUsers(ctx, arg, prepared any) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedUsers", reflect.TypeOf((*MockStore)(nil).GetAuthorizedUsers), ctx, arg, prepared) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedUsers", reflect.TypeOf((*MockStore)(nil).GetAuthorizedUsers), ctx, arg, prepared)
} }
// GetAuthorizedWorkspaceBuildParametersByBuildIDs mocks base method.
func (m *MockStore) GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIDs []uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.WorkspaceBuildParameter, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthorizedWorkspaceBuildParametersByBuildIDs", ctx, workspaceBuildIDs, prepared)
ret0, _ := ret[0].([]database.WorkspaceBuildParameter)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAuthorizedWorkspaceBuildParametersByBuildIDs indicates an expected call of GetAuthorizedWorkspaceBuildParametersByBuildIDs.
func (mr *MockStoreMockRecorder) GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx, workspaceBuildIDs, prepared any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspaceBuildParametersByBuildIDs", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspaceBuildParametersByBuildIDs), ctx, workspaceBuildIDs, prepared)
}
// GetAuthorizedWorkspaces mocks base method. // GetAuthorizedWorkspaces mocks base method.
func (m *MockStore) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesRow, error) { func (m *MockStore) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesRow, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@@ -3932,6 +3947,21 @@ func (mr *MockStoreMockRecorder) GetWorkspaceBuildParameters(ctx, workspaceBuild
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildParameters", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildParameters), ctx, workspaceBuildID) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildParameters", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildParameters), ctx, workspaceBuildID)
} }
// GetWorkspaceBuildParametersByBuildIDs mocks base method.
func (m *MockStore) GetWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIds []uuid.UUID) ([]database.WorkspaceBuildParameter, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWorkspaceBuildParametersByBuildIDs", ctx, workspaceBuildIds)
ret0, _ := ret[0].([]database.WorkspaceBuildParameter)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWorkspaceBuildParametersByBuildIDs indicates an expected call of GetWorkspaceBuildParametersByBuildIDs.
func (mr *MockStoreMockRecorder) GetWorkspaceBuildParametersByBuildIDs(ctx, workspaceBuildIds any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWorkspaceBuildParametersByBuildIDs", reflect.TypeOf((*MockStore)(nil).GetWorkspaceBuildParametersByBuildIDs), ctx, workspaceBuildIds)
}
// GetWorkspaceBuildStatsByTemplates mocks base method. // GetWorkspaceBuildStatsByTemplates mocks base method.
func (m *MockStore) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) { func (m *MockStore) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
+30
View File
@@ -226,6 +226,7 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([
type workspaceQuerier interface { type workspaceQuerier interface {
GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, error) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, error)
GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID, prepared rbac.PreparedAuthorized) ([]GetWorkspacesAndAgentsByOwnerIDRow, error) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context, ownerID uuid.UUID, prepared rbac.PreparedAuthorized) ([]GetWorkspacesAndAgentsByOwnerIDRow, error)
GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIDs []uuid.UUID, prepared rbac.PreparedAuthorized) ([]WorkspaceBuildParameter, error)
} }
// GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access. // GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access.
@@ -372,6 +373,35 @@ func (q *sqlQuerier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Conte
return items, nil return items, nil
} }
func (q *sqlQuerier) GetAuthorizedWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIDs []uuid.UUID, prepared rbac.PreparedAuthorized) ([]WorkspaceBuildParameter, error) {
authorizedFilter, err := prepared.CompileToSQL(ctx, rbac.ConfigWorkspaces())
if err != nil {
return nil, xerrors.Errorf("compile authorized filter: %w", err)
}
filtered, err := insertAuthorizedFilter(getWorkspaceBuildParametersByBuildIDs, fmt.Sprintf(" AND %s", authorizedFilter))
if err != nil {
return nil, xerrors.Errorf("insert authorized filter: %w", err)
}
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaceBuildParametersByBuildIDs :many\n%s", filtered)
rows, err := q.db.QueryContext(ctx, query, pq.Array(workspaceBuildIDs))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceBuildParameter
for rows.Next() {
var i WorkspaceBuildParameter
if err := rows.Scan(&i.WorkspaceBuildID, &i.Name, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
return items, nil
}
type userQuerier interface { type userQuerier interface {
GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, prepared rbac.PreparedAuthorized) ([]GetUsersRow, error) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, prepared rbac.PreparedAuthorized) ([]GetUsersRow, error)
} }
+1
View File
@@ -428,6 +428,7 @@ type sqlcQuerier interface {
GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error)
GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, error)
GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuildID uuid.UUID) ([]WorkspaceBuildParameter, error)
GetWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIds []uuid.UUID) ([]WorkspaceBuildParameter, error)
GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]GetWorkspaceBuildStatsByTemplatesRow, error)
GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error) GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error)
GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error) GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error)
+38
View File
@@ -17035,6 +17035,44 @@ func (q *sqlQuerier) GetWorkspaceBuildParameters(ctx context.Context, workspaceB
return items, nil return items, nil
} }
const getWorkspaceBuildParametersByBuildIDs = `-- name: GetWorkspaceBuildParametersByBuildIDs :many
SELECT
workspace_build_parameters.workspace_build_id, workspace_build_parameters.name, workspace_build_parameters.value
FROM
workspace_build_parameters
JOIN
workspace_builds ON workspace_builds.id = workspace_build_parameters.workspace_build_id
JOIN
workspaces ON workspaces.id = workspace_builds.workspace_id
WHERE
workspace_build_parameters.workspace_build_id = ANY($1 :: uuid[])
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaceBuildParametersByBuildIDs
-- @authorize_filter
`
func (q *sqlQuerier) GetWorkspaceBuildParametersByBuildIDs(ctx context.Context, workspaceBuildIds []uuid.UUID) ([]WorkspaceBuildParameter, error) {
rows, err := q.db.QueryContext(ctx, getWorkspaceBuildParametersByBuildIDs, pq.Array(workspaceBuildIds))
if err != nil {
return nil, err
}
defer rows.Close()
var items []WorkspaceBuildParameter
for rows.Next() {
var i WorkspaceBuildParameter
if err := rows.Scan(&i.WorkspaceBuildID, &i.Name, &i.Value); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertWorkspaceBuildParameters = `-- name: InsertWorkspaceBuildParameters :exec const insertWorkspaceBuildParameters = `-- name: InsertWorkspaceBuildParameters :exec
INSERT INTO INSERT INTO
workspace_build_parameters (workspace_build_id, name, value) workspace_build_parameters (workspace_build_id, name, value)
@@ -41,3 +41,18 @@ FROM (
) q1 ) q1
ORDER BY created_at DESC, name ORDER BY created_at DESC, name
LIMIT 100; LIMIT 100;
-- name: GetWorkspaceBuildParametersByBuildIDs :many
SELECT
workspace_build_parameters.*
FROM
workspace_build_parameters
JOIN
workspace_builds ON workspace_builds.id = workspace_build_parameters.workspace_build_id
JOIN
workspaces ON workspaces.id = workspace_builds.workspace_id
WHERE
workspace_build_parameters.workspace_build_id = ANY(@workspace_build_ids :: uuid[])
-- Authorize Filter clause will be injected below in GetAuthorizedWorkspaceBuildParametersByBuildIDs
-- @authorize_filter
;
+46
View File
@@ -0,0 +1,46 @@
package codersdk
import (
"context"
"encoding/json"
"net/http"
"strings"
"github.com/google/uuid"
"github.com/coder/terraform-provider-coder/v2/provider"
)
const AITaskPromptParameterName = provider.TaskPromptParameterName
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.
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)
}
+14
View File
@@ -0,0 +1,14 @@
package codersdk
// ExperimentalClient is a client for the experimental API.
// Its interface is not guaranteed to be stable and may change at any time.
// @typescript-ignore ExperimentalClient
type ExperimentalClient struct {
*Client
}
func NewExperimentalClient(client *Client) *ExperimentalClient {
return &ExperimentalClient{
Client: client,
}
}
+35 -1
View File
@@ -411,7 +411,11 @@ export type GetProvisionerDaemonsParams = {
* lexical scope. * lexical scope.
*/ */
class ApiMethods { class ApiMethods {
constructor(protected readonly axios: AxiosInstance) {} experimental: ExperimentalApiMethods;
constructor(protected readonly axios: AxiosInstance) {
this.experimental = new ExperimentalApiMethods(this.axios);
}
login = async ( login = async (
email: string, email: string,
@@ -2599,6 +2603,36 @@ class ApiMethods {
}; };
} }
// Experimental API methods call endpoints under the /api/experimental/ prefix.
// These endpoints are not stable and may change or be removed at any time.
//
// All methods must be defined with arrow function syntax. See the docstring
// above the ApiMethods class for a full explanation.
class ExperimentalApiMethods {
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;
};
}
// This is a hard coded CSRF token/cookie pair for local development. In prod, // This is a hard coded CSRF token/cookie pair for local development. In prod,
// the GoLang webserver generates a random cookie with a new token for each // the GoLang webserver generates a random cookie with a new token for each
// document request. For local development, we don't use the Go webserver for // document request. For local development, we don't use the Go webserver for
+8
View File
@@ -18,6 +18,14 @@ export interface AIProviderConfig {
readonly base_url: string; readonly base_url: string;
} }
// From codersdk/aitasks.go
export const AITaskPromptParameterName = "AI Prompt";
// From codersdk/aitasks.go
export interface AITasksPromptsResponse {
readonly prompts: Record<string, string>;
}
// From codersdk/apikey.go // From codersdk/apikey.go
export interface APIKey { export interface APIKey {
readonly id: string; readonly id: string;