phraser: one ask-and-parse step, and an empty generation says so (V-397)
PhraseQuery, PhraseSelf and the knowledge branch each wrote the same four steps: call the model, parse the contract, pick whichever of the two errors fired, wrap it with the path name. That is generate() now. Same fallbacks, same errors, same text to the owner. Two silences made visible, log only. A model that parsed cleanly and put an empty string in "response" left no trace: the nudge went out in template Russian and the reminder read its own text out, and nothing recorded that the model had answered with nothing. Both say so now.
This commit is contained in:
@@ -250,6 +250,12 @@ func (p *LLMPhraser) PhraseNudge(ctx context.Context, c loop.Candidate) (deliver
|
||||
// The model said nothing usable. Say it in Russian anyway — this text
|
||||
// goes straight to a Russian piper voice, so the old "water — care"
|
||||
// fallback was unspeakable.
|
||||
//
|
||||
// It says so in the log now (V-397). A clean parse of {"response":""}
|
||||
// left no trace at all: the nudge went out in template Russian and
|
||||
// nothing recorded that the model had been asked and had answered with
|
||||
// an empty field.
|
||||
log.Printf("phraser: PhraseNudge: empty response for rule %q, using the plain Russian fallback", c.Rule.Name)
|
||||
body = fallbackNudge(c)
|
||||
}
|
||||
if mood == "" {
|
||||
@@ -272,38 +278,47 @@ func (p *LLMPhraser) PhraseQuery(ctx context.Context, utterance string, notes []
|
||||
notes = nonEmpty(notes)
|
||||
if len(notes) == 0 {
|
||||
sys, prompt := p.knowledgePrompt(utterance)
|
||||
resp, err := p.chatWithSystem(ctx, sys, prompt, 768)
|
||||
text, raw, err := p.generate(ctx, "phrase query (knowledge)", sys, prompt, 768)
|
||||
if err != nil {
|
||||
return UnknownFallback(), fmt.Errorf("phrase query (knowledge): %w", err)
|
||||
return UnknownFallback(), err
|
||||
}
|
||||
if resp == "" {
|
||||
if raw == "" {
|
||||
return UnknownFallback(), errEmptyResponse
|
||||
}
|
||||
text, _, perr := parseResponseMood(resp)
|
||||
if perr != nil {
|
||||
return UnknownFallback(), fmt.Errorf("phrase query (knowledge): %w", perr)
|
||||
}
|
||||
if text != "" {
|
||||
return text, nil
|
||||
}
|
||||
return resp, nil
|
||||
return raw, nil
|
||||
}
|
||||
sys, prompt := p.evidencePrompt(utterance, notes)
|
||||
resp, err := p.chatWithSystem(ctx, sys, prompt, 768)
|
||||
text, _, perr := parseResponseMood(resp)
|
||||
if err != nil || perr != nil {
|
||||
text, raw, err := p.generate(ctx, "phrase query (evidence)", sys, prompt, 768)
|
||||
if err != nil {
|
||||
// Read the notes out rather than ship a broken fragment.
|
||||
cause := err
|
||||
if cause == nil {
|
||||
cause = perr
|
||||
}
|
||||
return SourcesFallback(strings.Join(notes, "; ")),
|
||||
fmt.Errorf("phrase query (evidence): %w", cause)
|
||||
return SourcesFallback(strings.Join(notes, "; ")), err
|
||||
}
|
||||
if text != "" {
|
||||
return text, nil
|
||||
}
|
||||
return resp, nil
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// generate asks the model one question and reads the reply contract off the
|
||||
// answer. text is what she said, raw is what the model actually wrote — the
|
||||
// callers that ship bare prose when the model skipped the JSON need both.
|
||||
//
|
||||
// where names the path for the error, which every caller wraps identically.
|
||||
// An error here always means the caller must use its fallback: a transport
|
||||
// failure, or a generation that opened a JSON object and never closed it.
|
||||
func (p *LLMPhraser) generate(ctx context.Context, where, sys, user string, maxTokens int) (text, raw string, err error) {
|
||||
raw, err = p.chatWithSystem(ctx, sys, user, maxTokens)
|
||||
if err != nil {
|
||||
return "", raw, fmt.Errorf("%s: %w", where, err)
|
||||
}
|
||||
text, _, perr := parseResponseMood(raw)
|
||||
if perr != nil {
|
||||
return "", raw, fmt.Errorf("%s: %w", where, perr)
|
||||
}
|
||||
return text, raw, nil
|
||||
}
|
||||
|
||||
// PhraseChat uses the LLM to respond conversationally, building a multi-turn
|
||||
@@ -390,6 +405,10 @@ func (p *LLMPhraser) PhraseReminder(ctx context.Context, d loop.ReminderDecision
|
||||
body, mood = "", ""
|
||||
}
|
||||
if body == "" {
|
||||
// Same silence as PhraseNudge had (V-397): she reads the reminder's own
|
||||
// text out, which is fine, but nothing said the model had produced
|
||||
// nothing.
|
||||
log.Printf("phraser: PhraseReminder: empty response, reading the reminder text out instead")
|
||||
body = text
|
||||
}
|
||||
if mood == "" {
|
||||
@@ -416,14 +435,9 @@ func (p *LLMPhraser) chat(ctx context.Context, userPrompt string) (string, error
|
||||
// is already a readable answer, which is why this needs no separate fallback.
|
||||
func (p *LLMPhraser) PhraseSelf(ctx context.Context, utterance, description string) (string, error) {
|
||||
sys, prompt := p.selfPrompt(utterance, description)
|
||||
resp, err := p.chatWithSystem(ctx, sys, prompt, 768)
|
||||
text, _, perr := parseResponseMood(resp)
|
||||
if err != nil || perr != nil {
|
||||
cause := err
|
||||
if cause == nil {
|
||||
cause = perr
|
||||
}
|
||||
return description, fmt.Errorf("phrase self: %w", cause)
|
||||
text, _, err := p.generate(ctx, "phrase self", sys, prompt, 768)
|
||||
if err != nil {
|
||||
return description, err
|
||||
}
|
||||
if text != "" {
|
||||
return text, nil
|
||||
|
||||
Reference in New Issue
Block a user