Documentation
¶
Overview ¶
Package composer holds the two ways a user tries rules out before they act: the rule composer, which turns free text into validated draft rules, and the rule tester, which runs saved or draft rules over recent mail. Neither saves anything, and neither can change a mailbox: all they are given of one is Reader.
Index ¶
- Constants
- Variables
- func ActionsSchema() map[string]any
- func CheckText(text string) string
- func CondSchema() map[string]any
- func RuleGrammar() string
- func RuleLine(r rules.Rule) string
- type CheckProgress
- type CheckRow
- type Composer
- type Conflict
- type Draft
- type FetchOptions
- type Output
- type Problem
- type Progress
- type Reader
- type Request
- type Result
- type Row
- type Scanner
- type Tester
Constants ¶
const ( DefaultLimit = 200 MaxLimit = 2000 )
Limits of one test run.
const MaxSamples = 5
MaxSamples is how many matching emails a draft card shows.
const MaxText = 4000
MaxText is the longest text the composer takes, in characters.
Variables ¶
var ErrModel = errors.New("the composer model failed")
ErrModel marks a failure of the generative model: it could not be reached, or its answer was not something drafts can be read from.
Functions ¶
func ActionsSchema ¶
ActionsSchema is a rule's list of actions in an output schema.
func CheckText ¶
CheckText says what is wrong with a text to compose from, or "" when it can be used.
func CondSchema ¶
CondSchema is a condition tree in an output schema: a leaf or an all/any group, with no other keys, two levels deep (deeper nodes are any object), so a model cannot wrap a leaf in an object named after its field. {} (no conditions) fits it too.
func RuleGrammar ¶
func RuleGrammar() string
RuleGrammar says how a rule's conditions and actions are written, from the one list of fields and action types the daemon accepts, so no prompt that asks for rules can drift from what validation takes.
Types ¶
type CheckProgress ¶
type CheckProgress struct {
Done int
Total int // emails this check goes through: at most the newest limit of the range
// Matched is how many emails the folder holds in the chosen range before the limit,
// known once the mail is listed. It is more than Total when the limit cut the range.
Matched int
ModelCalls int
Tokens int
CostUSD float64
}
CheckProgress reports how far a check has got. The daemon turns it into a check.progress event and into the "N of M checked", model-call and cost totals the screen shows.
type CheckRow ¶
type CheckRow struct {
Ref mail.MsgRef
From string
Subject string
ReceivedAt int64 // 0 = unknown
Outcome pipeline.Outcome
}
CheckRow is one checked email: how the real flow settled it, kept so a cleanup Sort can apply the same answer without asking the model again. Ref says where the email was when it was checked; Sort verifies it is still there. The display fields live only in the daemon's memory and are never logged or written to the database (MAI-44).
type Composer ¶
type Composer struct {
Store *store.Store
Gen models.Generator
Now func() time.Time // nil = time.Now
BodyChars int
}
Composer turns free text into draft rules. It saves nothing.
func (Composer) Compose ¶
Compose runs the compose flow: build the context (the user's rules, the folders that exist, the condition fields), have the model write drafts through a forced tool call, validate every draft with the validator saved rules get, attaching what is wrong to the draft instead of failing the request, and test the valid drafts on the account's newest mail. Nothing is saved.
type Conflict ¶
type Conflict struct {
RuleID int64 `json:"rule_id"`
Kind string `json:"kind"` // overlap | duplicate | shadowed
Note string `json:"note"`
}
Conflict is an existing rule a draft collides with.
type Draft ¶
type Draft struct {
Name string `json:"name"`
Said string `json:"said"`
Intent *string `json:"intent"`
Conditions rules.Cond `json:"conditions"`
Exceptions rules.Cond `json:"exceptions"`
Actions []rules.Action `json:"actions"`
// AccountID, Stack and Model are the rest of a rule, so a draft can be saved as it is.
// The composer does not write them: a new draft is for every account, does not stack
// and has no model of its own; a re-optimized rule keeps what it had.
AccountID *int64 `json:"account_id"`
Stack bool `json:"stack"`
Model string `json:"model"`
MinConfidence *float64 `json:"min_confidence"`
NewFolders []string `json:"new_folders"`
Question *string `json:"question"`
Conflicts []Conflict `json:"conflicts"`
Errors []Problem `json:"errors"`
MatchCount int `json:"match_count"`
// Tested is how many emails MatchCount is out of: the newest ones the draft was tested
// on, or, for a suggestion, the emails scanned. 0 when it was not tested.
Tested int `json:"tested"`
Samples []Row `json:"samples"`
}
Draft is one proposed rule, as a card to review. Its JSON is the contract's RuleDraft.
func ReadDraft ¶
ReadDraft reads one rule an AI wrote with no words of the owner's to cut its wording from (Said stays empty), validated as a composed draft is: a rule suggested from the mail. named lists the folders the AI proposed to create, so they count as named.
type FetchOptions ¶
type FetchOptions struct {
// NoBody reads the headers only: no text is fetched, and Summary.Body is empty.
NoBody bool
// BodyChars cuts the parsed text to this many characters; 0 keeps all the connector
// read.
BodyChars int
}
FetchOptions says how much of each email's text Scanner.Fetch reads.
type Output ¶
type Output struct {
Drafts []Draft
Unparsed []string // parts of the text that could not be turned into a rule
}
Output is what a compose call answers.
type Progress ¶
type Progress struct {
Done int // messages tested or passed over
Total int // messages the run will go through, known once the mail is listed
ModelCalls int // model calls made so far
}
Progress is how far a run has got. A run that finds no mail reports one 0 of 0.
type Reader ¶
type Reader interface {
FetchMany(ctx context.Context, refs []mail.MsgRef, maxBody int) ([]*message.Raw, error)
FetchSince(ctx context.Context, folder string, since time.Time, limit int) ([]mail.MsgRef, int, error)
}
Reader is the read-only part of mail.Mailbox. FetchMany reads with BODY.PEEK, so a tested message stays unread; there is no method here that could move or flag one.
type Request ¶
type Request struct {
UserID int64
Text string
// Rule, when set, is re-optimized: the model is given its original wording with Text
// and answers with a single draft to replace it.
Rule *rules.Rule
// Account is the mailbox the drafts are checked against: its folders are the ones that
// exist, and its newest mail is what the drafts are tested on. nil = no mailbox is
// connected: the folders of every account count, and nothing is tested.
Account *store.Account
Mailbox Reader // nil = the account is offline: the drafts are not tested
Decider pipeline.Decider // decides the test run; a nil Router leaves intent drafts untested
}
Request is one compose call.
type Result ¶
type Result struct {
Rows []Row `json:"results"` // newest first
Tested int `json:"tested"`
Matched int `json:"matched"` // a rule or sender rule would have applied
ModelCalls int `json:"model_calls"`
CostUSD float64 `json:"cost_usd"`
}
Result is a finished test run. Its JSON is the contract's TestResult.
type Row ¶
type Row struct {
From string `json:"from"`
Subject string `json:"subject"`
ReceivedAt *int64 `json:"received_at"`
Stage string `json:"stage"`
RuleID *int64 `json:"rule_id"` // null = no rule, or a draft (see rule_name)
RuleName string `json:"rule_name"`
Confidence float64 `json:"confidence"`
Reason string `json:"reason"`
Review bool `json:"review"` // below the threshold: it would go to Needs review
Actions []rules.Action `json:"actions"`
// contains filtered or unexported fields
}
Row is what one email would get. Its JSON is the contract's TestRow.
type Scanner ¶
type Scanner struct {
Store *store.Store // the contacts index, for the is_contact and replied_before signals
Mailbox Reader
AccountID int64
}
Scanner reads the mail already in one account's folders for a feature that is not in this package (ext.Mail): the same listing and BODY.PEEK fetches the tester uses, so nothing is marked read, and it is given only Reader, so it cannot move or flag anything.
func (Scanner) Fetch ¶
func (s Scanner) Fetch(ctx context.Context, refs []mail.MsgRef, opts FetchOptions, each func(ctx context.Context, i int, email *message.Summary, seen bool) error) error
Fetch reads the listed emails with BODY.PEEK, in batches, and calls each for every one with its index in refs, whether the owner has read it and its summary, nil when it is gone or cannot be read as an email. each is called for several emails at once; its first error ends the fetch and is returned.
func (Scanner) List ¶
func (s Scanner) List(ctx context.Context, folder string, since time.Time, limit int) ([]mail.MsgRef, int, error)
List lists the newest limit emails of folder received on or after since (zero = all of it), newest last, and how many the folder holds in that range before limit cut it.
type Tester ¶
type Tester struct {
Store *store.Store // the contacts index, for the is_contact and replied_before signals
Mailbox Reader
AccountID int64
// Decider is the same step the live pipeline decides with. Give it routers made with
// models.Router.For("test"), so the calls are booked as tests.
Decider pipeline.Decider
BodyChars int
}
Tester runs rules over the newest mail of one folder and reports what each email would get. Bodies are held in memory for one batch of fetched messages only.
func (Tester) Check ¶
func (t Tester) Check(ctx context.Context, rs []rules.Rule, senders []rules.SenderRule, folder string, since time.Time, limit int, progress func(CheckProgress)) ([]CheckRow, error)
Check runs every selected email of folder (the mail received since, newest limit kept) through the real flow — sender rules, conditions, then the decision model for rules with an intent, exactly as live processing decides — and returns one row per email with what was settled, newest first. It moves nothing and records nothing: the model is really asked and really paid (the caller books the calls under the "cleanup" purpose), but no mailbox is changed and no activity is written. progress, when set, is called once the mail is listed (0 of N) and after every email, never concurrently.
func (Tester) Run ¶
func (t Tester) Run(ctx context.Context, rs []rules.Rule, senders []rules.SenderRule, folder string, limit int, progress func(Progress)) (Result, error)
Run evaluates rs (and the sender rules) against the newest limit messages of folder. Rules with an id below 1 are drafts: they take part like saved rules and are reported by name only. progress, when set, is called once the mail is listed (0 done) and after every message, never concurrently. A message that is gone or unreadable by the time it is fetched is left out.