Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/agents_chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -1287,7 +1287,7 @@ func (m chatViewModel) handleStreamEvent(event codersdk.ChatStreamEvent) (chatVi
chatID: m.activeChatID,
event: codersdk.ChatStreamEvent{
Type: codersdk.ChatStreamEventTypeError,
Error: &codersdk.ChatStreamError{
Error: &codersdk.ChatError{
Message: fmt.Sprintf(
"failed to parse ask_user_question: %v",
err,
Expand Down
7 changes: 4 additions & 3 deletions cli/agents_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (m chatListModel) filteredChats() []codersdk.Chat {
filtered = append(filtered, chat)
continue
}
if chat.LastError != nil && strings.Contains(strings.ToLower(*chat.LastError), query) {
if chat.LastError != nil && strings.Contains(strings.ToLower(chat.LastError.Message), query) {
filtered = append(filtered, chat)
}
}
Expand Down Expand Up @@ -445,13 +445,14 @@ func (m chatListModel) View() string {
rowText := fmt.Sprintf("%s%s %s %s%s", rowPrefix, rowStyle.Render(title), status, m.styles.dimmedText.Render(timeAgo(row.chat.UpdatedAt)), extra)
lines = append(lines, rowText)

if row.chat.Status == codersdk.ChatStatusError && row.chat.LastError != nil {
if row.chat.Status == codersdk.ChatStatusError && row.chat.LastError != nil && row.chat.LastError.Message != "" {
lastError := row.chat.LastError.Message
errWidth := max(m.width-4, 20)
errPrefix := " "
if row.depth > 0 {
errPrefix += strings.Repeat(" ", row.depth)
}
lines = append(lines, errPrefix+m.styles.dimmedText.Render(m.styles.truncate(sanitizeTerminalRenderableText(*row.chat.LastError), errWidth)))
lines = append(lines, errPrefix+m.styles.dimmedText.Render(m.styles.truncate(sanitizeTerminalRenderableText(lastError), errWidth)))
}
}

Expand Down
2 changes: 1 addition & 1 deletion cli/agents_stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func TestConsumeChatStreamText(t *testing.T) {
{Type: codersdk.ChatStreamEventTypeMessage, Message: &codersdk.ChatMessage{ID: 1, ChatID: uuid.New(), Role: codersdk.ChatMessageRoleAssistant, Content: []codersdk.ChatMessagePart{{Type: codersdk.ChatMessagePartTypeText, Text: "Hello world"}}}},
{Type: codersdk.ChatStreamEventTypeStatus, Status: &codersdk.ChatStreamStatus{Status: codersdk.ChatStatusRunning}},
{Type: codersdk.ChatStreamEventTypeRetry, Retry: &codersdk.ChatStreamRetry{Attempt: 2, Error: "rate limited"}},
{Type: codersdk.ChatStreamEventTypeError, Error: &codersdk.ChatStreamError{Message: "boom"}},
{Type: codersdk.ChatStreamEventTypeError, Error: &codersdk.ChatError{Message: "boom"}},
} {
events <- event
}
Expand Down
2 changes: 1 addition & 1 deletion cli/agents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1070,7 +1070,7 @@ func TestAgents(t *testing.T) {
t.Parallel()
updated, cmd := applyStream(newTestChatViewModel(nil), codersdk.ChatStreamEvent{
Type: codersdk.ChatStreamEventTypeError,
Error: &codersdk.ChatStreamError{Message: "stream blew up"},
Error: &codersdk.ChatError{Message: "stream blew up"},
})
require.Nil(t, cmd)
require.Equal(t, "stream error: stream blew up", updated.err.Error())
Expand Down
34 changes: 31 additions & 3 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
"github.com/coder/coder/v2/coderd/x/chatd/chaterror"
"github.com/coder/coder/v2/coderd/x/chatd/chatprompt"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk/proto"
Expand Down Expand Up @@ -1607,6 +1608,34 @@ func nullTimePtr(v sql.NullTime) *time.Time {
return &value
}

const fallbackChatLastErrorMessage = "The chat request failed unexpectedly."

func decodeChatLastError(raw pqtype.NullRawMessage) *codersdk.ChatError {
if !raw.Valid {
return nil
}

var payload codersdk.ChatError
if err := json.Unmarshal(raw.RawMessage, &payload); err != nil {
return &codersdk.ChatError{
Message: fallbackChatLastErrorMessage,
Kind: chaterror.KindGeneric,
}
}

payload.Message = strings.TrimSpace(payload.Message)
payload.Detail = strings.TrimSpace(payload.Detail)
payload.Kind = strings.TrimSpace(payload.Kind)
payload.Provider = strings.TrimSpace(payload.Provider)
if payload.Kind == "" {
payload.Kind = chaterror.KindGeneric
}
if payload.Message == "" {
payload.Message = fallbackChatLastErrorMessage
}
return &payload
}

// Chat converts a database.Chat to a codersdk.Chat. It coalesces
// nil slices and maps to empty values for JSON serialization and
// derives RootChatID from the parent chain when not explicitly set.
Expand All @@ -1622,6 +1651,7 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
if labels == nil {
labels = map[string]string{}
}
lastError := decodeChatLastError(c.LastError)
chat := codersdk.Chat{
ID: c.ID,
OrganizationID: c.OrganizationID,
Expand All @@ -1636,9 +1666,7 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
MCPServerIDs: mcpServerIDs,
Labels: labels,
ClientType: codersdk.ChatClientType(c.ClientType),
}
if c.LastError.Valid {
chat.LastError = &c.LastError.String
LastError: lastError,
}
if c.PlanMode.Valid {
chat.PlanMode = codersdk.ChatPlanMode(c.PlanMode.ChatPlanMode)
Expand Down
94 changes: 93 additions & 1 deletion coderd/database/db2sdk/db2sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/x/chatd/chaterror"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk/proto"
)
Expand Down Expand Up @@ -916,6 +917,17 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
// field to codersdk.Chat, this test will fail until the
// converter is updated.
now := dbtime.Now()
lastErrorPayload := codersdk.ChatError{
Message: "boom",
Detail: "provider detail",
Kind: chaterror.KindGeneric,
Provider: "openai",
Retryable: true,
StatusCode: 503,
}
lastErrorRaw, err := json.Marshal(lastErrorPayload)
require.NoError(t, err)

input := database.Chat{
ID: uuid.New(),
OwnerID: uuid.New(),
Expand All @@ -929,7 +941,7 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
Title: "all-fields-test",
Status: database.ChatStatusRunning,
ClientType: database.ChatClientTypeUi,
LastError: sql.NullString{String: "boom", Valid: true},
LastError: pqtype.NullRawMessage{RawMessage: lastErrorRaw, Valid: true},
CreatedAt: now,
UpdatedAt: now,
Archived: true,
Expand Down Expand Up @@ -970,6 +982,8 @@ func TestChat_AllFieldsPopulated(t *testing.T) {

got := db2sdk.Chat(input, diffStatus, fileRows)

require.Equal(t, &lastErrorPayload, got.LastError)

v := reflect.ValueOf(got)
typ := v.Type()
// HasUnread is populated by ChatRowsWithChildren (which joins the
Expand Down Expand Up @@ -1053,6 +1067,84 @@ func TestChat_NilFilesOmitted(t *testing.T) {
require.Empty(t, result.Files)
}

func TestChat_LastErrorFallback(t *testing.T) {
t.Parallel()

const fallbackMessage = "The chat request failed unexpectedly."

tests := []struct {
name string
raw json.RawMessage
expectPayload *codersdk.ChatError
}{
{
name: "MalformedJSON",
raw: json.RawMessage(`{`),
expectPayload: &codersdk.ChatError{
Message: fallbackMessage,
Kind: chaterror.KindGeneric,
Retryable: false,
},
},
{
name: "MessageMissingPreservesMetadata",
raw: json.RawMessage(`{"kind":"timeout","provider":"openai","status_code":504}`),
expectPayload: &codersdk.ChatError{
Message: fallbackMessage,
Kind: "timeout",
Provider: "openai",
Retryable: false,
StatusCode: 504,
},
},
{
name: "WhitespaceMessageDefaultsKind",
raw: json.RawMessage(`{"message":" ","provider":"openai"}`),
expectPayload: &codersdk.ChatError{
Message: fallbackMessage,
Kind: chaterror.KindGeneric,
Provider: "openai",
Retryable: false,
},
},
{
name: "KindMissingDefaultsGeneric",
raw: json.RawMessage(`{"message":"OpenAI returned an unexpected error.","provider":"openai","status_code":502}`),
expectPayload: &codersdk.ChatError{
Message: "OpenAI returned an unexpected error.",
Kind: chaterror.KindGeneric,
Provider: "openai",
Retryable: false,
StatusCode: 502,
},
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

chat := database.Chat{
ID: uuid.New(),
OwnerID: uuid.New(),
LastModelConfigID: uuid.New(),
Title: "fallback payload",
Status: database.ChatStatusError,
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
LastError: pqtype.NullRawMessage{
RawMessage: tc.raw,
Valid: true,
},
}

result := db2sdk.Chat(chat, nil, nil)
require.Equal(t, tc.expectPayload, result.LastError)
})
}
}

func TestChat_MultipleFiles(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 1 addition & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE chats
ALTER COLUMN last_error TYPE text
USING last_error ->> 'message';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ALTER TABLE chats
ALTER COLUMN last_error TYPE jsonb
USING CASE
WHEN last_error IS NULL THEN NULL
ELSE jsonb_build_object(
'message', last_error,
'kind', 'generic'
)
END;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- Migration 424 adds chats.last_error as text. Seed one existing fixture
-- chat with a legacy plain-text error so migration 485 has a non-null row
-- to backfill, and add a second chat that leaves last_error NULL so the
-- migration fixture can assert both branches of the CASE expression.
UPDATE chats
SET last_error = 'Legacy provider failure'
WHERE id = '72c0438a-18eb-4688-ab80-e4c6a126ef96';

INSERT INTO chats (
id,
owner_id,
last_model_config_id,
title,
status,
created_at,
updated_at
)
SELECT
'5a4ac6a3-9dc5-440f-ae6b-5805e477bc59',
owner_id,
last_model_config_id,
'Fixture Chat With Null Error',
'waiting',
'2024-01-01 00:00:00+00',
'2024-01-01 00:00:00+00'
FROM chats
WHERE id = '72c0438a-18eb-4688-ab80-e4c6a126ef96';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- Migration 485 retypes chats.last_error to jsonb and backfills legacy
-- text rows into the structured persisted payload shape.
DO $$
DECLARE
payload jsonb;
BEGIN
SELECT last_error INTO STRICT payload
FROM chats
WHERE id = '72c0438a-18eb-4688-ab80-e4c6a126ef96';

IF payload ->> 'message' <> 'Legacy provider failure' THEN
RAISE EXCEPTION 'expected migrated last_error message, got %',
payload ->> 'message';
END IF;

IF payload ->> 'kind' <> 'generic' THEN
RAISE EXCEPTION 'expected migrated last_error kind, got %',
payload ->> 'kind';
END IF;

PERFORM 1
FROM chats
WHERE id = '5a4ac6a3-9dc5-440f-ae6b-5805e477bc59'
AND last_error IS NULL;
IF NOT FOUND THEN
RAISE EXCEPTION 'expected null last_error row to remain NULL after migration';
END IF;
END $$;
2 changes: 1 addition & 1 deletion coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 16 additions & 16 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading