mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat: add per-chat system prompt option (#24053)
Adds a `system_prompt` field to `CreateChatRequest` that allows API consumers to provide custom instructions when creating a chat. The per-chat prompt is stored as a separate system message (`role=system`, `visibility=model`) in the `chat_messages` table, inserted between the deployment system prompt and the workspace awareness message. Also moves deployment system prompt resolution from the HTTP handler (`resolvedChatSystemPrompt`) into `chatd.CreateChat` where it belongs. The handler no longer assembles system prompts — `CreateOptions.SystemPrompt` is now purely the per-chat user prompt, and the deployment prompt is resolved internally by chatd. No database schema changes required. **Message insertion order:** 1. Deployment system prompt (resolved by chatd, existing) 2. Per-chat user system prompt (new, from `CreateOptions.SystemPrompt`) 3. Workspace awareness (existing) 4. Initial user message (existing) 🤖 Generated with [Coder Agents](https://coder.com/agents)
This commit is contained in:
+54
-6
@@ -886,7 +886,8 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
|
||||
return xerrors.Errorf("insert chat: %w", err)
|
||||
}
|
||||
|
||||
systemPrompt := strings.TrimSpace(opts.SystemPrompt)
|
||||
deploymentPrompt := p.resolveDeploymentSystemPrompt(ctx)
|
||||
userPrompt := SanitizePromptText(opts.SystemPrompt)
|
||||
var workspaceAwareness string
|
||||
if opts.WorkspaceID.Valid {
|
||||
workspaceAwareness = "This chat is attached to a workspace. You can use workspace tools like execute, read_file, write_file, etc."
|
||||
@@ -908,16 +909,32 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
|
||||
ChatID: insertedChat.ID,
|
||||
}
|
||||
|
||||
if systemPrompt != "" {
|
||||
systemContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText(systemPrompt),
|
||||
if deploymentPrompt != "" {
|
||||
deploymentContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText(deploymentPrompt),
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal system prompt: %w", err)
|
||||
return xerrors.Errorf("marshal deployment system prompt: %w", err)
|
||||
}
|
||||
appendChatMessage(&msgParams, newChatMessage(
|
||||
database.ChatMessageRoleSystem,
|
||||
systemContent,
|
||||
deploymentContent,
|
||||
database.ChatMessageVisibilityModel,
|
||||
opts.ModelConfigID,
|
||||
chatprompt.CurrentContentVersion,
|
||||
))
|
||||
}
|
||||
|
||||
if userPrompt != "" {
|
||||
userPromptContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
|
||||
codersdk.ChatMessageText(userPrompt),
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("marshal user system prompt: %w", err)
|
||||
}
|
||||
appendChatMessage(&msgParams, newChatMessage(
|
||||
database.ChatMessageRoleSystem,
|
||||
userPromptContent,
|
||||
database.ChatMessageVisibilityModel,
|
||||
opts.ModelConfigID,
|
||||
chatprompt.CurrentContentVersion,
|
||||
@@ -5229,6 +5246,37 @@ func (p *Server) resolveUserCompactionThreshold(ctx context.Context, userID uuid
|
||||
return int32(val), true
|
||||
}
|
||||
|
||||
// resolveDeploymentSystemPrompt builds the deployment-level system
|
||||
// prompt from the built-in default and the admin-configured custom
|
||||
// prompt stored in site_configs.
|
||||
func (p *Server) resolveDeploymentSystemPrompt(ctx context.Context) string {
|
||||
config, err := p.db.GetChatSystemPromptConfig(ctx)
|
||||
if err != nil {
|
||||
// Fail open: use the built-in default so chats always have
|
||||
// some system guidance.
|
||||
p.logger.Error(ctx, "failed to fetch chat system prompt configuration, using default", slog.Error(err))
|
||||
return DefaultSystemPrompt
|
||||
}
|
||||
|
||||
sanitizedCustom := SanitizePromptText(config.ChatSystemPrompt)
|
||||
if sanitizedCustom == "" && strings.TrimSpace(config.ChatSystemPrompt) != "" {
|
||||
p.logger.Warn(ctx, "custom system prompt became empty after sanitization, omitting custom portion")
|
||||
}
|
||||
|
||||
var parts []string
|
||||
if config.IncludeDefaultSystemPrompt {
|
||||
parts = append(parts, DefaultSystemPrompt)
|
||||
}
|
||||
if sanitizedCustom != "" {
|
||||
parts = append(parts, sanitizedCustom)
|
||||
}
|
||||
result := strings.Join(parts, "\n\n")
|
||||
if result == "" {
|
||||
p.logger.Warn(ctx, "resolved system prompt is empty, no system prompt will be injected into chats")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// resolveUserPrompt fetches the user's custom chat prompt from the
|
||||
// database and wraps it in <user-instructions> tags. Returns empty
|
||||
// string if no prompt is set.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package chatd_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -110,19 +111,19 @@ func TestSpawnComputerUseAgent_SystemPromptFormat(t *testing.T) {
|
||||
|
||||
// The system message raw content is a JSON-encoded string.
|
||||
// It should contain the system prompt with the user prompt.
|
||||
var rawSystemContent string
|
||||
var foundPrompt bool
|
||||
for _, msg := range messages {
|
||||
if msg.Role != "system" {
|
||||
continue
|
||||
}
|
||||
if msg.Content.Valid {
|
||||
rawSystemContent = string(msg.Content.RawMessage)
|
||||
if msg.Content.Valid && strings.Contains(string(msg.Content.RawMessage), prompt) {
|
||||
foundPrompt = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Contains(t, rawSystemContent, prompt,
|
||||
"system prompt raw content should contain the user prompt")
|
||||
assert.True(t, foundPrompt,
|
||||
"at least one system message should contain the user prompt")
|
||||
}
|
||||
|
||||
func TestSpawnComputerUseAgent_ChildIsListedUnderParent(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user