mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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) {
|
||||
api.APIHandler = r
|
||||
|
||||
|
||||
@@ -3281,6 +3281,15 @@ func (q *querier) GetWorkspaceBuildParameters(ctx context.Context, workspaceBuil
|
||||
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) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
|
||||
return nil, err
|
||||
@@ -5266,6 +5275,10 @@ func (q *querier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Context,
|
||||
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
|
||||
// authenticated.
|
||||
func (q *querier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersParams, _ rbac.PreparedAuthorized) ([]database.GetUsersRow, error) {
|
||||
|
||||
@@ -2012,6 +2012,14 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
// No asserts here because SQLFilter.
|
||||
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) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
o := dbgen.Organization(s.T(), db, database.Organization{})
|
||||
|
||||
@@ -7960,6 +7960,11 @@ func (q *FakeQuerier) GetWorkspaceBuildParameters(_ context.Context, workspaceBu
|
||||
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) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
@@ -13901,6 +13906,30 @@ func (q *FakeQuerier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Cont
|
||||
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) {
|
||||
if err := validateDatabaseType(arg); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1873,6 +1873,13 @@ func (m queryMetricsStore) GetWorkspaceBuildParameters(ctx context.Context, work
|
||||
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) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetWorkspaceBuildStatsByTemplates(ctx, since)
|
||||
@@ -3343,6 +3350,13 @@ func (m queryMetricsStore) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context
|
||||
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) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAuthorizedUsers(ctx, arg, prepared)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStore) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]database.GetWorkspacesRow, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockStore) GetWorkspaceBuildStatsByTemplates(ctx context.Context, since time.Time) ([]database.GetWorkspaceBuildStatsByTemplatesRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
@@ -226,6 +226,7 @@ func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([
|
||||
type workspaceQuerier interface {
|
||||
GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, prepared rbac.PreparedAuthorized) ([]GetWorkspacesRow, 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.
|
||||
@@ -372,6 +373,35 @@ func (q *sqlQuerier) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx context.Conte
|
||||
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 {
|
||||
GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, prepared rbac.PreparedAuthorized) ([]GetUsersRow, error)
|
||||
}
|
||||
|
||||
@@ -428,6 +428,7 @@ type sqlcQuerier interface {
|
||||
GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error)
|
||||
GetWorkspaceBuildByWorkspaceIDAndBuildNumber(ctx context.Context, arg GetWorkspaceBuildByWorkspaceIDAndBuildNumberParams) (WorkspaceBuild, 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)
|
||||
GetWorkspaceBuildsByWorkspaceID(ctx context.Context, arg GetWorkspaceBuildsByWorkspaceIDParams) ([]WorkspaceBuild, error)
|
||||
GetWorkspaceBuildsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceBuild, error)
|
||||
|
||||
@@ -17035,6 +17035,44 @@ func (q *sqlQuerier) GetWorkspaceBuildParameters(ctx context.Context, workspaceB
|
||||
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
|
||||
INSERT INTO
|
||||
workspace_build_parameters (workspace_build_id, name, value)
|
||||
|
||||
@@ -41,3 +41,18 @@ FROM (
|
||||
) q1
|
||||
ORDER BY created_at DESC, name
|
||||
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
|
||||
;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -411,7 +411,11 @@ export type GetProvisionerDaemonsParams = {
|
||||
* lexical scope.
|
||||
*/
|
||||
class ApiMethods {
|
||||
constructor(protected readonly axios: AxiosInstance) {}
|
||||
experimental: ExperimentalApiMethods;
|
||||
|
||||
constructor(protected readonly axios: AxiosInstance) {
|
||||
this.experimental = new ExperimentalApiMethods(this.axios);
|
||||
}
|
||||
|
||||
login = async (
|
||||
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,
|
||||
// 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
|
||||
|
||||
Generated
+8
@@ -18,6 +18,14 @@ export interface AIProviderConfig {
|
||||
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
|
||||
export interface APIKey {
|
||||
readonly id: string;
|
||||
|
||||
Reference in New Issue
Block a user