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:
Thomas Cravey 2025-09-05 06:58:38 -05:00
parent 2f9ab6a414
commit 8dc52976eb
7 changed files with 512 additions and 21 deletions

View file

@ -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 dont omit important context.\n")
b.WriteString("- Keep it compact but dont 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 dont 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}})

View file

@ -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)
}