Implements Phase 5 of AGENTS.md §11 (FR-8, FR-15): - Message detail pane (GET /partials/message) with body, recommended action, reply identity, template picker, editable draft, and history. - internal/reply: OmniRoute (OpenAI-compatible) chat client and a template-based draft generator. All non-JEV models go through OmniRoute (§10). - internal/mailops: SMTP send over smtp.gmail.com:587 with STARTTLS and the app password, forcing IPv4 (§9); FR-15 reply identity (send as the arrived-at X-Pm-Original-To address, else "Sam Rolfe"); threading via In-Reply-To/References; -check-smtp safe verification; and a [Gmail]/Sent Mail thread check. - Migration v5 stores messages.message_id_header and drafts.sent_at; mailfetch parses Message-ID. - Send is user-triggered only: CSRF + UI confirm + confirm=yes guard, no auto-send, duplicate-send refused. A sent reply files the original under Replied-to/<address> and logs every step (FR-13). - Tests for reply, mailops SMTP/identity/thread check, store drafts and migration v5, and the server draft/edit/send guards. - docs/phase5-replies.md, README, .env.example, docker-compose updated. Co-Authored-By: Paperclip <noreply@paperclip.ing>
72 lines
2.4 KiB
Go
72 lines
2.4 KiB
Go
// Reply templates for Phase 5 (FR-8).
|
|
//
|
|
// A template is a short instruction that shapes the LLM draft. It is not the
|
|
// final text: the model fills it in from the message, the classification, and
|
|
// any extra note Sam types. Templates are built in for v1; a template editor is
|
|
// not in the Phase 5 requirements.
|
|
package reply
|
|
|
|
import "strings"
|
|
|
|
// Template is one reply shape.
|
|
type Template struct {
|
|
ID string
|
|
Name string
|
|
Description string
|
|
// Instruction is the template-specific part of the prompt.
|
|
Instruction string
|
|
}
|
|
|
|
// Templates is the built-in template list, in menu order.
|
|
var Templates = []Template{
|
|
{
|
|
ID: "quick-ack",
|
|
Name: "Quick acknowledgement",
|
|
Description: "Confirm receipt in one or two short sentences.",
|
|
Instruction: "Acknowledge that the message was received. If the sender asked for something, say when Sam will look at it. Do not promise anything Sam has not agreed to.",
|
|
},
|
|
{
|
|
ID: "answer",
|
|
Name: "Answer the question",
|
|
Description: "Answer the sender's question directly.",
|
|
Instruction: "Answer the sender's question directly and briefly. If the message does not contain a clear question, summarise what Sam understood and ask one clarifying question.",
|
|
},
|
|
{
|
|
ID: "confirm",
|
|
Name: "Confirm an arrangement",
|
|
Description: "Confirm a booking, time, or arrangement.",
|
|
Instruction: "Confirm the arrangement in the message: restate the key detail (date, time, or reference) and say it works for Sam.",
|
|
},
|
|
{
|
|
ID: "decline",
|
|
Name: "Politely decline",
|
|
Description: "Decline or say no thanks, politely.",
|
|
Instruction: "Politely decline the request or offer. Thank the sender and keep it warm but brief. Do not over-explain.",
|
|
},
|
|
}
|
|
|
|
// TemplateByID returns the template with the given id. ok is false when it is
|
|
// unknown. The id match is case-insensitive.
|
|
func TemplateByID(id string) (Template, bool) {
|
|
id = strings.ToLower(strings.TrimSpace(id))
|
|
for _, t := range Templates {
|
|
if t.ID == id {
|
|
return t, true
|
|
}
|
|
}
|
|
return Template{}, false
|
|
}
|
|
|
|
// TemplateForAction picks a sensible default template from the classifier's
|
|
// recommended action. An unknown action falls back to a quick acknowledgement.
|
|
func TemplateForAction(action string) Template {
|
|
switch strings.ToLower(strings.TrimSpace(action)) {
|
|
case "reply":
|
|
t, _ := TemplateByID("answer")
|
|
return t
|
|
default:
|
|
t, _ := TemplateByID("quick-ack")
|
|
return t
|
|
}
|
|
}
|