| 767 | } |
| 768 | |
| 769 | func generateManualTitle( |
| 770 | ctx context.Context, |
| 771 | messages []database.ChatMessage, |
| 772 | fallbackModel fantasy.LanguageModel, |
| 773 | ) (string, fantasy.Usage, error) { |
| 774 | turns := extractManualTitleTurns(messages) |
| 775 | selected := selectManualTitleTurnIndexes(turns) |
| 776 | |
| 777 | firstUserIndex := slices.IndexFunc(turns, func(turn manualTitleTurn) bool { |
| 778 | return turn.role == string(database.ChatMessageRoleUser) |
| 779 | }) |
| 780 | if firstUserIndex == -1 { |
| 781 | return "", fantasy.Usage{}, nil |
| 782 | } |
| 783 | firstUserText := truncateRunes(turns[firstUserIndex].text, maxLatestUserMessageRunes) |
| 784 | |
| 785 | conversationBlock, latestUserMsg := buildManualTitleContext(turns, selected) |
| 786 | systemPrompt := renderManualTitlePrompt( |
| 787 | conversationBlock, |
| 788 | firstUserText, |
| 789 | latestUserMsg, |
| 790 | ) |
| 791 | |
| 792 | titleCtx, cancel := context.WithTimeout(ctx, 30*time.Second) |
| 793 | defer cancel() |
| 794 | |
| 795 | userInput := strings.TrimSpace(latestUserMsg) |
| 796 | if userInput == "" { |
| 797 | userInput = strings.TrimSpace(firstUserText) |
| 798 | } |
| 799 | |
| 800 | title, usage, err := generateStructuredTitleWithUsage( |
| 801 | titleCtx, |
| 802 | fallbackModel, |
| 803 | systemPrompt, |
| 804 | userInput, |
| 805 | ) |
| 806 | if err != nil { |
| 807 | return "", usage, err |
| 808 | } |
| 809 | |
| 810 | return title, usage, nil |
| 811 | } |
| 812 | |
| 813 | const turnStatusLabelPrompt = "You write compact chat status labels for a sidebar or push notification. " + |
| 814 | "Given a chat title, current chat state, and the agent's latest message, populate the label field with a 2-5 word status label. " + |