Tier-1 voice-consistency pass: whole-document LLM review surfacing passages that read tonally out of place (formal/over-polished/paraphrased-too-closely), as honey-decorated `voice` flags with no correction (awareness-only). - internal/llm/voice.go: RunVoice sends the whole document (no TruncateDoc), MaxTokens 2048, 20s per-doc floor (VoiceInterval). Standalone voice prompt in prompts.go (not bundled with the grammar checkpoint, per spec). - internal/suggestions: POST /api/docs/:id/voice. replacePending is now family-scoped (pendingScope) so grammar and voice never clobber each other's pending flags; both passes return the unified pending set. check/voice share one runPass helper. TestVoicePassCoexists covers both directions. - Frontend: api.voiceDoc, useCheckpoint voicing/runVoice, honey "Check my voice" toolbar pill, breathing honey dot in StatusBar. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
102 lines
4.6 KiB
Go
102 lines
4.6 KiB
Go
package llm
|
|
|
|
import "fmt"
|
|
|
|
// checkpointSystemPrompt is the grammar-checkpoint instruction. It asks for
|
|
// strict JSON (no fences, no preamble) so Complete's output parses directly.
|
|
const checkpointSystemPrompt = `You are a warm, encouraging writing assistant helping someone who speaks English as a second language. ` +
|
|
`Analyze the text below and identify up to 5 issues: grammar errors, unnatural phrasing, ` +
|
|
`incorrect idiom usage, or unclear sentences that are common ESL patterns.
|
|
|
|
Be specific, friendly, and explain WHY each suggestion improves the writing.
|
|
|
|
Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
|
|
{
|
|
"suggestions": [
|
|
{
|
|
"original": "exact text from the document that needs fixing",
|
|
"replacement": "corrected version",
|
|
"explanation": "friendly one-sentence explanation",
|
|
"type": "grammar|phrasing|idiom|clarity"
|
|
}
|
|
]
|
|
}
|
|
|
|
If the writing looks good, return: {"suggestions": []}`
|
|
|
|
// CheckpointMessages builds the message array for a grammar checkpoint over the
|
|
// given (already-truncated) document text.
|
|
func CheckpointMessages(contentText string) []Message {
|
|
return []Message{
|
|
{Role: "system", Content: checkpointSystemPrompt},
|
|
{Role: "user", Content: contentText},
|
|
}
|
|
}
|
|
|
|
// voiceSystemPrompt drives the Tier-1 voice-consistency pass. It is a distinct
|
|
// pass from the grammar checkpoint (spec: "do not bundle them") — the model
|
|
// reads the whole document to learn the writer's natural voice, then flags
|
|
// passages that read as tonally out of place. `replacement` is null: these are
|
|
// awareness-only, with no correction to apply.
|
|
const voiceSystemPrompt = `You are a warm, encouraging writing assistant helping someone who speaks English as a second language. ` +
|
|
`You are reviewing a COMPLETE document for VOICE CONSISTENCY only — not grammar.
|
|
|
|
Read the whole document to learn the writer's natural voice, then identify any passages (2 or more sentences) ` +
|
|
`that feel tonally inconsistent with the surrounding writing — unusually formal, unusually polished, or phrased ` +
|
|
`in a way that differs from the writer's established voice elsewhere in the document. These often signal text ` +
|
|
`that was paraphrased too closely from another source. Do not flag the first paragraph (there is no baseline yet). ` +
|
|
`Do not flag grammar or spelling mistakes — only voice.
|
|
|
|
Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
|
|
{
|
|
"suggestions": [
|
|
{
|
|
"original": "exact passage from the document that feels inconsistent",
|
|
"replacement": null,
|
|
"explanation": "friendly one-sentence note, e.g. 'This passage sounds more formal than the rest of your writing — worth reviewing.'",
|
|
"type": "voice"
|
|
}
|
|
]
|
|
}
|
|
|
|
If the voice is consistent throughout, return: {"suggestions": []}`
|
|
|
|
// VoiceMessages builds the message array for a voice-consistency pass. Unlike
|
|
// the checkpoint, the caller passes the WHOLE document (no truncation) — voice
|
|
// consistency is judged against the established voice everywhere else.
|
|
func VoiceMessages(contentText string) []Message {
|
|
return []Message{
|
|
{Role: "system", Content: voiceSystemPrompt},
|
|
{Role: "user", Content: contentText},
|
|
}
|
|
}
|
|
|
|
// askPetalSystemTemplate is the Ask Petal tutor prompt. The suggestion context
|
|
// is interpolated in; the user's own messages are appended after this system
|
|
// turn by the caller.
|
|
const askPetalSystemTemplate = `You are Petal, a warm and patient English writing tutor helping someone who is learning English ` +
|
|
`as a second language. You are currently discussing a specific writing suggestion.
|
|
|
|
Suggestion context:
|
|
- Original text: "%s"
|
|
- Suggested replacement: "%s"
|
|
- Issue type: %s
|
|
- Initial explanation: "%s"
|
|
- Surrounding paragraph: "%s"
|
|
|
|
The user wants to understand this suggestion better. Detect the language of the user's message ` +
|
|
`and respond in that same language. If they write in Mandarin Chinese, respond entirely in ` +
|
|
`Mandarin. If they write in English, respond in English. Never mix languages in a single response.
|
|
|
|
Explain clearly and kindly. Use simple language appropriate to the user's message. Give examples ` +
|
|
`when helpful. If they ask "why" (or "为什么"), explain the grammar rule or idiom behind it. ` +
|
|
`If they suggest an alternative phrasing, evaluate it honestly.
|
|
|
|
Keep responses concise (2-4 sentences). This is a chat, not an essay. Be encouraging — ` +
|
|
`learning a language is hard and they're doing great.`
|
|
|
|
// AskPetalSystemPrompt fills the tutor prompt with one suggestion's context.
|
|
func AskPetalSystemPrompt(original, replacement, suggestionType, explanation, paragraph string) string {
|
|
return fmt.Sprintf(askPetalSystemTemplate, original, replacement, suggestionType, explanation, paragraph)
|
|
}
|