readysite / website / internal / assist / conversion.go
3.6 KB
conversion.go
package assist

import (
	"fmt"
	"strings"

	"github.com/readysite/readysite/pkg/assistant"
	"github.com/readysite/readysite/website/models"
)

// BuildMessages converts conversation history to AI message format.
// Uses summaries for older messages to reduce token usage.
func BuildMessages(conv *models.Conversation) []assistant.Message {
	msgs, _ := conv.Messages()
	result := make([]assistant.Message, 0, len(msgs))

	// Check for existing summaries
	summaries := GetAllSummaries(conv.ID)

	// Build a set of message IDs that are covered by summaries
	summarizedIDs := make(map[string]bool)
	for _, summary := range summaries {
		// Mark all messages between start and end as summarized
		inRange := false
		for _, msg := range msgs {
			if msg.ID == summary.StartMessageID {
				inRange = true
			}
			if inRange {
				summarizedIDs[msg.ID] = true
			}
			if msg.ID == summary.EndMessageID {
				break
			}
		}
	}

	// Add summaries as system context if we have any
	if len(summaries) > 0 {
		var summaryBuilder strings.Builder
		for _, summary := range summaries {
			fmt.Fprintf(&summaryBuilder, "Previous conversation summary (%d messages): %s\n", summary.MessageCount, summary.Summary)
		}
		// Add summary as a user message to provide context
		// Using user message because some providers don't support system messages mid-conversation
		result = append(result, assistant.NewUserMessage("[CONVERSATION CONTEXT]\n"+summaryBuilder.String()+"\nPlease continue our conversation with this context in mind."))
		result = append(result, assistant.NewAssistantMessage("I understand the context from our previous discussion. How can I help you now?"))
	}

	for _, msg := range msgs {
		// Skip pending or streaming messages
		if msg.Status == "pending" || msg.Status == "streaming" {
			continue
		}

		// Skip messages that have been summarized
		if summarizedIDs[msg.ID] {
			continue
		}

		switch msg.Role {
		case RoleUser:
			content := msg.Content

			// Append attached file content
			if files, _ := msg.Files(); len(files) > 0 {
				var fileContent strings.Builder
				for _, file := range files {
					if text, err := GetFileContent(file.ID); err == nil {
						fileContent.WriteString(fmt.Sprintf("\n\n--- Attached File: %s ---\n%s", file.Name, text))
					} else if file.IsImage() {
						fileContent.WriteString(fmt.Sprintf("\n\n[Attached Image: %s (%s)]", file.Name, file.MimeType))
					} else {
						// For non-text, non-image files, note that they're attached but can't be read
						fileContent.WriteString(fmt.Sprintf("\n\n[Attached File: %s (%s) - content not readable]", file.Name, file.MimeType))
					}
				}
				if fileContent.Len() > 0 {
					content += fileContent.String()
				}
			}

			result = append(result, assistant.NewUserMessage(content))
		case RoleAssistant:
			toolCalls, _ := GetToolCalls(msg)
			if len(toolCalls) > 0 {
				// Convert tool calls
				aiToolCalls := make([]assistant.ToolCall, len(toolCalls))
				for i, tc := range toolCalls {
					aiToolCalls[i] = assistant.ToolCall{
						ID:        tc.ID,
						Name:      tc.Name,
						Arguments: tc.Arguments,
					}
				}
				result = append(result, assistant.NewAssistantToolCallMessage(msg.Content, aiToolCalls))

				// Add tool results
				for _, tc := range toolCalls {
					if tc.Result != "" {
						result = append(result, assistant.NewToolResultMessage(tc.ID, tc.Result))
					} else if tc.Error != "" {
						result = append(result, assistant.NewToolResultMessage(tc.ID, "Error: "+tc.Error))
					}
				}
			} else if msg.Content != "" {
				result = append(result, assistant.NewAssistantMessage(msg.Content))
			}
		}
	}

	return result
}
← Back