Skip to content

Commit 945dddc

Browse files
committed
chore: add experiment agentic-chat, add tests for chat API routes
1 parent 85bae3c commit 945dddc

File tree

9 files changed

+156
-13
lines changed

9 files changed

+156
-13
lines changed

coderd/apidoc/docs.go

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/chat.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,29 +161,31 @@ func (api *API) postChatMessages(w http.ResponseWriter, r *http.Request) {
161161
return
162162
}
163163

164-
messages := make([]aisdk.Message, 0, len(dbMessages))
165-
for i, message := range dbMessages {
166-
err = json.Unmarshal(message.Content, &messages[i])
164+
messages := make([]codersdk.ChatMessage, 0)
165+
for _, dbMsg := range dbMessages {
166+
var msg codersdk.ChatMessage
167+
err = json.Unmarshal(dbMsg.Content, &msg)
167168
if err != nil {
168169
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
169170
Message: "Failed to unmarshal chat message",
170171
Detail: err.Error(),
171172
})
172173
return
173174
}
175+
messages = append(messages, msg)
174176
}
175177
messages = append(messages, req.Message)
176178

177179
client := codersdk.New(api.AccessURL)
178180
client.SetSessionToken(httpmw.APITokenFromRequest(r))
179181

180-
tools := make([]aisdk.Tool, len(toolsdk.All))
182+
tools := make([]aisdk.Tool, 0)
181183
handlers := map[string]toolsdk.GenericHandlerFunc{}
182-
for i, tool := range toolsdk.All {
183-
if tool.Tool.Schema.Required == nil {
184-
tool.Tool.Schema.Required = []string{}
184+
for _, tool := range toolsdk.All {
185+
if tool.Name == "coder_report_task" {
186+
continue // This tool requires an agent to run.
185187
}
186-
tools[i] = tool.Tool
188+
tools = append(tools, tool.Tool)
187189
handlers[tool.Tool.Name] = tool.Handler
188190
}
189191

coderd/chat_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package coderd_test
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/v2/coderd/coderdtest"
12+
"github.com/coder/coder/v2/coderd/database"
13+
"github.com/coder/coder/v2/coderd/database/dbgen"
14+
"github.com/coder/coder/v2/coderd/database/dbtime"
15+
"github.com/coder/coder/v2/codersdk"
16+
"github.com/coder/coder/v2/testutil"
17+
)
18+
19+
func TestChat(t *testing.T) {
20+
t.Parallel()
21+
22+
t.Run("ExperimentAgenticChatDisabled", func(t *testing.T) {
23+
t.Parallel()
24+
25+
client, _ := coderdtest.NewWithDatabase(t, nil)
26+
owner := coderdtest.CreateFirstUser(t, client)
27+
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
28+
29+
// Hit the endpoint to get the chat. It should return a 404.
30+
ctx := testutil.Context(t, testutil.WaitShort)
31+
_, err := memberClient.ListChats(ctx)
32+
require.Error(t, err, "list chats should fail")
33+
var sdkErr *codersdk.Error
34+
require.ErrorAs(t, err, &sdkErr, "request should fail with an SDK error")
35+
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
36+
})
37+
38+
t.Run("ChatCRUD", func(t *testing.T) {
39+
t.Parallel()
40+
41+
dv := coderdtest.DeploymentValues(t)
42+
dv.Experiments = []string{string(codersdk.ExperimentAgenticChat)}
43+
dv.AI.Value = codersdk.AIConfig{
44+
Providers: []codersdk.AIProviderConfig{
45+
{
46+
Type: "fake",
47+
APIKey: "",
48+
BaseURL: "http://localhost",
49+
Models: []string{"fake-model"},
50+
},
51+
},
52+
}
53+
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
54+
DeploymentValues: dv,
55+
})
56+
owner := coderdtest.CreateFirstUser(t, client)
57+
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
58+
59+
// Seed the database with some data.
60+
dbChat := dbgen.Chat(t, db, database.Chat{
61+
OwnerID: memberUser.ID,
62+
CreatedAt: dbtime.Now().Add(-time.Hour),
63+
UpdatedAt: dbtime.Now().Add(-time.Hour),
64+
Title: "This is a test chat",
65+
})
66+
_ = dbgen.ChatMessage(t, db, database.ChatMessage{
67+
ChatID: dbChat.ID,
68+
CreatedAt: dbtime.Now().Add(-time.Hour),
69+
Content: []byte(`[{"content": "Hello world"}]`),
70+
Model: "fake model",
71+
Provider: "fake",
72+
})
73+
74+
ctx := testutil.Context(t, testutil.WaitShort)
75+
76+
// Listing chats should return the chat we just inserted.
77+
chats, err := memberClient.ListChats(ctx)
78+
require.NoError(t, err, "list chats should succeed")
79+
require.Len(t, chats, 1, "response should have one chat")
80+
require.Equal(t, dbChat.ID, chats[0].ID, "unexpected chat ID")
81+
require.Equal(t, dbChat.Title, chats[0].Title, "unexpected chat title")
82+
require.Equal(t, dbChat.CreatedAt.UTC(), chats[0].CreatedAt.UTC(), "unexpected chat created at")
83+
require.Equal(t, dbChat.UpdatedAt.UTC(), chats[0].UpdatedAt.UTC(), "unexpected chat updated at")
84+
85+
// Fetching a single chat by ID should return the same chat.
86+
chat, err := memberClient.Chat(ctx, dbChat.ID)
87+
require.NoError(t, err, "get chat should succeed")
88+
require.Equal(t, chats[0], chat, "get chat should return the same chat")
89+
90+
// Listing chat messages should return the message we just inserted.
91+
messages, err := memberClient.ChatMessages(ctx, dbChat.ID)
92+
require.NoError(t, err, "list chat messages should succeed")
93+
require.Len(t, messages, 1, "response should have one message")
94+
require.Equal(t, "Hello world", messages[0].Content, "response should have the correct message content")
95+
96+
// Creating a new chat will fail because the model does not exist.
97+
// TODO: Test the message streaming functionality with a mock model.
98+
// Inserting a chat message will fail due to the model not existing.
99+
_, err = memberClient.CreateChatMessage(ctx, dbChat.ID, codersdk.CreateChatMessageRequest{
100+
Model: "echo",
101+
Message: codersdk.ChatMessage{
102+
Role: "user",
103+
Content: "Hello world",
104+
},
105+
Thinking: false,
106+
})
107+
require.Error(t, err, "create chat message should fail")
108+
var sdkErr *codersdk.Error
109+
require.ErrorAs(t, err, &sdkErr, "create chat should fail with an SDK error")
110+
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode(), "create chat should fail with a 400 when model does not exist")
111+
112+
// Creating a new chat message with malformed content should fail.
113+
res, err := memberClient.Request(ctx, http.MethodPost, "/api/v2/chats/"+dbChat.ID.String()+"/messages", strings.NewReader(`{malformed json}`))
114+
require.NoError(t, err)
115+
defer res.Body.Close()
116+
apiErr := codersdk.ReadBodyAsError(res)
117+
require.Contains(t, apiErr.Error(), "Failed to decode chat message")
118+
119+
_, err = memberClient.CreateChat(ctx)
120+
require.NoError(t, err, "create chat should succeed")
121+
chats, err = memberClient.ListChats(ctx)
122+
require.NoError(t, err, "list chats should succeed")
123+
require.Len(t, chats, 2, "response should have two chats")
124+
})
125+
}

