Files
picoclaw/pkg/agent/prompt_turn.go
T
2026-06-13 11:38:08 +02:00

241 lines
6.6 KiB
Go

package agent
import (
"fmt"
"strings"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/providers"
)
func promptBuildRequestForTurn(
ts *turnState,
history []providers.Message,
summary string,
currentMessage string,
media []string,
cfg *config.Config,
) PromptBuildRequest {
req := 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),
}
hasCallableTools := true
if ts.profile.Enabled {
hasCallableTools = turnProfileHasCallableTools(ts.profile, ts.agent.Tools.ToProviderDefs()) ||
turnProfileNativeSearchCallable(cfg, ts.profile, ts.agent)
}
if turnProfileSystemPromptOff(ts.profile) {
req.SuppressDefaultSystemPrompt = true
req.SuppressSkillContext = true
req.ToolUseFallback = hasCallableTools
}
if ts.profile.Enabled && !hasCallableTools {
req.SuppressToolUseRule = true
}
if turnProfileSkillsOff(ts.profile) {
req.SuppressSkillContext = true
}
if turnProfileCustomSkills(ts.profile) {
req.AllowedSkills = append([]string(nil), ts.profile.AllowedSkills...)
}
if ts.profile.Enabled && ts.profile.ToolsMode == config.TurnProfileModeCustom {
req.AllowedTools = append([]string(nil), ts.profile.AllowedTools...)
}
return req
}
func turnProfileNativeSearchCallable(
cfg *config.Config,
profile config.EffectiveTurnProfile,
agent *AgentInstance,
) bool {
if cfg == nil || agent == nil {
return false
}
if !cfg.Tools.IsToolEnabled("web") || !cfg.Tools.Web.PreferNative {
return false
}
if !turnProfileToolAllowed(profile, "web_search") {
return false
}
nativeProvider, ok := agent.Provider.(providers.NativeSearchCapable)
return ok && nativeProvider.SupportsNativeSearch()
}
func promptBuildRequestForProcessOptions(
agent *AgentInstance,
opts processOptions,
history []providers.Message,
summary string,
currentMessage string,
media []string,
) PromptBuildRequest {
req := PromptBuildRequest{
History: history,
Summary: summary,
CurrentMessage: currentMessage,
Media: append([]string(nil), media...),
Channel: opts.Channel,
ChatID: opts.ChatID,
SenderID: opts.SenderID,
SenderDisplayName: opts.SenderDisplayName,
ActiveSkills: activeSkillNames(agent, opts),
Overlays: promptOverlaysForOptions(opts),
}
profile := opts.TurnProfile
hasCallableTools := true
if profile.Enabled && agent != nil {
hasCallableTools = turnProfileHasCallableTools(profile, agent.Tools.ToProviderDefs())
}
if turnProfileSystemPromptOff(profile) {
req.SuppressDefaultSystemPrompt = true
req.SuppressSkillContext = true
req.ToolUseFallback = hasCallableTools
}
if profile.Enabled && !hasCallableTools {
req.SuppressToolUseRule = true
}
if turnProfileSkillsOff(profile) {
req.SuppressSkillContext = true
}
if turnProfileCustomSkills(profile) {
req.AllowedSkills = append([]string(nil), profile.AllowedSkills...)
}
if profile.Enabled && profile.ToolsMode == config.TurnProfileModeCustom {
req.AllowedTools = append([]string(nil), profile.AllowedTools...)
}
return req
}
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)
}
func userPromptMessage(content string, media []string) providers.Message {
msg := providers.Message{
Role: "user",
Content: content,
}
if len(media) > 0 {
msg.Media = append([]string(nil), media...)
}
return promptMessageWithMetadata(msg, PromptLayerTurn, PromptSlotMessage, PromptSourceUserMessage)
}
func toolResultPromptMessage(content, toolCallID string, media []string) providers.Message {
msg := providers.Message{
Role: "tool",
Content: content,
ToolCallID: toolCallID,
}
if len(media) > 0 {
msg.Media = append([]string(nil), media...)
}
return promptMessageWithMetadata(msg, PromptLayerTurn, PromptSlotToolResult, PromptSourceToolResult)
}
func toolImageFollowUpPromptMessage(media []string) providers.Message {
msg := providers.Message{
Role: "user",
Content: "[Loaded image from tool result above]",
}
if len(media) > 0 {
msg.Media = append([]string(nil), media...)
}
return promptMessageWithMetadata(msg, PromptLayerTurn, PromptSlotToolResult, PromptSourceToolResult)
}
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,
)
}