Skip to content

Commit 6553771

Browse files
feat(coderd): generate task names based on their prompt (#19335)
Closes #18159 If an Anthropic API key is available, we call out to Claude to generate a task name based on the user-provided prompt instead of our random name generator.
1 parent c429020 commit 6553771

File tree

4 files changed

+210
-2
lines changed

4 files changed

+210
-2
lines changed

coderd/aitasks.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import (
1010

1111
"github.com/google/uuid"
1212

13+
"cdr.dev/slog"
14+
1315
"github.com/coder/coder/v2/coderd/audit"
1416
"github.com/coder/coder/v2/coderd/database"
1517
"github.com/coder/coder/v2/coderd/httpapi"
1618
"github.com/coder/coder/v2/coderd/httpmw"
1719
"github.com/coder/coder/v2/coderd/rbac"
20+
"github.com/coder/coder/v2/coderd/taskname"
1821
"github.com/coder/coder/v2/codersdk"
1922
)
2023

@@ -104,8 +107,20 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
104107
return
105108
}
106109

110+
taskName := req.Name
111+
if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" {
112+
anthropicModel := taskname.GetAnthropicModelFromEnv()
113+
114+
generatedName, err := taskname.Generate(ctx, req.Prompt, taskname.WithAPIKey(anthropicAPIKey), taskname.WithModel(anthropicModel))
115+
if err != nil {
116+
api.Logger.Error(ctx, "unable to generate task name", slog.Error(err))
117+
} else {
118+
taskName = generatedName
119+
}
120+
}
121+
107122
createReq := codersdk.CreateWorkspaceRequest{
108-
Name: req.Name,
123+
Name: taskName,
109124
TemplateVersionID: req.TemplateVersionID,
110125
TemplateVersionPresetID: req.TemplateVersionPresetID,
111126
RichParameterValues: []codersdk.WorkspaceBuildParameter{

coderd/taskname/taskname.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package taskname
2+
3+
import (
4+
"context"
5+
"io"
6+
"os"
7+
8+
"github.com/anthropics/anthropic-sdk-go"
9+
anthropicoption "github.com/anthropics/anthropic-sdk-go/option"
10+
"golang.org/x/xerrors"
11+
12+
"github.com/coder/aisdk-go"
13+
"github.com/coder/coder/v2/codersdk"
14+
)
15+
16+
const (
17+
defaultModel = anthropic.ModelClaude3_5HaikuLatest
18+
systemPrompt = `Generate a short workspace name from this AI task prompt.
19+
20+
Requirements:
21+
- Only lowercase letters, numbers, and hyphens
22+
- Start with "task-"
23+
- End with a random number between 0-99
24+
- Maximum 32 characters total
25+
- Descriptive of the main task
26+
27+
Examples:
28+
- "Help me debug a Python script" → "task-python-debug-12"
29+
- "Create a React dashboard component" → "task-react-dashboard-93"
30+
- "Analyze sales data from Q3" → "task-analyze-q3-sales-37"
31+
- "Set up CI/CD pipeline" → "task-setup-cicd-44"
32+
33+
If you cannot create a suitable name:
34+
- Respond with "task-unnamed"
35+
- Do not end with a random number`
36+
)
37+
38+
var (
39+
ErrNoAPIKey = xerrors.New("no api key provided")
40+
ErrNoNameGenerated = xerrors.New("no task name generated")
41+
)
42+
43+
type options struct {
44+
apiKey string
45+
model anthropic.Model
46+
}
47+
48+
type Option func(o *options)
49+
50+
func WithAPIKey(apiKey string) Option {
51+
return func(o *options) {
52+
o.apiKey = apiKey
53+
}
54+
}
55+
56+
func WithModel(model anthropic.Model) Option {
57+
return func(o *options) {
58+
o.model = model
59+
}
60+
}
61+
62+
func GetAnthropicAPIKeyFromEnv() string {
63+
return os.Getenv("ANTHROPIC_API_KEY")
64+
}
65+
66+
func GetAnthropicModelFromEnv() anthropic.Model {
67+
return anthropic.Model(os.Getenv("ANTHROPIC_MODEL"))
68+
}
69+
70+
func Generate(ctx context.Context, prompt string, opts ...Option) (string, error) {
71+
o := options{}
72+
for _, opt := range opts {
73+
opt(&o)
74+
}
75+
76+
if o.model == "" {
77+
o.model = defaultModel
78+
}
79+
if o.apiKey == "" {
80+
return "", ErrNoAPIKey
81+
}
82+
83+
conversation := []aisdk.Message{
84+
{
85+
Role: "system",
86+
Parts: []aisdk.Part{{
87+
Type: aisdk.PartTypeText,
88+
Text: systemPrompt,
89+
}},
90+
},
91+
{
92+
Role: "user",
93+
Parts: []aisdk.Part{{
94+
Type: aisdk.PartTypeText,
95+
Text: prompt,
96+
}},
97+
},
98+
}
99+
100+
anthropicOptions := anthropic.DefaultClientOptions()
101+
anthropicOptions = append(anthropicOptions, anthropicoption.WithAPIKey(o.apiKey))
102+
anthropicClient := anthropic.NewClient(anthropicOptions...)
103+
104+
stream, err := anthropicDataStream(ctx, anthropicClient, o.model, conversation)
105+
if err != nil {
106+
return "", xerrors.Errorf("create anthropic data stream: %w", err)
107+
}
108+
109+
var acc aisdk.DataStreamAccumulator
110+
stream = stream.WithAccumulator(&acc)
111+
112+
if err := stream.Pipe(io.Discard); err != nil {
113+
return "", xerrors.Errorf("pipe data stream")
114+
}
115+
116+
if len(acc.Messages()) == 0 {
117+
return "", ErrNoNameGenerated
118+
}
119+
120+
generatedName := acc.Messages()[0].Content
121+
122+
if err := codersdk.NameValid(generatedName); err != nil {
123+
return "", xerrors.Errorf("generated name %v not valid: %w", generatedName, err)
124+
}
125+
126+
if generatedName == "task-unnamed" {
127+
return "", ErrNoNameGenerated
128+
}
129+
130+
return generatedName, nil
131+
}
132+
133+
func anthropicDataStream(ctx context.Context, client anthropic.Client, model anthropic.Model, input []aisdk.Message) (aisdk.DataStream, error) {
134+
messages, system, err := aisdk.MessagesToAnthropic(input)
135+
if err != nil {
136+
return nil, xerrors.Errorf("convert messages to anthropic format: %w", err)
137+
}
138+
139+
return aisdk.AnthropicToDataStream(client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{
140+
Model: model,
141+
MaxTokens: 24,
142+
System: system,
143+
Messages: messages,
144+
})), nil
145+
}

coderd/taskname/taskname_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package taskname_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/coderd/taskname"
10+
"github.com/coder/coder/v2/codersdk"
11+
"github.com/coder/coder/v2/testutil"
12+
)
13+
14+
const (
15+
anthropicEnvVar = "ANTHROPIC_API_KEY"
16+
)
17+
18+
func TestGenerateTaskName(t *testing.T) {
19+
t.Parallel()
20+
21+
t.Run("Fallback", func(t *testing.T) {
22+
t.Parallel()
23+
24+
ctx := testutil.Context(t, testutil.WaitShort)
25+
26+
name, err := taskname.Generate(ctx, "Some random prompt")
27+
require.ErrorIs(t, err, taskname.ErrNoAPIKey)
28+
require.Equal(t, "", name)
29+
})
30+
31+
t.Run("Anthropic", func(t *testing.T) {
32+
t.Parallel()
33+
34+
apiKey := os.Getenv(anthropicEnvVar)
35+
if apiKey == "" {
36+
t.Skipf("Skipping test as %s not set", anthropicEnvVar)
37+
}
38+
39+
ctx := testutil.Context(t, testutil.WaitShort)
40+
41+
name, err := taskname.Generate(ctx, "Create a finance planning app", taskname.WithAPIKey(apiKey))
42+
require.NoError(t, err)
43+
require.NotEqual(t, "", name)
44+
45+
err = codersdk.NameValid(name)
46+
require.NoError(t, err, "name should be valid")
47+
})
48+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ require (
477477
)
478478

479479
require (
480+
github.com/anthropics/anthropic-sdk-go v1.4.0
480481
github.com/brianvoe/gofakeit/v7 v7.3.0
481482
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
482483
github.com/coder/aisdk-go v0.0.9
@@ -500,7 +501,6 @@ require (
500501
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect
501502
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 // indirect
502503
github.com/Masterminds/semver/v3 v3.3.1 // indirect
503-
github.com/anthropics/anthropic-sdk-go v1.4.0 // indirect
504504
github.com/aquasecurity/go-version v0.0.1 // indirect
505505
github.com/aquasecurity/trivy v0.58.2 // indirect
506506
github.com/aws/aws-sdk-go v1.55.7 // indirect

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy