Connects the inbound SenderID, which was already carried end-to-end via InboundContext to PromptBuildRequest, into the per-turn user message so historical messages remember who said what. - userPromptMessage gains a senderID parameter. Real human IDs are sanitized via SanitizeMessageName; synthetic triggers (cron, heartbeat, async:*) are filtered out via IsSystemSenderID so they do not surface as distinct users. - pipeline_setup.go now passes ts.opts.Dispatch.SenderID() and uses AddFullMessage unconditionally. The previous AddMessage(role, content) shortcut for media-less turns silently dropped the new Name field, breaking history disambiguation. - context.go (BuildMessagesFromPrompt) propagates req.SenderID onto the current-turn user message that goes to the LLM. After this commit, sessions persist sender attribution and provider adapters receive it on each user message. Adapters that consume Name (OpenAI native field, Anthropic-style content prefix) land in follow-up commits. The existing "## Current Sender" block in buildDynamicContext is unchanged; it continues to describe the latest speaker in the system prompt while Name covers the per-message channel for history. Refs #2702.
139 lines
3.8 KiB
Go
139 lines
3.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/providers"
|
|
"github.com/sipeed/picoclaw/pkg/providers/messageutil"
|
|
)
|
|
|
|
func promptBuildRequestForTurn(
|
|
ts *turnState,
|
|
history []providers.Message,
|
|
summary string,
|
|
currentMessage string,
|
|
media []string,
|
|
) PromptBuildRequest {
|
|
return PromptBuildRequest{
|
|
History: history,
|
|
Summary: summary,
|
|
CurrentMessage: currentMessage,
|
|
Media: append([]string(nil), media...),
|
|
Channel: ts.channel,
|
|
ChatID: ts.chatID,
|
|
SenderID: ts.opts.Dispatch.SenderID(),
|
|
SenderDisplayName: ts.opts.SenderDisplayName,
|
|
ActiveSkills: activeSkillNames(ts.agent, ts.opts),
|
|
Overlays: promptOverlaysForOptions(ts.opts),
|
|
}
|
|
}
|
|
|
|
func promptOverlaysForOptions(opts processOptions) []PromptPart {
|
|
systemPrompt := strings.TrimSpace(opts.SystemPromptOverride)
|
|
if systemPrompt == "" {
|
|
return nil
|
|
}
|
|
|
|
return []PromptPart{
|
|
{
|
|
ID: "instruction.subturn_profile",
|
|
Layer: PromptLayerInstruction,
|
|
Slot: PromptSlotWorkspace,
|
|
Source: PromptSource{ID: PromptSourceSubTurnProfile, Name: "subturn.profile"},
|
|
Title: "SubTurn System Instructions",
|
|
Content: systemPrompt,
|
|
Stable: false,
|
|
Cache: PromptCacheNone,
|
|
},
|
|
}
|
|
}
|
|
|
|
func promptContentBlock(part PromptPart, cache *providers.CacheControl) providers.ContentBlock {
|
|
if cache == nil {
|
|
cache = cacheControlForPromptPart(part)
|
|
}
|
|
return providers.ContentBlock{
|
|
Type: "text",
|
|
Text: part.Content,
|
|
CacheControl: cache,
|
|
PromptLayer: string(part.Layer),
|
|
PromptSlot: string(part.Slot),
|
|
PromptSource: string(part.Source.ID),
|
|
}
|
|
}
|
|
|
|
func cacheControlForPromptPart(part PromptPart) *providers.CacheControl {
|
|
switch part.Cache {
|
|
case PromptCacheEphemeral:
|
|
return &providers.CacheControl{Type: "ephemeral"}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func promptMessageWithMetadata(
|
|
msg providers.Message,
|
|
layer PromptLayer,
|
|
slot PromptSlot,
|
|
source PromptSourceID,
|
|
) providers.Message {
|
|
msg.PromptLayer = string(layer)
|
|
msg.PromptSlot = string(slot)
|
|
msg.PromptSource = string(source)
|
|
return msg
|
|
}
|
|
|
|
func promptMessageWithDefaultMetadata(
|
|
msg providers.Message,
|
|
layer PromptLayer,
|
|
slot PromptSlot,
|
|
source PromptSourceID,
|
|
) providers.Message {
|
|
if strings.TrimSpace(msg.PromptSource) != "" {
|
|
return msg
|
|
}
|
|
return promptMessageWithMetadata(msg, layer, slot, source)
|
|
}
|
|
|
|
// userPromptMessage builds the per-turn user message that gets persisted
|
|
// to session history and sent to the LLM. senderID is the raw inbound
|
|
// sender ID; it is sanitized into the OpenAI-compatible Name field unless
|
|
// it identifies a system trigger (cron, heartbeat, async callback), in
|
|
// which case Name is left empty so synthetic events do not appear as
|
|
// distinct human users in the conversation.
|
|
func userPromptMessage(content string, media []string, senderID string) providers.Message {
|
|
msg := providers.Message{
|
|
Role: "user",
|
|
Content: content,
|
|
}
|
|
if !messageutil.IsSystemSenderID(senderID) {
|
|
msg.Name = messageutil.SanitizeMessageName(senderID)
|
|
}
|
|
if len(media) > 0 {
|
|
msg.Media = append([]string(nil), media...)
|
|
}
|
|
return promptMessageWithMetadata(msg, PromptLayerTurn, PromptSlotMessage, PromptSourceUserMessage)
|
|
}
|
|
|
|
func steeringPromptMessage(msg providers.Message) providers.Message {
|
|
return promptMessageWithDefaultMetadata(msg, PromptLayerTurn, PromptSlotSteering, PromptSourceSteering)
|
|
}
|
|
|
|
func subTurnResultPromptMessage(content string) providers.Message {
|
|
return promptMessageWithMetadata(
|
|
providers.Message{Role: "user", Content: fmt.Sprintf("[SubTurn Result] %s", content)},
|
|
PromptLayerTurn,
|
|
PromptSlotSubTurn,
|
|
PromptSourceSubTurnResult,
|
|
)
|
|
}
|
|
|
|
func interruptPromptMessage(content string) providers.Message {
|
|
return promptMessageWithMetadata(
|
|
providers.Message{Role: "user", Content: content},
|
|
PromptLayerTurn,
|
|
PromptSlotInterrupt,
|
|
PromptSourceInterrupt,
|
|
)
|
|
}
|