package agent import ( "context" "fmt" "log/slog" "strings" "time" "nofx/nofxi/internal/perception" "nofx/nofxi/internal/thinking" ) // Brain is the proactive intelligence layer. // It receives signals from Sentinel, processes news, and decides // when to proactively notify the user. type Brain struct { agent *Agent news *perception.NewsMonitor logger *slog.Logger stopCh chan struct{} // Debounce: don't spam the same signal recentSignals map[string]time.Time } // NewBrain creates the proactive brain. func NewBrain(agent *Agent, logger *slog.Logger) *Brain { return &Brain{ agent: agent, news: perception.NewNewsMonitor(logger), logger: logger, stopCh: make(chan struct{}), recentSignals: make(map[string]time.Time), } } // HandleSignal processes a market signal from the Sentinel. func (b *Brain) HandleSignal(signal perception.Signal) { // Debounce: same signal type + symbol within 10 minutes key := fmt.Sprintf("%s:%s", signal.Type, signal.Symbol) if last, ok := b.recentSignals[key]; ok && time.Since(last) < 10*time.Minute { return } b.recentSignals[key] = time.Now() // Format alert message emoji := map[string]string{ "info": "ℹ️", "warning": "⚠️", "critical": "🚨", } e := emoji[signal.Severity] if e == "" { e = "📊" } msg := fmt.Sprintf("%s *%s*\n\n%s\n\n_%s_", e, signal.Title, signal.Detail, signal.Timestamp.Format("15:04:05")) b.notifyAll(msg) } // StartNewsScan begins periodic news scanning. func (b *Brain) StartNewsScan(interval time.Duration) { go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-b.stopCh: return case <-ticker.C: b.scanNews() } } }() } // StartMarketBrief sends a morning/evening market brief. func (b *Brain) StartMarketBrief() { go func() { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for { select { case <-b.stopCh: return case now := <-ticker.C: hour := now.Hour() minute := now.Minute() // Morning brief at 08:30 if hour == 8 && minute == 30 { b.sendMarketBrief("morning") } // Evening brief at 20:30 if hour == 20 && minute == 30 { b.sendMarketBrief("evening") } } } }() } func (b *Brain) scanNews() { items, err := b.news.FetchNews() if err != nil { b.logger.Error("fetch news", "error", err) return } // Filter for high-impact news related to watched symbols for _, item := range items { if item.Sentiment == "neutral" { continue } if len(item.Symbols) == 0 { continue } // Only alert on recent news (last 10 minutes) if time.Since(item.Timestamp) > 10*time.Minute { continue } emoji := "📰" if item.Sentiment == "bullish" { emoji = "🟢" } else if item.Sentiment == "bearish" { emoji = "🔴" } msg := fmt.Sprintf("%s *%s*\n\n%s\n\n• Source: %s\n• Symbols: %s\n• Sentiment: %s", emoji, "News Alert", item.Title, item.Source, strings.Join(item.Symbols, ", "), strings.ToUpper(item.Sentiment), ) b.notifyAll(msg) } } func (b *Brain) sendMarketBrief(timeOfDay string) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() prompt := fmt.Sprintf(`Generate a brief %s market summary for crypto trading. Include: BTC/ETH price direction, key levels, market sentiment, any notable events. Be concise (under 200 words). Use trading emojis. Respond in Chinese. Current time: %s`, timeOfDay, time.Now().Format("2006-01-02 15:04:05")) resp, err := b.agent.thinker.Chat(ctx, []thinking.Message{ {Role: "system", Content: "You are NOFXi, a professional crypto trading AI. Respond in Chinese."}, {Role: "user", Content: prompt}, }) if err != nil { b.logger.Error("generate market brief", "error", err) return } title := "☀️ *早间市场简报*" if timeOfDay == "evening" { title = "🌙 *晚间市场简报*" } msg := fmt.Sprintf("%s\n\n%s\n\n_Generated by NOFXi 🤖_", title, resp) b.notifyAll(msg) } func (b *Brain) notifyAll(text string) { if b.agent.NotifyFunc == nil { return } for _, uid := range b.agent.config.Telegram.AllowedIDs { if err := b.agent.NotifyFunc(uid, text); err != nil { b.logger.Error("notify", "user_id", uid, "error", err) } } } // Stop stops the brain. func (b *Brain) Stop() { close(b.stopCh) }