auto deployment und tests

This commit is contained in:
Christoph K.
2026-03-20 07:07:38 +01:00
parent 0e7aa3e7f2
commit 8163f906cc
12 changed files with 500 additions and 66 deletions

View File

@@ -1,18 +1,26 @@
// agent.go Gemeinsames Interface für alle Agenten
package agents
// HistoryMessage repräsentiert eine vorherige Konversationsnachricht.
type HistoryMessage struct {
Role string // "user" oder "assistant"
Content string
}
// Request enthält die Eingabe für einen Agenten.
type Request struct {
Action string // z.B. "store", "list", "done", "summary"
Args []string // Argumente für die Aktion
Author string // Discord-Username (für Kontext)
Source string // Herkunft (z.B. "discord/#channelID")
Action string // z.B. "store", "list", "done", "summary"
Args []string // Argumente für die Aktion
Author string // Discord-Username (für Kontext)
Source string // Herkunft (z.B. "discord/#channelID")
History []HistoryMessage // Konversationsverlauf (für Chat-Gedächtnis)
}
// Response enthält die Ausgabe eines Agenten.
type Response struct {
Text string // Formattierte Antwort
Error error // Fehler, falls aufgetreten
Text string // Formattierte Antwort
Error error // Fehler, falls aufgetreten
RawAnswer string // Unformatierte LLM-Antwort (für Konversationsverlauf)
}
// Agent ist das gemeinsame Interface für alle Agenten.

View File

@@ -20,7 +20,7 @@ func (a *Agent) Handle(req agents.Request) agents.Response {
}
question := strings.Join(req.Args, " ")
answer, chunks, err := brain.AskQuery(question)
answer, chunks, err := brain.AskQuery(question, req.History)
if err != nil {
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
}
@@ -35,5 +35,5 @@ func (a *Agent) Handle(req agents.Request) agents.Response {
for _, chunk := range chunks {
fmt.Fprintf(&sb, "• %.1f%% %s\n", chunk.Score*100, chunk.Source)
}
return agents.Response{Text: sb.String()}
return agents.Response{Text: sb.String(), RawAnswer: answer}
}

View File

