feat: add feature to speak with the AI and create report from contexts
This commit is contained in:
@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tradarr/backend/internal/crypto"
|
||||
@ -24,14 +25,19 @@ Structure ton résumé ainsi :
|
||||
4. **Synthèse** : points d'attention prioritaires pour la journée`
|
||||
|
||||
type Pipeline struct {
|
||||
repo *models.Repository
|
||||
enc *crypto.Encryptor
|
||||
repo *models.Repository
|
||||
enc *crypto.Encryptor
|
||||
generating atomic.Bool
|
||||
}
|
||||
|
||||
func NewPipeline(repo *models.Repository, enc *crypto.Encryptor) *Pipeline {
|
||||
return &Pipeline{repo: repo, enc: enc}
|
||||
}
|
||||
|
||||
func (p *Pipeline) IsGenerating() bool {
|
||||
return p.generating.Load()
|
||||
}
|
||||
|
||||
func (p *Pipeline) BuildProvider(name, apiKey, endpoint string) (Provider, error) {
|
||||
provider, err := p.repo.GetActiveAIProvider()
|
||||
if err != nil {
|
||||
@ -45,6 +51,8 @@ func (p *Pipeline) BuildProvider(name, apiKey, endpoint string) (Provider, error
|
||||
}
|
||||
|
||||
func (p *Pipeline) GenerateForUser(ctx context.Context, userID string) (*models.Summary, error) {
|
||||
p.generating.Store(true)
|
||||
defer p.generating.Store(false)
|
||||
providerCfg, err := p.repo.GetActiveAIProvider()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get active provider: %w", err)
|
||||
@ -97,9 +105,10 @@ func (p *Pipeline) GenerateForUser(ctx context.Context, userID string) (*models.
|
||||
|
||||
// Passe 1 : filtrage par pertinence sur les titres si trop d'articles
|
||||
if len(articles) > maxArticles {
|
||||
fmt.Printf("pipeline: %d articles → filtering to %d via AI\n", len(articles), maxArticles)
|
||||
fmt.Printf("[pipeline] Passe 1 — filtrage : %d articles → sélection des %d plus pertinents…\n", len(articles), maxArticles)
|
||||
t1 := time.Now()
|
||||
articles = p.filterByRelevance(ctx, provider, symbols, articles, maxArticles)
|
||||
fmt.Printf("pipeline: %d articles retained after filtering\n", len(articles))
|
||||
fmt.Printf("[pipeline] Passe 1 — terminée en %s : %d articles retenus\n", time.Since(t1).Round(time.Second), len(articles))
|
||||
}
|
||||
|
||||
systemPrompt, _ := p.repo.GetSetting("ai_system_prompt")
|
||||
@ -108,11 +117,14 @@ func (p *Pipeline) GenerateForUser(ctx context.Context, userID string) (*models.
|
||||
}
|
||||
|
||||
// Passe 2 : résumé complet
|
||||
fmt.Printf("[pipeline] Passe 2 — résumé : génération sur %d articles…\n", len(articles))
|
||||
t2 := time.Now()
|
||||
prompt := buildPrompt(systemPrompt, symbols, articles)
|
||||
summary, err := provider.Summarize(ctx, prompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("AI summarize: %w", err)
|
||||
}
|
||||
fmt.Printf("[pipeline] Passe 2 — terminée en %s\n", time.Since(t2).Round(time.Second))
|
||||
|
||||
return p.repo.CreateSummary(userID, summary, &providerCfg.ID)
|
||||
}
|
||||
@ -123,13 +135,13 @@ func (p *Pipeline) filterByRelevance(ctx context.Context, provider Provider, sym
|
||||
prompt := buildFilterPrompt(symbols, articles, max)
|
||||
response, err := provider.Summarize(ctx, prompt)
|
||||
if err != nil {
|
||||
fmt.Printf("pipeline: filter AI call failed (%v), falling back to truncation\n", err)
|
||||
fmt.Printf("[pipeline] Passe 1 — échec (%v), repli sur troncature\n", err)
|
||||
return articles[:max]
|
||||
}
|
||||
|
||||
indices := parseIndexArray(response, len(articles))
|
||||
if len(indices) == 0 {
|
||||
fmt.Printf("pipeline: could not parse filter response, falling back to truncation\n")
|
||||
fmt.Printf("[pipeline] Passe 1 — réponse non parseable, repli sur troncature\n")
|
||||
return articles[:max]
|
||||
}
|
||||
|
||||
@ -201,6 +213,58 @@ func (p *Pipeline) GenerateForAll(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateReportAsync crée le rapport en DB (status=generating) et lance la génération en arrière-plan.
|
||||
func (p *Pipeline) GenerateReportAsync(reportID, excerpt, question string, mgr *ReportManager) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
mgr.Register(reportID, cancel)
|
||||
|
||||
go func() {
|
||||
defer cancel()
|
||||
defer mgr.Remove(reportID)
|
||||
|
||||
answer, err := p.callProviderForReport(ctx, excerpt, question)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
// annulé volontairement — le rapport est supprimé par le handler
|
||||
return
|
||||
}
|
||||
_ = p.repo.UpdateReport(reportID, "error", "", err.Error())
|
||||
return
|
||||
}
|
||||
_ = p.repo.UpdateReport(reportID, "done", answer, "")
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Pipeline) callProviderForReport(ctx context.Context, excerpt, question string) (string, error) {
|
||||
providerCfg, err := p.repo.GetActiveAIProvider()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get active provider: %w", err)
|
||||
}
|
||||
if providerCfg == nil {
|
||||
return "", fmt.Errorf("no active AI provider configured")
|
||||
}
|
||||
|
||||
apiKey := ""
|
||||
if providerCfg.APIKeyEncrypted != "" {
|
||||
apiKey, err = p.enc.Decrypt(providerCfg.APIKeyEncrypted)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decrypt API key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
provider, err := NewProvider(providerCfg.Name, apiKey, providerCfg.Model, providerCfg.Endpoint)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("build provider: %w", err)
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(
|
||||
"Tu es un assistant financier expert. L'utilisateur a sélectionné les extraits suivants d'un résumé de marché :\n\n%s\n\nQuestion de l'utilisateur : %s\n\nRéponds en français, de façon précise et orientée trading.",
|
||||
excerpt, question,
|
||||
)
|
||||
|
||||
return provider.Summarize(ctx, prompt)
|
||||
}
|
||||
|
||||
func buildPrompt(systemPrompt string, symbols []string, articles []models.Article) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(systemPrompt)
|
||||
|
||||
Reference in New Issue
Block a user