-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhandle_recommendation.go
More file actions
143 lines (127 loc) · 3.86 KB
/
handle_recommendation.go
File metadata and controls
143 lines (127 loc) · 3.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/ziyixi/todofy/utils"
pb "github.com/ziyixi/protos/go/todofy"
)
const (
TimeDurationToRecommendation = 24 * time.Hour
DefaultTopN = 3
MaxTopN = 10
taskSummarySplitter = "=========================\n"
)
// LLM retry configuration — var so tests can override.
var (
LLMMaxRetries = 3
LLMRetrySleep = 5 * time.Second
)
// TaskRecommendation represents a single recommended task entry.
type TaskRecommendation struct {
Rank int `json:"rank"`
Title string `json:"title"`
Reason string `json:"reason"`
}
// RecommendationResponse is the top-level JSON response.
type RecommendationResponse struct {
Tasks []TaskRecommendation `json:"tasks"`
Model string `json:"model"`
TaskCount int `json:"task_count"`
}
// HandleRecommendation queries recent tasks from the last 24 hours,
// asks the LLM to pick the top-N most important ones, and returns
// the result as a structured JSON array for consumption by other apps.
// Optional query parameter: ?top=N (default 3, max 10).
func HandleRecommendation(c *gin.Context) {
clients := c.MustGet(utils.KeyGRPCClients).(ClientProvider)
// Parse optional "top" query parameter
topN := DefaultTopN
if topStr := c.Query("top"); topStr != "" {
if n, err := strconv.Atoi(topStr); err == nil && n >= 1 && n <= MaxTopN {
topN = n
} else {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf(
"invalid top parameter: must be 1-%d", MaxTopN),
})
return
}
}
// Query recent tasks from the database
databaseClient := clients.GetClient("database").(pb.DataBaseServiceClient)
queryReq := &pb.QueryRecentRequest{
Type: pb.DatabaseType_DATABASE_TYPE_SQLITE,
TimeAgoInSeconds: int64(TimeDurationToRecommendation.Seconds()),
}
queryResp, err := databaseClient.QueryRecent(c, queryReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if len(queryResp.Entries) == 0 {
c.JSON(http.StatusOK, RecommendationResponse{
Tasks: []TaskRecommendation{},
TaskCount: 0,
})
return
}
// Build content from task summaries
content := taskSummarySplitter
for _, entry := range queryResp.Entries {
content += entry.Summary + "\n" + taskSummarySplitter
}
// Generate recommendation via LLM
prompt := fmt.Sprintf(
utils.DefaultPromptToRecommendTopTasks,
topN, topN, topN, topN,
)
recReq := &pb.LLMSummaryRequest{
ModelFamily: pb.ModelFamily_MODEL_FAMILY_GEMINI,
Model: utils.RecommendationModel,
Prompt: prompt,
Text: content,
}
llmClient := clients.GetClient("llm").(pb.LLMSummaryServiceClient)
var recResp *pb.LLMSummaryResponse
for attempt := 1; attempt <= LLMMaxRetries; attempt++ {
recResp, err = llmClient.Summarize(c, recReq)
if err == nil {
break
}
log.Printf(
"LLM Summarize attempt %d/%d failed: %v",
attempt, LLMMaxRetries, err,
)
if attempt < LLMMaxRetries {
time.Sleep(LLMRetrySleep)
}
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Parse the JSON array from LLM response
var tasks []TaskRecommendation
raw := strings.TrimSpace(recResp.Summary)
// Strip markdown code fences if the LLM wraps the output
raw = strings.TrimPrefix(raw, "```json")
raw = strings.TrimPrefix(raw, "```")
raw = strings.TrimSuffix(raw, "```")
raw = strings.TrimSpace(raw)
if err := json.Unmarshal([]byte(raw), &tasks); err != nil {
// Fallback: return raw text as a single entry so callers still get data
tasks = []TaskRecommendation{
{Rank: 1, Title: "recommendation", Reason: recResp.Summary},
}
}
c.JSON(http.StatusOK, RecommendationResponse{
Tasks: tasks,
Model: recResp.Model.String(),
TaskCount: len(queryResp.Entries),
})
}