feat(summarizer): add SummarizeForPush and use for Pushover; keep full WebUI/on-demand output; clamp only on push\ndocs: add AGENTS.md; revert CLAUDE.md release section
This commit is contained in:
parent
2f9ab6a414
commit
8dc52976eb
7 changed files with 512 additions and 21 deletions
|
|
@ -156,7 +156,14 @@ func (s *Server) handleTrigger(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
ctxSum, cancel := context.WithTimeout(ctx, tout)
|
||||
defer cancel()
|
||||
summary, err := s.Summarizer.Summarize(ctxSum, channel, msgs, window)
|
||||
var summary string
|
||||
var err error
|
||||
// If we will push, use a push-optimized prompt; otherwise allow full-length
|
||||
if strings.EqualFold(r.URL.Query().Get("push"), "1") || strings.EqualFold(r.URL.Query().Get("push"), "true") {
|
||||
summary, err = s.Summarizer.SummarizeForPush(ctxSum, channel, msgs, window)
|
||||
} else {
|
||||
summary, err = s.Summarizer.Summarize(ctxSum, channel, msgs, window)
|
||||
}
|
||||
if err != nil {
|
||||
if s.Logger != nil {
|
||||
s.Logger.Error("http trigger summarizer", "err", err)
|
||||
|
|
@ -167,13 +174,18 @@ func (s *Server) handleTrigger(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
// Only push if explicitly requested
|
||||
pushFlag := strings.EqualFold(r.URL.Query().Get("push"), "1") || strings.EqualFold(r.URL.Query().Get("push"), "true")
|
||||
if pushFlag && s.Notifier != nil {
|
||||
title := fmt.Sprintf("IRC digest %s (%s)", channel, window)
|
||||
_ = s.Notifier.Notify(ctx, title, summary)
|
||||
if s.Metrics != nil {
|
||||
atomic.AddInt64(&s.Metrics.NotificationsSent, 1)
|
||||
}
|
||||
}
|
||||
if pushFlag && s.Notifier != nil {
|
||||
title := fmt.Sprintf("IRC digest %s (%s)", channel, window)
|
||||
// Trim only for push notifications (Pushover limit ~1024)
|
||||
msg := strings.TrimSpace(summary)
|
||||
if len(msg) > 1000 {
|
||||
msg = msg[:1000] + "…"
|
||||
}
|
||||
_ = s.Notifier.Notify(ctx, title, msg)
|
||||
if s.Metrics != nil {
|
||||
atomic.AddInt64(&s.Metrics.NotificationsSent, 1)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte(summary))
|
||||
}
|
||||
|
|
@ -720,13 +732,16 @@ func (s *Server) handleTriggerJSON(w http.ResponseWriter, r *http.Request) {
|
|||
_, _ = w.Write([]byte("summarizer error"))
|
||||
return
|
||||
}
|
||||
if push && s.Notifier != nil {
|
||||
title := fmt.Sprintf("IRC digest %s (%s)", channel, dur)
|
||||
_ = s.Notifier.Notify(r.Context(), title, sum)
|
||||
if s.Metrics != nil {
|
||||
atomic.AddInt64(&s.Metrics.NotificationsSent, 1)
|
||||
}
|
||||
}
|
||||
if push && s.Notifier != nil {
|
||||
title := fmt.Sprintf("IRC digest %s (%s)", channel, dur)
|
||||
// Safety clamp for provider limits
|
||||
msg := strings.TrimSpace(sum)
|
||||
if len(msg) > 1024 { msg = msg[:1024] }
|
||||
_ = s.Notifier.Notify(r.Context(), title, msg)
|
||||
if s.Metrics != nil {
|
||||
atomic.AddInt64(&s.Metrics.NotificationsSent, 1)
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"summary": sum})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ func (o *OpenAI) Summarize(ctx context.Context, channel string, msgs []store.Mes
|
|||
b.WriteString("- Focus on what happened and why it matters.\n")
|
||||
b.WriteString("- Integrate linked content and pasted multi-line posts naturally.\n")
|
||||
b.WriteString("- Avoid rigid sections; use short paragraphs or light bullets if helpful.\n")
|
||||
b.WriteString("- Keep it compact but don’t omit important context.\n")
|
||||
b.WriteString("- Keep it compact but don’t omit important context.\n")
|
||||
|
||||
prompt := b.String()
|
||||
|
||||
sys := "You summarize IRC transcripts. Be concise, natural, and informative."
|
||||
|
|
@ -162,6 +163,95 @@ func (o *OpenAI) Summarize(ctx context.Context, channel string, msgs []store.Mes
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// SummarizeForPush produces a digest tailored for push notifications (e.g., Pushover ~1024 chars).
|
||||
// It uses a slightly more constrained prompt to encourage succinct output.
|
||||
func (o *OpenAI) SummarizeForPush(ctx context.Context, channel string, msgs []store.Message, window time.Duration) (string, error) {
|
||||
if o == nil || o.apiKey == "" {
|
||||
return "", nil
|
||||
}
|
||||
cfg := openai.DefaultConfig(o.apiKey)
|
||||
if strings.TrimSpace(o.baseURL) != "" {
|
||||
cfg.BaseURL = o.baseURL
|
||||
}
|
||||
client := openai.NewClientWithConfig(cfg)
|
||||
|
||||
grouped := groupMessages(msgs, o.groupWindow)
|
||||
if o.maxGroups > 0 && len(grouped) > o.maxGroups {
|
||||
grouped = grouped[len(grouped)-o.maxGroups:]
|
||||
}
|
||||
|
||||
links := extractLinks(grouped)
|
||||
var imageURLs []string
|
||||
var nonImageLinks []linkSnippet
|
||||
for _, l := range links {
|
||||
if isImageURL(l.url) { imageURLs = append(imageURLs, l.url) } else { nonImageLinks = append(nonImageLinks, l) }
|
||||
}
|
||||
if o.followLinks && len(nonImageLinks) > 0 {
|
||||
nonImageLinks = fetchLinkSnippets(ctx, nonImageLinks, o.linkTimeout, o.linkMaxBytes, o.maxLinks)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("Channel: ")
|
||||
b.WriteString(channel)
|
||||
b.WriteString("\nTime window: ")
|
||||
b.WriteString(window.String())
|
||||
b.WriteString("\n\nTranscript (grouped by author):\n")
|
||||
for _, g := range grouped {
|
||||
b.WriteString(g.time.Format(time.RFC3339))
|
||||
b.WriteString(" ")
|
||||
b.WriteString(g.author)
|
||||
b.WriteString(": ")
|
||||
b.WriteString(g.text)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if len(nonImageLinks) > 0 {
|
||||
b.WriteString("\nReferenced content (snippets):\n")
|
||||
for _, ln := range nonImageLinks {
|
||||
b.WriteString("- ")
|
||||
b.WriteString(ln.url)
|
||||
b.WriteString(" → ")
|
||||
b.WriteString(ln.snippet)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
b.WriteString("\nWrite a concise, readable summary of the conversation above.\n")
|
||||
b.WriteString("- Focus on what happened and why it matters.\n")
|
||||
b.WriteString("- Integrate linked content and pasted multi-line posts naturally.\n")
|
||||
b.WriteString("- Avoid rigid sections; use short paragraphs or light bullets if helpful.\n")
|
||||
b.WriteString("- Keep it compact but don’t omit important context.\n")
|
||||
b.WriteString("- Keep the final output under ~900 characters suitable for a single push notification.\n")
|
||||
prompt := b.String()
|
||||
|
||||
sys := "You summarize IRC transcripts for a push notification. Be concise, natural, and informative."
|
||||
|
||||
model := o.model
|
||||
if strings.TrimSpace(model) == "" { model = "gpt-4o-mini" }
|
||||
reasoningLike := strings.HasPrefix(model, "gpt-5") || strings.HasPrefix(model, "o1") || strings.Contains(model, "reasoning")
|
||||
|
||||
var userParts []openai.ChatMessagePart
|
||||
userParts = append(userParts, openai.ChatMessagePart{Type: openai.ChatMessagePartTypeText, Text: prompt})
|
||||
for _, u := range imageURLs {
|
||||
userParts = append(userParts, openai.ChatMessagePart{Type: openai.ChatMessagePartTypeImageURL, ImageURL: &openai.ChatMessageImageURL{URL: u}})
|
||||
}
|
||||
|
||||
req := openai.ChatCompletionRequest{
|
||||
Model: model,
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{Role: openai.ChatMessageRoleSystem, Content: sys},
|
||||
{Role: openai.ChatMessageRoleUser, MultiContent: userParts},
|
||||
},
|
||||
MaxCompletionTokens: o.maxTokens,
|
||||
}
|
||||
if !reasoningLike { req.Temperature = 0.3 }
|
||||
|
||||
resp, err := client.CreateChatCompletion(ctx, req)
|
||||
if err != nil { return "", err }
|
||||
if len(resp.Choices) == 0 { return localFallbackSummary(grouped, append(nonImageLinks, linksFromImages(imageURLs)...)), nil }
|
||||
out := strings.TrimSpace(resp.Choices[0].Message.Content)
|
||||
if out == "" { return localFallbackSummary(grouped, append(nonImageLinks, linksFromImages(imageURLs)...)), nil }
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, error) {
|
||||
if o == nil || o.apiKey == "" {
|
||||
return "", nil
|
||||
|
|
@ -274,7 +364,7 @@ func (o *OpenAI) SummarizeLink(ctx context.Context, rawURL string) (string, erro
|
|||
b.WriteString(content)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
b.WriteString("Write a short, skimmable summary of the page/video/image above. If relevant, include key takeaways and any notable cautions. Keep it under a few short paragraphs.")
|
||||
b.WriteString("Write a short, skimmable summary of the page/video/image above. If relevant, include key takeaways and any notable cautions. Keep it under a few short paragraphs.")
|
||||
userParts = append(userParts, openai.ChatMessagePart{Type: openai.ChatMessagePartTypeText, Text: b.String()})
|
||||
if img != "" {
|
||||
userParts = append(userParts, openai.ChatMessagePart{Type: openai.ChatMessagePartTypeImageURL, ImageURL: &openai.ChatMessageImageURL{URL: img}})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import (
|
|||
)
|
||||
|
||||
type Summarizer interface {
|
||||
Summarize(ctx context.Context, channel string, msgs []store.Message, window time.Duration) (string, error)
|
||||
SummarizeLink(ctx context.Context, rawURL string) (string, error)
|
||||
Summarize(ctx context.Context, channel string, msgs []store.Message, window time.Duration) (string, error)
|
||||
// SummarizeForPush creates a digest tuned for push notifications (e.g., Pushover limits).
|
||||
// Implementations should keep the output succinct and within ~1k characters.
|
||||
SummarizeForPush(ctx context.Context, channel string, msgs []store.Message, window time.Duration) (string, error)
|
||||
SummarizeLink(ctx context.Context, rawURL string) (string, error)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue