Skip to content

Commit b62a0a1

Browse files
authored
Merge pull request #565 from LydiaCai1203/feat-task-summary
fix: 修复 task summary 总结无关对话内容的问题
2 parents 0186dbf + 2cee447 commit b62a0a1

2 files changed

Lines changed: 90 additions & 38 deletions

File tree

backend/biz/task/service/tasksummary.go

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"sync"
1313
"time"
14+
"unicode"
1415

1516
"github.com/google/uuid"
1617
"github.com/samber/do"
@@ -295,20 +296,13 @@ func (r *tasklogConversationReader) Fetch(ctx context.Context, taskID uuid.UUID,
295296
if maxRounds <= 0 {
296297
maxRounds = 3
297298
}
298-
logRounds := maxRounds
299-
if strings.TrimSpace(initialContent) != "" {
300-
logRounds--
301-
}
302-
if logRounds < 0 {
303-
logRounds = 0
304-
}
305299
const pageSize = 20
306300

307301
var chunks []*tasklog.TurnChunk
308302
userRoundCount := 0
309303
cursor := ""
310304

311-
for logRounds > 0 {
305+
for {
312306
resp, err := r.gateway.QueryTurns(ctx, taskID, createdAt, cursor, pageSize, store)
313307
if err != nil {
314308
return nil, fmt.Errorf("failed to fetch task log history: %w", err)
@@ -322,7 +316,7 @@ func (r *tasklogConversationReader) Fetch(ctx context.Context, taskID uuid.UUID,
322316
if chunk == nil {
323317
continue
324318
}
325-
if (chunk.Event == "user-input" || chunk.Event == "reply-question") && userRoundCount >= logRounds {
319+
if (chunk.Event == "user-input" || chunk.Event == "reply-question") && userRoundCount >= maxRounds {
326320
stopPaging = true
327321
break
328322
}
@@ -332,7 +326,7 @@ func (r *tasklogConversationReader) Fetch(ctx context.Context, taskID uuid.UUID,
332326
}
333327
}
334328

335-
if stopPaging || userRoundCount >= logRounds || !resp.HasMore || resp.NextCursor == "" {
329+
if stopPaging || userRoundCount >= maxRounds || !resp.HasMore || resp.NextCursor == "" {
336330
break
337331
}
338332
cursor = resp.NextCursor
@@ -355,9 +349,6 @@ func buildSummaryConversation(ctx context.Context, logger *slog.Logger, taskID u
355349
})
356350

357351
var messages []llm.Message
358-
if initialContent != "" {
359-
messages = append(messages, llm.Message{Role: "user", Content: initialContent})
360-
}
361352

362353
agentMsg := []string{}
363354
for _, chunk := range chunks {
@@ -410,21 +401,38 @@ func buildSummaryConversation(ctx context.Context, logger *slog.Logger, taskID u
410401
}
411402
}
412403

413-
if len(messages) == 0 {
414-
return nil, errNoConversation
415-
}
416-
417404
if len(agentMsg) > 0 {
418405
agentContent := strings.Join(agentMsg, "")
419406
messages = append(messages, llm.Message{Role: "assistant", Content: agentContent})
420407
}
421408

409+
initialContent = strings.TrimSpace(initialContent)
410+
if userRoundCount < maxRounds && initialContent != "" {
411+
messages = append([]llm.Message{{Role: "user", Content: initialContent}}, messages...)
412+
}
413+
414+
if len(messages) == 0 {
415+
return nil, errNoConversation
416+
}
417+
422418
if logger != nil {
423-
logger.DebugContext(ctx, "conversation", "task_id", taskID, "messages_count", len(messages), "messages", messages)
419+
logger.DebugContext(ctx, "task summary conversation", "task_id", taskID, "messages_count", len(messages), "conversation", formatSummaryConversation(messages))
424420
}
425421
return messages, nil
426422
}
427423

424+
func formatSummaryConversation(messages []llm.Message) []map[string]any {
425+
conversation := make([]map[string]any, 0, len(messages))
426+
for i, msg := range messages {
427+
conversation = append(conversation, map[string]any{
428+
"index": i,
429+
"role": msg.Role,
430+
"content": msg.Content,
431+
})
432+
}
433+
return conversation
434+
}
435+
428436
func userInputContent(decoded []byte) string {
429437
var payload userInputPayload
430438
if err := json.Unmarshal(decoded, &payload); err == nil && len(payload.Content) > 0 {
@@ -458,8 +466,8 @@ func (s *TaskSummaryService) generateSummary(ctx context.Context, conversation [
458466
- 不超过%d字
459467
- 不要标点结尾
460468
- 只输出标题,不要解释
461-
- 第一条用户消息是任务原始需求,必须优先依据它生成标题
462-
- 后续对话只作为补充上下文,不要把上下文交接、压缩摘要或运行状态总结当成任务标题
469+
- 只根据用户的实质需求生成标题,不要根据示例、助手回复或运行状态编造需求
470+
- 如果早期输入为空泛或无意义,但后续用户消息补充了明确需求,以后续明确需求为准
463471
- 重点关注用户想要完成什么目标,而不是 AI 问了什么问题
464472
- 标题要具体,让人一看就知道用户想做什么
465473
- 如果是开发任务:说明做的是什么应用/功能(如"开发五子棋游戏")
@@ -488,24 +496,23 @@ func (s *TaskSummaryService) generateSummary(ctx context.Context, conversation [
488496

489497
func fallbackSummaryFromConversation(conversation []llm.Message, maxChars int) (string, bool) {
490498
userInputs := make([]string, 0, len(conversation))
491-
hasAssistant := false
492499
for _, msg := range conversation {
493-
switch msg.Role {
494-
case "user":
500+
if msg.Role == "user" {
495501
content := strings.TrimSpace(msg.Content)
496502
if content != "" {
497503
userInputs = append(userInputs, content)
498504
}
499-
case "assistant":
500-
if strings.TrimSpace(msg.Content) != "" {
501-
hasAssistant = true
502-
}
503505
}
504506
}
505-
if hasAssistant || len(userInputs) != 1 || !isLowInformationInput(userInputs[0]) {
507+
if len(userInputs) == 0 {
506508
return "", false
507509
}
508-
return truncateSummary(userInputs[0], maxChars), true
510+
for _, input := range userInputs {
511+
if !isLowInformationInput(input) {
512+
return "", false
513+
}
514+
}
515+
return truncateSummary(userInputs[len(userInputs)-1], maxChars), true
509516
}
510517

511518
func isLowInformationInput(input string) bool {
@@ -515,7 +522,12 @@ func isLowInformationInput(input string) bool {
515522
case "hi", "hello", "hey", "你好", "您好", "嗨", "哈喽", "hello there", "ok", "okay", "嗯", "嗯嗯", "额":
516523
return true
517524
}
518-
return false
525+
for _, r := range normalized {
526+
if unicode.IsLetter(r) {
527+
return false
528+
}
529+
}
530+
return true
519531
}
520532

521533
func truncateSummary(s string, maxChars int) string {

backend/biz/task/service/tasksummary_test.go

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func TestBuildSummaryConversationUsesUserInputPayloadContent(t *testing.T) {
8383
taskID,
8484
[]*tasklog.TurnChunk{{Event: "user-input", Data: []byte(base64.StdEncoding.EncodeToString(payload)), Timestamp: 1}},
8585
1,
86-
3,
86+
1,
8787
"",
8888
)
8989
if err != nil {
@@ -149,25 +149,39 @@ func TestTasklogConversationReaderKeepsOnlyRecentMaxRoundsInSinglePage(t *testin
149149
assertMessage(t, messages[1], "assistant", "最新助手")
150150
}
151151

152-
func TestTasklogConversationReaderAnchorsSummaryToInitialContent(t *testing.T) {
152+
func TestTasklogConversationReaderUsesInitialContentWhenLogRoundsAreInsufficient(t *testing.T) {
153+
gateway := &fakeTasklogGateway{responses: []*tasklog.QueryTurnsResp{{}}}
154+
reader := newTasklogConversationReader(gateway, slog.New(slog.NewTextHandler(io.Discard, nil)))
155+
156+
messages, err := reader.Fetch(context.Background(), uuid.New(), time.Now(), consts.LogStoreClickHouse, "修复 ClickHouse 日志查询失败", 3)
157+
if err != nil {
158+
t.Fatalf("Fetch() error = %v", err)
159+
}
160+
if gateway.calls != 1 {
161+
t.Fatalf("gateway calls = %d, want 1", gateway.calls)
162+
}
163+
if len(messages) != 1 {
164+
t.Fatalf("len(messages) = %d, want 1: %#v", len(messages), messages)
165+
}
166+
assertMessage(t, messages[0], "user", "修复 ClickHouse 日志查询失败")
167+
}
168+
169+
func TestTasklogConversationReaderDoesNotUseInitialContentWhenLogRoundsAreEnough(t *testing.T) {
153170
gateway := &fakeTasklogGateway{responses: []*tasklog.QueryTurnsResp{{
154171
Chunks: []*tasklog.TurnChunk{
155-
{Event: "user-input", Data: []byte(base64.StdEncoding.EncodeToString([]byte("不相干的上下文摘要"))), Timestamp: 1},
172+
{Event: "user-input", Data: []byte(base64.StdEncoding.EncodeToString([]byte("实现登录页"))), Timestamp: 1},
156173
},
157174
}}}
158175
reader := newTasklogConversationReader(gateway, slog.New(slog.NewTextHandler(io.Discard, nil)))
159176

160-
messages, err := reader.Fetch(context.Background(), uuid.New(), time.Now(), consts.LogStoreClickHouse, "修复 ClickHouse 日志查询失败", 1)
177+
messages, err := reader.Fetch(context.Background(), uuid.New(), time.Now(), consts.LogStoreClickHouse, "111", 1)
161178
if err != nil {
162179
t.Fatalf("Fetch() error = %v", err)
163180
}
164-
if gateway.calls != 0 {
165-
t.Fatalf("gateway calls = %d, want 0", gateway.calls)
166-
}
167181
if len(messages) != 1 {
168182
t.Fatalf("len(messages) = %d, want 1: %#v", len(messages), messages)
169183
}
170-
assertMessage(t, messages[0], "user", "修复 ClickHouse 日志查询失败")
184+
assertMessage(t, messages[0], "user", "实现登录页")
171185
}
172186

173187
func TestFallbackSummaryForLowInformationGreeting(t *testing.T) {
@@ -182,6 +196,32 @@ func TestFallbackSummaryForLowInformationGreeting(t *testing.T) {
182196
}
183197
}
184198

199+
func TestFallbackSummaryForNumericInput(t *testing.T) {
200+
summary, ok := fallbackSummaryFromConversation([]llm.Message{
201+
{Role: "user", Content: "111"},
202+
}, 300)
203+
if !ok {
204+
t.Fatal("expected fallback summary")
205+
}
206+
if summary != "111" {
207+
t.Fatalf("summary = %q, want 111", summary)
208+
}
209+
}
210+
211+
func TestFallbackSummaryForAllLowInformationInputsUsesLatest(t *testing.T) {
212+
summary, ok := fallbackSummaryFromConversation([]llm.Message{
213+
{Role: "user", Content: "111"},
214+
{Role: "assistant", Content: "请提供更多信息"},
215+
{Role: "user", Content: "222"},
216+
}, 300)
217+
if !ok {
218+
t.Fatal("expected fallback summary")
219+
}
220+
if summary != "222" {
221+
t.Fatalf("summary = %q, want 222", summary)
222+
}
223+
}
224+
185225
func mustJSON(t *testing.T, v any) []byte {
186226
t.Helper()
187227
data, err := json.Marshal(v)

0 commit comments

Comments
 (0)