mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-18 01:42:42 +00:00
adventure: author Pete's dispatches in his voice with the local LLM
emitFact now runs the final fact through authorDispatch, which asks the local Ollama model for a warm-reporter headline and lede and ships them on the Fact. Pete guards and publishes them, falling back to its own templates on anything it rejects — so authoring is best-effort by design: LLM off, a timeout, a malformed generation, or an over-length pair all return an empty prose pair and Pete templates the fact. The names allowed in the prose are the fact's Actors, built from the post-opt-out subject/opponent, so what the model may say and what Pete's guard permits are the same list. Synchronous like the holdem tip rewrite, but on a tight 15s budget: news facts are infrequent and a template now beats a voiced dispatch late. With no route for Pete to call back into this box, the voice lives in the prompt here rather than in a Pete-owned inference endpoint.
This commit is contained in:
81
internal/plugin/pete_dispatch_voice_test.go
Normal file
81
internal/plugin/pete_dispatch_voice_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gogobee/internal/peteclient"
|
||||
)
|
||||
|
||||
func TestParseDispatch(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
raw string
|
||||
wantOK bool
|
||||
wantHeadPre string
|
||||
}{
|
||||
{
|
||||
name: "clean json",
|
||||
raw: `{"headline": "Josie cleared the Ossuary.", "lede": "Alone, no less."}`,
|
||||
wantOK: true,
|
||||
wantHeadPre: "Josie cleared",
|
||||
},
|
||||
{
|
||||
name: "wrapped in prose and fences",
|
||||
raw: "Sure! Here you go:\n```json\n{\"headline\":\"A win.\",\"lede\":\"Nice one.\"}\n```",
|
||||
wantOK: true,
|
||||
wantHeadPre: "A win.",
|
||||
},
|
||||
{
|
||||
name: "think block stripped",
|
||||
raw: "<think>let me consider the tone</think>\n{\"headline\":\"Held the line.\",\"lede\":\"Proud of you all.\"}",
|
||||
wantOK: true,
|
||||
wantHeadPre: "Held the line.",
|
||||
},
|
||||
{name: "no json", raw: "I could not write that.", wantOK: false},
|
||||
{name: "empty headline", raw: `{"headline":"","lede":"body"}`, wantOK: false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
h, l, ok := parseDispatch(c.raw)
|
||||
if ok != c.wantOK {
|
||||
t.Fatalf("ok = %v, want %v (h=%q l=%q)", ok, c.wantOK, h, l)
|
||||
}
|
||||
if ok && !strings.HasPrefix(h, c.wantHeadPre) {
|
||||
t.Errorf("headline = %q, want prefix %q", h, c.wantHeadPre)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildDispatchPrompt pins the two properties the prose-guard depends on:
|
||||
// the allowed names are stated verbatim, and only the set facts appear (no empty
|
||||
// labels for the model to fill in with invention).
|
||||
func TestBuildDispatchPrompt(t *testing.T) {
|
||||
f := peteclient.Fact{
|
||||
EventType: "boss_kill", Subject: "Josie", Boss: "the Bone Warden",
|
||||
Zone: "the Ossuary", Level: 14, Actors: []string{"Josie"},
|
||||
}
|
||||
p := buildDispatchPrompt(f)
|
||||
|
||||
if !strings.Contains(p, "ONLY these adventurer names, exactly as written: Josie") {
|
||||
t.Errorf("prompt does not constrain names to Actors:\n%s", p)
|
||||
}
|
||||
if !strings.Contains(p, "the Bone Warden") || !strings.Contains(p, "the Ossuary") {
|
||||
t.Error("prompt dropped a supplied fact")
|
||||
}
|
||||
// Unset fields must not appear as empty labels.
|
||||
if strings.Contains(p, "region:") || strings.Contains(p, "milestone:") {
|
||||
t.Errorf("prompt lists an unset fact:\n%s", p)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildDispatchPromptRealmEvent: a realm-level fact with no named adventurer
|
||||
// still produces a usable prompt that tells the model there is no name to use.
|
||||
func TestBuildDispatchPromptRealmEvent(t *testing.T) {
|
||||
f := peteclient.Fact{EventType: "siege_start", Boss: "the Horde", Stakes: "the whole town"}
|
||||
p := buildDispatchPrompt(f)
|
||||
if !strings.Contains(p, "no named adventurer") {
|
||||
t.Errorf("realm event prompt missing the no-name note:\n%s", p)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user