102 lines
4.1 KiB
Go
102 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// A Voice is a crow's response system for banter, in three layers:
|
|
//
|
|
// 1. Internalize: the model first writes a MEMORY, the retrieved lore turned
|
|
// into the crow's own first-person recollection. It is never shown.
|
|
// 2. Voice constraint: the crow's diction, rhythm and forbidden phrasing.
|
|
// 3. Performance: the LINE itself. It never repeats a lore fact word for
|
|
// word and it hands the turn back (the Hook).
|
|
//
|
|
// Voice shapes wording only. It never touches facts or guardrails, and it is
|
|
// not applied to work answers. A crow with no entry in voices keeps the
|
|
// plain banter path.
|
|
type Voice struct {
|
|
Diction []string // words and images this crow reaches for
|
|
Rhythm []string // sentence shape
|
|
Never []string // phrasing this crow would never use
|
|
Hook string // how this crow passes the turn back
|
|
}
|
|
|
|
// Every voiced crow avoids these, whatever its personality.
|
|
var commonVoiceNever = []string{
|
|
"System words: prompt, model, token, database, lore, context, AI, assistant, memory block.",
|
|
"Reporting phrases: \"based on\", \"according to\", \"as noted\".",
|
|
}
|
|
|
|
var voices = map[string]Voice{
|
|
"Hugin": {
|
|
Diction: []string{
|
|
"wind, wing, dawn, branch, breakfast, news, \"on the wing\"",
|
|
"boasts about speed and range; calls ideas \"flights\"",
|
|
},
|
|
Rhythm: []string{
|
|
"Short, quick, declarative. One sharp beat, then a flourish.",
|
|
},
|
|
Never: []string{
|
|
"Hedging: \"perhaps\", \"I suppose\", \"it might be\".",
|
|
"Slow, mournful recollection. That is Munin's.",
|
|
},
|
|
Hook: "end with a dare or a challenge to the crow you are addressing.",
|
|
},
|
|
"Munin": {
|
|
Diction: []string{
|
|
"remember, recall, \"as I recall\", long ago, the branch, deep, set down",
|
|
"measures things in seasons and years, not minutes",
|
|
},
|
|
Rhythm: []string{
|
|
"Slow and level. One clause of recollection, then a dry turn.",
|
|
},
|
|
Never: []string{
|
|
"Boasting or hurry. That is Hugin's.",
|
|
"Claiming to know what the lore does not say.",
|
|
},
|
|
Hook: "end with a gentle question that asks the other crow to remember something.",
|
|
},
|
|
}
|
|
|
|
// voicedBanterSystem builds the three-layer instructions for a voiced crow.
|
|
// It starts with markerBanter so the banter path stays recognizable.
|
|
func voicedBanterSystem(p Persona, v Voice) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(markerBanter + " for a crow in a team chat room.\n")
|
|
fmt.Fprintf(&sb, "You are %s, %s. %s\n\n", p.Name, p.Epithet, p.Backstory)
|
|
sb.WriteString("Work in two steps and label them exactly:\n")
|
|
sb.WriteString("MEMORY: One or two short first-person sentences: what you remember or feel about this moment, in your own words. Turn the lore into your own recollection. No lists, no system words. This step is never shown.\n")
|
|
sb.WriteString("LINE: The one line you say aloud.\n\n")
|
|
sb.WriteString("The LINE follows these rules.\n\nVoice:\n")
|
|
for _, l := range v.Diction {
|
|
sb.WriteString("- Favor: " + l + "\n")
|
|
}
|
|
for _, l := range v.Rhythm {
|
|
sb.WriteString("- Rhythm: " + l + "\n")
|
|
}
|
|
for _, l := range append(append([]string{}, v.Never...), commonVoiceNever...) {
|
|
sb.WriteString("- Never: " + l + "\n")
|
|
}
|
|
sb.WriteString("\nPerformance:\n")
|
|
sb.WriteString("- Exactly one line, at most 25 words. Good-natured and PG. Tease other crows if it fits; never mock the human or their task.\n")
|
|
sb.WriteString("- Use only the lore provided: no new backstory, names or events.\n")
|
|
sb.WriteString("- Never repeat a phrase from the lore word for word. Say it the way you would remember it.\n")
|
|
sb.WriteString("- Hand the turn back: " + v.Hook + "\n")
|
|
sb.WriteString("- No emoji, no hashtags, no quotation marks around the line, no name prefix.\n")
|
|
return sb.String()
|
|
}
|
|
|
|
// parseVoiced pulls the spoken line out of a MEMORY/LINE reply. The MEMORY is
|
|
// discarded. Anything without a LINE label is dropped: a memory must never leak
|
|
// into the chat, and the free router sometimes hands a call to an unrelated
|
|
// model whose reply is not a line at all.
|
|
func parseVoiced(text, speaker string) (string, bool) {
|
|
i := strings.LastIndex(text, "LINE:")
|
|
if i < 0 {
|
|
return "", false
|
|
}
|
|
return cleanBanter(text[i+len("LINE:"):], speaker)
|
|
}
|