feat: implement AI integration with backend configuration and rule-based enrichment actions
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
)
|
||||
|
||||
// LoadConfig fetches current AI provider configuration from system settings.
|
||||
func LoadConfig(database *db.DB) ai.Config {
|
||||
settings, _ := database.GetSettingsMap()
|
||||
enabled := settings["ai_enabled"] == "true" || settings["ai_enabled"] == "1"
|
||||
provider := settings["ai_provider"]
|
||||
if provider == "" {
|
||||
provider = "openai"
|
||||
}
|
||||
baseURL := settings["ai_base_url"]
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.openai.com/v1"
|
||||
}
|
||||
model := settings["ai_model"]
|
||||
if model == "" {
|
||||
model = "gpt-4o-mini"
|
||||
}
|
||||
return ai.Config{
|
||||
Enabled: enabled,
|
||||
Provider: provider,
|
||||
BaseURL: baseURL,
|
||||
APIKey: settings["ai_api_key"],
|
||||
Model: model,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEnrichmentHandler creates a worker action handler for AI variant enrichment.
|
||||
func NewEnrichmentHandler(database *db.DB, client *ai.Client, 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 action skipped: API key is not configured in Settings > AI & Integrations",
|
||||
zap.String("entry_id", entry.ID),
|
||||
zap.String("provider", cfg.Provider),
|
||||
)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
prompt, _ := params["prompt"].(string)
|
||||
if prompt == "" {
|
||||
prompt, _ = params["custom_prompt"].(string)
|
||||
}
|
||||
vKey, _ := params["variant_key"].(string)
|
||||
if vKey == "" {
|
||||
vKey = "ai_summary"
|
||||
}
|
||||
cType, _ := params["content_type"].(string)
|
||||
if cType == "" {
|
||||
cType = "text"
|
||||
}
|
||||
|
||||
resultText, err := client.GenerateEnrichment(ctx, cfg, prompt, entry)
|
||||
if err != nil {
|
||||
log.Error("AI enrichment generation failed",
|
||||
zap.String("entry_id", entry.ID),
|
||||
zap.String("model", cfg.Model),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variant := models.EntryVariant{
|
||||
EntryID: entry.ID,
|
||||
VariantKey: vKey,
|
||||
ContentType: cType,
|
||||
TextValue: resultText,
|
||||
Metadata: map[string]interface{}{
|
||||
"model": cfg.Model,
|
||||
"provider": cfg.Provider,
|
||||
"prompt_used": prompt,
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
log.Info("AI enrichment completed",
|
||||
zap.String("entry_id", entry.ID),
|
||||
zap.String("variant_key", vKey),
|
||||
zap.String("model", cfg.Model),
|
||||
)
|
||||
|
||||
return &worker.ActionResult{
|
||||
ResultVariant: &variant,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user