auto deployment und tests
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -24,16 +25,23 @@ import (
|
||||
"my-brain-importer/internal/config"
|
||||
)
|
||||
|
||||
// maxHistoryPairs ist die maximale Anzahl gespeicherter Gesprächspaare pro Channel.
|
||||
const maxHistoryPairs = 10
|
||||
|
||||
var (
|
||||
dg *discordgo.Session
|
||||
botUser *discordgo.User
|
||||
daemonStop = make(chan struct{})
|
||||
dg *discordgo.Session
|
||||
botUser *discordgo.User
|
||||
daemonStop = make(chan struct{})
|
||||
|
||||
researchAgent agents.Agent
|
||||
memoryAgent agents.Agent
|
||||
taskAgent agents.Agent
|
||||
toolAgent agents.Agent
|
||||
|
||||
// Konversationsverlauf pro Channel (in-memory, wird bei Neustart zurückgesetzt).
|
||||
historyMu sync.Mutex
|
||||
historyCache = make(map[string][]agents.HistoryMessage)
|
||||
|
||||
commands = []*discordgo.ApplicationCommand{
|
||||
{
|
||||
Name: "ask",
|
||||
@@ -96,6 +104,8 @@ var (
|
||||
Description: "Neuen Task hinzufügen",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "text", Description: "Der Task", Required: true},
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "faellig", Description: "Fälligkeitsdatum (YYYY-MM-DD)", Required: false},
|
||||
{Type: discordgo.ApplicationCommandOptionString, Name: "prioritaet", Description: "Priorität: hoch, mittel, niedrig", Required: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -145,6 +155,29 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// getHistory gibt den gespeicherten Gesprächsverlauf für einen Channel zurück.
|
||||
func getHistory(channelID string) []agents.HistoryMessage {
|
||||
historyMu.Lock()
|
||||
defer historyMu.Unlock()
|
||||
msgs := historyCache[channelID]
|
||||
result := make([]agents.HistoryMessage, len(msgs))
|
||||
copy(result, msgs)
|
||||
return result
|
||||
}
|
||||
|
||||
// addToHistory speichert eine Nachricht im Gesprächsverlauf eines Channels.
|
||||
func addToHistory(channelID, role, content string) {
|
||||
historyMu.Lock()
|
||||
defer historyMu.Unlock()
|
||||
msgs := historyCache[channelID]
|
||||
msgs = append(msgs, agents.HistoryMessage{Role: role, Content: content})
|
||||
// Maximal maxHistoryPairs Paare (= maxHistoryPairs*2 Nachrichten) behalten
|
||||
if len(msgs) > maxHistoryPairs*2 {
|
||||
msgs = msgs[len(msgs)-maxHistoryPairs*2:]
|
||||
}
|
||||
historyCache[channelID] = msgs
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
@@ -187,6 +220,7 @@ func main() {
|
||||
defer dg.Close()
|
||||
|
||||
registerCommands()
|
||||
sendWelcomeMessage()
|
||||
go startDaemon()
|
||||
|
||||
fmt.Println("✅ Bot läuft. Drücke CTRL+C zum Beenden.")
|
||||
@@ -227,8 +261,18 @@ func onInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
switch data.Name {
|
||||
case "ask", "research":
|
||||
question := data.Options[0].StringValue()
|
||||
channelID := i.ChannelID
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return researchAgent.Handle(agents.Request{Action: agents.ActionQuery, Args: []string{question}})
|
||||
resp := researchAgent.Handle(agents.Request{
|
||||
Action: agents.ActionQuery,
|
||||
Args: []string{question},
|
||||
History: getHistory(channelID),
|
||||
})
|
||||
if resp.RawAnswer != "" {
|
||||
addToHistory(channelID, "user", question)
|
||||
addToHistory(channelID, "assistant", resp.RawAnswer)
|
||||
}
|
||||
return resp
|
||||
})
|
||||
|
||||
case "asknobrain":
|
||||
@@ -277,10 +321,24 @@ func handleMemoryCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
|
||||
func handleTaskCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
sub := i.ApplicationCommandData().Options[0]
|
||||
req := agents.Request{Action: sub.Name} // sub.Name ist bereits der Action-String (add/list/done/delete)
|
||||
if len(sub.Options) > 0 {
|
||||
req := agents.Request{Action: sub.Name}
|
||||
|
||||
if sub.Name == "add" && len(sub.Options) > 0 {
|
||||
// Baue args mit Text + optionalen Flags für due/priority
|
||||
args := []string{sub.Options[0].StringValue()}
|
||||
for _, opt := range sub.Options[1:] {
|
||||
switch opt.Name {
|
||||
case "faellig":
|
||||
args = append(args, "--due", opt.StringValue())
|
||||
case "prioritaet":
|
||||
args = append(args, "--priority", opt.StringValue())
|
||||
}
|
||||
}
|
||||
req.Args = args
|
||||
} else if len(sub.Options) > 0 {
|
||||
req.Args = []string{sub.Options[0].StringValue()}
|
||||
}
|
||||
|
||||
handleAgentResponse(s, i, func() agents.Response {
|
||||
return taskAgent.Handle(req)
|
||||
})
|
||||
@@ -350,7 +408,7 @@ func onMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
|
||||
slog.Info("Mention", "user", m.Author.Username, "channel", m.ChannelID, "nachricht", question)
|
||||
s.ChannelTyping(m.ChannelID)
|
||||
resp := routeMessage(question, getAuthorFromMessage(m))
|
||||
resp := routeMessage(question, getAuthorFromMessage(m), m.ChannelID)
|
||||
if resp.Error != nil {
|
||||
slog.Error("Mention-Fehler", "fehler", resp.Error)
|
||||
}
|
||||
@@ -368,10 +426,10 @@ func SendMessage(channelID, text string) error {
|
||||
|
||||
// routeMessage leitet eine @mention-Nachricht an den passenden Agenten weiter.
|
||||
// Unterstützte Präfixe: "email ...", "task ...", "remember ...", sonst Research.
|
||||
func routeMessage(text, author string) agents.Response {
|
||||
func routeMessage(text, author, channelID string) agents.Response {
|
||||
words := strings.Fields(text)
|
||||
if len(words) == 0 {
|
||||
return agents.Response{Text: "Stell mir eine Frage oder nutze: `email summary`, `email unread`, `email remind`, `task list`, `task add <text>`, `task done <id>`, `remember <text>`"}
|
||||
return agents.Response{Text: "Stell mir eine Frage oder nutze: `email summary`, `email unread`, `email remind`, `task list`, `task add <text> [--due YYYY-MM-DD] [--priority hoch]`, `task done <id>`, `remember <text>`"}
|
||||
}
|
||||
|
||||
cmd := strings.ToLower(words[0])
|
||||
@@ -402,7 +460,16 @@ func routeMessage(text, author string) agents.Response {
|
||||
})
|
||||
|
||||
default:
|
||||
return researchAgent.Handle(agents.Request{Action: agents.ActionQuery, Args: words})
|
||||
resp := researchAgent.Handle(agents.Request{
|
||||
Action: agents.ActionQuery,
|
||||
Args: words,
|
||||
History: getHistory(channelID),
|
||||
})
|
||||
if resp.RawAnswer != "" {
|
||||
addToHistory(channelID, "user", text)
|
||||
addToHistory(channelID, "assistant", resp.RawAnswer)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -413,6 +480,44 @@ func getAuthorFromMessage(m *discordgo.MessageCreate) string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// sendWelcomeMessage schickt beim Bot-Start eine Begrüßung in den konfigurierten Daemon-Channel.
|
||||
func sendWelcomeMessage() {
|
||||
channelID := config.Cfg.Daemon.ChannelID
|
||||
if channelID == "" {
|
||||
return
|
||||
}
|
||||
msg := `🤖 **Brain-Bot ist online!**
|
||||
|
||||
**Slash-Commands:**
|
||||
` + "```" + `
|
||||
/ask <frage> – Wissensdatenbank abfragen
|
||||
/research <frage> – Alias für /ask
|
||||
/asknobrain <frage> – Direkt ans LLM (kein RAG)
|
||||
/memory store <text> – Text in Wissensdatenbank speichern
|
||||
/memory ingest – Markdown-Notizen neu einlesen
|
||||
/task add <text> [--due YYYY-MM-DD] [--priority hoch|mittel|niedrig]
|
||||
/task list – Alle Tasks anzeigen
|
||||
/task done <id> – Task erledigen
|
||||
/task delete <id> – Task löschen
|
||||
/email summary – Letzte Emails zusammenfassen
|
||||
/email unread – Ungelesene Emails zusammenfassen
|
||||
/email remind – Termine aus Emails extrahieren
|
||||
` + "```" + `
|
||||
**@Mention:**
|
||||
` + "```" + `
|
||||
@Brain <frage> – Wissensdatenbank (mit Chat-Gedächtnis)
|
||||
@Brain task add <text> [--due ...] [--priority ...]
|
||||
@Brain task list / done / delete
|
||||
@Brain email summary / unread / remind
|
||||
@Brain remember <text>
|
||||
` + "```" + `
|
||||
⚙️ Daemon aktiv: Email-Check + tägliches Morgen-Briefing`
|
||||
|
||||
if _, err := dg.ChannelMessageSend(channelID, msg); err != nil {
|
||||
log.Printf("⚠️ Willkommensnachricht konnte nicht gesendet werden: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getAuthor(i *discordgo.InteractionCreate) string {
|
||||
if i.Member != nil {
|
||||
return i.Member.User.Username
|
||||
@@ -445,8 +550,8 @@ func startDaemon() {
|
||||
emailTicker := time.NewTicker(emailInterval)
|
||||
defer emailTicker.Stop()
|
||||
|
||||
taskTimer := scheduleDaily(reminderHour, 0)
|
||||
defer taskTimer.Stop()
|
||||
briefingTimer := scheduleDaily(reminderHour, 0)
|
||||
defer briefingTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -469,29 +574,11 @@ func startDaemon() {
|
||||
slog.Info("Daemon: Keine neuen Emails")
|
||||
}
|
||||
|
||||
case <-taskTimer.C:
|
||||
slog.Info("Daemon: Task-Reminder gestartet")
|
||||
store := task.NewStore()
|
||||
open, err := store.OpenTasks()
|
||||
if err != nil {
|
||||
slog.Error("Daemon Task-Fehler", "fehler", err)
|
||||
continue
|
||||
}
|
||||
if len(open) > 0 {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "📋 **Tägliche Task-Erinnerung** – %d offene Tasks:\n\n", len(open))
|
||||
for _, t := range open {
|
||||
shortID := t.ID
|
||||
if len(shortID) > 6 {
|
||||
shortID = shortID[len(shortID)-6:]
|
||||
}
|
||||
fmt.Fprintf(&sb, "⬜ `%s` – %s\n", shortID, t.Text)
|
||||
}
|
||||
dg.ChannelMessageSend(channelID, sb.String())
|
||||
slog.Info("Daemon: Task-Reminder gesendet", "offene_tasks", len(open))
|
||||
}
|
||||
taskTimer.Stop()
|
||||
taskTimer = scheduleDaily(reminderHour, 0)
|
||||
case <-briefingTimer.C:
|
||||
slog.Info("Daemon: Morgen-Briefing gestartet")
|
||||
dailyBriefing(channelID)
|
||||
briefingTimer.Stop()
|
||||
briefingTimer = scheduleDaily(reminderHour, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,6 +591,63 @@ func scheduleDaily(hour, minute int) *time.Timer {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
d := next.Sub(now)
|
||||
log.Printf("⏰ Nächster Task-Reminder in %v (um %02d:%02d)", d.Round(time.Minute), hour, minute)
|
||||
log.Printf("⏰ Nächstes Morgen-Briefing in %v (um %02d:%02d)", d.Round(time.Minute), hour, minute)
|
||||
return time.NewTimer(d)
|
||||
}
|
||||
|
||||
// dailyBriefing sendet eine kombinierte Morgen-Zusammenfassung: offene Tasks + ungelesene Emails.
|
||||
func dailyBriefing(channelID string) {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
tomorrow := today.Add(24 * time.Hour)
|
||||
|
||||
// Tasks
|
||||
var taskSection strings.Builder
|
||||
store := task.NewStore()
|
||||
open, err := store.OpenTasks()
|
||||
if err != nil {
|
||||
slog.Error("Daemon Briefing Task-Fehler", "fehler", err)
|
||||
} else if len(open) > 0 {
|
||||
fmt.Fprintf(&taskSection, "📋 **Offene Tasks (%d):**\n", len(open))
|
||||
for _, t := range open {
|
||||
shortID := t.ID
|
||||
if len(shortID) > 6 {
|
||||
shortID = shortID[len(shortID)-6:]
|
||||
}
|
||||
urgency := ""
|
||||
if t.DueDate != nil {
|
||||
due := t.DueDate.Truncate(24 * time.Hour)
|
||||
switch {
|
||||
case due.Before(today):
|
||||
urgency = " ⚠️ **ÜBERFÄLLIG**"
|
||||
case due.Equal(today):
|
||||
urgency = " 🔴 *heute fällig*"
|
||||
case due.Equal(tomorrow):
|
||||
urgency = " 🟡 *morgen fällig*"
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&taskSection, "⬜ `%s` – %s%s\n", shortID, t.Text, urgency)
|
||||
}
|
||||
}
|
||||
|
||||
// Emails
|
||||
var emailSection string
|
||||
notify, err := email.SummarizeUnread()
|
||||
if err != nil {
|
||||
slog.Error("Daemon Briefing Email-Fehler", "fehler", err)
|
||||
} else if notify != "📭 Keine ungelesenen Emails." {
|
||||
emailSection = "\n\n📧 **Ungelesene Emails:**\n" + notify
|
||||
}
|
||||
|
||||
var msg strings.Builder
|
||||
fmt.Fprintf(&msg, "☀️ **Morgen-Briefing** – %s\n\n", time.Now().Format("02.01.2006"))
|
||||
if taskSection.Len() > 0 {
|
||||
msg.WriteString(taskSection.String())
|
||||
}
|
||||
msg.WriteString(emailSection)
|
||||
if taskSection.Len() == 0 && emailSection == "" {
|
||||
msg.WriteString("✨ Keine offenen Tasks und keine ungelesenen Emails. Guten Morgen!")
|
||||
}
|
||||
|
||||
dg.ChannelMessageSend(channelID, msg.String())
|
||||
slog.Info("Daemon: Morgen-Briefing gesendet", "offene_tasks", len(open))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user