|
| 1 | +package coderd |
| 2 | + |
| 3 | +import ( |
| 4 | +"context" |
| 5 | +"fmt" |
| 6 | +"io" |
| 7 | +"net/http" |
| 8 | + |
| 9 | +"github.com/kylecarbs/aisdk-go" |
| 10 | +"golang.org/x/xerrors" |
| 11 | + |
| 12 | +"cdr.dev/slog" |
| 13 | + |
| 14 | +"github.com/mitchellh/mapstructure" |
| 15 | + |
| 16 | +"github.com/coder/coder/v2/coderd/ai" |
| 17 | +"github.com/coder/coder/v2/coderd/httpapi" |
| 18 | +"github.com/coder/coder/v2/codersdk" |
| 19 | +) |
| 20 | + |
| 21 | +constsystemPrompt=`You are a helpful assistant that can create Coder workspaces. Coder workspaces are ephemeral development environments created for specific coding tasks. |
| 22 | +
|
| 23 | +Whenever you name a workspace based on a task prompt, use the following examples as a guide: |
| 24 | +
|
| 25 | +# Example 1 |
| 26 | +
|
| 27 | +Task prompt: make the background purple |
| 28 | +Workspace name: purple-bg |
| 29 | +Task title: Make the background purple |
| 30 | +
|
| 31 | +# Example 2 |
| 32 | +
|
| 33 | +Task prompt: refactor the UI components to use MUI |
| 34 | +Workspace name: refactor-ui-to-mui |
| 35 | +Task title: MUI Refactor |
| 36 | +
|
| 37 | +# Example 3 |
| 38 | +
|
| 39 | +Task prompt: hey look through the repository and find all the places where we use postgres. then use that as a guide to refactor the app to use supabase. |
| 40 | +Workspace name: migrate-pg-supabase |
| 41 | +Task title: Supabase Migration |
| 42 | +
|
| 43 | +# Example 4 |
| 44 | +
|
| 45 | +Task prompt: Look through our BigQuery dataset and generate a report on the top deployments using the prebuilds feature. |
| 46 | +Workspace name: bq-prebuilds-report |
| 47 | +Task title: BigQuery Prebuilds Report |
| 48 | +
|
| 49 | +# Example 5 |
| 50 | +
|
| 51 | +Task prompt: address this issue: https://github.com/coder/coder/issues/18159 |
| 52 | +Workspace name: gh-issue-18159 |
| 53 | +Task title: GitHub Issue coder/coder#18159` |
| 54 | + |
| 55 | +typecreateWorkspaceToolArgsstruct { |
| 56 | +WorkspaceNamestring`mapstructure:"name"` |
| 57 | +TaskTitlestring`mapstructure:"task_title"` |
| 58 | +} |
| 59 | + |
| 60 | +constcreateWorkspaceToolName="create_workspace" |
| 61 | + |
| 62 | +varcreateWorkspaceTool= aisdk.Tool{ |
| 63 | +Name:createWorkspaceToolName, |
| 64 | +Description:"Create a workspace", |
| 65 | +Schema: aisdk.Schema{ |
| 66 | +Required: []string{"name","task_title"}, |
| 67 | +Properties:map[string]any{ |
| 68 | +"name":map[string]any{ |
| 69 | +"type":"string", |
| 70 | +"description":"Name of the workspace to create.", |
| 71 | +}, |
| 72 | +"task_title":map[string]any{ |
| 73 | +"type":"string", |
| 74 | +"description":"Title of the task to create the workspace for. Max 48 characters.", |
| 75 | +}, |
| 76 | +}, |
| 77 | +}, |
| 78 | +} |
| 79 | + |
| 80 | +funcgenerateNameAndTitle(ctx context.Context,logger slog.Logger,provider*ai.LanguageModel,modelIDstring,taskPromptstring) (createWorkspaceToolArgs,error) { |
| 81 | +stream,err:=provider.StreamFunc(ctx, ai.StreamOptions{ |
| 82 | +Model:modelID, |
| 83 | +SystemPrompt:systemPrompt, |
| 84 | +Tools: []aisdk.Tool{createWorkspaceTool}, |
| 85 | +Messages: []aisdk.Message{ |
| 86 | +{ |
| 87 | +Role:"user", |
| 88 | +Parts: []aisdk.Part{ |
| 89 | +{ |
| 90 | +Type:aisdk.PartTypeText, |
| 91 | +Text:fmt.Sprintf("Use the create_workspace tool to create a workspace based on the following task prompt:\n```\n%s\n```",taskPrompt), |
| 92 | +}, |
| 93 | +}, |
| 94 | +}, |
| 95 | +}, |
| 96 | +}) |
| 97 | +iferr!=nil { |
| 98 | +returncreateWorkspaceToolArgs{},xerrors.Errorf("failed to generate workspace name: %w",err) |
| 99 | +} |
| 100 | +result:=createWorkspaceToolArgs{} |
| 101 | +stream=stream.WithToolCalling(func(toolCall aisdk.ToolCall) aisdk.ToolCallResult { |
| 102 | +iftoolCall.Name==createWorkspaceToolName { |
| 103 | +err:=mapstructure.Decode(toolCall.Args,&result) |
| 104 | +iferr!=nil { |
| 105 | +logger.Error(ctx,"failed to decode tool call args",slog.Error(err)) |
| 106 | +returnnil |
| 107 | +} |
| 108 | +} |
| 109 | +returnnil |
| 110 | +}) |
| 111 | +iferr:=stream.Pipe(io.Discard);err!=nil { |
| 112 | +returncreateWorkspaceToolArgs{},xerrors.Errorf("failed to pipe stream: %w",err) |
| 113 | +} |
| 114 | +ifresult== (createWorkspaceToolArgs{}) { |
| 115 | +returncreateWorkspaceToolArgs{},xerrors.New("no tool call found in the AI response") |
| 116 | +} |
| 117 | +returnresult,nil |
| 118 | +} |
| 119 | + |
| 120 | +// @Summary Generate a task title and workspace name based on a task prompt |
| 121 | +// @ID generate-task-title-and-workspace-name-by-task-prompt |
| 122 | +// @Security CoderSessionToken |
| 123 | +// @Produce json |
| 124 | +// @Tags Tasks |
| 125 | +// @Param task_prompt query string true "Task prompt" |
| 126 | +// @Success 200 {object} codersdk.TaskTitleAndWorkspaceNameResponse |
| 127 | +// @Router /ai-tasks/name [get] |
| 128 | +func (api*API)TaskTitleAndWorkspaceName(rw http.ResponseWriter,r*http.Request) { |
| 129 | +var ( |
| 130 | +ctx=r.Context() |
| 131 | +taskPrompt=r.URL.Query().Get("task_prompt") |
| 132 | +) |
| 133 | +iftaskPrompt=="" { |
| 134 | +httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{ |
| 135 | +Message:"Task prompt is required", |
| 136 | +}) |
| 137 | +return |
| 138 | +} |
| 139 | + |
| 140 | +modelID:="gpt-4.1-nano" |
| 141 | +provider,ok:=api.LanguageModels[modelID] |
| 142 | +if!ok { |
| 143 | +httpapi.Write(ctx,rw,http.StatusServiceUnavailable, codersdk.Response{ |
| 144 | +Message:fmt.Sprintf("Language model %s not found",modelID), |
| 145 | +}) |
| 146 | +return |
| 147 | +} |
| 148 | + |
| 149 | +// Limit the task prompt to avoid burning tokens. The first 1024 characters |
| 150 | +// are likely enough to generate a good workspace name and task title. |
| 151 | +iflen(taskPrompt)>1024 { |
| 152 | +taskPrompt=taskPrompt[:1024] |
| 153 | +} |
| 154 | + |
| 155 | +result,err:=generateNameAndTitle(ctx,api.Logger,&provider,modelID,taskPrompt) |
| 156 | +iferr!=nil { |
| 157 | +httpapi.Write(ctx,rw,http.StatusInternalServerError, codersdk.Response{ |
| 158 | +Message:"Failed to generate workspace name and task title", |
| 159 | +Detail:err.Error(), |
| 160 | +}) |
| 161 | +return |
| 162 | +} |
| 163 | +truncatedTaskTitle:=result.TaskTitle |
| 164 | +iflen(truncatedTaskTitle)>64 { |
| 165 | +truncatedTaskTitle=truncatedTaskTitle[:64] |
| 166 | +} |
| 167 | +httpapi.Write(ctx,rw,http.StatusOK, codersdk.TaskTitleAndWorkspaceNameResponse{ |
| 168 | +TaskTitle:truncatedTaskTitle, |
| 169 | +WorkspaceName:result.WorkspaceName, |
| 170 | +}) |
| 171 | +} |