coderd/coderd.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1001,8 +1001,12 @@ func New(options *Options) *API {
10011001
r.Get("/{fileID}", api.fileByID)
10021002
r.Post("/", api.postFile)
10031003
})
1004+
// Chats are an experimental feature
10041005
r.Route("/chats", func(r chi.Router) {
1005-
r.Use(apiKeyMiddleware)
1006+
r.Use(
1007+
apiKeyMiddleware,
1008+
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentAgenticChat),
1009+
)
10061010
r.Get("/", api.listChats)
10071011
r.Post("/", api.postChats)
10081012
r.Route("/{chat}", func(r chi.Router) {

codersdk/chat.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ func (c *Client) ListChats(ctx context.Context) ([]Chat, error) {
4040
return nil, xerrors.Errorf("execute request: %w", err)
4141
}
4242
defer res.Body.Close()
43+
if res.StatusCode != http.StatusOK {
44+
return nil, ReadBodyAsError(res)
45+
}
4346

4447
var chats []Chat
4548
return chats, json.NewDecoder(res.Body).Decode(&chats)

codersdk/deployment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3328,6 +3328,7 @@ const (
33283328
ExperimentWebPush Experiment = "web-push" // Enables web push notifications through the browser.
33293329
ExperimentDynamicParameters Experiment = "dynamic-parameters" // Enables dynamic parameters when creating a workspace.
33303330
ExperimentWorkspacePrebuilds Experiment = "workspace-prebuilds" // Enables the new workspace prebuilds feature.
3331+
ExperimentAgenticChat Experiment = "agentic-chat" // Enables the new agentic AI chat feature.
33313332
)
33323333

33333334
// ExperimentsSafe should include all experiments that are safe for

docs/reference/api/schemas.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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