auto deployment und tests
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
bin/
|
bin/
|
||||||
config.yml
|
config.yml
|
||||||
|
deploy.env
|
||||||
ask
|
ask
|
||||||
discord
|
discord
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ import (
|
|||||||
"my-brain-importer/internal/config"
|
"my-brain-importer/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxHistoryPairs ist die maximale Anzahl gespeicherter Gesprächspaare pro Channel.
|
||||||
|
const maxHistoryPairs = 10
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dg *discordgo.Session
|
dg *discordgo.Session
|
||||||
botUser *discordgo.User
|
botUser *discordgo.User
|
||||||
@@ -34,6 +38,10 @@ var (
|
|||||||
taskAgent agents.Agent
|
taskAgent agents.Agent
|
||||||
toolAgent 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{
|
commands = []*discordgo.ApplicationCommand{
|
||||||
{
|
{
|
||||||
Name: "ask",
|
Name: "ask",
|
||||||
@@ -96,6 +104,8 @@ var (
|
|||||||
Description: "Neuen Task hinzufügen",
|
Description: "Neuen Task hinzufügen",
|
||||||
Options: []*discordgo.ApplicationCommandOption{
|
Options: []*discordgo.ApplicationCommandOption{
|
||||||
{Type: discordgo.ApplicationCommandOptionString, Name: "text", Description: "Der Task", Required: true},
|
{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() {
|
func main() {
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
|
|
||||||
@@ -187,6 +220,7 @@ func main() {
|
|||||||
defer dg.Close()
|
defer dg.Close()
|
||||||
|
|
||||||
registerCommands()
|
registerCommands()
|
||||||
|
sendWelcomeMessage()
|
||||||
go startDaemon()
|
go startDaemon()
|
||||||
|
|
||||||
fmt.Println("✅ Bot läuft. Drücke CTRL+C zum Beenden.")
|
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 {
|
switch data.Name {
|
||||||
case "ask", "research":
|
case "ask", "research":
|
||||||
question := data.Options[0].StringValue()
|
question := data.Options[0].StringValue()
|
||||||
|
channelID := i.ChannelID
|
||||||
handleAgentResponse(s, i, func() agents.Response {
|
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":
|
case "asknobrain":
|
||||||
@@ -277,10 +321,24 @@ func handleMemoryCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|||||||
|
|
||||||
func handleTaskCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
func handleTaskCommand(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||||
sub := i.ApplicationCommandData().Options[0]
|
sub := i.ApplicationCommandData().Options[0]
|
||||||
req := agents.Request{Action: sub.Name} // sub.Name ist bereits der Action-String (add/list/done/delete)
|
req := agents.Request{Action: sub.Name}
|
||||||
if len(sub.Options) > 0 {
|
|
||||||
|
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()}
|
req.Args = []string{sub.Options[0].StringValue()}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAgentResponse(s, i, func() agents.Response {
|
handleAgentResponse(s, i, func() agents.Response {
|
||||||
return taskAgent.Handle(req)
|
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)
|
slog.Info("Mention", "user", m.Author.Username, "channel", m.ChannelID, "nachricht", question)
|
||||||
s.ChannelTyping(m.ChannelID)
|
s.ChannelTyping(m.ChannelID)
|
||||||
resp := routeMessage(question, getAuthorFromMessage(m))
|
resp := routeMessage(question, getAuthorFromMessage(m), m.ChannelID)
|
||||||
if resp.Error != nil {
|
if resp.Error != nil {
|
||||||
slog.Error("Mention-Fehler", "fehler", resp.Error)
|
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.
|
// routeMessage leitet eine @mention-Nachricht an den passenden Agenten weiter.
|
||||||
// Unterstützte Präfixe: "email ...", "task ...", "remember ...", sonst Research.
|
// 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)
|
words := strings.Fields(text)
|
||||||
if len(words) == 0 {
|
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])
|
cmd := strings.ToLower(words[0])
|
||||||
@@ -402,7 +460,16 @@ func routeMessage(text, author string) agents.Response {
|
|||||||
})
|
})
|
||||||
|
|
||||||
default:
|
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"
|
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 {
|
func getAuthor(i *discordgo.InteractionCreate) string {
|
||||||
if i.Member != nil {
|
if i.Member != nil {
|
||||||
return i.Member.User.Username
|
return i.Member.User.Username
|
||||||
@@ -445,8 +550,8 @@ func startDaemon() {
|
|||||||
emailTicker := time.NewTicker(emailInterval)
|
emailTicker := time.NewTicker(emailInterval)
|
||||||
defer emailTicker.Stop()
|
defer emailTicker.Stop()
|
||||||
|
|
||||||
taskTimer := scheduleDaily(reminderHour, 0)
|
briefingTimer := scheduleDaily(reminderHour, 0)
|
||||||
defer taskTimer.Stop()
|
defer briefingTimer.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -469,29 +574,11 @@ func startDaemon() {
|
|||||||
slog.Info("Daemon: Keine neuen Emails")
|
slog.Info("Daemon: Keine neuen Emails")
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-taskTimer.C:
|
case <-briefingTimer.C:
|
||||||
slog.Info("Daemon: Task-Reminder gestartet")
|
slog.Info("Daemon: Morgen-Briefing gestartet")
|
||||||
store := task.NewStore()
|
dailyBriefing(channelID)
|
||||||
open, err := store.OpenTasks()
|
briefingTimer.Stop()
|
||||||
if err != nil {
|
briefingTimer = scheduleDaily(reminderHour, 0)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,6 +591,63 @@ func scheduleDaily(hour, minute int) *time.Timer {
|
|||||||
next = next.Add(24 * time.Hour)
|
next = next.Add(24 * time.Hour)
|
||||||
}
|
}
|
||||||
d := next.Sub(now)
|
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)
|
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))
|
||||||
|
}
|
||||||
|
|||||||
16
deploy.env.example
Normal file
16
deploy.env.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Deploy-Konfiguration – Kopieren als deploy.env und anpassen
|
||||||
|
# deploy.env wird NICHT in Git eingecheckt
|
||||||
|
|
||||||
|
DEPLOY_HOST=192.168.1.118
|
||||||
|
DEPLOY_USER=todo
|
||||||
|
DEPLOY_PASS=geheim
|
||||||
|
|
||||||
|
# Zielverzeichnis auf dem Server
|
||||||
|
DEPLOY_DIR=/home/jacek/brain-bot
|
||||||
|
|
||||||
|
# Systemd-Servicename
|
||||||
|
SERVICE_NAME=brain-bot
|
||||||
|
|
||||||
|
# config.yml mitdeployen? (true/false)
|
||||||
|
# false = config.yml bleibt auf dem Server unangetastet
|
||||||
|
DEPLOY_CONFIG=true
|
||||||
100
deploy.sh
Executable file
100
deploy.sh
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy.sh – Baut den Bot und deployt ihn per SSH auf den Server
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Credentials aus deploy.env laden
|
||||||
|
ENV_FILE="$(dirname "$0")/deploy.env"
|
||||||
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
|
echo "❌ deploy.env nicht gefunden. Kopiere deploy.env.example nach deploy.env und passe sie an."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
source "$ENV_FILE"
|
||||||
|
|
||||||
|
: "${DEPLOY_HOST:?DEPLOY_HOST fehlt in deploy.env}"
|
||||||
|
: "${DEPLOY_USER:?DEPLOY_USER fehlt in deploy.env}"
|
||||||
|
: "${DEPLOY_PASS:?DEPLOY_PASS fehlt in deploy.env}"
|
||||||
|
: "${DEPLOY_DIR:=/home/${DEPLOY_USER}/brain-bot}"
|
||||||
|
: "${SERVICE_NAME:=brain-bot}"
|
||||||
|
: "${DEPLOY_CONFIG:=true}"
|
||||||
|
|
||||||
|
# Sudo-Passwort: standardmäßig gleich wie SSH-Passwort
|
||||||
|
SUDO_PASS="${SUDO_PASS:-$DEPLOY_PASS}"
|
||||||
|
|
||||||
|
SSH_OPTS="-o StrictHostKeyChecking=no -o BatchMode=no"
|
||||||
|
|
||||||
|
if ! command -v sshpass &>/dev/null; then
|
||||||
|
echo "❌ sshpass nicht installiert: sudo apt install sshpass"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh_cmd() {
|
||||||
|
sshpass -p "$DEPLOY_PASS" ssh $SSH_OPTS "${DEPLOY_USER}@${DEPLOY_HOST}" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
scp_cmd() {
|
||||||
|
sshpass -p "$DEPLOY_PASS" scp $SSH_OPTS "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# sudo ohne TTY: Passwort per stdin mit -S
|
||||||
|
sudo_cmd() {
|
||||||
|
ssh_cmd "echo '${SUDO_PASS}' | sudo -S $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build ────────────────────────────────────────────────────────────────────
|
||||||
|
echo "🔨 Baue Linux-Binary (amd64)..."
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o bin/discord-linux ./cmd/discord/
|
||||||
|
echo "✅ Binary gebaut"
|
||||||
|
|
||||||
|
# ── Dateien übertragen ───────────────────────────────────────────────────────
|
||||||
|
echo "📁 Zielverzeichnis ${DEPLOY_DIR} auf ${DEPLOY_HOST}..."
|
||||||
|
ssh_cmd "mkdir -p '${DEPLOY_DIR}'"
|
||||||
|
|
||||||
|
echo "🚀 Übertrage Binary..."
|
||||||
|
scp_cmd bin/discord-linux "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_DIR}/discord"
|
||||||
|
ssh_cmd "chmod +x '${DEPLOY_DIR}/discord'"
|
||||||
|
|
||||||
|
if [[ "$DEPLOY_CONFIG" == "true" ]]; then
|
||||||
|
echo "📋 Übertrage config.yml..."
|
||||||
|
scp_cmd config.yml "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_DIR}/config.yml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Systemd-Service ──────────────────────────────────────────────────────────
|
||||||
|
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||||
|
|
||||||
|
if ! ssh_cmd "test -f '${SERVICE_FILE}'" 2>/dev/null; then
|
||||||
|
echo "📝 Installiere systemd-Service..."
|
||||||
|
|
||||||
|
# Service-Datei zuerst in /tmp schreiben (kein sudo nötig), dann verschieben
|
||||||
|
TMP_SERVICE="/tmp/${SERVICE_NAME}.service"
|
||||||
|
ssh_cmd "cat > '${TMP_SERVICE}'" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=my-brain-importer Discord Bot
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=${DEPLOY_DIR}/discord
|
||||||
|
WorkingDirectory=${DEPLOY_DIR}
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
User=${DEPLOY_USER}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo_cmd "mv '${TMP_SERVICE}' '${SERVICE_FILE}'"
|
||||||
|
sudo_cmd "systemctl daemon-reload"
|
||||||
|
sudo_cmd "systemctl enable '${SERVICE_NAME}'"
|
||||||
|
echo "✅ Service installiert und aktiviert"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Neustart ─────────────────────────────────────────────────────────────────
|
||||||
|
echo "🔄 Starte Service neu..."
|
||||||
|
# unmask falls der Service als masked markiert ist
|
||||||
|
sudo_cmd "systemctl unmask '${SERVICE_NAME}'" 2>/dev/null || true
|
||||||
|
sudo_cmd "systemctl restart '${SERVICE_NAME}'"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployment abgeschlossen!"
|
||||||
|
echo " Status: ssh ${DEPLOY_USER}@${DEPLOY_HOST} 'systemctl status ${SERVICE_NAME}'"
|
||||||
|
echo " Logs: ssh ${DEPLOY_USER}@${DEPLOY_HOST} 'journalctl -u ${SERVICE_NAME} -f'"
|
||||||
@@ -1,18 +1,26 @@
|
|||||||
// agent.go – Gemeinsames Interface für alle Agenten
|
// agent.go – Gemeinsames Interface für alle Agenten
|
||||||
package agents
|
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.
|
// Request enthält die Eingabe für einen Agenten.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
Action string // z.B. "store", "list", "done", "summary"
|
Action string // z.B. "store", "list", "done", "summary"
|
||||||
Args []string // Argumente für die Aktion
|
Args []string // Argumente für die Aktion
|
||||||
Author string // Discord-Username (für Kontext)
|
Author string // Discord-Username (für Kontext)
|
||||||
Source string // Herkunft (z.B. "discord/#channelID")
|
Source string // Herkunft (z.B. "discord/#channelID")
|
||||||
|
History []HistoryMessage // Konversationsverlauf (für Chat-Gedächtnis)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response enthält die Ausgabe eines Agenten.
|
// Response enthält die Ausgabe eines Agenten.
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Text string // Formattierte Antwort
|
Text string // Formattierte Antwort
|
||||||
Error error // Fehler, falls aufgetreten
|
Error error // Fehler, falls aufgetreten
|
||||||
|
RawAnswer string // Unformatierte LLM-Antwort (für Konversationsverlauf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent ist das gemeinsame Interface für alle Agenten.
|
// 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, " ")
|
question := strings.Join(req.Args, " ")
|
||||||
|
|
||||||
answer, chunks, err := brain.AskQuery(question)
|
answer, chunks, err := brain.AskQuery(question, req.History)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
|
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 {
|
for _, chunk := range chunks {
|
||||||
fmt.Fprintf(&sb, "• %.1f%% – %s\n", chunk.Score*100, chunk.Source)
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"my-brain-importer/internal/agents"
|
"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 {
|
func (a *Agent) add(req agents.Request) agents.Response {
|
||||||
if len(req.Args) == 0 {
|
if len(req.Args) == 0 {
|
||||||
return agents.Response{Text: "❌ Kein Task-Text angegeben."}
|
return agents.Response{Text: "❌ Kein Task-Text angegeben."}
|
||||||
}
|
}
|
||||||
text := strings.Join(req.Args, " ")
|
text, priority, dueDate := parseAddArgs(req.Args)
|
||||||
t, err := a.store.Add(text)
|
if text == "" {
|
||||||
|
return agents.Response{Text: "❌ Kein Task-Text angegeben."}
|
||||||
|
}
|
||||||
|
t, err := a.store.Add(text, priority, dueDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return agents.Response{Error: err, Text: fmt.Sprintf("❌ Fehler: %v", err)}
|
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 {
|
if len(shortID) > 6 {
|
||||||
shortID = shortID[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 {
|
func (a *Agent) list() agents.Response {
|
||||||
@@ -59,6 +100,9 @@ func (a *Agent) list() agents.Response {
|
|||||||
return agents.Response{Text: "📋 Keine Tasks vorhanden."}
|
return agents.Response{Text: "📋 Keine Tasks vorhanden."}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
today := time.Now().Truncate(24 * time.Hour)
|
||||||
|
tomorrow := today.Add(24 * time.Hour)
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString("📋 **Task-Liste:**\n\n")
|
sb.WriteString("📋 **Task-Liste:**\n\n")
|
||||||
openCount := 0
|
openCount := 0
|
||||||
@@ -73,7 +117,39 @@ func (a *Agent) list() agents.Response {
|
|||||||
if len(shortID) > 6 {
|
if len(shortID) > 6 {
|
||||||
shortID = shortID[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))
|
fmt.Fprintf(&sb, "\n*%d offen, %d gesamt*", openCount, len(tasks))
|
||||||
return agents.Response{Text: sb.String()}
|
return agents.Response{Text: sb.String()}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ type Task struct {
|
|||||||
Done bool `json:"done"`
|
Done bool `json:"done"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
DoneAt *time.Time `json:"done_at,omitempty"`
|
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.
|
// 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.
|
// 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()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
@@ -83,6 +85,8 @@ func (s *Store) Add(text string) (Task, error) {
|
|||||||
Text: text,
|
Text: text,
|
||||||
Done: false,
|
Done: false,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
|
Priority: priority,
|
||||||
|
DueDate: dueDate,
|
||||||
}
|
}
|
||||||
tasks = append(tasks, t)
|
tasks = append(tasks, t)
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,52 @@ func (cl *Client) FetchUnread() ([]Message, error) {
|
|||||||
return parseMessages(msgs), nil
|
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 {
|
func parseMessages(msgs []*imapclient.FetchMessageBuffer) []Message {
|
||||||
result := make([]Message, 0, len(msgs))
|
result := make([]Message, 0, len(msgs))
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func Summarize() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SummarizeUnread fasst ungelesene Emails zusammen.
|
// SummarizeUnread fasst ungelesene Emails zusammen.
|
||||||
|
// Wenn email.processed_folder konfiguriert ist, werden die Emails danach dorthin verschoben.
|
||||||
func SummarizeUnread() (string, error) {
|
func SummarizeUnread() (string, error) {
|
||||||
cl, err := Connect()
|
cl, err := Connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -26,7 +27,16 @@ func SummarizeUnread() (string, error) {
|
|||||||
}
|
}
|
||||||
defer cl.Close()
|
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 {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Emails abrufen: %w", err)
|
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")
|
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.
|
// ExtractReminders sucht in den letzten Emails nach Terminen/Deadlines.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
openai "github.com/sashabaranov/go-openai"
|
openai "github.com/sashabaranov/go-openai"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
|
"my-brain-importer/internal/agents"
|
||||||
"my-brain-importer/internal/config"
|
"my-brain-importer/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +26,8 @@ type KnowledgeChunk struct {
|
|||||||
|
|
||||||
// AskQuery sucht relevante Chunks und generiert eine LLM-Antwort.
|
// AskQuery sucht relevante Chunks und generiert eine LLM-Antwort.
|
||||||
// Gibt die Antwort als String und die verwendeten Quellen zurück.
|
// Gibt die Antwort als String und die verwendeten Quellen zurück.
|
||||||
func AskQuery(question string) (string, []KnowledgeChunk, error) {
|
// history enthält vorherige Gesprächsnachrichten (optional, nil für stateless).
|
||||||
|
func AskQuery(question string, history []agents.HistoryMessage) (string, []KnowledgeChunk, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = metadata.AppendToOutgoingContext(ctx, "api-key", config.Cfg.Qdrant.APIKey)
|
ctx = metadata.AppendToOutgoingContext(ctx, "api-key", config.Cfg.Qdrant.APIKey)
|
||||||
|
|
||||||
@@ -58,17 +60,29 @@ Basierend auf diesen Informationen, beantworte bitte folgende Frage:
|
|||||||
|
|
||||||
slog.Debug("[LLM] AskQuery Prompt",
|
slog.Debug("[LLM] AskQuery Prompt",
|
||||||
"model", config.Cfg.Chat.Model,
|
"model", config.Cfg.Chat.Model,
|
||||||
|
"history_len", len(history),
|
||||||
"system", systemPrompt,
|
"system", systemPrompt,
|
||||||
"user", userPrompt,
|
"user", userPrompt,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
msgs := []openai.ChatCompletionMessage{
|
||||||
|
{Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
|
||||||
|
}
|
||||||
|
for _, h := range history {
|
||||||
|
msgs = append(msgs, openai.ChatCompletionMessage{
|
||||||
|
Role: h.Role,
|
||||||
|
Content: h.Content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
msgs = append(msgs, openai.ChatCompletionMessage{
|
||||||
|
Role: openai.ChatMessageRoleUser,
|
||||||
|
Content: userPrompt,
|
||||||
|
})
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
|
stream, err := chatClient.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
|
||||||
Model: config.Cfg.Chat.Model,
|
Model: config.Cfg.Chat.Model,
|
||||||
Messages: []openai.ChatCompletionMessage{
|
Messages: msgs,
|
||||||
{Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
|
|
||||||
{Role: openai.ChatMessageRoleUser, Content: userPrompt},
|
|
||||||
},
|
|
||||||
Temperature: 0.7,
|
Temperature: 0.7,
|
||||||
MaxTokens: 500,
|
MaxTokens: 500,
|
||||||
})
|
})
|
||||||
@@ -102,7 +116,7 @@ func Ask(question string) {
|
|||||||
fmt.Printf("🤔 Frage: \"%s\"\n\n", question)
|
fmt.Printf("🤔 Frage: \"%s\"\n\n", question)
|
||||||
fmt.Println("🔍 Durchsuche lokale Wissensdatenbank...")
|
fmt.Println("🔍 Durchsuche lokale Wissensdatenbank...")
|
||||||
|
|
||||||
answer, chunks, err := AskQuery(question)
|
answer, chunks, err := AskQuery(question, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("❌ %v", err)
|
log.Fatalf("❌ %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type Config struct {
|
|||||||
TLS bool `yaml:"tls"`
|
TLS bool `yaml:"tls"`
|
||||||
StartTLS bool `yaml:"starttls"`
|
StartTLS bool `yaml:"starttls"`
|
||||||
Folder string `yaml:"folder"`
|
Folder string `yaml:"folder"`
|
||||||
|
ProcessedFolder string `yaml:"processed_folder"` // Zielordner nach Zusammenfassung (leer = kein Verschieben)
|
||||||
Model string `yaml:"model"` // Optional: überschreibt chat.model für Email-Zusammenfassungen
|
Model string `yaml:"model"` // Optional: überschreibt chat.model für Email-Zusammenfassungen
|
||||||
} `yaml:"email"`
|
} `yaml:"email"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user