@@ -4,6 +4,7 @@ package task
import (
"fmt"
"strings"
"time"
"my-brain-importer/internal/agents"
)
@@ -34,12 +35,41 @@ func (a *Agent) Handle(req agents.Request) agents.Response {
}
}
// parseAddArgs parst Text, --due YYYY-MM-DD und --priority WERT aus den Args.
func parseAddArgs(args []string) (text, priority string, dueDate *time.Time) {
var textParts []string
for i := 0; i < len(args); i++ {
switch args[i] {
case "--due", "-d":
i++
if i < len(args) {
t, err := time.Parse("2006-01-02", args[i])
if err == nil {
dueDate = &t
}
}
case "--priority", "-p":
i++
if i < len(args) {
priority = strings.ToLower(args[i])
}
default:
textParts = append(textParts, args[i])
}
}
text = strings.Join(textParts, " ")
return
}
func (a *Agent) add(req agents.Request) agents.Response {
if len(req.Args) == 0 {
return agents.Response{Text: "❌ Kein Task-Text angegeben."}
}
text := strings.Join(req.Args, " ")
t, err := a.store.Add(text)
text, priority, dueDate := parseAddArgs(req.Args)
if text == "" {
return agents.Response{Text: "❌ Kein Task-Text angegeben."}
}
t, err := a.store.Add(text, priority, dueDate)
if err != nil {
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
}
@@ -47,7 +77,18 @@ func (a *Agent) add(req agents.Request) agents.Response {
if len(shortID) > 6 {
shortID = shortID[len(shortID)-6:]
}
return agents.Response{Text: fmt.Sprintf("✅ Task hinzugefügt: `%s` (ID: `%s`)", t.Text, shortID)}
var extras []string
if t.Priority != "" {
extras = append(extras, "Priorität: "+t.Priority)
}
if t.DueDate != nil {
extras = append(extras, "Fällig: "+t.DueDate.Format("02.01.2006"))
}
info := ""
if len(extras) > 0 {
info = " (" + strings.Join(extras, ", ") + ")"
}
return agents.Response{Text: fmt.Sprintf("✅ Task hinzugefügt: `%s`%s (ID: `%s`)", t.Text, info, shortID)}
}
func (a *Agent) list() agents.Response {
@@ -59,6 +100,9 @@ func (a *Agent) list() agents.Response {
return agents.Response{Text: "📋 Keine Tasks vorhanden."}
}
today := time.Now().Truncate(24 * time.Hour)
tomorrow := today.Add(24 * time.Hour)
var sb strings.Builder
sb.WriteString("📋 **Task-Liste:**\n\n")
openCount := 0
@@ -73,7 +117,39 @@ func (a *Agent) list() agents.Response {
if len(shortID) > 6 {
shortID = shortID[len(shortID)-6:]
}
fmt.Fprintf(&sb, "%s `%s` %s\n", status, shortID, t.Text)
var meta []string
if t.Priority != "" {
switch t.Priority {
case "hoch":
meta = append(meta, "🔴 hoch")
case "mittel":
meta = append(meta, "🟡 mittel")
case "niedrig":
meta = append(meta, "🟢 niedrig")
default:
meta = append(meta, t.Priority)
}
}
if t.DueDate != nil && !t.Done {
due := t.DueDate.Truncate(24 * time.Hour)
switch {
case due.Before(today):
meta = append(meta, "⏰ **ÜBERFÄLLIG** "+t.DueDate.Format("02.01."))
case due.Equal(today):
meta = append(meta, "⏰ heute fällig")
case due.Equal(tomorrow):
meta = append(meta, "📅 morgen fällig")
default:
meta = append(meta, "📅 "+t.DueDate.Format("02.01.2006"))
}
}
line := fmt.Sprintf("%s `%s` %s", status, shortID, t.Text)
if len(meta) > 0 {
line += " · " + strings.Join(meta, " · ")
}
sb.WriteString(line + "\n")
}
fmt.Fprintf(&sb, "\n*%d offen, %d gesamt*", openCount, len(tasks))
return agents.Response{Text: sb.String()}

View File

@@ -18,6 +18,8 @@ type Task struct {
Done bool `json:"done"`
CreatedAt time.Time `json:"created_at"`
DoneAt *time.Time `json:"done_at,omitempty"`
DueDate *time.Time `json:"due_date,omitempty"`
Priority string `json:"priority,omitempty"` // "hoch", "mittel", "niedrig"
}
// Store verwaltet tasks.json mit atomischen Schreiboperationen.
@@ -69,7 +71,7 @@ func (s *Store) save(tasks []Task) error {
}
// Add fügt einen neuen Task hinzu.
func (s *Store) Add(text string) (Task, error) {
func (s *Store) Add(text, priority string, dueDate *time.Time) (Task, error) {
s.mu.Lock()
defer s.mu.Unlock()
@@ -83,6 +85,8 @@ func (s *Store) Add(text string) (Task, error) {
Text: text,
Done: false,
CreatedAt: time.Now(),
Priority: priority,
DueDate: dueDate,
}
tasks = append(tasks, t)

View File

@@ -126,6 +126,52 @@ func (cl *Client) FetchUnread() ([]Message, error) {
return parseMessages(msgs), nil
}
// FetchUnreadSeqNums holt ungelesene Emails und gibt zusätzlich die Sequenznummern zurück.
// Selektiert den Ordner im Lese-Schreib-Modus (für nachfolgendes Verschieben).
func (cl *Client) FetchUnreadSeqNums() ([]Message, []uint32, error) {
folder := config.Cfg.Email.Folder
if folder == "" {
folder = "INBOX"
}
if _, err := cl.c.Select(folder, nil).Wait(); err != nil {
return nil, nil, fmt.Errorf("IMAP select: %w", err)
}
searchData, err := cl.c.Search(&imap.SearchCriteria{
NotFlag: []imap.Flag{imap.FlagSeen},
}, nil).Wait()
if err != nil {
return nil, nil, fmt.Errorf("IMAP search: %w", err)
}
seqNums := searchData.AllSeqNums()
if len(seqNums) == 0 {
return nil, nil, nil
}
var seqSet imap.SeqSet
seqSet.AddNum(seqNums...)
msgs, err := cl.c.Fetch(seqSet, &imap.FetchOptions{Envelope: true}).Collect()
if err != nil {
return nil, nil, fmt.Errorf("IMAP fetch: %w", err)
}
return parseMessages(msgs), seqNums, nil
}
// MoveMessages verschiebt Nachrichten in einen anderen IMAP-Ordner.
// Der Ordner muss im Lese-Schreib-Modus selektiert sein (via FetchUnreadSeqNums).
func (cl *Client) MoveMessages(seqNums []uint32, destFolder string) error {
var seqSet imap.SeqSet
seqSet.AddNum(seqNums...)
if _, err := cl.c.Move(seqSet, destFolder).Wait(); err != nil {
return fmt.Errorf("IMAP move: %w", err)
}
return nil
}
func parseMessages(msgs []*imapclient.FetchMessageBuffer) []Message {
result := make([]Message, 0, len(msgs))
for _, msg := range msgs {

View File

@@ -19,6 +19,7 @@ func Summarize() (string, error) {
}
// SummarizeUnread fasst ungelesene Emails zusammen.
// Wenn email.processed_folder konfiguriert ist, werden die Emails danach dorthin verschoben.
func SummarizeUnread() (string, error) {
cl, err := Connect()
if err != nil {
@@ -26,7 +27,16 @@ func SummarizeUnread() (string, error) {
}
defer cl.Close()
msgs, err := cl.FetchUnread()
processedFolder := config.Cfg.Email.ProcessedFolder
var msgs []Message
var seqNums []uint32
if processedFolder != "" {
msgs, seqNums, err = cl.FetchUnreadSeqNums()
} else {
msgs, err = cl.FetchUnread()
}
if err != nil {
return "", fmt.Errorf("Emails abrufen: %w", err)
}
@@ -35,7 +45,21 @@ func SummarizeUnread() (string, error) {
}
slog.Info("Email-Zusammenfassung gestartet", "anzahl", len(msgs), "typ", "unread")
return summarizeWithLLM(msgs, "Fasse diese ungelesenen Emails zusammen. Hebe wichtige, dringende oder actionable Emails hervor.")
result, err := summarizeWithLLM(msgs, "Fasse diese ungelesenen Emails zusammen. Hebe wichtige, dringende oder actionable Emails hervor.")
if err != nil {
return "", err
}
// Nach dem Zusammenfassen: Emails in Processed-Ordner verschieben
if processedFolder != "" && len(seqNums) > 0 {
if moveErr := cl.MoveMessages(seqNums, processedFolder); moveErr != nil {
slog.Warn("Emails konnten nicht verschoben werden", "fehler", moveErr, "ordner", processedFolder)
} else {
slog.Info("Emails verschoben", "anzahl", len(seqNums), "ordner", processedFolder)
}
}
return result, nil
}
// ExtractReminders sucht in den letzten Emails nach Terminen/Deadlines.