This commit is contained in:
+35
-98
@@ -5,7 +5,6 @@ import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -91,8 +90,6 @@ func GeminiCandidateLimit(total int) int {
|
||||
}
|
||||
|
||||
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats, error) {
|
||||
const chunkSize = 8
|
||||
const maxConcurrentBatches = 2
|
||||
if service == nil {
|
||||
return nil, GeminiBatchStats{}, fmt.Errorf("gemini service is not configured")
|
||||
}
|
||||
@@ -102,76 +99,28 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
|
||||
CandidateCap: limit,
|
||||
Requested: min(limit, len(ranked)),
|
||||
}
|
||||
type batchResult struct {
|
||||
index int
|
||||
recommendations []AIRecommendation
|
||||
err error
|
||||
}
|
||||
batches := make([][]SearchResult, 0, (limit+chunkSize-1)/chunkSize)
|
||||
for start := 0; start < limit; start += chunkSize {
|
||||
end := start + chunkSize
|
||||
if end > limit {
|
||||
end = limit
|
||||
}
|
||||
batches = append(batches, ranked[start:end])
|
||||
}
|
||||
stats.Batches = len(batches)
|
||||
if len(batches) == 0 {
|
||||
stats.Batches = limit
|
||||
if limit == 0 {
|
||||
return []AIRecommendation{}, stats, nil
|
||||
}
|
||||
|
||||
results := make([]batchResult, len(batches))
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, maxConcurrentBatches)
|
||||
for idx, batch := range batches {
|
||||
wg.Add(1)
|
||||
go func(batchIndex int, candidates []SearchResult) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
recommended, err := service.Recommend(query, candidates)
|
||||
results[batchIndex] = batchResult{
|
||||
index: batchIndex,
|
||||
recommendations: recommended,
|
||||
err: err,
|
||||
}
|
||||
}(idx, batch)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
merged := make([]AIRecommendation, 0, len(ranked))
|
||||
seen := map[string]bool{}
|
||||
for _, batch := range results {
|
||||
if batch.err != nil {
|
||||
recovered, recoveredErrs := recoverGeminiBatchSequentially(service, query, ranked, batch.index*chunkSize)
|
||||
if len(recovered) > 0 {
|
||||
stats.SequentialRetried++
|
||||
stats.Succeeded++
|
||||
for _, item := range recovered {
|
||||
if item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
seen[item.Link] = true
|
||||
merged = append(merged, item)
|
||||
}
|
||||
if len(recoveredErrs) > 0 {
|
||||
stats.Failed++
|
||||
for _, recoveredErr := range recoveredErrs {
|
||||
if len(stats.Errors) < 5 {
|
||||
stats.Errors = append(stats.Errors, recoveredErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
for idx := 0; idx < limit; idx++ {
|
||||
recommendations, err := recoverGeminiCandidateSequentially(service, query, ranked[idx])
|
||||
if err != nil {
|
||||
stats.Failed++
|
||||
if len(stats.Errors) < 5 {
|
||||
stats.Errors = append(stats.Errors, batch.err.Error())
|
||||
stats.Errors = append(stats.Errors, err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
stats.Succeeded++
|
||||
for _, item := range batch.recommendations {
|
||||
if len(recommendations) == 0 {
|
||||
continue
|
||||
}
|
||||
stats.SequentialRetried++
|
||||
for _, item := range recommendations {
|
||||
if item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
@@ -185,12 +134,12 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
|
||||
case len(merged) > 0 && stats.Failed == 0:
|
||||
return merged, stats, nil
|
||||
case len(merged) > 0 && stats.Failed > 0:
|
||||
return merged, stats, fmt.Errorf("gemini vision partially failed on %d of %d batches", stats.Failed, stats.Batches)
|
||||
return merged, stats, fmt.Errorf("gemini vision partially failed on %d of %d candidates", stats.Failed, stats.Batches)
|
||||
case stats.Failed == stats.Batches:
|
||||
if len(stats.Errors) > 0 {
|
||||
return nil, stats, fmt.Errorf("gemini vision failed for all batches: %s", strings.Join(stats.Errors, "; "))
|
||||
return nil, stats, fmt.Errorf("gemini vision failed for all candidates: %s", strings.Join(stats.Errors, "; "))
|
||||
}
|
||||
return nil, stats, fmt.Errorf("gemini vision failed for all batches")
|
||||
return nil, stats, fmt.Errorf("gemini vision failed for all candidates")
|
||||
default:
|
||||
return nil, stats, fmt.Errorf("gemini vision returned no candidate evaluations")
|
||||
}
|
||||
@@ -232,23 +181,21 @@ func RandomizeTopRecommendations(items []AIRecommendation, window int) []AIRecom
|
||||
return shuffled
|
||||
}
|
||||
|
||||
func recoverGeminiBatchSequentially(service *GeminiService, query string, ranked []SearchResult, startIndex int) ([]AIRecommendation, []string) {
|
||||
recovered := make([]AIRecommendation, 0, 8)
|
||||
errs := make([]string, 0, 4)
|
||||
endIndex := min(startIndex+8, len(ranked))
|
||||
for idx := startIndex; idx < endIndex; idx++ {
|
||||
recs, err := service.Recommend(query, []SearchResult{ranked[idx]})
|
||||
func recoverGeminiCandidateSequentially(service *GeminiService, query string, candidate SearchResult) ([]AIRecommendation, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
recs, err := service.Recommend(query, []SearchResult{candidate})
|
||||
if err != nil {
|
||||
if len(errs) < 4 {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
lastErr = err
|
||||
time.Sleep(450 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
recovered = append(recovered, recs...)
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
return recs, nil
|
||||
}
|
||||
return recovered, errs
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("gemini vision sequential retry returned no result")
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func NeedsSupplementalExploration(items []AIRecommendation) bool {
|
||||
@@ -266,7 +213,7 @@ func NeedsSupplementalExploration(items []AIRecommendation) bool {
|
||||
negativeCount++
|
||||
}
|
||||
}
|
||||
if recommendedCount >= 3 {
|
||||
if recommendedCount >= 5 {
|
||||
return false
|
||||
}
|
||||
return negativeCount >= max(2, len(items)/2)
|
||||
@@ -293,7 +240,7 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
||||
seen := map[string]bool{}
|
||||
|
||||
for _, item := range recommended {
|
||||
if !item.Recommended {
|
||||
if !item.Recommended || shouldExcludeRecommendation(item) {
|
||||
continue
|
||||
}
|
||||
if item.Link == "" || seen[item.Link] {
|
||||
@@ -304,29 +251,12 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
||||
}
|
||||
|
||||
for _, item := range recommended {
|
||||
if item.Recommended || item.Link == "" || seen[item.Link] || len(merged) >= limit {
|
||||
if item.Recommended || item.Link == "" || seen[item.Link] || len(merged) >= limit || shouldExcludeRecommendation(item) {
|
||||
continue
|
||||
}
|
||||
seen[item.Link] = true
|
||||
merged = append(merged, item)
|
||||
}
|
||||
|
||||
for _, item := range ranked {
|
||||
if len(merged) >= limit || item.Link == "" || seen[item.Link] {
|
||||
continue
|
||||
}
|
||||
seen[item.Link] = true
|
||||
merged = append(merged, AIRecommendation{
|
||||
Title: item.Title,
|
||||
Link: item.Link,
|
||||
Snippet: item.Snippet,
|
||||
ThumbnailURL: item.ThumbnailURL,
|
||||
PreviewVideoURL: item.PreviewVideoURL,
|
||||
Source: item.Source,
|
||||
Reason: GeminiFallbackReason,
|
||||
Recommended: false,
|
||||
})
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -336,3 +266,10 @@ func max(a, b int) int {
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func shouldExcludeRecommendation(item AIRecommendation) bool {
|
||||
if strings.Contains(item.Reason, GeminiFallbackReason) {
|
||||
return true
|
||||
}
|
||||
return looksNegativeReason(item.Reason)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user