Feat: impl attempt

This commit is contained in:
Grail Finder
2026-01-16 16:53:19 +03:00
parent f5d76eb605
commit eb44b1e4b2
6 changed files with 486 additions and 30 deletions

106
bot.go
View File

@@ -18,6 +18,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -68,6 +69,111 @@ var (
LocalModels = []string{} LocalModels = []string{}
) )
// parseKnownToTag extracts known_to list from content using configured tag.
// Returns cleaned content and list of character names.
func parseKnownToTag(content string) (string, []string) {
if cfg == nil || !cfg.CharSpecificContextEnabled {
return content, nil
}
tag := cfg.CharSpecificContextTag
if tag == "" {
tag = "__known_to_chars__"
}
// Pattern: tag + list + "__"
pattern := regexp.QuoteMeta(tag) + `(.*?)__`
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(content, -1)
if len(matches) == 0 {
return content, nil
}
// There may be multiple tags; we combine all.
var knownTo []string
cleaned := content
for _, match := range matches {
if len(match) < 2 {
continue
}
// Remove the entire matched tag from content
cleaned = strings.Replace(cleaned, match[0], "", 1)
list := strings.TrimSpace(match[1])
if list == "" {
continue
}
parts := strings.Split(list, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
knownTo = append(knownTo, p)
}
}
}
// Also remove any leftover trailing "__" that might be orphaned? Not needed.
return strings.TrimSpace(cleaned), knownTo
}
// processMessageTag processes a message for known_to tag and sets KnownTo field.
// It also ensures the sender's role is included in KnownTo.
// If KnownTo already set (e.g., from DB), preserves it unless new tag found.
func processMessageTag(msg models.RoleMsg) models.RoleMsg {
if cfg == nil || !cfg.CharSpecificContextEnabled {
return msg
}
// If KnownTo already set, assume tag already processed (content cleaned).
// However, we still check for new tags (maybe added later).
cleaned, knownTo := parseKnownToTag(msg.Content)
if cleaned != msg.Content {
msg.Content = cleaned
}
// If tag found, replace KnownTo with new list (merge with existing?)
// For simplicity, if knownTo is not nil, replace.
if knownTo != nil {
msg.KnownTo = knownTo
}
// Ensure sender role is in KnownTo
if msg.Role != "" {
senderAdded := false
for _, k := range msg.KnownTo {
if k == msg.Role {
senderAdded = true
break
}
}
if !senderAdded {
msg.KnownTo = append(msg.KnownTo, msg.Role)
}
}
return msg
}
// filterMessagesForCharacter returns messages visible to the specified character.
// If CharSpecificContextEnabled is false, returns all messages.
func filterMessagesForCharacter(messages []models.RoleMsg, character string) []models.RoleMsg {
if cfg == nil || !cfg.CharSpecificContextEnabled || character == "" {
return messages
}
filtered := make([]models.RoleMsg, 0, len(messages))
for _, msg := range messages {
// If KnownTo is nil or empty, message is visible to all
if len(msg.KnownTo) == 0 {
filtered = append(filtered, msg)
continue
}
// Check if character is in KnownTo list
found := false
for _, k := range msg.KnownTo {
if k == character {
found = true
break
}
}
if found {
filtered = append(filtered, msg)
}
}
return filtered
}
// cleanNullMessages removes messages with null or empty content to prevent API issues // cleanNullMessages removes messages with null or empty content to prevent API issues
func cleanNullMessages(messages []models.RoleMsg) []models.RoleMsg { func cleanNullMessages(messages []models.RoleMsg) []models.RoleMsg {
// // deletes tool calls which we don't want for now // // deletes tool calls which we don't want for now

View File

@@ -286,4 +286,322 @@ func TestConvertJSONToMapStringString(t *testing.T) {
} }
}) })
} }
}
func TestParseKnownToTag(t *testing.T) {
tests := []struct {
name string
content string
enabled bool
tag string
wantCleaned string
wantKnownTo []string
}{
{
name: "feature disabled returns original",
content: "Hello __known_to_chars__Alice__",
enabled: false,
tag: "__known_to_chars__",
wantCleaned: "Hello __known_to_chars__Alice__",
wantKnownTo: nil,
},
{
name: "no tag returns original",
content: "Hello Alice",
enabled: true,
tag: "__known_to_chars__",
wantCleaned: "Hello Alice",
wantKnownTo: nil,
},
{
name: "single tag with one char",
content: "Hello __known_to_chars__Alice__",
enabled: true,
tag: "__known_to_chars__",
wantCleaned: "Hello",
wantKnownTo: []string{"Alice"},
},
{
name: "single tag with two chars",
content: "Secret __known_to_chars__Alice,Bob__ message",
enabled: true,
tag: "__known_to_chars__",
wantCleaned: "Secret message",
wantKnownTo: []string{"Alice", "Bob"},
},
{
name: "tag at beginning",
content: "__known_to_chars__Alice__ Hello",
enabled: true,
tag: "__known_to_chars__",
wantCleaned: "Hello",
wantKnownTo: []string{"Alice"},
},
{
name: "tag at end",
content: "Hello __known_to_chars__Alice__",
enabled: true,
tag: "__known_to_chars__",
wantCleaned: "Hello",
wantKnownTo: []string{"Alice"},
},
{
name: "multiple tags",
content: "First __known_to_chars__Alice__ then __known_to_chars__Bob__",
enabled: true,
tag: "__known_to_chars__",
wantCleaned: "First then",
wantKnownTo: []string{"Alice", "Bob"},
},
{
name: "custom tag",
content: "Secret __secret__Alice,Bob__ message",
enabled: true,
tag: "__secret__",
wantCleaned: "Secret message",
wantKnownTo: []string{"Alice", "Bob"},
},
{
name: "empty list",
content: "Secret __known_to_chars____",
enabled: true,
tag: "__known_to_chars__",
wantCleaned: "Secret",
wantKnownTo: nil,
},
{
name: "whitespace around commas",
content: "__known_to_chars__ Alice , Bob , Carl __",
enabled: true,
tag: "__known_to_chars__",
wantCleaned: "",
wantKnownTo: []string{"Alice", "Bob", "Carl"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up config
testCfg := &config.Config{
CharSpecificContextEnabled: tt.enabled,
CharSpecificContextTag: tt.tag,
}
cfg = testCfg
cleaned, knownTo := parseKnownToTag(tt.content)
if cleaned != tt.wantCleaned {
t.Errorf("parseKnownToTag() cleaned = %q, want %q", cleaned, tt.wantCleaned)
}
if len(knownTo) != len(tt.wantKnownTo) {
t.Errorf("parseKnownToTag() knownTo length = %v, want %v", len(knownTo), len(tt.wantKnownTo))
t.Logf("got: %v", knownTo)
t.Logf("want: %v", tt.wantKnownTo)
} else {
for i, got := range knownTo {
if got != tt.wantKnownTo[i] {
t.Errorf("parseKnownToTag() knownTo[%d] = %q, want %q", i, got, tt.wantKnownTo[i])
}
}
}
})
}
}
func TestProcessMessageTag(t *testing.T) {
tests := []struct {
name string
msg models.RoleMsg
enabled bool
tag string
wantMsg models.RoleMsg
}{
{
name: "feature disabled returns unchanged",
msg: models.RoleMsg{
Role: "Alice",
Content: "Secret __known_to_chars__Bob__",
},
enabled: false,
tag: "__known_to_chars__",
wantMsg: models.RoleMsg{
Role: "Alice",
Content: "Secret __known_to_chars__Bob__",
KnownTo: nil,
},
},
{
name: "no tag, no knownTo",
msg: models.RoleMsg{
Role: "Alice",
Content: "Hello everyone",
},
enabled: true,
tag: "__known_to_chars__",
wantMsg: models.RoleMsg{
Role: "Alice",
Content: "Hello everyone",
KnownTo: []string{"Alice"},
},
},
{
name: "tag with Bob, adds Alice automatically",
msg: models.RoleMsg{
Role: "Alice",
Content: "Secret __known_to_chars__Bob__",
},
enabled: true,
tag: "__known_to_chars__",
wantMsg: models.RoleMsg{
Role: "Alice",
Content: "Secret",
KnownTo: []string{"Bob", "Alice"},
},
},
{
name: "tag already includes sender",
msg: models.RoleMsg{
Role: "Alice",
Content: "__known_to_chars__Alice,Bob__",
},
enabled: true,
tag: "__known_to_chars__",
wantMsg: models.RoleMsg{
Role: "Alice",
Content: "",
KnownTo: []string{"Alice", "Bob"},
},
},
{
name: "knownTo already set (from DB), tag still processed",
msg: models.RoleMsg{
Role: "Alice",
Content: "Secret __known_to_chars__Bob__",
KnownTo: []string{"Alice"}, // from previous processing
},
enabled: true,
tag: "__known_to_chars__",
wantMsg: models.RoleMsg{
Role: "Alice",
Content: "Secret",
KnownTo: []string{"Bob", "Alice"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testCfg := &config.Config{
CharSpecificContextEnabled: tt.enabled,
CharSpecificContextTag: tt.tag,
}
cfg = testCfg
got := processMessageTag(tt.msg)
if got.Content != tt.wantMsg.Content {
t.Errorf("processMessageTag() content = %q, want %q", got.Content, tt.wantMsg.Content)
}
if len(got.KnownTo) != len(tt.wantMsg.KnownTo) {
t.Errorf("processMessageTag() KnownTo length = %v, want %v", len(got.KnownTo), len(tt.wantMsg.KnownTo))
t.Logf("got: %v", got.KnownTo)
t.Logf("want: %v", tt.wantMsg.KnownTo)
} else {
// order may differ; check membership
for _, want := range tt.wantMsg.KnownTo {
found := false
for _, gotVal := range got.KnownTo {
if gotVal == want {
found = true
break
}
}
if !found {
t.Errorf("processMessageTag() missing KnownTo entry %q, got %v", want, got.KnownTo)
}
}
}
})
}
}
func TestFilterMessagesForCharacter(t *testing.T) {
messages := []models.RoleMsg{
{Role: "system", Content: "System message", KnownTo: nil}, // visible to all
{Role: "Alice", Content: "Hello everyone", KnownTo: nil}, // visible to all
{Role: "Alice", Content: "Secret for Bob", KnownTo: []string{"Alice", "Bob"}},
{Role: "Bob", Content: "Reply to Alice", KnownTo: []string{"Alice", "Bob"}},
{Role: "Alice", Content: "Private to Carl", KnownTo: []string{"Alice", "Carl"}},
{Role: "Carl", Content: "Hi all", KnownTo: nil}, // visible to all
}
tests := []struct {
name string
enabled bool
character string
wantIndices []int // indices from original messages that should be included
}{
{
name: "feature disabled returns all",
enabled: false,
character: "Alice",
wantIndices: []int{0,1,2,3,4,5},
},
{
name: "character empty returns all",
enabled: true,
character: "",
wantIndices: []int{0,1,2,3,4,5},
},
{
name: "Alice sees all including Carl-private",
enabled: true,
character: "Alice",
wantIndices: []int{0,1,2,3,4,5},
},
{
name: "Bob sees Alice-Bob secrets and all public",
enabled: true,
character: "Bob",
wantIndices: []int{0,1,2,3,5},
},
{
name: "Carl sees Alice-Carl secret and public",
enabled: true,
character: "Carl",
wantIndices: []int{0,1,4,5},
},
{
name: "David sees only public messages",
enabled: true,
character: "David",
wantIndices: []int{0,1,5},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testCfg := &config.Config{
CharSpecificContextEnabled: tt.enabled,
CharSpecificContextTag: "__known_to_chars__",
}
cfg = testCfg
got := filterMessagesForCharacter(messages, tt.character)
if len(got) != len(tt.wantIndices) {
t.Errorf("filterMessagesForCharacter() returned %d messages, want %d", len(got), len(tt.wantIndices))
t.Logf("got: %v", got)
return
}
for i, idx := range tt.wantIndices {
if got[i].Content != messages[idx].Content {
t.Errorf("filterMessagesForCharacter() message %d content = %q, want %q", i, got[i].Content, messages[idx].Content)
}
}
})
}
} }

