auto deployment und tests
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user