De tre sidste af de fem. Alle tre samme sted hen: det, man ellers selv skulle sidde og gøre klokken syv om morgenen. Dagens plan. Én besked om morgenen om det, der forfalder i dag, og det, der er sprunget over. Tallene tælles i basen og gives til modellen som kendsgerninger, den ikke må regne om — en plan, der siger fire opgaver, hvor der er tre, er værre end ingen plan. Er der ingen model, står tallene alene, og det er stadig det, man havde brug for at vide. Er der hverken noget, der forfalder, eller noget, der er sprunget over, kommer der ingen besked: en morgen uden noget at gøre er en god morgen, og en besked om ingenting lærer folk at lade være med at åbne beskeder. Timen er personens egen — klokken syv er syv forskellige steder — og jobbet kører hver time frem for på et klokkeslæt, så en instans, der sov, ikke springer dagen over. Flytter man timen, ryger dagens mærke, så man kan se virkningen i dag i stedet for i morgen. Mails, der giver mening. Opgaven hed, hvad mailen hed: "Anders Jensen: SV: SV: Vedr. levering uge 12". Nok til at genkende, ikke nok til at gøre noget ved. Nu skriver modellen den handling, mailen beder om, og lader være, hvis den ikke beder om nogen. Ét kald pr. gennemløb frem for ét pr. mail — femogtyve mails er ellers femogtyve gange ventetid — og emnet bliver stående i beskrivelsen, så en misforstået mail stadig kan læses. Falder modellen ud, hedder opgaven afsender og emne som før: en model, der er nede, må ikke kunne stoppe posten. Og svar på et spørgsmål. "Hvad lovede jeg Anders i august?" — svaret står spredt over tre noter, og ingen af dem indeholder ordet "lovede". Søgningen kræver, at alle ord optræder, hvilket er rigtigt for et søgefelt og forkert for et spørgsmål, så kandidaterne hentes ord for ord og de noter, flere ord rammer, kommer øverst. Modellen får kun dem: den slår ikke selv noget op, så et svar kan ikke handle om nogen andens noter. Og svaret bærer, hvad det står på — en model, der siger "det gjorde du den 14.", er kun brugbar, hvis man kan slå op i det, den læste det i. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
120 lines
3.9 KiB
Go
120 lines
3.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kristianwind/verdande/internal/ai"
|
|
"github.com/kristianwind/verdande/internal/quickadd"
|
|
"github.com/kristianwind/verdande/internal/store"
|
|
)
|
|
|
|
// En mail, der bliver til en brugbar opgave.
|
|
//
|
|
// Uden det her hedder opgaven, hvad mailen hed: "Anders Jensen: SV: SV: Vedr.
|
|
// levering uge 12". Det er nok til at genkende den og ikke nok til at gøre noget
|
|
// ved den — og en indbakke af den slags linjer er præcis den bunke, man havde
|
|
// mail for at slippe for.
|
|
//
|
|
// Ét kald pr. gennemløb frem for ét pr. mail. Femogtyve mails er femogtyve gange
|
|
// ventetid og femogtyve gange betaling for det samme spørgsmål, og et gennemløb,
|
|
// der tager fem minutter, er et, der overlapper med det næste.
|
|
//
|
|
// Falder det ud, er der ikke sket noget: opgaven hedder afsender og emne, som den
|
|
// altid har gjort. En model, der er nede, må ikke kunne stoppe posten.
|
|
|
|
// mailForAI er én mail, som modellen får den at se.
|
|
type mailForAI struct {
|
|
From string
|
|
Subject string
|
|
Snippet string
|
|
}
|
|
|
|
// mailTaskLines beder modellen om én linje pr. mail, i quick-add-syntaks.
|
|
//
|
|
// Returnerer en tabel fra mailens plads i listen til linjen. Ikke en liste, fordi
|
|
// en model, der springer en mail over, ellers ville flytte alle de følgende
|
|
// linjer én plads — og så ville en opgave hedde noget, der stod i en anden mail.
|
|
// Det er den samme forsigtighed som ved oprydningen i indbakken, og den samme
|
|
// grund: en forskydning på én er den slags fejl, ingen opdager.
|
|
func (s *Server) mailTaskLines(ctx context.Context, user *store.User, mails []mailForAI) map[int]string {
|
|
if len(mails) == 0 {
|
|
return nil
|
|
}
|
|
cfg, err := s.aiConfigFor(ctx, user)
|
|
if err != nil || !cfg.Configured() {
|
|
return nil
|
|
}
|
|
|
|
lines := make([]string, 0, len(mails))
|
|
for i, m := range mails {
|
|
line := strconv.Itoa(i+1) + ". " + m.From + " — " + m.Subject
|
|
if m.Snippet != "" {
|
|
line += "\n " + firstWords(m.Snippet, 40)
|
|
}
|
|
lines = append(lines, line)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 90*time.Second)
|
|
defer cancel()
|
|
|
|
today := time.Now().In(userLocation(user.Timezone)).Format("2006-01-02")
|
|
out, err := ai.New(cfg).TasksFromMail(ctx, lines, today, user.Locale)
|
|
if err != nil {
|
|
s.log.Info("mail tasks without the model", "err", err, "user", user.ID)
|
|
return nil
|
|
}
|
|
|
|
byIndex := map[int]string{}
|
|
for i, sug := range out {
|
|
n, line := numberedLine(sug.Line)
|
|
if n == 0 {
|
|
n = i + 1
|
|
}
|
|
if n < 1 || n > len(mails) || strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
byIndex[n-1] = line
|
|
}
|
|
return byIndex
|
|
}
|
|
|
|
// applyMailLine lægger modellens linje ind i opgaven, hvis der er en.
|
|
//
|
|
// Linjen går gennem quick-add-parseren som alt andet, så en mail, hvor der står
|
|
// "inden fredag", får en dato — og "#Firma" i en linje, modellen fandt på, flytter
|
|
// ingenting, for projektet slås op blandt dem, der findes.
|
|
//
|
|
// Emnet bliver stående i beskrivelsen. Modellen har skrevet en titel ud fra det,
|
|
// og hvis den har misforstået mailen, skal det oprindelige stadig kunne læses ved
|
|
// siden af — ellers er den eneste vej tilbage at finde mailen frem igen.
|
|
func (s *Server) applyMailLine(ctx context.Context, user *store.User, task *store.Task, line, subject string) {
|
|
if strings.TrimSpace(line) == "" {
|
|
return
|
|
}
|
|
parsed := quickadd.Parse(line, time.Now().In(userLocation(user.Timezone)), user.Locale)
|
|
if strings.TrimSpace(parsed.Content) == "" {
|
|
return
|
|
}
|
|
|
|
task.Content = parsed.Content
|
|
if parsed.Priority != 0 {
|
|
task.Priority = parsed.Priority
|
|
}
|
|
if parsed.DueDate != "" {
|
|
task.DueDate = parsed.DueDate
|
|
}
|
|
if parsed.Project != "" {
|
|
if id, err := s.db.ProjectByName(ctx, user.ID, parsed.Project); err == nil {
|
|
if _, err := store.RequireProjectRole(ctx, s.db, id, user.ID, store.RoleEditor); err == nil {
|
|
task.ProjectID = id
|
|
}
|
|
}
|
|
}
|
|
if subject != "" {
|
|
task.Description = strings.TrimSpace(subject + "\n\n" + task.Description)
|
|
}
|
|
}
|