View File

@@ -43,3 +43,5 @@ DBPATH = "gflt.db"
FilePickerDir = "." # Directory where file picker should start FilePickerDir = "." # Directory where file picker should start
FilePickerExts = "png,jpg,jpeg,gif,webp" # Comma-separated list of allowed file extensions for file picker FilePickerExts = "png,jpg,jpeg,gif,webp" # Comma-separated list of allowed file extensions for file picker
EnableMouse = false # Enable mouse support in the UI EnableMouse = false # Enable mouse support in the UI
CharSpecificContextEnabled = false
CharSpecificContextTag = "__known_to_chars__"

View File

@@ -61,10 +61,12 @@ type Config struct {
WhisperBinaryPath string `toml:"WhisperBinaryPath"` WhisperBinaryPath string `toml:"WhisperBinaryPath"`
WhisperModelPath string `toml:"WhisperModelPath"` WhisperModelPath string `toml:"WhisperModelPath"`
STT_LANG string `toml:"STT_LANG"` STT_LANG string `toml:"STT_LANG"`
DBPATH string `toml:"DBPATH"` DBPATH string `toml:"DBPATH"`
FilePickerDir string `toml:"FilePickerDir"` FilePickerDir string `toml:"FilePickerDir"`
FilePickerExts string `toml:"FilePickerExts"` FilePickerExts string `toml:"FilePickerExts"`
EnableMouse bool `toml:"EnableMouse"` EnableMouse bool `toml:"EnableMouse"`
CharSpecificContextEnabled bool `toml:"CharSpecificContextEnabled"`
CharSpecificContextTag string `toml:"CharSpecificContextTag"`
} }
func LoadConfig(fn string) (*Config, error) { func LoadConfig(fn string) (*Config, error) {

66
llm.go
View File

@@ -34,6 +34,24 @@ func ClearImageAttachment() {
imageAttachmentPath = "" imageAttachmentPath = ""
} }
// filterMessagesForCurrentCharacter filters messages based on char-specific context.
// Returns filtered messages and the bot persona role (target character).
func filterMessagesForCurrentCharacter(messages []models.RoleMsg) ([]models.RoleMsg, string) {
if cfg == nil || !cfg.CharSpecificContextEnabled {
botPersona := cfg.AssistantRole
if cfg.WriteNextMsgAsCompletionAgent != "" {
botPersona = cfg.WriteNextMsgAsCompletionAgent
}
return messages, botPersona
}
botPersona := cfg.AssistantRole
if cfg.WriteNextMsgAsCompletionAgent != "" {
botPersona = cfg.WriteNextMsgAsCompletionAgent
}
filtered := filterMessagesForCharacter(messages, botPersona)
return filtered, botPersona
}
type ChunkParser interface { type ChunkParser interface {
ParseChunk([]byte) (*models.TextChunk, error) ParseChunk([]byte) (*models.TextChunk, error)
FormMsg(msg, role string, cont bool) (io.Reader, error) FormMsg(msg, role string, cont bool) (io.Reader, error)
@@ -113,6 +131,7 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro
} }
if msg != "" { // otherwise let the bot to continue if msg != "" { // otherwise let the bot to continue
newMsg := models.RoleMsg{Role: role, Content: msg} newMsg := models.RoleMsg{Role: role, Content: msg}
newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg) chatBody.Messages = append(chatBody.Messages, newMsg)
} }
if !resume { if !resume {
@@ -136,17 +155,14 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro
// add to chat body // add to chat body
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
} }
messages := make([]string, len(chatBody.Messages)) filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
for i, m := range chatBody.Messages { messages := make([]string, len(filteredMessages))
for i, m := range filteredMessages {
messages[i] = m.ToPrompt() messages[i] = m.ToPrompt()
} }
prompt := strings.Join(messages, "\n") prompt := strings.Join(messages, "\n")
// strings builder? // strings builder?
if !resume { if !resume {
botPersona := cfg.AssistantRole
if cfg.WriteNextMsgAsCompletionAgent != "" {
botPersona = cfg.WriteNextMsgAsCompletionAgent
}
botMsgStart := "\n" + botPersona + ":\n" botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart prompt += botMsgStart
} }
@@ -270,6 +286,7 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) {
// Create a simple text message // Create a simple text message
newMsg = models.NewRoleMsg(role, msg) newMsg = models.NewRoleMsg(role, msg)
} }
newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg) chatBody.Messages = append(chatBody.Messages, newMsg)
logger.Debug("LCPChat FormMsg: added message to chatBody", "role", newMsg.Role, "content_len", len(newMsg.Content), "message_count_after_add", len(chatBody.Messages)) logger.Debug("LCPChat FormMsg: added message to chatBody", "role", newMsg.Role, "content_len", len(newMsg.Content), "message_count_after_add", len(chatBody.Messages))
} }
@@ -291,12 +308,13 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) {
} }
} }
// openai /v1/chat does not support custom roles; needs to be user, assistant, system // openai /v1/chat does not support custom roles; needs to be user, assistant, system
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
bodyCopy := &models.ChatBody{ bodyCopy := &models.ChatBody{
Messages: make([]models.RoleMsg, len(chatBody.Messages)), Messages: make([]models.RoleMsg, len(filteredMessages)),
Model: chatBody.Model, Model: chatBody.Model,
Stream: chatBody.Stream, Stream: chatBody.Stream,
} }
for i, msg := range chatBody.Messages { for i, msg := range filteredMessages {
if msg.Role == cfg.UserRole { if msg.Role == cfg.UserRole {
bodyCopy.Messages[i] = msg bodyCopy.Messages[i] = msg
bodyCopy.Messages[i].Role = "user" bodyCopy.Messages[i].Role = "user"
@@ -348,6 +366,7 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader
logger.Debug("formmsg deepseekercompletion", "link", cfg.CurrentAPI) logger.Debug("formmsg deepseekercompletion", "link", cfg.CurrentAPI)
if msg != "" { // otherwise let the bot to continue if msg != "" { // otherwise let the bot to continue
newMsg := models.RoleMsg{Role: role, Content: msg} newMsg := models.RoleMsg{Role: role, Content: msg}
newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg) chatBody.Messages = append(chatBody.Messages, newMsg)
} }
if !resume { if !resume {
@@ -372,17 +391,14 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader
// add to chat body // add to chat body
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
} }
messages := make([]string, len(chatBody.Messages)) filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
for i, m := range chatBody.Messages { messages := make([]string, len(filteredMessages))
for i, m := range filteredMessages {
messages[i] = m.ToPrompt() messages[i] = m.ToPrompt()
} }
prompt := strings.Join(messages, "\n") prompt := strings.Join(messages, "\n")
// strings builder? // strings builder?
if !resume { if !resume {
botPersona := cfg.AssistantRole
if cfg.WriteNextMsgAsCompletionAgent != "" {
botPersona = cfg.WriteNextMsgAsCompletionAgent
}
botMsgStart := "\n" + botPersona + ":\n" botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart prompt += botMsgStart
} }
@@ -432,6 +448,7 @@ func (ds DeepSeekerChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
logger.Debug("formmsg deepseekerchat", "link", cfg.CurrentAPI) logger.Debug("formmsg deepseekerchat", "link", cfg.CurrentAPI)
if msg != "" { // otherwise let the bot continue if msg != "" { // otherwise let the bot continue
newMsg := models.RoleMsg{Role: role, Content: msg} newMsg := models.RoleMsg{Role: role, Content: msg}
newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg) chatBody.Messages = append(chatBody.Messages, newMsg)
} }
if !resume { if !resume {
@@ -451,12 +468,13 @@ func (ds DeepSeekerChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages)) logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages))
} }
} }
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
bodyCopy := &models.ChatBody{ bodyCopy := &models.ChatBody{
Messages: make([]models.RoleMsg, len(chatBody.Messages)), Messages: make([]models.RoleMsg, len(filteredMessages)),
Model: chatBody.Model, Model: chatBody.Model,
Stream: chatBody.Stream, Stream: chatBody.Stream,
} }
for i, msg := range chatBody.Messages { for i, msg := range filteredMessages {
if msg.Role == cfg.UserRole || i == 1 { if msg.Role == cfg.UserRole || i == 1 {
bodyCopy.Messages[i] = msg bodyCopy.Messages[i] = msg
bodyCopy.Messages[i].Role = "user" bodyCopy.Messages[i].Role = "user"
@@ -502,6 +520,7 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader
logger.Debug("formmsg openroutercompletion", "link", cfg.CurrentAPI) logger.Debug("formmsg openroutercompletion", "link", cfg.CurrentAPI)
if msg != "" { // otherwise let the bot to continue if msg != "" { // otherwise let the bot to continue
newMsg := models.RoleMsg{Role: role, Content: msg} newMsg := models.RoleMsg{Role: role, Content: msg}
newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg) chatBody.Messages = append(chatBody.Messages, newMsg)
} }
if !resume { if !resume {
@@ -525,17 +544,14 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader
// add to chat body // add to chat body
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg}) chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
} }
messages := make([]string, len(chatBody.Messages)) filteredMessages, botPersona := filterMessagesForCurrentCharacter(chatBody.Messages)
for i, m := range chatBody.Messages { messages := make([]string, len(filteredMessages))
for i, m := range filteredMessages {
messages[i] = m.ToPrompt() messages[i] = m.ToPrompt()
} }
prompt := strings.Join(messages, "\n") prompt := strings.Join(messages, "\n")
// strings builder? // strings builder?
if !resume { if !resume {
botPersona := cfg.AssistantRole
if cfg.WriteNextMsgAsCompletionAgent != "" {
botPersona = cfg.WriteNextMsgAsCompletionAgent
}
botMsgStart := "\n" + botPersona + ":\n" botMsgStart := "\n" + botPersona + ":\n"
prompt += botMsgStart prompt += botMsgStart
} }
@@ -619,6 +635,7 @@ func (or OpenRouterChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
// Create a simple text message // Create a simple text message
newMsg = models.NewRoleMsg(role, msg) newMsg = models.NewRoleMsg(role, msg)
} }
newMsg = processMessageTag(newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg) chatBody.Messages = append(chatBody.Messages, newMsg)
} }
if !resume { if !resume {
@@ -639,12 +656,13 @@ func (or OpenRouterChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
} }
} }
// Create copy of chat body with standardized user role // Create copy of chat body with standardized user role
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
bodyCopy := &models.ChatBody{ bodyCopy := &models.ChatBody{
Messages: make([]models.RoleMsg, len(chatBody.Messages)), Messages: make([]models.RoleMsg, len(filteredMessages)),
Model: chatBody.Model, Model: chatBody.Model,
Stream: chatBody.Stream, Stream: chatBody.Stream,
} }
for i, msg := range chatBody.Messages { for i, msg := range filteredMessages {
bodyCopy.Messages[i] = msg bodyCopy.Messages[i] = msg
// Standardize role if it's a user role // Standardize role if it's a user role
if bodyCopy.Messages[i].Role == cfg.UserRole { if bodyCopy.Messages[i].Role == cfg.UserRole {

View File

@@ -93,6 +93,7 @@ type RoleMsg struct {
Content string `json:"-"` Content string `json:"-"`
ContentParts []interface{} `json:"-"` ContentParts []interface{} `json:"-"`
ToolCallID string `json:"tool_call_id,omitempty"` // For tool response messages ToolCallID string `json:"tool_call_id,omitempty"` // For tool response messages
KnownTo []string `json:"known_to,omitempty"`
hasContentParts bool // Flag to indicate which content type to marshal hasContentParts bool // Flag to indicate which content type to marshal
} }
@@ -104,10 +105,12 @@ func (m RoleMsg) MarshalJSON() ([]byte, error) {
Role string `json:"role"` Role string `json:"role"`
Content []interface{} `json:"content"` Content []interface{} `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"`
KnownTo []string `json:"known_to,omitempty"`
}{ }{
Role: m.Role, Role: m.Role,
Content: m.ContentParts, Content: m.ContentParts,
ToolCallID: m.ToolCallID, ToolCallID: m.ToolCallID,
KnownTo: m.KnownTo,
} }
return json.Marshal(aux) return json.Marshal(aux)
} else { } else {
@@ -116,10 +119,12 @@ func (m RoleMsg) MarshalJSON() ([]byte, error) {
Role string `json:"role"` Role string `json:"role"`
Content string `json:"content"` Content string `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"`
KnownTo []string `json:"known_to,omitempty"`
}{ }{
Role: m.Role, Role: m.Role,
Content: m.Content, Content: m.Content,
ToolCallID: m.ToolCallID, ToolCallID: m.ToolCallID,
KnownTo: m.KnownTo,
} }
return json.Marshal(aux) return json.Marshal(aux)
} }
@@ -132,11 +137,13 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
Role string `json:"role"` Role string `json:"role"`
Content []interface{} `json:"content"` Content []interface{} `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"`
KnownTo []string `json:"known_to,omitempty"`
} }
if err := json.Unmarshal(data, &structured); err == nil && len(structured.Content) > 0 { if err := json.Unmarshal(data, &structured); err == nil && len(structured.Content) > 0 {
m.Role = structured.Role m.Role = structured.Role
m.ContentParts = structured.Content m.ContentParts = structured.Content
m.ToolCallID = structured.ToolCallID m.ToolCallID = structured.ToolCallID
m.KnownTo = structured.KnownTo
m.hasContentParts = true m.hasContentParts = true
return nil return nil
} }
@@ -146,6 +153,7 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
Role string `json:"role"` Role string `json:"role"`
Content string `json:"content"` Content string `json:"content"`
ToolCallID string `json:"tool_call_id,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"`
KnownTo []string `json:"known_to,omitempty"`
} }
if err := json.Unmarshal(data, &simple); err != nil { if err := json.Unmarshal(data, &simple); err != nil {
return err return err
@@ -153,6 +161,7 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
m.Role = simple.Role m.Role = simple.Role
m.Content = simple.Content m.Content = simple.Content
m.ToolCallID = simple.ToolCallID m.ToolCallID = simple.ToolCallID
m.KnownTo = simple.KnownTo
m.hasContentParts = false m.hasContentParts = false
return nil return nil
} }
@@ -363,7 +372,8 @@ func (cb *ChatBody) MakeStopSlice() []string {
for _, m := range cb.Messages { for _, m := range cb.Messages {
namesMap[m.Role] = struct{}{} namesMap[m.Role] = struct{}{}
} }
ss := []string{"<|im_end|>"} ss := make([]string, 0, 1+len(namesMap))
ss = append(ss, "<|im_end|>")
for k := range namesMap { for k := range namesMap {
ss = append(ss, k+":\n") ss = append(ss, k+":\n")
} }
@@ -523,7 +533,7 @@ type LCPModels struct {
} }
func (lcp *LCPModels) ListModels() []string { func (lcp *LCPModels) ListModels() []string {
resp := []string{} resp := make([]string, 0, len(lcp.Data))
for _, model := range lcp.Data { for _, model := range lcp.Data {
resp = append(resp, model.ID) resp = append(resp, model.ID)
} }