4 Commits

Author SHA1 Message Date
Grail Finder
e42eb96371 Doc: update 2026-02-10 11:27:06 +03:00
Grail Finder
46a33baabb Enha: stop tts if msg not for user 2026-02-10 11:25:05 +03:00
Grail Finder
875de679cf Merge branch 'feat/char-secrets' 2026-02-10 11:05:09 +03:00
Grail Finder
3b542421e3 Enha: sort chat table (by updated_at) 2026-01-14 10:06:15 +03:00
9 changed files with 30 additions and 20 deletions

View File

@@ -8,6 +8,7 @@ made with use of [tview](https://github.com/rivo/tview)
- tts/stt (run make commands to get deps); - tts/stt (run make commands to get deps);
- image input; - image input;
- function calls (function calls are implemented natively, to avoid calling outside sources); - function calls (function calls are implemented natively, to avoid calling outside sources);
- [character specific context (unique feature)](char-specific-context.md)
#### how it looks #### how it looks
![how it looks](assets/ex01.png) ![how it looks](assets/ex01.png)

1
bot.go
View File

@@ -874,6 +874,7 @@ out:
// Process the new message to check for known_to tags in LLM response // Process the new message to check for known_to tags in LLM response
newMsg = *processMessageTag(&newMsg) newMsg = *processMessageTag(&newMsg)
chatBody.Messages = append(chatBody.Messages, newMsg) chatBody.Messages = append(chatBody.Messages, newMsg)
stopTTSIfNotForUser(&newMsg)
} }
cleanChatBody() cleanChatBody()
refreshChatDisplay() refreshChatDisplay()

View File

@@ -113,16 +113,7 @@ When `AutoTurn` is enabled, the system can automatically trigger responses from
## Cardmaking with multiple characters ## Cardmaking with multiple characters
So far only json format supports multiple characters. So far only json format supports multiple characters.
Card example: [card example](sysprompts/alice_bob_carl.json)
```
{
"sys_prompt": "This is a chat between Alice, Bob and Carl. Normally what is said by any character is seen by all others. But characters also might write messages intended to specific targets if their message contain string tag '@{CharName1,CharName2,CharName3}@'.\nFor example:\nAlice:\n\"Hey, Bob. I have a secret for you... (ooc: @Bob@)\"\nThis message would be seen only by Bob and Alice (sender always sees their own message).",
"role": "Alice",
"filepath": "sysprompts/alice_bob_carl.json",
"chars": ["Alice", "Bob", "Carl"],
"first_msg": "Hey guys! Want to play Alias like game? I'll tell Bob a word and he needs to describe that word so Carl can guess what it was?"
}
```
## Limitations & Caveats ## Limitations & Caveats
@@ -131,7 +122,7 @@ Card example:
Characterspecific context relies on the `/completion` endpoint (or other completionstyle endpoints) where the LLM is presented with a raw text prompt containing the entire filtered history. It does **not** work with OpenAIstyle `/v1/chat/completions` endpoints, because those endpoints enforce a fixed role set (`user`/`assistant`/`system`) and strip custom role names and metadata. Characterspecific context relies on the `/completion` endpoint (or other completionstyle endpoints) where the LLM is presented with a raw text prompt containing the entire filtered history. It does **not** work with OpenAIstyle `/v1/chat/completions` endpoints, because those endpoints enforce a fixed role set (`user`/`assistant`/`system`) and strip custom role names and metadata.
### TTS ### TTS
Although text message might be hidden from user character. If TTS is enabled it will be read. Although text message might be hidden from user character. If TTS is enabled it will be read until tags are parsed. If message should not be viewed by user, tts will stop.
### Tag Parsing ### Tag Parsing

View File

