Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ca5748699 | |||
| b43bb265b5 | |||
| b9a24334ea | |||
| c97aebf55a |
@@ -69,19 +69,30 @@ protocol; the config in `deploy/mavend.json` (with `${VAR}` env expansion from g
|
|||||||
|
|
||||||
## Routing — read this before touching the router
|
## Routing — read this before touching the router
|
||||||
|
|
||||||
`internal/router/` has TWO layered engines and the committed default is an **interim
|
`internal/router/` has TWO layered engines. **The LLM router is now the default and it is
|
||||||
stopgap, not the intended design** (see memory `routing-architecture-target`):
|
on in deploy** — this section used to say it was wired `nil`, which stopped being true on
|
||||||
|
2026-07-31.
|
||||||
|
|
||||||
- **Target (REARCH.md):** LLM-as-router. One resident Qwen3-1.7B (`llmrouter.go`) emits
|
- **LLM router (the intended design, REARCH.md):** the resident Qwen3-1.7B (`llmrouter.go`)
|
||||||
GBNF-constrained structured JSON, and the SAME model phrases replies. Embedder is demoted
|
emits GBNF-constrained structured JSON, and the SAME model phrases replies. Embedder is
|
||||||
from a routing gate to a RAG hint.
|
demoted from a routing gate to a RAG hint. Wired at `voice.go:214` via
|
||||||
- **Current stopgap:** `llmrouter` is wired `nil` (around `voice.go`), so the
|
`pickLLMRouter(cfg.Voice.UseLLMRouter(), llmClient)`; the flag is `voice.llm_router`
|
||||||
`classifier.go` + `embedder.go` nearest-neighbour cascade actually runs. It routes by
|
(`config.go`), `DefaultLLMRouter` is **on**, and `deploy/mavend.json` sets it `true`.
|
||||||
similarity to frozen seed phrases — the known cause of weak RU query handling.
|
- **Classifier cascade (the failure floor, not dead code):** `classifier.go` +
|
||||||
|
`embedder.go` nearest-neighbour over frozen seed phrases. It runs when the LLM router is
|
||||||
|
off, when there is no llama-server to talk to (`pickLLMRouter` logs that and degrades),
|
||||||
|
and on any per-turn LLM error. Do not delete it — routing by seed similarity is the known
|
||||||
|
cause of weak RU query handling, but a turn must never break on the model.
|
||||||
|
|
||||||
Cascade order: `stage0.go` exact-match fast-path → LLM router (when non-nil) → classifier
|
Cascade order: `stage0.go` exact-match fast-path → LLM router (when non-nil) → classifier
|
||||||
fallback. Any LLM error falls through to the classifier so a turn never breaks on the model.
|
fallback. Any LLM error falls through to the classifier so a turn never breaks on the model.
|
||||||
|
|
||||||
|
Measured on the 77-case RU fixture (`MODEL-BAKEOFF-31-07-2026.md`): the classifier scores
|
||||||
|
36.8% full accuracy at p50 31ms; Qwen3-1.7B scores 67.5% intent-only / 72.7% through the
|
||||||
|
cascade at p50 ≈2.7s. Accuracy roughly doubled, latency is ~90× worse, and that trade was
|
||||||
|
accepted deliberately. Still open: `Confidence: 1.0` is hardcoded in `llmrouter.go`, so the
|
||||||
|
LLM path never asks for clarification (6/6 refusal cases missed) — Vikunja #359.
|
||||||
|
|
||||||
## LLM output contract
|
## LLM output contract
|
||||||
|
|
||||||
All phrasing paths emit `{"response":"...","mood":"..."}` (parsed in `replier_llm.go` and
|
All phrasing paths emit `{"response":"...","mood":"..."}` (parsed in `replier_llm.go` and
|
||||||
@@ -94,7 +105,11 @@ workspace enforces that the Go and relabelling prompts remain identical.
|
|||||||
## Non-goals (hard constraints)
|
## Non-goals (hard constraints)
|
||||||
|
|
||||||
Not a nag, not autonomous. Maven's persona is **feminine** — Russian
|
Not a nag, not autonomous. Maven's persona is **feminine** — Russian
|
||||||
self-reference must use feminine forms (the user is male; see memory `maven-persona-gender`).
|
self-reference must use feminine forms — `рада`, not `рад`; `поняла`, not `понял`. The owner
|
||||||
|
is male and is addressed informally: "ты", singular, never "вы"/"ваш" and never "он"/"его"
|
||||||
|
(she talks TO him, not about him). Pet names ("милый", "дорогой") are forbidden; his name
|
||||||
|
("Ками") is not. The eval enforces this: `CheckAddress`, `CheckFeminine` and `CheckCringe` in
|
||||||
|
`internal/phraser/eval/checks.go`, scored by `make eval-phrasing`.
|
||||||
|
|
||||||
**"Never phones home" is DEPRECATED** (owner's call, 2026-07-31). It used to be a hard
|
**"Never phones home" is DEPRECATED** (owner's call, 2026-07-31). It used to be a hard
|
||||||
constraint and it is not one any more: a 0.8B — and a 1.7B — does not know enough to answer
|
constraint and it is not one any more: a 0.8B — and a 1.7B — does not know enough to answer
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ Settles Vikunja **#278 / #250**.
|
|||||||
- Same fixture and scorer as `ROUTING-EVAL-31-07-2026.md`: `internal/router/eval/`
|
- Same fixture and scorer as `ROUTING-EVAL-31-07-2026.md`: `internal/router/eval/`
|
||||||
(`ru_routing_v1.json`, 76 held-out cases).
|
(`ru_routing_v1.json`, 76 held-out cases).
|
||||||
- Reproduce: `MAVEN_LLM_URL=http://127.0.0.1:<port> make eval-router`
|
- Reproduce: `MAVEN_LLM_URL=http://127.0.0.1:<port> make eval-router`
|
||||||
(`TestLLMRouterBaseline`). Note: there is no `make eval-models` target.
|
(`TestLLMRouterBaseline`). (This line used to say there is no `make eval-models` target.
|
||||||
|
There is one now — start a server with the gguf you want, then
|
||||||
|
`make eval-models MAVEN_LLM_URL=http://127.0.0.1:<port>`. It runs only the LLM test, since
|
||||||
|
the classifier baselines do not depend on the model.)
|
||||||
- All three models served by the same `llama-server` flags — `-c 2048 -ngl 99 -t 6`, only
|
- All three models served by the same `llama-server` flags — `-c 2048 -ngl 99 -t 6`, only
|
||||||
`-m` and `--port` differ. One server at a time on an otherwise idle box, so latencies are
|
`-m` and `--port` differ. One server at a time on an otherwise idle box, so latencies are
|
||||||
real and not contention.
|
real and not contention.
|
||||||
@@ -207,10 +210,14 @@ swapped again when the CPT lands.
|
|||||||
- Routing is one run per model, not three. The gaps between families are far larger
|
- Routing is one run per model, not three. The gaps between families are far larger
|
||||||
than the run-to-run spread seen on the talk fixture, but the 2B-vs-1.7B gap (62.3
|
than the run-to-run spread seen on the talk fixture, but the 2B-vs-1.7B gap (62.3
|
||||||
vs 67.5) is not safe to call on one run.
|
vs 67.5) is not safe to call on one run.
|
||||||
- The routing numbers only reach production once the LLM router is wired on. It is
|
- ~~The routing numbers only reach production once the LLM router is wired on. It is
|
||||||
still `nil`.
|
still `nil`.~~ **Resolved the same evening:** the LLM router is wired at `voice.go:214`
|
||||||
- `/mnt/hdd1/llms/LFM2.5/Qwen3-1.7B-UD-Q4_K_XL.gguf` is a 293 MB truncated download
|
behind `voice.llm_router`, the default is on, and `deploy/mavend.json` sets it `true`.
|
||||||
in the wrong directory. The good 1.13 GB copy is in `qwen3/`. Delete the stray one.
|
These numbers are the production path now, so the p50 ≈2.7s is a real per-turn cost and
|
||||||
|
not a bench artifact.
|
||||||
|
- ~~`/mnt/hdd1/llms/LFM2.5/Qwen3-1.7B-UD-Q4_K_XL.gguf` is a 293 MB truncated download
|
||||||
|
in the wrong directory.~~ **Deleted 2026-07-31.** The good 1.13 GB copy in `qwen3/` is
|
||||||
|
what `deploy/mavend.json` loads.
|
||||||
- Harness: `scratchpad/bakeoff.sh`, one server at a time, health-checked before each
|
- Harness: `scratchpad/bakeoff.sh`, one server at a time, health-checked before each
|
||||||
run, `/v1/models` recorded per run. Never run two LLM consumers at once — see the
|
run, `/v1/models` recorded per run. Never run two LLM consumers at once — see the
|
||||||
contamination note in `TALK-EVAL-31-07-2026.md`.
|
contamination note in `TALK-EVAL-31-07-2026.md`.
|
||||||
|
|||||||
+57
-6
@@ -3,6 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kami/maven/internal/dialogue"
|
"github.com/kami/maven/internal/dialogue"
|
||||||
@@ -21,8 +23,12 @@ const clarifyTTL = 90 * time.Second
|
|||||||
// raw utterance, chat and system have nothing to fill in. For those a clarify
|
// raw utterance, chat and system have nothing to fill in. For those a clarify
|
||||||
// decision keeps the canned "не поняла" reply — inventing a question for noise
|
// decision keeps the canned "не поняла" reply — inventing a question for noise
|
||||||
// is worse than admitting she missed it.
|
// is worse than admitting she missed it.
|
||||||
|
// A reminder wants BOTH what to remind about and when. Subject first: "напомни
|
||||||
|
// в 11" has a time and nothing to say at 11, and a reminder with no subject is
|
||||||
|
// not worth setting. Order here is the order she asks in — she still only asks
|
||||||
|
// about the first one missing.
|
||||||
var wantedSlots = map[router.Intent][]dialogue.Slot{
|
var wantedSlots = map[router.Intent][]dialogue.Slot{
|
||||||
router.IntentReminder: {dialogue.SlotTime},
|
router.IntentReminder: {dialogue.SlotText, dialogue.SlotTime},
|
||||||
router.IntentFact: {dialogue.SlotKey},
|
router.IntentFact: {dialogue.SlotKey},
|
||||||
router.IntentAct: {dialogue.SlotFn},
|
router.IntentAct: {dialogue.SlotFn},
|
||||||
}
|
}
|
||||||
@@ -35,7 +41,8 @@ var wantedSlots = map[router.Intent][]dialogue.Slot{
|
|||||||
// questions, so there is no gender agreement to get wrong; the feminine
|
// questions, so there is no gender agreement to get wrong; the feminine
|
||||||
// self-reference lives in the reply she gives when she drops the request.
|
// self-reference lives in the reply she gives when she drops the request.
|
||||||
var clarifyQuestions = map[dialogue.Slot]string{
|
var clarifyQuestions = map[dialogue.Slot]string{
|
||||||
dialogue.SlotTime: "На когда напомнить?",
|
dialogue.SlotTime: "Когда?",
|
||||||
|
dialogue.SlotText: "О чём напомнить?",
|
||||||
dialogue.SlotKey: "Что записать?",
|
dialogue.SlotKey: "Что записать?",
|
||||||
dialogue.SlotFn: "Что сделать?",
|
dialogue.SlotFn: "Что сделать?",
|
||||||
}
|
}
|
||||||
@@ -45,11 +52,55 @@ var clarifyQuestions = map[dialogue.Slot]string{
|
|||||||
// landed. Feminine self-reference ("поняла"), as everywhere.
|
// landed. Feminine self-reference ("поняла"), as everywhere.
|
||||||
const clarifyGaveUp = "Прости, я не поняла. Скажи, пожалуйста, по-другому."
|
const clarifyGaveUp = "Прости, я не поняла. Скажи, пожалуйста, по-другому."
|
||||||
|
|
||||||
// clarifyExpired — his answer came after the TTL, so the parked request is
|
// clarifyExpiredVariants — his answer came after the TTL, so the parked request
|
||||||
// already gone. Same tone as clarifyGaveUp, different reason: too much time
|
// is already gone. Same tone as clarifyGaveUp, different reason: too much time
|
||||||
// passed, not "I did not understand". Feminine self-reference ("ждала",
|
// passed, not "I did not understand". Feminine self-reference ("ждала",
|
||||||
// "отпустила"); he is addressed with a plain imperative.
|
// "отпустила"); he is addressed with a plain imperative.
|
||||||
const clarifyExpired = "Прости, я слишком долго ждала ответа и отпустила прошлую просьбу. Если она ещё нужна, скажи заново."
|
//
|
||||||
|
// Five phrasings, not one. This is the line he hears whenever he walks off
|
||||||
|
// mid-request, so it is the line that repeats most — and the same sentence every
|
||||||
|
// time is what makes a house assistant sound like a kiosk. They all carry the
|
||||||
|
// same two facts (the old request is gone; say it again if it still matters),
|
||||||
|
// because the wording may vary and the meaning may not.
|
||||||
|
//
|
||||||
|
// Fixed templates rather than model output, for the same reason as
|
||||||
|
// clarifyQuestions: this text has to be right every time, and it is not worth a
|
||||||
|
// generation to say something this small.
|
||||||
|
var clarifyExpiredVariants = []string{
|
||||||
|
"Прости, я слишком долго ждала ответа и отпустила прошлую просьбу. Если она ещё нужна, скажи заново.",
|
||||||
|
"Кажется, прошлая просьба уже не важна — я её отпустила. Если я ошибаюсь, повтори.",
|
||||||
|
"Ты как-то резко замолчал, и я не стала ждать дальше. Если та просьба ещё нужна, скажи заново.",
|
||||||
|
"Я не дождалась ответа и убрала прошлую просьбу. Повтори, если она всё ещё нужна.",
|
||||||
|
"Столько времени прошло, что я отпустила прошлую просьбу. Скажи заново, если она в силе.",
|
||||||
|
}
|
||||||
|
|
||||||
|
// clarifyExpiredLine picks one of them at random.
|
||||||
|
func clarifyExpiredLine() string {
|
||||||
|
return clarifyExpiredVariants[rand.Intn(len(clarifyExpiredVariants))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// isClarifyExpired reports whether s opens with any of the expiry lines. The
|
||||||
|
// notice is glued in front of this turn's reply (see withNotice), so a caller
|
||||||
|
// checking for it has to match a prefix, not the whole string.
|
||||||
|
func isClarifyExpired(s string) bool {
|
||||||
|
for _, v := range clarifyExpiredVariants {
|
||||||
|
if strings.HasPrefix(s, v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// trimClarifyExpired strips a leading expiry notice, leaving this turn's actual
|
||||||
|
// reply. "" ⇒ the notice was the whole thing.
|
||||||
|
func trimClarifyExpired(s string) string {
|
||||||
|
for _, v := range clarifyExpiredVariants {
|
||||||
|
if strings.HasPrefix(s, v) {
|
||||||
|
return strings.TrimSpace(strings.TrimPrefix(s, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
// clarifyExpiredNotice returns that line when a parked question had just timed
|
// clarifyExpiredNotice returns that line when a parked question had just timed
|
||||||
// out, and "" when nothing was parked. Call it right after
|
// out, and "" when nothing was parked. Call it right after
|
||||||
@@ -63,7 +114,7 @@ func (h *reactiveHandler) clarifyExpiredNotice() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
log.Printf("voice: clarify — parked question expired, telling him and routing the words fresh")
|
log.Printf("voice: clarify — parked question expired, telling him and routing the words fresh")
|
||||||
return clarifyExpired
|
return clarifyExpiredLine()
|
||||||
}
|
}
|
||||||
|
|
||||||
// withNotice glues the expiry notice in front of this turn's reply. One turn
|
// withNotice glues the expiry notice in front of this turn's reply. One turn
|
||||||
|
|||||||
@@ -56,10 +56,13 @@ func TestClarifyQuestionForMissingSlot(t *testing.T) {
|
|||||||
want string
|
want string
|
||||||
asked bool
|
asked bool
|
||||||
}{
|
}{
|
||||||
{"reminder without a time", clarifyDec(router.IntentReminder, router.Slots{Text: "напомни позвонить маме"}, "напомни позвонить маме"), "На когда напомнить?", true},
|
{"reminder without a time", clarifyDec(router.IntentReminder, router.Slots{Text: "напомни позвонить маме"}, "напомни позвонить маме"), "Когда?", true},
|
||||||
{"fact without a key", clarifyDec(router.IntentFact, router.Slots{Text: "запиши"}, "запиши"), "Что записать?", true},
|
{"fact without a key", clarifyDec(router.IntentFact, router.Slots{Text: "запиши"}, "запиши"), "Что записать?", true},
|
||||||
{"act without a fn", clarifyDec(router.IntentAct, router.Slots{Text: "сделай это"}, "сделай это"), "Что сделать?", true},
|
{"act without a fn", clarifyDec(router.IntentAct, router.Slots{Text: "сделай это"}, "сделай это"), "Что сделать?", true},
|
||||||
{"reminder that already has a time", clarifyDec(router.IntentReminder, router.Slots{HasTime: true}, "напомни в 11"), "", false},
|
// A time with nothing to say at that time is still half a reminder, so
|
||||||
|
// the subject is what she asks about — not silence.
|
||||||
|
{"reminder that has a time but no subject", clarifyDec(router.IntentReminder, router.Slots{HasTime: true}, "напомни в 11"), "О чём напомнить?", true},
|
||||||
|
{"reminder that has both", clarifyDec(router.IntentReminder, router.Slots{Text: "позвонить маме", HasTime: true}, "напомни в 11 позвонить маме"), "", false},
|
||||||
{"chat is never worth a question", clarifyDec(router.IntentChat, router.Slots{Text: "мгм"}, "мгм"), "", false},
|
{"chat is never worth a question", clarifyDec(router.IntentChat, router.Slots{Text: "мгм"}, "мгм"), "", false},
|
||||||
{"query is never worth a question", clarifyDec(router.IntentQuery, router.Slots{Text: "а"}, "а"), "", false},
|
{"query is never worth a question", clarifyDec(router.IntentQuery, router.Slots{Text: "а"}, "а"), "", false},
|
||||||
}
|
}
|
||||||
@@ -78,7 +81,7 @@ func TestClarifyReminderCompletesOnAnswer(t *testing.T) {
|
|||||||
h, st, _ := newClarifyHandler(t)
|
h, st, _ := newClarifyHandler(t)
|
||||||
|
|
||||||
question, asked := h.askClarify(clarifyDec(router.IntentReminder, router.Slots{Text: "напомни позвонить маме"}, "напомни позвонить маме"))
|
question, asked := h.askClarify(clarifyDec(router.IntentReminder, router.Slots{Text: "напомни позвонить маме"}, "напомни позвонить маме"))
|
||||||
if !asked || question != "На когда напомнить?" {
|
if !asked || question != "Когда?" {
|
||||||
t.Fatalf("expected the time question, got %q asked=%v", question, asked)
|
t.Fatalf("expected the time question, got %q asked=%v", question, asked)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +155,7 @@ func TestClarifyAsksThreeTimesThenSaysSo(t *testing.T) {
|
|||||||
if !handled {
|
if !handled {
|
||||||
t.Fatalf("answer %d must be consumed as an answer", i)
|
t.Fatalf("answer %d must be consumed as an answer", i)
|
||||||
}
|
}
|
||||||
if reply != "На когда напомнить?" {
|
if reply != "Когда?" {
|
||||||
t.Fatalf("attempt %d should ask again, got %q", i, reply)
|
t.Fatalf("attempt %d should ask again, got %q", i, reply)
|
||||||
}
|
}
|
||||||
if h.clarifyStore.Get(voiceDialogueID, h.now()) == nil {
|
if h.clarifyStore.Get(voiceDialogueID, h.now()) == nil {
|
||||||
@@ -304,17 +307,17 @@ func TestClarifyExpiryIsAnnouncedAndWordsStillRoute(t *testing.T) {
|
|||||||
*now = now.Add(clarifyTTL + time.Second)
|
*now = now.Add(clarifyTTL + time.Second)
|
||||||
|
|
||||||
reply := h.handleText(ctx, "как дела")
|
reply := h.handleText(ctx, "как дела")
|
||||||
if !strings.HasPrefix(reply, clarifyExpired) {
|
if !isClarifyExpired(reply) {
|
||||||
t.Fatalf("expired question must be announced first, got %q", reply)
|
t.Fatalf("expired question must be announced first, got %q", reply)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(strings.TrimPrefix(reply, clarifyExpired)) == "" {
|
if trimClarifyExpired(reply) == "" {
|
||||||
t.Fatalf("the new words must still be answered, got only the notice: %q", reply)
|
t.Fatalf("the new words must still be answered, got only the notice: %q", reply)
|
||||||
}
|
}
|
||||||
if h.clarifyStore.Get(voiceDialogueID, h.now()) != nil {
|
if h.clarifyStore.Get(voiceDialogueID, h.now()) != nil {
|
||||||
t.Fatal("the expired question must be gone")
|
t.Fatal("the expired question must be gone")
|
||||||
}
|
}
|
||||||
// The notice is said once, not on every later utterance.
|
// The notice is said once, not on every later utterance.
|
||||||
if reply := h.handleText(ctx, "как дела"); strings.Contains(reply, clarifyExpired) {
|
if reply := h.handleText(ctx, "как дела"); isClarifyExpired(reply) {
|
||||||
t.Fatalf("notice repeated on a later turn: %q", reply)
|
t.Fatalf("notice repeated on a later turn: %q", reply)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func newLLMReplier(c completer, block func() string) *llmReplier {
|
|||||||
return &llmReplier{c: c, stub: voice.NewStubReplier(), block: block}
|
return &llmReplier{c: c, stub: voice.NewStubReplier(), block: block}
|
||||||
}
|
}
|
||||||
|
|
||||||
const replySystem = `Ты — Maven, домашняя ассистентка (о себе — в женском роде). Владелец — мужчина, говоришь с ним на "ты", в единственном числе; никогда не "вы"/"ваш" и не "он"/"его". Подтверди действие РОВНО ОДНИМ коротким предложением (≤120 символов), тепло и по-русски. Не задавай вопросов, не повторяй слова, не добавляй ничего после точки. Отвечай ТОЛЬКО одним объектом JSON с полями "response" (текст) и "mood" (ровно одно из: neutral, happy, thinking, tired, confused).
|
const replySystem = `Ты — Maven, домашняя ассистентка (о себе — в женском роде). Владелец — мужчина, говоришь с ним на "ты", в единственном числе; никогда не "вы"/"ваш" и не "он"/"его". Подтверди действие РОВНО ОДНИМ коротким предложением (≤120 символов), по-русски, спокойно и без официальных формулировок. Не задавай вопросов, не повторяй слова, не добавляй ничего после точки. Отвечай ТОЛЬКО одним объектом JSON с полями "response" (текст) и "mood" (ровно одно из: neutral, happy, thinking, tired, confused).
|
||||||
Пример: {"response": "Записала, что ты выпил стакан воды.", "mood": "neutral"}
|
Пример: {"response": "Записала, что ты выпил стакан воды.", "mood": "neutral"}
|
||||||
Никогда не пиши "..." в поле response.`
|
Никогда не пиши "..." в поле response.`
|
||||||
|
|
||||||
|
|||||||
@@ -551,12 +551,21 @@ func (p *LLMPhraser) chatWithSystem(ctx context.Context, system, user string, ma
|
|||||||
// Russian only, feminine self-reference, second person masculine (the owner is
|
// Russian only, feminine self-reference, second person masculine (the owner is
|
||||||
// a man). She talks TO him, informally, singular — never "вы", never "он".
|
// a man). She talks TO him, informally, singular — never "вы", never "он".
|
||||||
// One short sentence — the nudge is spoken aloud.
|
// One short sentence — the nudge is spoken aloud.
|
||||||
|
//
|
||||||
|
// What the ban on обращения forbids is pet names ("дорогой", "милый"), not his
|
||||||
|
// name: "Ками, ноутбук на трёх процентах" is exactly how she talks, and the
|
||||||
|
// unqualified word read as forbidding that too. Hence "ласковые обращения".
|
||||||
|
//
|
||||||
|
// The examples also never claim a physical act. She has no hands and no smart
|
||||||
|
// plug — she can tell him the battery is at three percent, she cannot put the
|
||||||
|
// laptop on charge. An example that says she did teaches the model to invent
|
||||||
|
// actions Maven never took, which is worse than a missing nudge.
|
||||||
const nudgeSystem = `Ты — Maven, домашняя ассистентка. О себе говоришь в женском роде ("я проверила", "я записала"). Владелец — мужчина, обращайся к нему в мужском роде ("ты пил", "ты забыл").
|
const nudgeSystem = `Ты — Maven, домашняя ассистентка. О себе говоришь в женском роде ("я проверила", "я записала"). Владелец — мужчина, обращайся к нему в мужском роде ("ты пил", "ты забыл").
|
||||||
Говоришь с ним на "ты", в единственном числе ("выпей", "встань"). Никогда не "вы"/"вас"/"ваш" и никогда "он"/"его" — ты говоришь ему, а не о нём.
|
Говоришь с ним на "ты", в единственном числе ("выпей", "встань"). Никогда не "вы"/"вас"/"ваш" и никогда "он"/"его" — ты говоришь ему, а не о нём.
|
||||||
|
|
||||||
Пиши ОДНО короткое напоминание по-русски: не больше 120 символов и не больше 16 слов. Только по делу.
|
Пиши ОДНО короткое напоминание по-русски: не больше 120 символов и не больше 16 слов. Только по делу.
|
||||||
|
|
||||||
Запрещено: обращения ("дорогой", "милый"), эмодзи, извинения ("прости", "извини"), вопросы о самочувствии, похвала, больше одного восклицательного знака, английские слова кроме имён сервисов.
|
Запрещено: ласковые обращения ("дорогой", "милый"), эмодзи, извинения ("прости", "извини"), вопросы о самочувствии, похвала, больше одного восклицательного знака, английские слова кроме имён сервисов.
|
||||||
|
|
||||||
Отвечай ТОЛЬКО одним объектом JSON с полями "response" и "mood".
|
Отвечай ТОЛЬКО одним объектом JSON с полями "response" и "mood".
|
||||||
"response" — сам текст напоминания.
|
"response" — сам текст напоминания.
|
||||||
@@ -564,7 +573,7 @@ const nudgeSystem = `Ты — Maven, домашняя ассистентка. О
|
|||||||
|
|
||||||
Так выглядит правильный ответ по форме. Темы здесь посторонние — их в запросе не будет:
|
Так выглядит правильный ответ по форме. Темы здесь посторонние — их в запросе не будет:
|
||||||
{"response": "Стиральная машина закончила. Развесь бельё.", "mood": "neutral"}
|
{"response": "Стиральная машина закончила. Развесь бельё.", "mood": "neutral"}
|
||||||
{"response": "Ноутбук на трёх процентах. Я поставила его на зарядку.", "mood": "confused"}
|
{"response": "Ками, ноутбук на трёх процентах. Поставь его на зарядку.", "mood": "confused"}
|
||||||
|
|
||||||
Это примеры ФОРМЫ, а не темы. Пиши только про ту ситуацию, которую тебе дали в запросе. Не копируй примеры и никогда не пиши "..." в поле response.`
|
Это примеры ФОРМЫ, а не темы. Пиши только про ту ситуацию, которую тебе дали в запросе. Не копируй примеры и никогда не пиши "..." в поле response.`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user