diff options
| -rw-r--r-- | bot.go | 9 | ||||
| -rw-r--r-- | char-specific-context.md | 162 | ||||
| -rw-r--r-- | sysprompts/alice_bob_carl.json | 2 |
3 files changed, 142 insertions, 31 deletions
@@ -1251,15 +1251,18 @@ func triggerPrivateMessageResponses(msg models.RoleMsg) { } // Check each character in the KnownTo list for _, recipient := range msg.KnownTo { - // Skip if this is the user character or the sender of the message - if recipient == cfg.UserRole || recipient == userCharacter || recipient == msg.Role || recipient == cfg.ToolRole { + if recipient == msg.Role || recipient == cfg.ToolRole { + // weird cases, skip continue } + // Skip if this is the user character or the sender of the message + if recipient == cfg.UserRole || recipient == userCharacter { + return // user in known_to => users turn + } // Trigger the recipient character to respond by simulating a prompt // that indicates it's their turn triggerMsg := recipient + ":\n" // Call chatRound with the trigger message to make the recipient respond - // chatRound(triggerMsg, recipient, tv, false, false) crr := &models.ChatRoundReq{ UserMsg: triggerMsg, Role: recipient, diff --git a/char-specific-context.md b/char-specific-context.md index c1a7bd6..423572b 100644 --- a/char-specific-context.md +++ b/char-specific-context.md @@ -1,39 +1,149 @@ -say we have a chat (system card) with three or more characters: -Alice, Bob and Carl. -the chat uses /completion endpoint (as oposed to /v1/chat/completion of openai) to the same llm on all chars. -Alice needs to pass info to Bob without Carl knowing the content (or perhaps even that communication occured at all). -Issue is that being in the same chat history (chatBody), llm shares context for each char. -Even if message passed through the tool calls, Carl can see a tool call with the arguments. -If we delete tool calls and their responses, then both Bob and Alice would have to re-request that secret info each time it is their turn, which is absurd. +# Character-Specific Context -concept of char specific context: -let every message to have a `KnownTo` field (type []string); -which could be empty (to everyone) or have speicifc names ([]string{"Alice", "Bob"}) -so when that's character turn (which we track in `WriteNextMsgAsCompletionAgent`, then that message is injected at its proper index position (means every message should know it's index?) into chatBody (chat history). +**/completion only feature; won't work with /v1/chat** -indexes are tricky. -what happens if msg is deleted? will every following message decrement their index? so far edit/copy functionality take in consideration position of existing messages in order. -how to avoid two messages with the same index? if Alices letter is send as secret and assigned index: 5. Then Carl's turn we have that secret message excluded, so his action would get also index 5. -Perhaps instead of indexes we should only keep message order by timestamps (time.Time)? +## Overview -so we need to think of some sort of tag that llm could add into the message, to make sure it is to be known by that specific target char, some weird string that would not occur naturally, that we could parse: -__known_to_chars__Alice,Bob__ +Character-Specific Context is a feature that enables private communication between characters in a multi-character chat. When enabled, messages can be tagged with a special marker indicating which characters should "know" about (see) that message. This allows for secret conversations, private information sharing, and roleplaying scenarios where certain characters are not privy to all communications. +(This feature works by filtering the chat history for each character based on the `KnownTo` field associated with each message. Only messages that are intended for a particular character (or are public) are included in that character's view of the conversation.) -for ex. +## How It Works + +### Tagging Messages + +Messages can be tagged with a special string (by default `__known_to_chars__`) followed by a comma-separated list of character names. The tag can appear anywhere in the message content. **After csv of characters tag should be closed with `__` (for regexp to know where it ends).** + +**Example:** +``` Alice: __known_to_chars__Bob__ Can you keep a secret? -Bob: I also have a secret for you Alice __known_to_chars__Alice__ +``` + +**To avoid breaking immersion, it is better to place the tag in (ooc:)** +``` +Alice: (ooc: __known_to_chars__Bob__) Can you keep a secret? +``` + +This message will be visible only to Alice (the sender) and Bob. The tag is parsed by `parseKnownToTag` and the resulting list of character names is stored in the `KnownTo` field of the message (`RoleMsg`). The sender is automatically added to the `KnownTo` list (if not already present) by `processMessageTag`. + +Multiple tags can be used in a single message; all mentioned characters are combined into the `KnownTo` list. + +### Filtering Chat History + +When it's a character's turn to respond, the function `filterMessagesForCharacter` filters the full message list, returning only those messages where: + +- `KnownTo` is empty (message is public), OR +- `KnownTo` contains the character's name. + +System messages (`role == "system"`) are always visible to all characters. + +The filtered history is then used to construct the prompt sent to the LLM. This ensures each character only sees messages they are supposed to know about. + +### Configuration + +Two configuration settings control this feature: + +- `CharSpecificContextEnabled` – boolean; enables or disables the feature globally. +- `CharSpecificContextTag` – string; the tag used to mark private messages. Default is `__known_to_chars__`. + +These are set in `config.toml` (see `config.example.toml` for the default values). + +### Processing Pipeline + +1. **Message Creation** – When a message is added to the chat (by a user or LLM), `processMessageTag` scans its content for the known‑to tag. +2. **Storage** – The parsed `KnownTo` list is stored with the message in the database. +3. **Filtering** – Whenever the chat history is needed (e.g., for an LLM request), `filterMessagesForCharacter` is called with the target character (the one whose turn it is). The filtered list is used for the prompt. +4. **Display** – The TUI also uses the same filtering when showing the conversation for a selected character (see “Writing as…”). + +## Usage Examples + +### Basic Private Message + +Alice wants to tell Bob something without Carl knowing: + +``` +Alice: __known_to_chars__Bob__ Meet me at the library tonight. +``` + +Result: +- Alice (sender) sees the message. +- Bob sees the message. +- Carl does **not** see the message in his chat history. + +### Multi‑recipient Secret + +Alice shares a secret with Bob and Carl, but not David: + +``` +Alice: (ooc: __known_to_chars__Bob,Carl__) The treasure is hidden under the old oak. +``` + +### Public Message + +A message without any tag (or with an empty `KnownTo`) is visible to all characters. + +``` +Alice: Hello everyone! +``` + +### User‑Role Considerations + +The human user can assume any character’s identity via the “Writing as…” feature (`cfg.UserRole` and `cfg.WriteNextMsgAs`). When the user writes as a character, the same filtering rules apply: the user will see only the messages that character would see. + +## Interaction with AutoTurn and WriteNextMsgAsCompletionAgent + +### WriteNextMsgAsCompletionAgent + +This configuration variable determines which character the LLM should respond as. It is used by `filterMessagesForCurrentCharacter` to select the target character for filtering. If `WriteNextMsgAsCompletionAgent` is set, the LLM will reply in the voice of that character, and only messages visible to that character will be included in the prompt. + +### AutoTurn + +Normally llm and user (human) take turns writting messages. With private messages there is an issue, where llm can write a private message that will not be visible for character who user controls, so for a human it would appear that llm did not respond. It is desirable in this case, for llm to answer to itself, larping as target character for that private message. + +When `AutoTurn` is enabled, the system can automatically trigger responses from llm as characters who have received a private message. The logic in `triggerPrivateMessageResponses` checks the `KnownTo` list of the last message and, for each recipient that is not the user (or the sender), queues a chat round for that character. This creates a chain of private replies without user intervention. + +**Example flow:** +1. Alice (llm) sends a private message to Bob (llm) (`KnownTo = ["Alice","Bob"]`). +2. Carl (user) sees nothing. +3. `AutoTurn` detects this and queues a response from Bob. +4. Bob replies (potentially also privately). +5. The conversation continues automatically until public message is made, or Carl (user) was included in `KnownTo`. + + +## Cardmaking with multiple characters + +So far only json format supports multiple characters. +Card example: +``` +{ + "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 '__known_to_chars__{CharName1,CharName2,CharName3}__'.\nFor example:\nAlice:\n\"Hey, Bob. I have a secret for you... (ooc: __known_to_chars__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 + +### Endpoint Compatibility -tag can be anywhere in the message. Sender should be also included in KnownTo, so we should parse sender name and add them to KnownTo. +Character‑specific context relies on the `/completion` endpoint (or other completion‑style endpoints) where the LLM is presented with a raw text prompt containing the entire filtered history. It does **not** work with OpenAI‑style `/v1/chat/completions` endpoints, because those endpoints enforce a fixed role set (`user`/`assistant`/`system`) and strip custom role names and metadata. -also need to consider user case (as in human chatting with llm). User also can assume any char identity to write the message and ideally the same rules should affect user's chars. -user has "Writing as {char}" (vars: persona and cfg.UserRole) -on persona change we should update tui text view to have atual for that character chat history +### Tag Parsing -Again, this is not going to work with openais /v1/chat endpoint since it converts all characters to user/assistant; so it is completion only feature. It also might cause unwanted effects, so we better have an option in config to switch this context editing on/off. +- The tag is case‑sensitive. +- Whitespace around character names is trimmed. +- If the tag appears multiple times, all mentioned characters are combined. +### Database Storage -alternative approach to the tag string would be to have a judge agent to determine after each message what characters should hae access to it. but it means to make an additional call to llm after each msg. +The `KnownTo` field is stored as a JSON array in the database. Existing messages that were created before enabling the feature will have an empty `KnownTo` and thus be visible to all characters. +## Relevant Configuration -need to update character card loader to support multiple characters +```toml +CharSpecificContextEnabled = true +CharSpecificContextTag = "__known_to_chars__" +AutoTurn = false +``` diff --git a/sysprompts/alice_bob_carl.json b/sysprompts/alice_bob_carl.json index 8c7b8e2..b2a0ac5 100644 --- a/sysprompts/alice_bob_carl.json +++ b/sysprompts/alice_bob_carl.json @@ -1,8 +1,6 @@ { "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 '__known_to_chars__{CharName1,CharName2,CharName3}__'.\nFor example:\nAlice:\n\"Hey, Bob. I have a secret for you... (ooc: __known_to_chars__Bob__)\"\nThis message would be seen only by Bob and Alice (sender always sees their own message).", "role": "Alice", - "role2": "Bob", - "role3": "Carl", "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?" |
