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) {
|
r.Route("/api/v2", func(r chi.Router) {
|
||||||
api.APIHandler = r
|
api.APIHandler = r
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
;
|
||||||
|
|||||||
@@ -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.
|
* 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
|
||||||
|
|||||||
Generated
+8
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user