mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
Backend portion of experimental `AgenticChat` feature: - Adds database tables for chats and chat messages - Adds functionality to stream messages from LLM providers using `kylecarbs/aisdk-go` - Adds API routes with relevant functionality (list, create, update chats, insert chat message) - Adds experiment `codersdk.AgenticChat` --------- Co-authored-by: Kyle Carberry <kyle@carberry.com>
126 lines
4.6 KiB
Go
126 lines
4.6 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestChat(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("ExperimentAgenticChatDisabled", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _ := coderdtest.NewWithDatabase(t, nil)
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
// Hit the endpoint to get the chat. It should return a 404.
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
_, err := memberClient.ListChats(ctx)
|
|
require.Error(t, err, "list chats should fail")
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr, "request should fail with an SDK error")
|
|
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
|
})
|
|
|
|
t.Run("ChatCRUD", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
dv.Experiments = []string{string(codersdk.ExperimentAgenticChat)}
|
|
dv.AI.Value = codersdk.AIConfig{
|
|
Providers: []codersdk.AIProviderConfig{
|
|
{
|
|
Type: "fake",
|
|
APIKey: "",
|
|
BaseURL: "http://localhost",
|
|
Models: []string{"fake-model"},
|
|
},
|
|
},
|
|
}
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
// Seed the database with some data.
|
|
dbChat := dbgen.Chat(t, db, database.Chat{
|
|
OwnerID: memberUser.ID,
|
|
CreatedAt: dbtime.Now().Add(-time.Hour),
|
|
UpdatedAt: dbtime.Now().Add(-time.Hour),
|
|
Title: "This is a test chat",
|
|
})
|
|
_ = dbgen.ChatMessage(t, db, database.ChatMessage{
|
|
ChatID: dbChat.ID,
|
|
CreatedAt: dbtime.Now().Add(-time.Hour),
|
|
Content: []byte(`[{"content": "Hello world"}]`),
|
|
Model: "fake model",
|
|
Provider: "fake",
|
|
})
|
|
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
// Listing chats should return the chat we just inserted.
|
|
chats, err := memberClient.ListChats(ctx)
|
|
require.NoError(t, err, "list chats should succeed")
|
|
require.Len(t, chats, 1, "response should have one chat")
|
|
require.Equal(t, dbChat.ID, chats[0].ID, "unexpected chat ID")
|
|
require.Equal(t, dbChat.Title, chats[0].Title, "unexpected chat title")
|
|
require.Equal(t, dbChat.CreatedAt.UTC(), chats[0].CreatedAt.UTC(), "unexpected chat created at")
|
|
require.Equal(t, dbChat.UpdatedAt.UTC(), chats[0].UpdatedAt.UTC(), "unexpected chat updated at")
|
|
|
|
// Fetching a single chat by ID should return the same chat.
|
|
chat, err := memberClient.Chat(ctx, dbChat.ID)
|
|
require.NoError(t, err, "get chat should succeed")
|
|
require.Equal(t, chats[0], chat, "get chat should return the same chat")
|
|
|
|
// Listing chat messages should return the message we just inserted.
|
|
messages, err := memberClient.ChatMessages(ctx, dbChat.ID)
|
|
require.NoError(t, err, "list chat messages should succeed")
|
|
require.Len(t, messages, 1, "response should have one message")
|
|
require.Equal(t, "Hello world", messages[0].Content, "response should have the correct message content")
|
|
|
|
// Creating a new chat will fail because the model does not exist.
|
|
// TODO: Test the message streaming functionality with a mock model.
|
|
// Inserting a chat message will fail due to the model not existing.
|
|
_, err = memberClient.CreateChatMessage(ctx, dbChat.ID, codersdk.CreateChatMessageRequest{
|
|
Model: "echo",
|
|
Message: codersdk.ChatMessage{
|
|
Role: "user",
|
|
Content: "Hello world",
|
|
},
|
|
Thinking: false,
|
|
})
|
|
require.Error(t, err, "create chat message should fail")
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr, "create chat should fail with an SDK error")
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode(), "create chat should fail with a 400 when model does not exist")
|
|
|
|
// Creating a new chat message with malformed content should fail.
|
|
res, err := memberClient.Request(ctx, http.MethodPost, "/api/v2/chats/"+dbChat.ID.String()+"/messages", strings.NewReader(`{malformed json}`))
|
|
require.NoError(t, err)
|
|
defer res.Body.Close()
|
|
apiErr := codersdk.ReadBodyAsError(res)
|
|
require.Contains(t, apiErr.Error(), "Failed to decode chat message")
|
|
|
|
_, err = memberClient.CreateChat(ctx)
|
|
require.NoError(t, err, "create chat should succeed")
|
|
chats, err = memberClient.ListChats(ctx)
|
|
require.NoError(t, err, "list chats should succeed")
|
|
require.Len(t, chats, 2, "response should have two chats")
|
|
})
|
|
}
|