diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 63efd3e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,40 +0,0 @@ -# Repository Guidelines - -## Architecture Overview -- Role: Additional IRC client (not a bot) connected to a soju bouncer with your same nick. Uses a distinct clientId/username suffix for per‑client history. -- Ingestion: Raw IRC via `internal/soju` with optional CHATHISTORY backfill, quiet hours, and rate limiting for alerts. -- Storage: SQLite (`internal/store`) for channel logs and metadata. -- Notifications: Pushover (`internal/notifier`) for mentions and scheduled digests. -- Summaries: OpenAI LLM (`internal/summarizer`, default gpt‑5) for link and channel summaries. -- Web UI: `internal/httpapi` serves SSE live view, link cards, inline article summaries, and an on‑demand summary page. - -## Project Structure -- `cmd/sojuboy/`: entrypoint and service wiring -- `internal/{soju,store,notifier,summarizer,httpapi,scheduler}`: core packages -- Root: `Dockerfile`, `docker-compose.yml`, `README.md`, `CHANGELOG.md` - -## Build, Run, and Deploy -- Local: `go build -o sojuboy ./cmd/sojuboy` or `go run ./cmd/sojuboy` -- Docker (Synology‑friendly): `docker compose up --build` (exposes `8080`) -- Health: `./sojuboy --health`, `curl :8080/healthz`, `curl :8080/ready` -- Example .env: `SOJU_HOST=host` `SOJU_PORT=6697` `SOJU_TLS=true` `IRC_NICK=nick` `IRC_USERNAME=user/network@sojuboy` `IRC_PASSWORD=...` `CHANNELS=#chan1,#chan2` `NOTIFIER=pushover` `PUSHOVER_USER_KEY=...` `PUSHOVER_API_TOKEN=...` `LLM_PROVIDER=openai` `OPENAI_API_KEY=...` `DIGEST_CRON=0 0 9 * * *` `HTTP_TOKEN=longtoken` - -## Coding Style & Naming -- Format: `gofmt -s -w .`; lint: `go vet ./...`; deps: `go mod tidy` -- Indentation: Go uses tabs; YAML/JS use 2 spaces -- Naming: packages lower‑case; exported `CamelCase`; files `snake_case.go` -- Logging: use `slog` via `internal/logging`; include context - -## Testing Guidelines -- Use standard `testing` with table‑driven tests; place beside code (`*_test.go`) -- For store tests, use in‑memory DB: `store.Open(ctx, ":memory:")` -- Run: `go test ./...` and `go test -cover ./...` - -## Commits & PRs -- Commits: short, imperative; conventional tags like `feat(ui):`, `fix(store):`, `docs:` (see history) -- PRs: description, linked issues, repro/verification steps; screenshots for Web UI changes -- Update `README.md`/`CHANGELOG.md` when flags, endpoints, or UX change - -## Security & Config -- Keep secrets in `.env` (git‑ignored); never commit API keys -- Synology: map `/data` volume; protect UI with `HTTP_TOKEN` (cookie/Bearer/query); bind `127.0.0.1:8080` behind DSM Reverse Proxy if desired diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 1651e53..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,339 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -### Building and Running -- `go build -o sojuboy ./cmd/sojuboy` - Build the binary -- `go run ./cmd/sojuboy` - Run directly with Go -- `docker-compose up -d --build` - Build and run with Docker Compose -- `docker build -t sojuboy .` - Build Docker image - -### Health Checks -- `./sojuboy --health` - Local readiness check (exits 0/1) -- `curl http://localhost:8080/healthz` - HTTP health check -- `curl http://localhost:8080/ready` - HTTP readiness check (requires soju connection) - -### Testing and Validation -- `go mod tidy` - Clean up module dependencies -- `go vet ./...` - Run Go static analysis -- `go fmt ./...` - Format Go code - -## Architecture Overview - -This is a Go service that acts as an IRC bouncer companion for soju, providing notifications, message storage, and AI summaries. - -### Core Components - -**Entry Point**: `cmd/sojuboy/main.go` - Main application entry point with service wiring and configuration - -**Core Modules** (all in `internal/`): -- `config/` - Environment-based configuration loading -- `soju/` - IRC client for soju bouncer connection with IRCv3 capability negotiation -- `store/` - SQLite storage layer with WAL mode enabled -- `notifier/` - Pushover notification system (pluggable interface) -- `summarizer/` - OpenAI integration for AI digests and link summarization -- `scheduler/` - Cron-based digest scheduling and retention jobs -- `httpapi/` - HTTP API server with Web UI (templates + static assets) -- `logging/` - Structured logging setup - -### Key Architectural Patterns - -**IRC Integration**: Uses raw IRC protocol with sorcix/irc parser, implements irssi-style authentication for soju multi-client support (`username/network@client` format) - -**Message Flow**: IRC messages → storage → mention detection → notifications + AI processing - -**Web Architecture**: Go stdlib `net/http` with `html/template`, embedded static assets, SSE for real-time updates - -**AI Integration**: OpenAI client with GPT-5 default (fallback to GPT-4o-mini), separate prompts for conversation digests vs link summarization - -### Configuration - -All configuration via environment variables (see README.md for complete reference). Key patterns: -- Soju connection: `SOJU_HOST`, `SOJU_PORT`, `SOJU_TLS`, `IRC_USERNAME` format critical for per-client history -- Required for functionality: `OPENAI_API_KEY`, `PUSHOVER_USER_KEY`, `PUSHOVER_API_TOKEN` -- Default data path: `/data/app.db` (SQLite with WAL mode) - -### HTTP API Structure - -- Authentication via `HTTP_TOKEN` (cookie or Bearer/query param) -- JSON APIs: `/tail`, `/history`, `/trigger`, `/linkcard`, `/linksummary`, `/metrics` -- Web UI with SSE tail, infinite scroll, link cards, on-demand summarization -- Channel-based operations (URL encode `#` as `%23` in requests) - -### Storage Schema - -SQLite with modernc.org/sqlite driver: -- Messages table with server-time timestamps -- Channel-based organization -- Retention management (daily cleanup at 03:00) -- WAL mode for concurrent access - -### Development Notes - -- Go 1.23+ required (see go.mod) -- Static binary compilation for distroless Docker image -- No external build tools (Make, npm, etc.) - pure Go toolchain -- Configuration via .env file for local development -- Docker health checks via `--health` flag - -## Code Review Findings - -### 🚀 Major Refactors for iOS Features - -#### 1. Extract Message Processing Service -The `alert` function in `main.go:143-212` is doing too much: -- Message storage -- Notification logic -- Rate limiting -- DM vs channel handling - -**Recommendation:** Create `internal/messaging/processor.go` with separate concerns: -```go -type MessageProcessor struct { - store Store - notifiers []Notifier // Multi-target for iOS + Pushover - rateLimiter RateLimiter - broadcaster Broadcaster -} -``` - -#### 2. Multi-Target Notification System -Current system hardcoded to single Pushover notifier. For iOS: -```go -type NotificationDispatcher struct { - targets []Notifier // Pushover, APNs, etc. -} -``` - -#### 3. Extract Rate Limiter -Move `rateLimiter` struct to `internal/ratelimit/` package for reusability and testing. - -### ⚡ Critical Performance Issues - -#### 1. SQLite Connection Bottleneck -`store.go:30` - `SetMaxOpenConns(1)` is extremely restrictive: -```go -// Current: Major bottleneck -db.SetMaxOpenConns(1) - -// Better: Allow concurrent reads -db.SetMaxOpenConns(5) -db.SetMaxIdleConns(2) -``` - -#### 2. Inefficient Array Reversal -`store.go:147-150` - Manual reversal is inefficient: -```sql --- Better: Use SQL ORDER BY instead -SELECT ... ORDER BY at ASC LIMIT ? -``` - -#### 3. OpenAI Client Recreation -`openai.go:60` - Client created per request. Should use singleton pattern with connection pooling. - -### 🗑️ Dead Code Cleanup - -1. **Entire unused package:** `internal/ircclient/base64.go` - appears completely unused -2. **Unused imports:** `io` package in `main.go` only for health check discard -3. **Dead helper:** `strconvI()` in `summarizer.go:475` - just use `strconv.Itoa` directly -4. **Rarely used export:** `config.GetEnvInt()` - used once, could be private - -### 🔧 Easy Wins - -#### 1. Fix Deprecated API -`templates.go:48` - `strings.Title()` is deprecated: -```go -// Replace with -data["Title"] = cases.Title(language.English).String(base) -``` - -#### 2. Hard-coded Values -- Health check port hardcoded to `8080` - use configured listen address -- Text limits hardcoded in summarizer - make configurable -- Temperature values hardcoded - make configurable - -#### 3. Database Schema -Missing indexes for performance: -```sql -CREATE INDEX IF NOT EXISTS idx_messages_author ON messages(author); -CREATE INDEX IF NOT EXISTS idx_messages_time ON messages(at); -``` - -### 🛡️ Error Handling Improvements - -#### 1. Silent Failures -`pushover.go:24` - Returns `nil` on error conditions without logging: -```go -// Better error handling -if p == nil || p.app == nil || p.userKey == "" { - return errors.New("pushover not configured") -} -``` - -#### 2. Message Truncation -No user feedback when messages truncated to 1024 chars. - -### 📱 iOS-Specific Optimizations - -#### 1. Message Broadcaster Enhancement -Current SSE broadcasting is basic - needs: -- Connection state management -- Message queuing for disconnected clients -- WebSocket support alongside SSE - -#### 2. Auth Token Management -Simple token auth needs enhancement for mobile: -- JWT tokens with refresh capability -- Device-specific sessions -- Proper expiration handling - -### 🎯 Implementation Priority - -**Phase 1 (High Impact):** -1. Fix SQLite connection limit -2. Extract message processor service -3. Remove dead code -4. Fix deprecated APIs - -**Phase 2 (iOS Foundation):** -1. Multi-target notification system -2. Enhanced message broadcasting -3. JWT authentication improvements - -**Phase 3 (Performance):** -1. OpenAI client pooling -2. Database index optimization -3. Rate limiter improvements - -## Application Behavior & Quirks - -### Message Storage Logic -- **DM Handling**: Direct messages TO the bot are stored under the sender's nick, not the bot's nick (`main.go:146-151`) -- **Message Deduplication**: Uses `INSERT OR IGNORE` with optional `msgid` for soju message deduplication -- **Channel Normalization**: All channel lookups are case-insensitive via `lower()` SQL function - -### Notification Logic -- **Rate Limiting**: Per-channel + per-keyword combinations, not global (`main.go:199`) -- **Quiet Hours**: Local timezone-based, supports midnight wrap-around (`config.go:267-271`) -- **Urgent Keywords**: Bypass quiet hours but still respect rate limits -- **Backfill Suppression**: Messages older than 5 minutes are not notified unless `NOTIFY_BACKFILL=true` - -### IRC Protocol Specifics -- **Authentication**: Uses raw PASS/USER, not SASL (despite config hint) -- **Capability Negotiation**: Requests IRCv3 `draft/event-playback` for automatic message replay -- **Fallback History**: CHATHISTORY LATEST only used when event-playback unavailable -- **Connection Management**: Exponential backoff (1s → 30s max) with automatic reconnection - -### AI Summarization Behavior -- **Message Grouping**: Same author within `SUMM_GROUP_WINDOW` (default 120s) are concatenated -- **Link Processing**: Extracts URLs via regex, fetches with readability for clean text -- **Image Handling**: Direct image URLs sent as vision inputs to GPT-5 -- **Fallback Logic**: Local text-based summary if OpenAI fails -- **Model Selection**: Auto-detects reasoning models (gpt-5, o1) to skip temperature setting - -### Web UI Implementation -- **SSE Streaming**: Real-time message delivery with 15s heartbeat -- **Link Cards**: Cached 24h, special handling for X/Twitter oEmbed and YouTube -- **Authentication**: Cookie-based with 7-day expiration, secure flag auto-detection -- **Channel Selector**: Populated from DB + config fallback for empty databases - -### Storage Patterns -- **WAL Mode**: Enabled for concurrent read access despite single connection limit -- **Retention**: Daily cleanup at 03:00 local time, configurable days -- **Timestamps**: Always stored as UTC, server-time tags preferred over local time -- **Migration**: Best-effort schema updates with ignored errors for missing columns - -### Error Handling Patterns -- **Silent Failures**: Many subsystems return `nil` errors to prevent cascading failures -- **Graceful Degradation**: Missing summarizer/notifier doesn't break core IRC functionality -- **Connection Resilience**: IRC client handles ERROR messages and reconnects automatically - -### Security Considerations -- **Token Storage**: HTTP_TOKEN stored in plain text, suitable for internal deployments -- **CORS**: No CORS headers - designed for same-origin web UI access -- **TLS**: Configurable per connection, defaults to enabled -- **Input Sanitization**: Minimal - relies on template escaping and database parameterization - -### Performance Characteristics -- **Memory Usage**: Link/summary caches unbounded - potential memory leak over time -- **Concurrent Requests**: Severely limited by single DB connection -- **OpenAI Rate Limits**: No built-in rate limiting or retry logic -- **SSE Scalability**: In-memory subscriber map, no persistence across restarts - -### Development Workflow -- **Hot Reload**: Not supported - requires full restart for config/code changes -- **Logging**: Structured JSON to stdout, debug mode shows IRC protocol messages -- **Health Monitoring**: Separate metrics endpoint for Prometheus integration -- **Docker**: Multi-stage build with distroless final image for security - -### Common Gotchas -- **Channel Names**: Must URL-encode `#` as `%23` in HTTP requests -- **Time Formats**: Mix of RFC3339 and RFC3339Nano depending on context -- **Case Sensitivity**: IRC nicks/channels case-insensitive, but stored as-received -- **CHATHISTORY**: Requires soju-specific timestamp format, may not work with other bouncers -- **Health Check**: Hard-coded to port 8080 regardless of configured listen address - -## Planned iOS App Development - -### Context -User wants to build a native iOS Swift app that consumes this Go server's HTTP APIs. The server should remain in Go (excellent fit for backend services), while the iOS app provides a native mobile experience. - -### Required Server Enhancements for iOS - -**1. iOS Push Notifications (Priority 1)** -- Add Apple Push Notification service (APNs) integration alongside Pushover -- New endpoints: `POST /api/devices` (device token registration) -- Enhanced notification system supporting multiple targets -- Environment variables: `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_BUNDLE_ID`, `APNS_KEY_PATH`, `APNS_SANDBOX` - -**2. Enhanced Real-time Communication** -- Add WebSocket support alongside existing SSE (better for iOS background handling) -- Message queuing for disconnected clients -- New endpoints: `GET /api/ws?channel=#chan`, `GET /api/missed?since=` - -**3. Mobile-Optimized Authentication** -- JWT tokens with refresh capability (current simple token is too basic) -- Device-specific authentication for multiple iOS devices -- New endpoints: `POST /api/auth/login`, `POST /api/auth/refresh`, `POST /api/auth/logout` - -**4. Offline Support & Sync** -- Cursor-based pagination (improve current `before` parameter approach) -- Message read/unread state tracking per device -- Enhanced endpoints: `GET /api/sync?cursor=&limit=100`, `POST /api/read-receipts`, `GET /api/unread-count` - -**5. iOS-Specific Optimizations** -- Batch operations: `POST /api/linkcards/batch` -- Image size variants: `GET /api/linkcard?url=...&size=thumb` -- Performance optimizations for mobile bandwidth - -### iOS App Feature Parity Goals -The native app should match current web UI functionality: -- Live chat view with real-time updates -- Link cards and on-demand link summarization -- Native push notifications (replacing Pushover for mobile users) -- On-demand AI summarization -- Channel switching and history browsing -- Infinite scroll for chat history - -### Database Schema Extensions Needed -- `devices` table - iOS device tokens and user associations -- `read_receipts` table - per-device read state tracking -- `message_queue` table - offline message queueing - -### Implementation Priority for iOS Support -1. APNs integration (native notifications) -2. WebSocket + connection management -3. JWT authentication system -4. Offline sync capabilities -5. Mobile-specific API optimizations - -### Architecture Decision: Keep Go Backend -**Question Asked:** Could this project be rewritten in Swift? -**Answer:** Technically feasible but not recommended. - -**Pros of Swift rewrite:** Strong type safety, modern concurrency (async/await), cross-platform potential -**Cons:** Limited ecosystem for IRC libraries, SQLite libraries, cron scheduling, HTTP templating; larger Docker images; significant development effort to recreate working functionality - -**Recommended Approach:** Keep Go backend (excellent fit for network services, mature ecosystem, single static binary deployment) + build native Swift iOS frontend. This leverages both platforms' strengths effectively. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 60e8277..0000000 --- a/TODO.md +++ /dev/null @@ -1 +0,0 @@ -[ ] Add Search fucntionality - to search chat logs diff --git a/cmd/sojuboy/main.go b/cmd/sojuboy/main.go index f163063..25c1a0c 100644 --- a/cmd/sojuboy/main.go +++ b/cmd/sojuboy/main.go @@ -252,17 +252,14 @@ func main() { logger.Error("digest fetch", "err", err) continue } - summary, err := sum.SummarizeForPush(ctx, ch, msgs, window) + summary, err := sum.Summarize(ctx, ch, msgs, window) if err != nil { logger.Error("digest summarize", "err", err) continue } if nt != nil { title := "IRC digest " + ch + " (" + window.String() + ")" - // Safety clamp for provider limits - msg := strings.TrimSpace(summary) - if len(msg) > 1024 { msg = msg[:1024] } - if err := nt.Notify(ctx, title, msg); err != nil { + if err := nt.Notify(ctx, title, summary); err != nil { logger.Error("digest notify", "err", err) } else { atomic.AddInt64(&metrics.NotificationsSent, 1) diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 93b9049..655f89d 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -156,13 +156,7 @@ func (s *Server) handleTrigger(w http.ResponseWriter, r *http.Request) { } ctxSum, cancel := context.WithTimeout(ctx, tout) defer cancel() - var summary string - // 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) - } + summary, err := s.Summarizer.Summarize(ctxSum, channel, msgs, window) if err != nil { if s.Logger != nil { s.Logger.Error("http trigger summarizer", "err", err) @@ -173,18 +167,13 @@ 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) - // 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) - } - } + 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) + } + } w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte(summary)) } @@ -731,16 +720,13 @@ 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) - // 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) - } - } + 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) + } + } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{"summary": sum}) } diff --git a/internal/summarizer/openai.go b/internal/summarizer/openai.go index cc9bf8d..0fc5d10 100644 --- a/internal/summarizer/openai.go +++ b/internal/summarizer/openai.go @@ -111,8 +111,7 @@ 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." @@ -163,95 +162,6 @@ 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 @@ -364,7 +274,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}}) diff --git a/internal/summarizer/summarizer.go b/internal/summarizer/summarizer.go index b057336..dd416b9 100644 --- a/internal/summarizer/summarizer.go +++ b/internal/summarizer/summarizer.go @@ -8,9 +8,6 @@ import ( ) type Summarizer interface { - 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) + Summarize(ctx context.Context, channel string, msgs []store.Message, window time.Duration) (string, error) + SummarizeLink(ctx context.Context, rawURL string) (string, error) }