Files
rsspipe/internal/actions/ai/triage.go
T
haopengzhan dfbf646332
CI Quality Checks & Tests / Gitleaks Secret Detection (push) Failing after 47s
CI Quality Checks & Tests / Frontend Typecheck & Build (push) Successful in 1m19s
CI Quality Checks & Tests / Go Lint & Unit Tests (push) Failing after 3h11m47s
feat: implement AI integration with backend configuration and rule-based enrichment actions
2026-08-26 22:42:23 -07:00

89 lines
2.5 KiB
Go

package ai
import (
"context"
"fmt"
"time"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/ai"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/worker"
"go.uber.org/zap"
)
// NewTriageHandler creates a worker action handler for AI autonomous triage decisions.
func NewTriageHandler(database *db.DB, client *ai.Client, pool *worker.Pool, log *zap.Logger) worker.ActionHandler {
return func(ctx context.Context, entry models.FeedEntry, params map[string]interface{}) (*worker.ActionResult, error) {
cfg := LoadConfig(database)
if cfg.APIKey == "" && cfg.Provider != "ollama" {
log.Warn("AI triage action skipped: API key is not configured in Settings > AI & Integrations",
zap.String("entry_id", entry.ID),
)
return nil, nil
}
policyPrompt, _ := params["prompt"].(string)
var allowedActions []string
if rawAllowed, ok := params["allowed_actions"].([]interface{}); ok {
for _, a := range rawAllowed {
allowedActions = append(allowedActions, fmt.Sprintf("%v", a))
}
} else if strList, ok := params["allowed_actions"].([]string); ok {
allowedActions = strList
}
if len(allowedActions) == 0 {
allowedActions = []string{"mark_read", "mark_starred", "set_variant"}
}
decision, err := client.GenerateAgentDecision(ctx, cfg, policyPrompt, allowedActions, entry)
if err != nil {
log.Error("AI triage decision failed",
zap.String("entry_id", entry.ID),
zap.Error(err),
)
return nil, err
}
log.Info("AI Triage agent executed successfully",
zap.String("entry_id", entry.ID),
zap.String("reasoning", decision.Reasoning),
zap.Int("actions_approved", len(decision.Actions)),
)
res := &worker.ActionResult{}
for _, act := range decision.Actions {
switch act.Type {
case "mark_read":
trueVal := true
res.MarkRead = &trueVal
case "mark_starred":
trueVal := true
res.MarkStarred = &trueVal
case "set_variant":
vKey, _ := act.Params["variant_key"].(string)
if vKey == "" {
vKey, _ = act.Params["key"].(string)
}
vVal, _ := act.Params["value"].(string)
if vKey != "" {
v := models.EntryVariant{
EntryID: entry.ID,
VariantKey: vKey,
ContentType: "text",
TextValue: vVal,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
res.ResultVariant = &v
}
default:
pool.Dispatch(ctx, act, entry, 0)
}
}
return res, nil
}
}