the reply seam takes the turn's context (V-638)
voice.Replier.Reply had no context, so llmReplier phrased under context.Background() and the only bound on a reply was phraser.timeout, 60s in deploy. Both call sites already held a context. The stub ignores it: it makes no model call.
This commit is contained in:
@@ -571,7 +571,7 @@ func (h *reactiveHandler) finishClarified(ctx context.Context, dec router.Decisi
|
|||||||
}
|
}
|
||||||
reply := h.applyAction(ctx, dec)
|
reply := h.applyAction(ctx, dec)
|
||||||
if reply == "" {
|
if reply == "" {
|
||||||
reply = h.replier.Reply(dec)
|
reply = h.replier.Reply(ctx, dec)
|
||||||
}
|
}
|
||||||
if reply == "" {
|
if reply == "" {
|
||||||
// Belt: an empty reply here would be a silent drop.
|
// Belt: an empty reply here would be a silent drop.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func newLLMReplier(c phraser.Completer, block func() string) *llmReplier {
|
|||||||
|
|
||||||
// Reply never fails: a clarify, a model error and an unusable generation all
|
// Reply never fails: a clarify, a model error and an unusable generation all
|
||||||
// answer from the stub, which is what keeps a turn from breaking on the model.
|
// answer from the stub, which is what keeps a turn from breaking on the model.
|
||||||
func (r *llmReplier) Reply(d router.Decision) string {
|
func (r *llmReplier) Reply(ctx context.Context, d router.Decision) string {
|
||||||
if d.Clarify {
|
if d.Clarify {
|
||||||
// The deck, not the stub's single sentence: a clarify she cannot turn
|
// The deck, not the stub's single sentence: a clarify she cannot turn
|
||||||
// into a question is the line he hears most often when she misses him,
|
// into a question is the line he hears most often when she misses him,
|
||||||
@@ -39,14 +39,14 @@ func (r *llmReplier) Reply(d router.Decision) string {
|
|||||||
// что ты выпел стакан воды" for "я выпил воды".
|
// что ты выпел стакан воды" for "я выпил воды".
|
||||||
return phraser.FactAck(d.Utterance)
|
return phraser.FactAck(d.Utterance)
|
||||||
}
|
}
|
||||||
out, err := r.p.PhraseReply(context.Background(), d)
|
out, err := r.p.PhraseReply(ctx, d)
|
||||||
if err != nil || out == "" {
|
if err != nil || out == "" {
|
||||||
return r.stub.Reply(d)
|
return r.stub.Reply(ctx, d)
|
||||||
}
|
}
|
||||||
// The persona checks, on the live path (personaguard.go). A reply that
|
// The persona checks, on the live path (personaguard.go). A reply that
|
||||||
// leaks reasoning or calls him "вы" is worse than a flat one.
|
// leaks reasoning or calls him "вы" is worse than a flat one.
|
||||||
if _, ok := guardSpoken("reply", out); !ok {
|
if _, ok := guardSpoken("reply", out); !ok {
|
||||||
return r.stub.Reply(d)
|
return r.stub.Reply(ctx, d)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func (s stubCompleter) Complete(_ context.Context, _ llm.Req) (string, error) {
|
|||||||
|
|
||||||
func TestLLMReplierPassesTheModelReplyThrough(t *testing.T) {
|
func TestLLMReplierPassesTheModelReplyThrough(t *testing.T) {
|
||||||
r := newLLMReplier(stubCompleter{out: `{"response":"записала, кофе закончился","mood":"neutral"}`}, nil)
|
r := newLLMReplier(stubCompleter{out: `{"response":"записала, кофе закончился","mood":"neutral"}`}, nil)
|
||||||
got := r.Reply(router.Decision{Intent: router.IntentNote, Slots: router.Slots{Text: "кофе закончился"}})
|
got := r.Reply(context.Background(), router.Decision{Intent: router.IntentNote, Slots: router.Slots{Text: "кофе закончился"}})
|
||||||
if got != "записала, кофе закончился" {
|
if got != "записала, кофе закончился" {
|
||||||
t.Errorf("got %q, want %q", got, "записала, кофе закончился")
|
t.Errorf("got %q, want %q", got, "записала, кофе закончился")
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ func TestLLMReplierFallsBackToStubOnEmpty(t *testing.T) {
|
|||||||
// the clarify deck rather than the stub's single sentence.
|
// the clarify deck rather than the stub's single sentence.
|
||||||
func TestLLMReplierClarifyReadsTheDeck(t *testing.T) {
|
func TestLLMReplierClarifyReadsTheDeck(t *testing.T) {
|
||||||
r := newLLMReplier(stubCompleter{out: "я всё поняла"}, nil)
|
r := newLLMReplier(stubCompleter{out: "я всё поняла"}, nil)
|
||||||
got := r.Reply(router.Decision{Clarify: true, Utterance: "мгм"})
|
got := r.Reply(context.Background(), router.Decision{Clarify: true, Utterance: "мгм"})
|
||||||
if got == "я всё поняла" {
|
if got == "я всё поняла" {
|
||||||
t.Fatal("a clarify must not be phrased by the model")
|
t.Fatal("a clarify must not be phrased by the model")
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ func TestLLMReplierClarifyReadsTheDeck(t *testing.T) {
|
|||||||
t.Errorf("on clarify: got %q, want %q", got, want)
|
t.Errorf("on clarify: got %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
// Two different misses do not sound identical.
|
// Two different misses do not sound identical.
|
||||||
if same := r.Reply(router.Decision{Clarify: true, Utterance: "а"}); same == got {
|
if same := r.Reply(context.Background(), router.Decision{Clarify: true, Utterance: "а"}); same == got {
|
||||||
t.Log("two utterances hashed to the same line, which is allowed but should be rare")
|
t.Log("two utterances hashed to the same line, which is allowed but should be rare")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,14 +60,14 @@ func TestLLMReplierClarifyReadsTheDeck(t *testing.T) {
|
|||||||
// produce, which is the same claim without pinning one wording.
|
// produce, which is the same claim without pinning one wording.
|
||||||
func assertAck(t *testing.T, r *llmReplier, d router.Decision, key, what string) {
|
func assertAck(t *testing.T, r *llmReplier, d router.Decision, key, what string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if got := r.Reply(d); !phraser.IsAck(key, nil, got) {
|
if got := r.Reply(context.Background(), d); !phraser.IsAck(key, nil, got) {
|
||||||
t.Errorf("on %s: got %q, want a %q line", what, got, key)
|
t.Errorf("on %s: got %q, want a %q line", what, got, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertStub(t *testing.T, r *llmReplier, d router.Decision, what string) {
|
func assertStub(t *testing.T, r *llmReplier, d router.Decision, what string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
got, want := r.Reply(d), voice.NewStubReplier().Reply(d)
|
got, want := r.Reply(context.Background(), d), voice.NewStubReplier().Reply(context.Background(), d)
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("on %s: got %q, want stub %q", what, got, want)
|
t.Errorf("on %s: got %q, want stub %q", what, got, want)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -458,7 +458,7 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
|
|||||||
|
|
||||||
// 9. replier — phrase the reply across the router decision.
|
// 9. replier — phrase the reply across the router decision.
|
||||||
if replyText == "" {
|
if replyText == "" {
|
||||||
replyText = h.replier.Reply(dec)
|
replyText = h.replier.Reply(ctx, dec)
|
||||||
}
|
}
|
||||||
return withNotice(expiredNotice, replyText)
|
return withNotice(expiredNotice, replyText)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
package voice
|
package voice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/kami/maven/internal/phraser"
|
"github.com/kami/maven/internal/phraser"
|
||||||
"github.com/kami/maven/internal/router"
|
"github.com/kami/maven/internal/router"
|
||||||
)
|
)
|
||||||
@@ -38,8 +40,10 @@ import (
|
|||||||
// decision's Intent + Slots + Clarify. The Intent largely names the reply
|
// decision's Intent + Slots + Clarify. The Intent largely names the reply
|
||||||
// shape (act/reminder/fact/note/query/clarify); the Slots carry the
|
// shape (act/reminder/fact/note/query/clarify); the Slots carry the
|
||||||
// specifics that personalise it ("got it: water at 14:00").
|
// specifics that personalise it ("got it: water at 14:00").
|
||||||
|
// The context is the turn's, and it is the only bound an LLM-backed impl has
|
||||||
|
// besides the phraser timeout (V-638). A floor impl ignores it.
|
||||||
type Replier interface {
|
type Replier interface {
|
||||||
Reply(d router.Decision) string
|
Reply(ctx context.Context, d router.Decision) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// StubReplier — the deterministic, no-model floor. Canned per intent;
|
// StubReplier — the deterministic, no-model floor. Canned per intent;
|
||||||
@@ -54,7 +58,8 @@ func NewStubReplier() *StubReplier { return &StubReplier{} }
|
|||||||
|
|
||||||
// Reply dispatches on Intent + Clarify. Each branch is short; the LLM impl
|
// Reply dispatches on Intent + Clarify. Each branch is short; the LLM impl
|
||||||
// will replace this with prompted text and the same dispatch shape.
|
// will replace this with prompted text and the same dispatch shape.
|
||||||
func (s *StubReplier) Reply(d router.Decision) string {
|
// It makes no model call, so the context is unused.
|
||||||
|
func (s *StubReplier) Reply(_ context.Context, d router.Decision) string {
|
||||||
if d.Clarify {
|
if d.Clarify {
|
||||||
return "не совсем поняла — можешь переформулировать?"
|
return "не совсем поняла — можешь переформулировать?"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user