b18f608594
Call sites take the fallback text and log the error instead of treating a canned string as success. phraseSource drops the text entirely — its callers hold the passage and read it back better than "вот что я нашла: <passage>". The talk scorer's before-and-after model probe (the #395 workaround) goes; the run now fails only when every case errored, which is the honest "nothing was measured" condition. TalkFixture gets its own schema version so the two fixtures can be versioned apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
2.8 KiB
Go
65 lines
2.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
|
|
"github.com/kami/maven/internal/phraser"
|
|
)
|
|
|
|
// worldPhraser — the naming half of the degradation rule (docs/offload.md), as
|
|
// the query sources see it. Only *phraser.LLMPhraser implements it, so the
|
|
// Stub and every test double stay exactly as they are.
|
|
type worldPhraser interface {
|
|
PhraseWorld(ctx context.Context, utterance string, sources []string) (string, error)
|
|
}
|
|
|
|
// worldGap — what he hears when the question is about the world, the workstation
|
|
// model is the one configured to answer it, and that machine is not answering.
|
|
//
|
|
// It says the true thing. The resident 1.7B is not a worse answer here, it is an
|
|
// invented one: "Война и мир" came back with Левитан as its author, and a
|
|
// question about his meeting came back as a swimming competition in Nottingham.
|
|
// Naming the gap is the rule CLAUDE.md already applies to a sibling service
|
|
// being down.
|
|
const worldGap = "сейчас не могу ответить — большая модель недоступна, а придумывать не хочу."
|
|
|
|
// phraseWorld asks the world model, or reports the gap.
|
|
//
|
|
// The three outcomes come straight from LLMPhraser.PhraseWorld: no workstation
|
|
// configured means the resident model answers as it always has, a workstation
|
|
// that is up answers, and a workstation that is down returns
|
|
// phraser.ErrNoWorldModel. A phraser that has no world seam at all — the Stub,
|
|
// and the doubles in the tests — is the first of those three.
|
|
func (h *reactiveHandler) phraseWorld(ctx context.Context, utterance string, sources []string) (string, error) {
|
|
if h.phraser == nil {
|
|
return "", phraser.ErrNoWorldModel
|
|
}
|
|
if w, ok := h.phraser.(worldPhraser); ok {
|
|
return w.PhraseWorld(ctx, utterance, sources)
|
|
}
|
|
return h.phraser.PhraseQuery(ctx, utterance, sources)
|
|
}
|
|
|
|
// phraseSource asks the world model to answer from a passage someone already
|
|
// fetched — a live search result, a ZIM article, a page he named. It returns ""
|
|
// rather than the gap phrase, because these callers hold something better than a
|
|
// gap: the passage itself, which their own floor reads back to him. Nothing is
|
|
// invented either way, and a real quote beats "не могу сейчас".
|
|
func (h *reactiveHandler) phraseSource(ctx context.Context, name, utterance string, sources []string) string {
|
|
reply, err := h.phraseWorld(ctx, utterance, sources)
|
|
switch {
|
|
case errors.Is(err, phraser.ErrNoWorldModel):
|
|
log.Printf("voice: %s: no world model, reading the source back instead", name)
|
|
return ""
|
|
case err != nil:
|
|
// The resident phraser answers this call with its fallback text and the
|
|
// error together. Drop the text: these callers hold the passage itself
|
|
// and read it back better than "вот что я нашла: <passage>" does.
|
|
log.Printf("voice: %s: phrase: %v", name, err)
|
|
return ""
|
|
}
|
|
return reply
|
|
}
|