diff --git a/coderd/chatd/chatd.go b/coderd/chatd/chatd.go index b1f1c2de00..313709681f 100644 --- a/coderd/chatd/chatd.go +++ b/coderd/chatd/chatd.go @@ -192,6 +192,7 @@ const ( // SendMessageOptions controls user message insertion with busy-state behavior. type SendMessageOptions struct { ChatID uuid.UUID + CreatedBy uuid.UUID Content []fantasy.Content ContentFileIDs map[int]uuid.UUID ModelConfigID *uuid.UUID @@ -209,6 +210,7 @@ type SendMessageResult struct { // EditMessageOptions controls in-place user message edits. type EditMessageOptions struct { ChatID uuid.UUID + CreatedBy uuid.UUID EditedMessageID int64 Content []fantasy.Content ContentFileIDs map[int]uuid.UUID @@ -223,6 +225,7 @@ type EditMessageResult struct { // PromoteQueuedOptions controls queued-message promotion. type PromoteQueuedOptions struct { ChatID uuid.UUID + CreatedBy uuid.UUID QueuedMessageID int64 ModelConfigID *uuid.UUID } @@ -266,7 +269,8 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C return xerrors.Errorf("marshal system prompt: %w", err) } _, err = tx.InsertChatMessage(ctx, database.InsertChatMessageParams{ - ChatID: insertedChat.ID, + ChatID: insertedChat.ID, + CreatedBy: uuid.NullUUID{}, ModelConfigID: uuid.NullUUID{ UUID: opts.ModelConfigID, Valid: true, @@ -303,6 +307,7 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C }, Role: "user", Content: userContent, + CreatedBy: uuid.NullUUID{UUID: opts.OwnerID, Valid: opts.OwnerID != uuid.Nil}, Visibility: database.ChatMessageVisibilityBoth, InputTokens: sql.NullInt64{}, OutputTokens: sql.NullInt64{}, @@ -421,6 +426,7 @@ func (p *Server) SendMessage( lockedChat, modelConfigID, content, + opts.CreatedBy, ) if err != nil { return err @@ -736,6 +742,7 @@ func (p *Server) PromoteQueued( RawMessage: targetContent, Valid: len(targetContent) > 0, }, + opts.CreatedBy, ) if err != nil { return err @@ -881,12 +888,14 @@ func insertUserMessageAndSetPending( lockedChat database.Chat, modelConfigID uuid.UUID, content pqtype.NullRawMessage, + createdBy uuid.UUID, ) (database.ChatMessage, database.Chat, error) { message, err := insertChatMessageWithStore(ctx, store, database.InsertChatMessageParams{ ChatID: lockedChat.ID, ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true}, Role: "user", Content: content, + CreatedBy: uuid.NullUUID{UUID: createdBy, Valid: createdBy != uuid.Nil}, Visibility: database.ChatMessageVisibilityBoth, InputTokens: sql.NullInt64{}, OutputTokens: sql.NullInt64{}, @@ -1948,6 +1957,7 @@ func (p *Server) processChat(ctx context.Context, chat database.Chat) { RawMessage: nextQueued.Content, Valid: len(nextQueued.Content) > 0, }, + CreatedBy: uuid.NullUUID{UUID: chat.OwnerID, Valid: chat.OwnerID != uuid.Nil}, Visibility: database.ChatMessageVisibilityBoth, InputTokens: sql.NullInt64{}, OutputTokens: sql.NullInt64{}, @@ -2296,6 +2306,7 @@ func (p *Server) runChat( hasUsage := step.Usage != (fantasy.Usage{}) assistantMessage, insertErr := tx.InsertChatMessage(persistCtx, database.InsertChatMessageParams{ ChatID: chat.ID, + CreatedBy: uuid.NullUUID{}, ModelConfigID: uuid.NullUUID{UUID: modelConfig.ID, Valid: true}, Role: string(fantasy.MessageRoleAssistant), Content: assistantContent, @@ -2329,6 +2340,7 @@ func (p *Server) runChat( toolMessage, insertErr := tx.InsertChatMessage(persistCtx, database.InsertChatMessageParams{ ChatID: chat.ID, + CreatedBy: uuid.NullUUID{}, ModelConfigID: uuid.NullUUID{UUID: modelConfig.ID, Valid: true}, Role: string(fantasy.MessageRoleTool), Content: resultContent, @@ -2613,6 +2625,7 @@ func (p *Server) persistChatContextSummary( txErr := p.db.InTx(func(tx database.Store) error { _, txErr := tx.InsertChatMessage(ctx, database.InsertChatMessageParams{ ChatID: chatID, + CreatedBy: uuid.NullUUID{}, ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true}, Role: string(fantasy.MessageRoleUser), Content: pqtype.NullRawMessage{ @@ -2635,6 +2648,7 @@ func (p *Server) persistChatContextSummary( assistantMessage, txErr := tx.InsertChatMessage(ctx, database.InsertChatMessageParams{ ChatID: chatID, + CreatedBy: uuid.NullUUID{}, ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true}, Role: string(fantasy.MessageRoleAssistant), Content: assistantContent, @@ -2658,6 +2672,7 @@ func (p *Server) persistChatContextSummary( toolMessage, txErr := tx.InsertChatMessage(ctx, database.InsertChatMessageParams{ ChatID: chatID, + CreatedBy: uuid.NullUUID{}, ModelConfigID: uuid.NullUUID{UUID: modelConfigID, Valid: true}, Role: string(fantasy.MessageRoleTool), Content: toolResult, diff --git a/coderd/chatd/subagent.go b/coderd/chatd/subagent.go index ec1b63a76d..9c4f2ec99f 100644 --- a/coderd/chatd/subagent.go +++ b/coderd/chatd/subagent.go @@ -289,8 +289,15 @@ func (p *Server) sendSubagentMessage( return database.Chat{}, ErrSubagentNotDescendant } + // Look up the target chat to get the owner for CreatedBy. + targetChat, err := p.db.GetChatByID(ctx, targetChatID) + if err != nil { + return database.Chat{}, xerrors.Errorf("get target chat: %w", err) + } + sendResult, err := p.SendMessage(ctx, SendMessageOptions{ ChatID: targetChatID, + CreatedBy: targetChat.OwnerID, Content: []fantasy.Content{fantasy.TextContent{Text: message}}, BusyBehavior: busyBehavior, }) diff --git a/coderd/chats.go b/coderd/chats.go index 67ad866ee2..7f1b172b91 100644 --- a/coderd/chats.go +++ b/coderd/chats.go @@ -606,6 +606,7 @@ func (api *API) unarchiveChat(rw http.ResponseWriter, r *http.Request) { // EXPERIMENTAL: this endpoint is experimental and is subject to change. func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + apiKey := httpmw.APIKey(r) chat := httpmw.ChatParam(r) chatID := chat.ID @@ -635,6 +636,7 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) { ctx, chatd.SendMessageOptions{ ChatID: chatID, + CreatedBy: apiKey.UserID, Content: contentBlocks, ContentFileIDs: contentFileIDs, ModelConfigID: req.ModelConfigID, @@ -672,6 +674,7 @@ func (api *API) postChatMessages(rw http.ResponseWriter, r *http.Request) { // EXPERIMENTAL: this endpoint is experimental and is subject to change. func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + apiKey := httpmw.APIKey(r) chat := httpmw.ChatParam(r) if api.chatDaemon == nil { @@ -708,6 +711,7 @@ func (api *API) patchChatMessage(rw http.ResponseWriter, r *http.Request) { editResult, editErr := api.chatDaemon.EditMessage(ctx, chatd.EditMessageOptions{ ChatID: chat.ID, + CreatedBy: apiKey.UserID, EditedMessageID: messageID, Content: contentBlocks, ContentFileIDs: contentFileIDs, @@ -774,6 +778,7 @@ func (api *API) deleteChatQueuedMessage(rw http.ResponseWriter, r *http.Request) // EXPERIMENTAL: this endpoint is experimental and is subject to change. func (api *API) promoteChatQueuedMessage(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + apiKey := httpmw.APIKey(r) chat := httpmw.ChatParam(r) chatID := chat.ID @@ -797,6 +802,7 @@ func (api *API) promoteChatQueuedMessage(rw http.ResponseWriter, r *http.Request promoteResult, txErr := api.chatDaemon.PromoteQueued(ctx, chatd.PromoteQueuedOptions{ ChatID: chatID, + CreatedBy: apiKey.UserID, QueuedMessageID: queuedMessageID, }) diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 0d6fbb1fd1..0c9e152ea1 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -1059,9 +1059,14 @@ func ChatMessage(m database.ChatMessage) codersdk.ChatMessage { if !m.ModelConfigID.Valid { modelConfigID = nil } + createdBy := &m.CreatedBy.UUID + if !m.CreatedBy.Valid { + createdBy = nil + } msg := codersdk.ChatMessage{ ID: m.ID, ChatID: m.ChatID, + CreatedBy: createdBy, ModelConfigID: modelConfigID, CreatedAt: m.CreatedAt, Role: m.Role, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index cccccb710c..49868621a3 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -1215,7 +1215,8 @@ CREATE TABLE chat_messages ( cache_creation_tokens bigint, cache_read_tokens bigint, context_limit bigint, - compressed boolean DEFAULT false NOT NULL + compressed boolean DEFAULT false NOT NULL, + created_by uuid ); CREATE SEQUENCE chat_messages_id_seq diff --git a/coderd/database/migrations/000431_add_created_by_to_chat_messages.down.sql b/coderd/database/migrations/000431_add_created_by_to_chat_messages.down.sql new file mode 100644 index 0000000000..bb62b1d265 --- /dev/null +++ b/coderd/database/migrations/000431_add_created_by_to_chat_messages.down.sql @@ -0,0 +1 @@ +ALTER TABLE chat_messages DROP COLUMN created_by; diff --git a/coderd/database/migrations/000431_add_created_by_to_chat_messages.up.sql b/coderd/database/migrations/000431_add_created_by_to_chat_messages.up.sql new file mode 100644 index 0000000000..1d2501de51 --- /dev/null +++ b/coderd/database/migrations/000431_add_created_by_to_chat_messages.up.sql @@ -0,0 +1 @@ +ALTER TABLE chat_messages ADD COLUMN created_by uuid; diff --git a/coderd/database/models.go b/coderd/database/models.go index f1f31313cf..325608d624 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3952,6 +3952,7 @@ type ChatMessage struct { CacheReadTokens sql.NullInt64 `db:"cache_read_tokens" json:"cache_read_tokens"` ContextLimit sql.NullInt64 `db:"context_limit" json:"context_limit"` Compressed bool `db:"compressed" json:"compressed"` + CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"` } type ChatModelConfig struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index b3262a792d..606b1877d7 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3342,7 +3342,7 @@ func (q *sqlQuerier) GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds [ const getChatMessageByID = `-- name: GetChatMessageByID :one SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by FROM chat_messages WHERE @@ -3368,13 +3368,14 @@ func (q *sqlQuerier) GetChatMessageByID(ctx context.Context, id int64) (ChatMess &i.CacheReadTokens, &i.ContextLimit, &i.Compressed, + &i.CreatedBy, ) return i, err } const getChatMessagesByChatID = `-- name: GetChatMessagesByChatID :many SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by FROM chat_messages WHERE @@ -3415,6 +3416,7 @@ func (q *sqlQuerier) GetChatMessagesByChatID(ctx context.Context, arg GetChatMes &i.CacheReadTokens, &i.ContextLimit, &i.Compressed, + &i.CreatedBy, ); err != nil { return nil, err } @@ -3446,7 +3448,7 @@ WITH latest_compressed_summary AS ( 1 ) SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by FROM chat_messages WHERE @@ -3511,6 +3513,7 @@ func (q *sqlQuerier) GetChatMessagesForPromptByChatID(ctx context.Context, chatI &i.CacheReadTokens, &i.ContextLimit, &i.Compressed, + &i.CreatedBy, ); err != nil { return nil, err } @@ -3654,7 +3657,7 @@ func (q *sqlQuerier) GetChatsByOwnerID(ctx context.Context, arg GetChatsByOwnerI const getLastChatMessageByRole = `-- name: GetLastChatMessageByRole :one SELECT - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by FROM chat_messages WHERE @@ -3690,6 +3693,7 @@ func (q *sqlQuerier) GetLastChatMessageByRole(ctx context.Context, arg GetLastCh &i.CacheReadTokens, &i.ContextLimit, &i.Compressed, + &i.CreatedBy, ) return i, err } @@ -3809,13 +3813,14 @@ WITH updated_chat AS ( UPDATE chats SET - last_model_config_id = $2::uuid + last_model_config_id = $3::uuid WHERE id = $1::uuid - AND $2::uuid IS NOT NULL + AND $3::uuid IS NOT NULL ) INSERT INTO chat_messages ( chat_id, + created_by, model_config_id, role, content, @@ -3831,24 +3836,26 @@ INSERT INTO chat_messages ( ) VALUES ( $1::uuid, $2::uuid, - $3::text, - $4::jsonb, - $5::chat_message_visibility, - $6::bigint, + $3::uuid, + $4::text, + $5::jsonb, + $6::chat_message_visibility, $7::bigint, $8::bigint, $9::bigint, $10::bigint, $11::bigint, $12::bigint, - COALESCE($13::boolean, FALSE) + $13::bigint, + COALESCE($14::boolean, FALSE) ) RETURNING - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by ` type InsertChatMessageParams struct { ChatID uuid.UUID `db:"chat_id" json:"chat_id"` + CreatedBy uuid.NullUUID `db:"created_by" json:"created_by"` ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"` Role string `db:"role" json:"role"` Content pqtype.NullRawMessage `db:"content" json:"content"` @@ -3866,6 +3873,7 @@ type InsertChatMessageParams struct { func (q *sqlQuerier) InsertChatMessage(ctx context.Context, arg InsertChatMessageParams) (ChatMessage, error) { row := q.db.QueryRowContext(ctx, insertChatMessage, arg.ChatID, + arg.CreatedBy, arg.ModelConfigID, arg.Role, arg.Content, @@ -3896,6 +3904,7 @@ func (q *sqlQuerier) InsertChatMessage(ctx context.Context, arg InsertChatMessag &i.CacheReadTokens, &i.ContextLimit, &i.Compressed, + &i.CreatedBy, ) return i, err } @@ -4130,7 +4139,7 @@ SET WHERE id = $3::bigint RETURNING - id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed + id, chat_id, model_config_id, created_at, role, content, visibility, input_tokens, output_tokens, total_tokens, reasoning_tokens, cache_creation_tokens, cache_read_tokens, context_limit, compressed, created_by ` type UpdateChatMessageByIDParams struct { @@ -4158,6 +4167,7 @@ func (q *sqlQuerier) UpdateChatMessageByID(ctx context.Context, arg UpdateChatMe &i.CacheReadTokens, &i.ContextLimit, &i.Compressed, + &i.CreatedBy, ) return i, err } diff --git a/coderd/database/queries/chats.sql b/coderd/database/queries/chats.sql index 70e90ffa77..0bed55f398 100644 --- a/coderd/database/queries/chats.sql +++ b/coderd/database/queries/chats.sql @@ -192,6 +192,7 @@ WITH updated_chat AS ( ) INSERT INTO chat_messages ( chat_id, + created_by, model_config_id, role, content, @@ -206,6 +207,7 @@ INSERT INTO chat_messages ( compressed ) VALUES ( @chat_id::uuid, + sqlc.narg('created_by')::uuid, sqlc.narg('model_config_id')::uuid, @role::text, sqlc.narg('content')::jsonb, diff --git a/codersdk/chats.go b/codersdk/chats.go index f6f53e7342..0f54deef80 100644 --- a/codersdk/chats.go +++ b/codersdk/chats.go @@ -49,6 +49,7 @@ type Chat struct { type ChatMessage struct { ID int64 `json:"id"` ChatID uuid.UUID `json:"chat_id" format:"uuid"` + CreatedBy *uuid.UUID `json:"created_by,omitempty" format:"uuid"` ModelConfigID *uuid.UUID `json:"model_config_id,omitempty" format:"uuid"` CreatedAt time.Time `json:"created_at" format:"date-time"` Role string `json:"role"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 53c30e2ebe..b7aee91bca 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1150,6 +1150,7 @@ export const ChatInputPartTypes: ChatInputPartType[] = [ export interface ChatMessage { readonly id: number; readonly chat_id: string; + readonly created_by?: string; readonly model_config_id?: string; readonly created_at: string; readonly role: string;