fe489dff6d
attentionq.go, repair.go and internal/router/complaint.go carry the last
hand-written Russian patterns of the V-522 sweep, and they live on task/467.
internal/lexicon, internal/morph and cmd/mavend/topics.go live here. One of
the two had to move.
Four conflicts, and one of them is a real collision rather than a mechanical
one. Both branches wrote the narrative stage 0 rule. This side had
NarrativeQueryGrammars, plural, with the rest-of-day rule beside it and the
verb alternation built from the lexicon; task/467 had NarrativeQueryGrammar,
singular, which extracts the topic into Slots.Text, refuses a bare "расскажи",
and excludes the shapes that are chat ("расскажи о себе", "историю на ночь").
Resolved by keeping this side's container and this side's lexicon-built
pattern, and taking every behaviour only the other side had: the topic slot,
the empty-topic refusal, chatNarrativeTopics, and its wiring position after
TaskCaptureGrammar so "запиши" still beats "расскажи".
The rest: queryFeeds keeps task/467's conditional claim (V-474 supersedes the
unconditional one), rank.go keeps Spoken and drops pluralTasksRU because
say.CountWord is the one copy of Russian count agreement, and vendor/ was
re-vendored — the merged modules.txt claimed replaces for nexus and praxis
that neither go.mod has.
Routing fixture 58/82, unchanged from both sides.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
3.8 KiB
Go
101 lines
3.8 KiB
Go
package main
|
||
|
||
import (
|
||
"context"
|
||
"log"
|
||
|
||
"github.com/kami/maven/internal/dialogue"
|
||
"github.com/kami/maven/internal/ipc"
|
||
"github.com/kami/maven/internal/phraser"
|
||
"github.com/kami/maven/internal/router"
|
||
"github.com/kami/maven/internal/store"
|
||
"github.com/kami/maven/internal/tasks"
|
||
)
|
||
|
||
// Task capture on the voice/chat path (Vikunja #130).
|
||
//
|
||
// Two halves, both deliberately small:
|
||
//
|
||
// - captureTaskFromNote runs at the top of actionNote. An utterance that
|
||
// explicitly files a task ("добавь в задачи купить молоко") goes to the task
|
||
// store instead of the note store. Anything without an explicit marker is
|
||
// still a note — see router.ParseTaskCapture for why "надо бы поспать" must
|
||
// not become a task.
|
||
// - queryTasks is a query source that reads the list back.
|
||
//
|
||
// Nothing here speaks unprompted. Tasks are answered when asked about; no tick
|
||
// rule reads the table.
|
||
|
||
// captureTaskFromNote claims the turn when the utterance explicitly files a
|
||
// task, returning the reply. ("", false) hands the turn back to the note path.
|
||
func (h *reactiveHandler) captureTaskFromNote(ctx context.Context, dec router.Decision) (string, bool) {
|
||
cap, ok := router.ParseTaskCapture(dec.Utterance)
|
||
if !ok {
|
||
return "", false
|
||
}
|
||
resp, err := h.api.CaptureTask(ctx, ipc.CaptureTaskReq{
|
||
Text: cap.Text,
|
||
Source: "tap:voice",
|
||
Status: store.TaskOpen, // he stated it himself — not a candidate
|
||
Weight: cap.Weight, // 0 unless he said "срочно" / "важно"
|
||
Ts: h.now(),
|
||
})
|
||
if err != nil {
|
||
log.Printf("voice: capture task: %v", err)
|
||
return phraser.Ack(phraser.FailTask, nil), true
|
||
}
|
||
if resp.Promoted {
|
||
// It was a candidate Maven derived from something she read, and he has
|
||
// now said it himself. Saying "уже в списке" here would be answering a
|
||
// confirmation with a shrug.
|
||
return phraser.Ack(phraser.AckTaskUrgent, map[string]string{"text": cap.Text}), true
|
||
}
|
||
if !resp.Created {
|
||
return phraser.Ack(phraser.AckTaskDuplicate, nil), true
|
||
}
|
||
return phraser.Ack(phraser.AckTask, map[string]string{"text": cap.Text}), true
|
||
}
|
||
|
||
// queryTasks — "какие у меня задачи?", "что мне нужно сделать?".
|
||
//
|
||
// Reads the live set and recites it in priority order (Vikunja #129). The order
|
||
// is computed by internal/tasks from what he told her — deadlines, the urgency
|
||
// he stated, how long a task has been sitting — never asked of the model. The
|
||
// rendering is the package's too, so the spoken list and the /tasks page can
|
||
// never disagree about what comes first.
|
||
func (h *reactiveHandler) queryTasks(ctx context.Context, t *queryTurn) (string, bool) {
|
||
if !router.IsTaskListQuery(t.dec.Utterance) {
|
||
return "", false
|
||
}
|
||
live, err := h.api.ListTasks(ctx, "live")
|
||
if err != nil {
|
||
log.Printf("voice: list tasks: %v", err)
|
||
return "не получилось посмотреть задачи.", true
|
||
}
|
||
ranked := tasks.Rank(taskItems(live), h.now())
|
||
// Bind what she is about to say, in the order she says it, so "второй"
|
||
// means the second task he heard (ordinal.go).
|
||
spoken := tasks.Spoken(ranked)
|
||
cands := make([]dialogue.Candidate, 0, len(spoken))
|
||
for _, r := range spoken {
|
||
cands = append(cands, dialogue.Candidate{Kind: "task", Ref: r.ID, Label: r.Text})
|
||
}
|
||
h.offerCandidates(cands)
|
||
return tasks.FormatRU(ranked), true
|
||
}
|
||
|
||
// taskItems maps wire rows onto the ranker's input. Written here rather than in
|
||
// internal/tasks so the ranker stays a pure package with no ipc (and therefore
|
||
// no store, and therefore no cgo) dependency — the same posture as
|
||
// internal/morning and internal/memory.
|
||
func taskItems(ts []ipc.Task) []tasks.Item {
|
||
out := make([]tasks.Item, len(ts))
|
||
for i, t := range ts {
|
||
out[i] = tasks.Item{
|
||
ID: t.ID, Text: t.Text, Status: t.Status,
|
||
Created: t.CreatedTs, Due: t.Due, Weight: t.Weight,
|
||
}
|
||
}
|
||
return out
|
||
}
|