Merge the chat QA: per-reach dialogue session, degrade test (V-45)
This commit is contained in:
@@ -61,7 +61,7 @@ func (h *reactiveHandler) actionChat(ctx context.Context, dec router.Decision) s
|
|||||||
if h.phraser == nil {
|
if h.phraser == nil {
|
||||||
return "поговорили."
|
return "поговорили."
|
||||||
}
|
}
|
||||||
history := h.chatHistory()
|
history := h.chatHistory(ctx)
|
||||||
// The phraser hands back its own fallback text alongside the error, so the
|
// The phraser hands back its own fallback text alongside the error, so the
|
||||||
// turn survives a dead server and the failure still reaches the log.
|
// turn survives a dead server and the failure still reaches the log.
|
||||||
reply, err := h.phraser.PhraseChat(ctx, dec.Utterance, history)
|
reply, err := h.phraser.PhraseChat(ctx, dec.Utterance, history)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func (h *reactiveHandler) queryTasks(ctx context.Context, t *queryTurn) (string,
|
|||||||
for _, r := range spoken {
|
for _, r := range spoken {
|
||||||
cands = append(cands, dialogue.Candidate{Kind: "task", Ref: r.ID, Label: r.Text})
|
cands = append(cands, dialogue.Candidate{Kind: "task", Ref: r.ID, Label: r.Text})
|
||||||
}
|
}
|
||||||
h.offerCandidates(cands)
|
h.offerCandidates(ctx, cands)
|
||||||
return tasks.FormatRU(ranked), true
|
return tasks.FormatRU(ranked), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/llm"
|
||||||
|
"github.com/kami/maven/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The chat path must answer when llama-server is down (Vikunja #45 step 4).
|
||||||
|
// Both halves of a turn call the model — the router and the replier — and each
|
||||||
|
// has its own floor: the cascade falls to the classifier, the replier falls to
|
||||||
|
// the stub. This wires a client at a closed port so both floors are exercised
|
||||||
|
// by a dial error rather than by a stubbed error value.
|
||||||
|
func TestChatAnswersWithNoLlamaServer(t *testing.T) {
|
||||||
|
h, _, _ := newClarifyHandler(t)
|
||||||
|
dead := llm.New("http://127.0.0.1:1", 500*time.Millisecond)
|
||||||
|
emb := router.NewHashEmbedder(1024)
|
||||||
|
h.recall.embedder = emb
|
||||||
|
h.router = buildRouter(emb, h.matcher, 0.55, pickLLMRouter(true, dead))
|
||||||
|
h.replier = newLLMReplier(dead, nil)
|
||||||
|
|
||||||
|
ctx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web"))
|
||||||
|
for _, utt := range []string{
|
||||||
|
"привет",
|
||||||
|
"запиши что кофе закончился",
|
||||||
|
"что у меня сегодня",
|
||||||
|
} {
|
||||||
|
reply := h.handleText(ctx, "web", utt)
|
||||||
|
if reply == "" {
|
||||||
|
t.Errorf("%q answered with nothing; a dead model must degrade to the stub", utt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// daemonAPI.Chat reports an error only when the voice path was never wired.
|
||||||
|
// A turn that reaches handleText always carries text, which is what keeps
|
||||||
|
// mavweb's /api/chat off its error branch when the model is down.
|
||||||
|
func TestChatAPIErrsOnlyWhenUnwired(t *testing.T) {
|
||||||
|
d := &daemonAPI{}
|
||||||
|
if _, err := d.Chat(context.Background(), "web", "привет"); err == nil {
|
||||||
|
t.Fatal("an unwired daemon must say so")
|
||||||
|
}
|
||||||
|
d.chatFn = func(context.Context, string, string) string { return "" }
|
||||||
|
if _, err := d.Chat(context.Background(), "web", "привет"); err != nil {
|
||||||
|
t.Fatalf("a wired daemon must not error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -305,9 +305,9 @@ func (h *reactiveHandler) reaskOrGiveUp(ctx context.Context, q *dialogue.Pending
|
|||||||
func (h *reactiveHandler) finishClarified(ctx context.Context, dec router.Decision) string {
|
func (h *reactiveHandler) finishClarified(ctx context.Context, dec router.Decision) string {
|
||||||
if h.dialogueSessions != nil {
|
if h.dialogueSessions != nil {
|
||||||
now := h.now()
|
now := h.now()
|
||||||
prev := h.dialogueSessions.Get(voiceDialogueID, now)
|
prev := h.dialogueSessions.Get(dialogueIDOf(ctx), now)
|
||||||
dec = followUpMerge(prev, dec, now)
|
dec = followUpMerge(prev, dec, now)
|
||||||
h.rememberTurn(prev, dec, now)
|
h.rememberTurn(ctx, prev, dec, now)
|
||||||
}
|
}
|
||||||
reply := h.applyAction(ctx, dec)
|
reply := h.applyAction(ctx, dec)
|
||||||
if reply == "" {
|
if reply == "" {
|
||||||
@@ -323,7 +323,7 @@ func (h *reactiveHandler) finishClarified(ctx context.Context, dec router.Decisi
|
|||||||
// rememberTurn stores this turn as the dialogue session the next follow-up
|
// rememberTurn stores this turn as the dialogue session the next follow-up
|
||||||
// inherits from, carrying up to 4 prior turns of history for anaphora. Capped so
|
// inherits from, carrying up to 4 prior turns of history for anaphora. Capped so
|
||||||
// one long conversation can't grow the session unboundedly.
|
// one long conversation can't grow the session unboundedly.
|
||||||
func (h *reactiveHandler) rememberTurn(prev *dialogue.Session, dec router.Decision, now time.Time) {
|
func (h *reactiveHandler) rememberTurn(ctx context.Context, prev *dialogue.Session, dec router.Decision, now time.Time) {
|
||||||
var history []dialogue.Turn
|
var history []dialogue.Turn
|
||||||
if prev != nil {
|
if prev != nil {
|
||||||
history = append(history, dialogue.Turn{
|
history = append(history, dialogue.Turn{
|
||||||
@@ -359,7 +359,7 @@ func (h *reactiveHandler) rememberTurn(prev *dialogue.Session, dec router.Decisi
|
|||||||
if !dec.Continued && (dec.Intent == router.IntentSystem || dec.Intent == router.IntentQuery) {
|
if !dec.Continued && (dec.Intent == router.IntentSystem || dec.Intent == router.IntentQuery) {
|
||||||
slots.Text = dec.Utterance
|
slots.Text = dec.Utterance
|
||||||
}
|
}
|
||||||
h.dialogueSessions.Put(voiceDialogueID, &dialogue.Session{
|
h.dialogueSessions.Put(dialogueIDOf(ctx), &dialogue.Session{
|
||||||
Intent: dialogue.Intent(dec.Intent),
|
Intent: dialogue.Intent(dec.Intent),
|
||||||
Slots: slots,
|
Slots: slots,
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -149,12 +150,13 @@ func TestRememberTurnRefreshesTheTopic(t *testing.T) {
|
|||||||
now: func() time.Time { return contNow },
|
now: func() time.Time { return contNow },
|
||||||
dialogueSessions: dialogue.NewSessionStore(2 * time.Minute),
|
dialogueSessions: dialogue.NewSessionStore(2 * time.Minute),
|
||||||
}
|
}
|
||||||
h.rememberTurn(nil, router.Decision{
|
ctx := context.Background()
|
||||||
|
h.rememberTurn(ctx, nil, router.Decision{
|
||||||
Intent: router.IntentQuery, Utterance: "во сколько у меня встреча",
|
Intent: router.IntentQuery, Utterance: "во сколько у меня встреча",
|
||||||
}, contNow)
|
}, contNow)
|
||||||
// The second turn arrives with the first turn's Text already merged in.
|
// The second turn arrives with the first turn's Text already merged in.
|
||||||
prev := h.dialogueSessions.Get(voiceDialogueID, contNow)
|
prev := h.dialogueSessions.Get(voiceDialogueID, contNow)
|
||||||
h.rememberTurn(prev, router.Decision{
|
h.rememberTurn(ctx, prev, router.Decision{
|
||||||
Intent: router.IntentQuery,
|
Intent: router.IntentQuery,
|
||||||
Utterance: "какие у меня планы",
|
Utterance: "какие у меня планы",
|
||||||
Slots: router.Slots{Text: "во сколько у меня встреча"},
|
Slots: router.Slots{Text: "во сколько у меня встреча"},
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The list she read at the mic is not the list a browser is looking at
|
||||||
|
// (Vikunja #45 step 3). The clarify store was keyed per reach in #466; the
|
||||||
|
// dialogue session was still one slot for the box, so "второй" typed on the web
|
||||||
|
// closed the second task she had recited out loud.
|
||||||
|
func TestCandidatesDoNotCrossReaches(t *testing.T) {
|
||||||
|
h, st, _ := newClarifyHandler(t)
|
||||||
|
voiceCtx := withDialogueID(context.Background(), dialogueIDFor(sourceVoice, ""))
|
||||||
|
webCtx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web"))
|
||||||
|
|
||||||
|
ids := seedTasks(t, st, "купить хлеб", "позвонить маме")
|
||||||
|
putCandidates(h, voiceCtx, ids, "купить хлеб", "позвонить маме")
|
||||||
|
|
||||||
|
if reply, handled := h.resolveCandidate(webCtx, "первую сделал", sourceText); handled {
|
||||||
|
t.Fatalf("a web turn picked from the list she read aloud: %q", reply)
|
||||||
|
}
|
||||||
|
live, err := st.ListTasks(context.Background(), "live")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list tasks: %v", err)
|
||||||
|
}
|
||||||
|
if len(live) != 2 {
|
||||||
|
t.Fatalf("%d tasks live, want 2 — the web turn moved one", len(live))
|
||||||
|
}
|
||||||
|
// The reach that was offered the list still owns it.
|
||||||
|
if _, handled := h.resolveCandidate(voiceCtx, "первую сделал", sourceVoice); !handled {
|
||||||
|
t.Fatal("the mic lost its own list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A selection writes a fact, so the fact must name the reach the words arrived
|
||||||
|
// on. It said "tap:voice" for a typed turn.
|
||||||
|
func TestCandidateProvenanceFollowsTheReach(t *testing.T) {
|
||||||
|
h, st, _ := newClarifyHandler(t)
|
||||||
|
ctx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web"))
|
||||||
|
ids := seedTasks(t, st, "купить хлеб")
|
||||||
|
putCandidates(h, ctx, ids, "купить хлеб")
|
||||||
|
|
||||||
|
if _, handled := h.resolveCandidate(ctx, "первую сделал", sourceText); !handled {
|
||||||
|
t.Fatal("the pick was not acted on")
|
||||||
|
}
|
||||||
|
done, err := st.ListTasks(context.Background(), "done")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list tasks: %v", err)
|
||||||
|
}
|
||||||
|
if len(done) != 1 {
|
||||||
|
t.Fatalf("%d tasks done, want 1", len(done))
|
||||||
|
}
|
||||||
|
if by := done[0].ResolvedBy; by != string(sourceText) {
|
||||||
|
t.Errorf("resolved_by = %q, want %q", by, sourceText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anaphora is per reach too: an ellipsis typed on the web must not continue the
|
||||||
|
// question he asked at the mic. Both surfaces stay usable at once, which is the
|
||||||
|
// case a single-owner box actually hits — a phone open while he talks.
|
||||||
|
func TestAnaphoraDoesNotCrossReaches(t *testing.T) {
|
||||||
|
h, _, _ := newClarifyHandler(t)
|
||||||
|
voiceCtx := withDialogueID(context.Background(), dialogueIDFor(sourceVoice, ""))
|
||||||
|
webCtx := withDialogueID(context.Background(), dialogueIDFor(sourceText, "web"))
|
||||||
|
now := h.now()
|
||||||
|
|
||||||
|
h.rememberTurn(voiceCtx, nil, router.Decision{
|
||||||
|
Intent: router.IntentQuery, Utterance: "во сколько у меня встреча",
|
||||||
|
}, now)
|
||||||
|
|
||||||
|
if sess := h.dialogueSessions.Get(dialogueIDOf(webCtx), now); sess != nil {
|
||||||
|
t.Fatalf("the web reach inherited the mic's turn: %+v", sess)
|
||||||
|
}
|
||||||
|
sess := h.dialogueSessions.Get(dialogueIDOf(voiceCtx), now)
|
||||||
|
if sess == nil || !strings.Contains(sess.Slots.Text, "встреча") {
|
||||||
|
t.Fatalf("the mic lost its own turn: %+v", sess)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"github.com/kami/maven/internal/router"
|
"github.com/kami/maven/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
// voiceDialogueID — the dialogue-session key for the microphone, and the
|
// voiceDialogueID — the dialogue-session and clarify key for the microphone.
|
||||||
// clarify key for it too. This is a single-user box (ponytail), so one slot
|
// This is a single-user box (ponytail), so one slot per reach suffices; a
|
||||||
// suffices; a second speaker would need per-speaker ids, which waits on
|
// second speaker would need per-speaker ids, which waits on voice-print
|
||||||
// voice-print attribution (see PROGRESS multi-user deferral).
|
// attribution (see PROGRESS multi-user deferral).
|
||||||
const voiceDialogueID = "voice"
|
const voiceDialogueID = "voice"
|
||||||
|
|
||||||
// textDialogueID — the clarify key for a text turn that named no conversation.
|
// textDialogueID — the clarify key for a text turn that named no conversation.
|
||||||
|
|||||||
@@ -97,20 +97,20 @@ func parseCandidateVerb(text string) (status, say string, ok bool) {
|
|||||||
// offerCandidates records the list she just read, so his next words can pick
|
// offerCandidates records the list she just read, so his next words can pick
|
||||||
// from it. Best effort: no session store, or a session that expired between the
|
// from it. Best effort: no session store, or a session that expired between the
|
||||||
// question and the answer, means the words route normally.
|
// question and the answer, means the words route normally.
|
||||||
func (h *reactiveHandler) offerCandidates(cands []dialogue.Candidate) {
|
func (h *reactiveHandler) offerCandidates(ctx context.Context, cands []dialogue.Candidate) {
|
||||||
if h.dialogueSessions == nil || len(cands) == 0 {
|
if h.dialogueSessions == nil || len(cands) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.dialogueSessions.SetCandidates(voiceDialogueID, h.now(), cands)
|
h.dialogueSessions.SetCandidates(dialogueIDOf(ctx), h.now(), cands)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveCandidate handles "второй", "первую сделал", "последнюю убери" against
|
// resolveCandidate handles "второй", "первую сделал", "последнюю убери" against
|
||||||
// the list she just read.
|
// the list she just read.
|
||||||
func (h *reactiveHandler) resolveCandidate(ctx context.Context, text string) (string, bool) {
|
func (h *reactiveHandler) resolveCandidate(ctx context.Context, text string, src turnSource) (string, bool) {
|
||||||
if h.dialogueSessions == nil {
|
if h.dialogueSessions == nil {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
sess := h.dialogueSessions.Get(voiceDialogueID, h.now())
|
sess := h.dialogueSessions.Get(dialogueIDOf(ctx), h.now())
|
||||||
if sess == nil || len(sess.Candidates) == 0 {
|
if sess == nil || len(sess.Candidates) == 0 {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
@@ -133,13 +133,13 @@ func (h *reactiveHandler) resolveCandidate(ctx context.Context, text string) (st
|
|||||||
// a sentence, and the second half is the next turn.
|
// a sentence, and the second half is the next turn.
|
||||||
return pick.Label, true
|
return pick.Label, true
|
||||||
}
|
}
|
||||||
if err := h.api.SetTaskStatus(ctx, pick.Ref, status, h.now(), "tap:voice"); err != nil {
|
if err := h.api.SetTaskStatus(ctx, pick.Ref, status, h.now(), string(src)); err != nil {
|
||||||
log.Printf("voice: candidate %d → %s: %v", pick.Ref, status, err)
|
log.Printf("voice: candidate %d → %s: %v", pick.Ref, status, err)
|
||||||
return "не получилось изменить задачу.", true
|
return "не получилось изменить задачу.", true
|
||||||
}
|
}
|
||||||
// Spent: the list she read is no longer the list, and a second ordinal
|
// Spent: the list she read is no longer the list, and a second ordinal
|
||||||
// against it would close the wrong task.
|
// against it would close the wrong task.
|
||||||
h.dialogueSessions.SetCandidates(voiceDialogueID, h.now(), nil)
|
h.dialogueSessions.SetCandidates(dialogueIDOf(ctx), h.now(), nil)
|
||||||
log.Printf("voice: candidate %d (%q) → %s", pick.Ref, pick.Label, status)
|
log.Printf("voice: candidate %d (%q) → %s", pick.Ref, pick.Label, status)
|
||||||
return say + ": " + pick.Label, true
|
return say + ": " + pick.Label, true
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-12
@@ -38,7 +38,7 @@ func TestParseOrdinalReadsThePosition(t *testing.T) {
|
|||||||
|
|
||||||
func TestOrdinalPassesWithNothingOffered(t *testing.T) {
|
func TestOrdinalPassesWithNothingOffered(t *testing.T) {
|
||||||
h, _, _ := newClarifyHandler(t)
|
h, _, _ := newClarifyHandler(t)
|
||||||
if _, handled := h.resolveCandidate(context.Background(), "второй"); handled {
|
if _, handled := h.resolveCandidate(context.Background(), "второй", sourceVoice); handled {
|
||||||
t.Error("an ordinal with no list behind it was claimed")
|
t.Error("an ordinal with no list behind it was claimed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,14 +47,14 @@ func TestOrdinalReadsBackWithoutAVerb(t *testing.T) {
|
|||||||
h, st, _ := newClarifyHandler(t)
|
h, st, _ := newClarifyHandler(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ids := seedTasks(t, st, "купить хлеб", "позвонить маме")
|
ids := seedTasks(t, st, "купить хлеб", "позвонить маме")
|
||||||
putCandidates(h, ids, "купить хлеб", "позвонить маме")
|
putCandidates(h, context.Background(), ids, "купить хлеб", "позвонить маме")
|
||||||
|
|
||||||
reply, handled := h.resolveCandidate(ctx, "второй")
|
reply, handled := h.resolveCandidate(ctx, "второй", sourceVoice)
|
||||||
if !handled || !strings.Contains(reply, "позвонить маме") {
|
if !handled || !strings.Contains(reply, "позвонить маме") {
|
||||||
t.Fatalf("a bare ordinal did not read the task back: %q handled=%v", reply, handled)
|
t.Fatalf("a bare ordinal did not read the task back: %q handled=%v", reply, handled)
|
||||||
}
|
}
|
||||||
// Still live: naming one is often the first half of a sentence.
|
// Still live: naming one is often the first half of a sentence.
|
||||||
if _, handled := h.resolveCandidate(ctx, "первый"); !handled {
|
if _, handled := h.resolveCandidate(ctx, "первый", sourceVoice); !handled {
|
||||||
t.Error("the list was spent by a read-back")
|
t.Error("the list was spent by a read-back")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,9 +63,9 @@ func TestOrdinalWithAVerbMovesTheTask(t *testing.T) {
|
|||||||
h, st, _ := newClarifyHandler(t)
|
h, st, _ := newClarifyHandler(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ids := seedTasks(t, st, "купить хлеб", "позвонить маме")
|
ids := seedTasks(t, st, "купить хлеб", "позвонить маме")
|
||||||
putCandidates(h, ids, "купить хлеб", "позвонить маме")
|
putCandidates(h, context.Background(), ids, "купить хлеб", "позвонить маме")
|
||||||
|
|
||||||
reply, handled := h.resolveCandidate(ctx, "первую сделал")
|
reply, handled := h.resolveCandidate(ctx, "первую сделал", sourceVoice)
|
||||||
if !handled || !strings.Contains(reply, "купить хлеб") {
|
if !handled || !strings.Contains(reply, "купить хлеб") {
|
||||||
t.Fatalf("the pick was not acted on: %q handled=%v", reply, handled)
|
t.Fatalf("the pick was not acted on: %q handled=%v", reply, handled)
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ func TestOrdinalWithAVerbMovesTheTask(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Spent: a second ordinal against a list that no longer holds would close
|
// Spent: a second ordinal against a list that no longer holds would close
|
||||||
// the wrong task.
|
// the wrong task.
|
||||||
if _, handled := h.resolveCandidate(ctx, "второй"); handled {
|
if _, handled := h.resolveCandidate(ctx, "второй", sourceVoice); handled {
|
||||||
t.Error("the list survived the pick it was spent on")
|
t.Error("the list survived the pick it was spent on")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,9 +88,9 @@ func TestOrdinalWithAVerbMovesTheTask(t *testing.T) {
|
|||||||
func TestOrdinalPastTheEndSaysHowMany(t *testing.T) {
|
func TestOrdinalPastTheEndSaysHowMany(t *testing.T) {
|
||||||
h, st, _ := newClarifyHandler(t)
|
h, st, _ := newClarifyHandler(t)
|
||||||
ids := seedTasks(t, st, "купить хлеб")
|
ids := seedTasks(t, st, "купить хлеб")
|
||||||
putCandidates(h, ids, "купить хлеб")
|
putCandidates(h, context.Background(), ids, "купить хлеб")
|
||||||
|
|
||||||
reply, handled := h.resolveCandidate(context.Background(), "третий")
|
reply, handled := h.resolveCandidate(context.Background(), "третий", sourceVoice)
|
||||||
if !handled || !strings.Contains(reply, "1") {
|
if !handled || !strings.Contains(reply, "1") {
|
||||||
t.Fatalf("a position she never read was not answered: %q handled=%v", reply, handled)
|
t.Fatalf("a position she never read was not answered: %q handled=%v", reply, handled)
|
||||||
}
|
}
|
||||||
@@ -118,11 +118,13 @@ func seedTasks(t *testing.T, st *store.Store, texts ...string) []int64 {
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
func putCandidates(h *reactiveHandler, ids []int64, labels ...string) {
|
// putCandidates binds a list to the reach the ctx names, the way queryTasks
|
||||||
|
// does when she recites one.
|
||||||
|
func putCandidates(h *reactiveHandler, ctx context.Context, ids []int64, labels ...string) {
|
||||||
cands := make([]dialogue.Candidate, 0, len(ids))
|
cands := make([]dialogue.Candidate, 0, len(ids))
|
||||||
for i, id := range ids {
|
for i, id := range ids {
|
||||||
cands = append(cands, dialogue.Candidate{Kind: "task", Ref: id, Label: labels[i]})
|
cands = append(cands, dialogue.Candidate{Kind: "task", Ref: id, Label: labels[i]})
|
||||||
}
|
}
|
||||||
h.dialogueSessions.Put(voiceDialogueID, &dialogue.Session{Timestamp: h.now()})
|
h.dialogueSessions.Put(dialogueIDOf(ctx), &dialogue.Session{Timestamp: h.now()})
|
||||||
h.offerCandidates(cands)
|
h.offerCandidates(ctx, cands)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -132,8 +132,9 @@ type reactiveHandler struct {
|
|||||||
// The production dateparser will replace StubDateTimeParser here too.
|
// The production dateparser will replace StubDateTimeParser here too.
|
||||||
timeParser router.DateTimeParser
|
timeParser router.DateTimeParser
|
||||||
|
|
||||||
// dialogueSessions carries slots across turns for follow-ups (single-user
|
// dialogueSessions carries slots across turns for follow-ups. Keyed by the
|
||||||
// box → one session slot, keyed voiceDialogueID). nil ⇒ no carry-over.
|
// reach the turn arrived on (dialogueIDOf), like the clarify store: one
|
||||||
|
// slot per reach, not one for the box. nil ⇒ no carry-over.
|
||||||
dialogueSessions *dialogue.SessionStore
|
dialogueSessions *dialogue.SessionStore
|
||||||
|
|
||||||
// clarifyStore parks the request behind an open question she asked (see
|
// clarifyStore parks the request behind an open question she asked (see
|
||||||
@@ -313,7 +314,7 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
|
|||||||
// just read (ordinal.go). Before routing, and only when a list is actually
|
// just read (ordinal.go). Before routing, and only when a list is actually
|
||||||
// bound to the session: with nothing offered, "второй" is an ordinary word
|
// bound to the session: with nothing offered, "второй" is an ordinary word
|
||||||
// and keeps routing.
|
// and keeps routing.
|
||||||
if reply, handled := h.resolveCandidate(ctx, text); handled {
|
if reply, handled := h.resolveCandidate(ctx, text, src); handled {
|
||||||
return withNotice(expiredNotice, reply)
|
return withNotice(expiredNotice, reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +330,7 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
|
|||||||
)
|
)
|
||||||
now := h.now()
|
now := h.now()
|
||||||
if h.dialogueSessions != nil {
|
if h.dialogueSessions != nil {
|
||||||
prev = h.dialogueSessions.Get(voiceDialogueID, now)
|
prev = h.dialogueSessions.Get(dialogueIDOf(ctx), now)
|
||||||
}
|
}
|
||||||
cont := false
|
cont := false
|
||||||
if dec, cont = continuationDecision(prev, text, now); cont {
|
if dec, cont = continuationDecision(prev, text, now); cont {
|
||||||
@@ -361,7 +362,7 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
|
|||||||
dec = followUpMerge(prev, dec, now)
|
dec = followUpMerge(prev, dec, now)
|
||||||
}
|
}
|
||||||
if !dec.Clarify {
|
if !dec.Clarify {
|
||||||
h.rememberTurn(prev, dec, now)
|
h.rememberTurn(ctx, prev, dec, now)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,12 +493,12 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision)
|
|||||||
// chatHistory collects dialogue turns from the session store for the current
|
// chatHistory collects dialogue turns from the session store for the current
|
||||||
// conversation. Returns prior user utterances (newest last) up to a depth of
|
// conversation. Returns prior user utterances (newest last) up to a depth of
|
||||||
// 4 turns. Returns nil when there's no session or no history.
|
// 4 turns. Returns nil when there's no session or no history.
|
||||||
func (h *reactiveHandler) chatHistory() []dialogue.Turn {
|
func (h *reactiveHandler) chatHistory(ctx context.Context) []dialogue.Turn {
|
||||||
if h.dialogueSessions == nil {
|
if h.dialogueSessions == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
now := h.now()
|
now := h.now()
|
||||||
prev := h.dialogueSessions.Get(voiceDialogueID, now)
|
prev := h.dialogueSessions.Get(dialogueIDOf(ctx), now)
|
||||||
if prev == nil {
|
if prev == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user