Turn a mail into task candidates, and into nothing else (#246)
The extraction half. internal/email.Extractor asks the resident Qwen3-1.7B, under a GBNF grammar, what one message requires of him, and returns at most three short candidates with an optional date. Everything it can produce is a row in `tasks` with status "candidate", written through the intake seam #130 built for exactly this (Source "email:<mailbox>", Evidence = the subject line). No reminder, no fact, no note, no calendar event. That bound is the design: a reminder FIRES, so a 1.7B misreading "встреча была в четверг" as a future appointment would wake him up about it, whereas a wrong candidate is a line he dismisses in one click. A due date the model read out of the mail is stored on the candidate, where no scheduler reads it — the review page sorts by it. Relative wording ("до пятницы") is deliberately left in the text rather than resolved to a date the model would get wrong. The prompt is written against the two things a small model does here: it summarises when asked to extract, and it invents an obligation out of a polite closing line. Hence the demand for a verb phrase, and an explicit empty array — most mail contains no task, and a model with no way to say "nothing" says something. Wiring: core owns extraction because llama-server lives in core's process, so the reader hands messages over a new ipc.MethodIngestMail. It is a Server hook (like StepUp/UnlockFn), not a CoreAPI method — not a store operation, and no CoreAPI implementation should have to carry it. The hook stays nil without an `email` config block or without a llama-server phraser, so the method answers ErrUnknownMethod: off unless configured, twice over. There is no keyword fallback on purpose — "the subject became a task" is a mailbox rendered as a to-do list, not extraction. Privacy: junk is refused before the model is called, mail text is never search input, extraction errors carry byte counts rather than the reply, the stored evidence is a truncated subject, and the log line names the mailbox and the UID only.
This commit is contained in:
@@ -138,6 +138,44 @@ type CaptureTaskResp struct {
|
||||
Created bool `json:"created"`
|
||||
}
|
||||
|
||||
// IngestMailReq — one message a mail reader has fetched, handed to core for
|
||||
// extraction (Vikunja #246).
|
||||
//
|
||||
// The mail reader (cmd/mavmaild) holds the IMAP credential and core never sees
|
||||
// it, the same split mavpoll uses for the zenmoney token. What crosses this
|
||||
// boundary is only the message text, because extraction runs on the resident
|
||||
// model and llama-server lives inside core's process.
|
||||
//
|
||||
// Body is already plaintext and truncated by internal/email; core does not
|
||||
// re-parse MIME and never stores the body. Junk means the reader's header
|
||||
// filter already classified the message as bulk — core is told rather than
|
||||
// asked, so a junk message can be counted without a model call.
|
||||
//
|
||||
// This method is available only when core has an email block configured AND a
|
||||
// llama-server phraser; otherwise it answers ErrUnknownMethod, which is what
|
||||
// "off unless configured" looks like at the wire.
|
||||
type IngestMailReq struct {
|
||||
Mailbox string `json:"mailbox"`
|
||||
UID uint32 `json:"uid"`
|
||||
From string `json:"from,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Junk bool `json:"junk,omitempty"`
|
||||
}
|
||||
|
||||
// IngestMailResp — what core did with the message. TaskIDs are the rows
|
||||
// CaptureTask returned; Created counts the ones that were new (a re-read
|
||||
// mailbox dedupes to Created=0). Skipped is set when nothing was asked of the
|
||||
// model at all — junk, or an empty message.
|
||||
//
|
||||
// Nothing here echoes the mail back. The reader logs counts.
|
||||
type IngestMailResp struct {
|
||||
TaskIDs []int64 `json:"task_ids,omitempty"`
|
||||
Created int `json:"created"`
|
||||
Skipped bool `json:"skipped,omitempty"`
|
||||
}
|
||||
|
||||
type listTasksReq struct {
|
||||
Status string `json:"status"` // "" all | "live" | candidate|open|done|dropped
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user