@@ -45,6 +45,18 @@ func refreshChatDisplay() {
}) })
} }
func stopTTSIfNotForUser(msg *models.RoleMsg) {
viewingAs := cfg.UserRole
if cfg.WriteNextMsgAs != "" {
viewingAs = cfg.WriteNextMsgAs
}
// stop tts if msg is not for user
if cfg.CharSpecificContextEnabled &&
!slices.Contains(msg.KnownTo, viewingAs) && cfg.TTS_ENABLED {
TTSDoneChan <- true
}
}
func colorText() { func colorText() {
text := textView.GetText(false) text := textView.GetText(false)
quoteReplacer := strings.NewReplacer( quoteReplacer := strings.NewReplacer(

View File

@@ -167,7 +167,6 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
} }
func (m *RoleMsg) ToText(i int) string { func (m *RoleMsg) ToText(i int) string {
icon := fmt.Sprintf("(%d)", i)
// Convert content to string representation // Convert content to string representation
var contentStr string var contentStr string
if !m.hasContentParts { if !m.hasContentParts {
@@ -193,7 +192,7 @@ func (m *RoleMsg) ToText(i int) string {
// since icon and content are separated by \n // since icon and content are separated by \n
contentStr, _ = strings.CutPrefix(contentStr, m.Role+":") contentStr, _ = strings.CutPrefix(contentStr, m.Role+":")
// if !strings.HasPrefix(contentStr, m.Role+":") { // if !strings.HasPrefix(contentStr, m.Role+":") {
icon = fmt.Sprintf("(%d) <%s>: ", i, m.Role) icon := fmt.Sprintf("(%d) <%s>: ", i, m.Role)
// } // }
textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, contentStr) textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, contentStr)
return strings.ReplaceAll(textMsg, "\n\n", "\n") return strings.ReplaceAll(textMsg, "\n\n", "\n")

View File

@@ -1,5 +1,5 @@
{ {
"sys_prompt": "This is a chat between Alice, Bob and Carl. Normally all message are public (seen by everyone). But characters also able to make messages intended to specific targets using '@' tag. Usually tag is provided inside of out of character clause: (ooc: @charname@), but will be parsed if put anywhere in the message.\nTO SEND A PRIVATE MESSAGE:\n- Include a recipient tag in this exact format: @CharacterName@\n- The tag can be anywhere in your message\n- Example: \"Don't tell others this secret. (ooc: @Bob@)\"\n- For immersion sake it is better if private messages are given in context of whispering, passing notes, or being alone in some space: Alice: *leans closer to Carl and whispers* \"I forgot to turn off the car, could you watch my bag for a cuple of minutes? (ooc: @Carl@)\"\n- Only the sender and tagged recipients will see that message.\nRECEIVING MESSAGES:\n- You only see messages where you are the sender OR you are tagged in the recipient tag\n- Public messages (without tags) are seen by everyone.\nEXAMPLE FORMAT:\nAlice: \"Public message everyone sees\"\nAlice: \"Private message only for Bob @Bob@\"\n(if Diana joins the conversation, and Alice wants to exclude her) Alice: *Grabs Bob and Carl, and pulls them away* \"Listen boys, let's meet this friday again!\" (ooc: @Bob,Carl@; Diana is not trustworthy)\nWHEN TO USE:\n- Most of the time public messages (no tag) are the best choice. Private messages (with tag) are mostly for the passing secrets or information that is described or infered as private.\n- Game of 20 questions. Guys are putting paper sickers on the forehead with names written on them. So in this case only person who gets the sticker put on them does not see the writting on it.\nBob: *Puts sticker with 'JACK THE RIPPER' written on it, on Alices forehead* (ooc: @Carl).\nCarl: \"Alright, we're ready.\"\nAlice: \"Good. So, am I a fictional character or a real one?\"", "sys_prompt": "This is a chat between Alice, Bob and Carl. Normally all message are public (seen by everyone). But characters also able to make messages intended to specific targets using '@' tag. Usually tag is provided inside of out of character clause: (ooc: @charname@), but will be parsed if put anywhere in the message.\nTO SEND A PRIVATE MESSAGE:\n- Include a recipient tag in this exact format: @CharacterName@\n- The tag can be anywhere in your message\n- Example: \"(ooc: @Bob@) Don't tell others this secret.\"\n- For immersion sake it is better if private messages are given in context of whispering, passing notes, or being alone in some space: Alice: (ooc: @Carl@) *leans closer to Carl and whispers* \"I forgot to turn off the car, could you watch my bag for a cuple of minutes?\"\n- Only the sender and tagged recipients will see that message.\nRECEIVING MESSAGES:\n- You only see messages where you are the sender OR you are tagged in the recipient tag\n- Public messages (without tags) are seen by everyone.\nEXAMPLE FORMAT:\nAlice: \"Public message everyone sees\"\nAlice: (ooc: @Bob@)\n\"Private message only for Bob\"\n(if Diana joins the conversation, and Alice wants to exclude her) Alice: (ooc: @Bob,Carl@; Diana is not trustworthy)\n*Grabs Bob and Carl, and pulls them away* \"Listen boys, let's meet this friday again!\"\nWHEN TO USE:\n- Most of the time public messages (no tag) are the best choice. Private messages (with tag) are mostly for the passing secrets or information that is described or infered as private.\n- Game of 20 questions. Guys are putting paper sickers on the forehead with names written on them. So in this case only person who gets the sticker put on them does not see the writting on it.\nBob: *Puts sticker with 'JACK THE RIPPER' written on it, on Alices forehead* (ooc: @Carl).\nCarl: \"Alright, we're ready.\"\nAlice: \"Good. So, am I a fictional character or a real one?\"",
"role": "Alice", "role": "Alice",
"filepath": "sysprompts/alice_bob_carl.json", "filepath": "sysprompts/alice_bob_carl.json",
"chars": ["Alice", "Bob", "Carl"], "chars": ["Alice", "Bob", "Carl"],

View File

@@ -23,6 +23,15 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
chatList[i] = name chatList[i] = name
i++ i++
} }
// Sort chatList by UpdatedAt field in descending order (most recent first)
for i := 0; i < len(chatList)-1; i++ {
for j := i + 1; j < len(chatList); j++ {
if chatMap[chatList[i]].UpdatedAt.Before(chatMap[chatList[j]].UpdatedAt) {
// Swap chatList[i] and chatList[j]
chatList[i], chatList[j] = chatList[j], chatList[i]
}
}
}
// Add 1 extra row for header // Add 1 extra row for header
rows, cols := len(chatMap)+1, len(actions)+4 // +2 for name, +2 for timestamps rows, cols := len(chatMap)+1, len(actions)+4 // +2 for name, +2 for timestamps
chatActTable := tview.NewTable(). chatActTable := tview.NewTable().

View File

@@ -330,6 +330,7 @@ func memorise(args map[string]string) []byte {
Topic: args["topic"], Topic: args["topic"],
Mind: args["data"], Mind: args["data"],
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
CreatedAt: time.Now(),
} }
if _, err := store.Memorise(memory); err != nil { if _, err := store.Memorise(memory); err != nil {
logger.Error("failed to save memory", "err", err, "memoory", memory) logger.Error("failed to save memory", "err", err, "memoory", memory)

8
tui.go
View File

@@ -1120,12 +1120,8 @@ func init() {
} }
} }
// I need keybind for tts to shut up // I need keybind for tts to shut up
if event.Key() == tcell.KeyCtrlA { if event.Key() == tcell.KeyCtrlA && cfg.TTS_ENABLED {
// textArea.SetText("pressed ctrl+A", true) TTSDoneChan <- true
if cfg.TTS_ENABLED {
// audioStream.TextChan <- chunk
TTSDoneChan <- true
}
} }
if event.Key() == tcell.KeyCtrlW { if event.Key() == tcell.KeyCtrlW {
// INFO: continue bot/text message // INFO: continue bot/text message