chatpb/helpers.go

185 lines
4.1 KiB
Go

package chatpb
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
"go.wit.com/log"
"google.golang.org/protobuf/proto"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
func (c *Chats) AddGeminiComment(s string) *Chat {
chat := new(Chat)
chat.From = Who_GEMINI
chat.Content = s
chat.Ctime = timestamppb.New(time.Now())
c.AppendNew(chat)
return chat
}
func (c *Chats) AddUserComment(s string) *Chat {
chat := new(Chat)
chat.From = Who_USER
chat.Content = s
c.AppendNew(chat)
return chat
}
func UnmarshalChats(data []byte) (*Chats, error) {
c := new(Chats)
err := c.Unmarshal(data)
return c, err
}
func UnmarshalChatsTEXT(data []byte) (*Chats, error) {
c := new(Chats)
err := c.UnmarshalTEXT(data)
return c, err
}
func (all *Chats) AddFile(filename string) error {
// Nil checks for safety.
if all == nil {
return fmt.Errorf("cannot call AddFile on a nil *Chats object")
}
if all.Chats == nil {
all.Chats = make([]*Chat, 0)
}
data, err := os.ReadFile(filename)
if err != nil {
log.Fatalf("Error reading file %s: %v", filename, err)
return err
}
logData, err := UnmarshalChatsTEXT(data)
if err != nil {
log.Fatalf("Error unmarshaling log file %s: %v", filename, err)
return err
}
// Iterate through the top-level messages from the source file.
for _, chatGroup := range logData.GetChats() {
if len(chatGroup.GetEntries()) > 0 {
// NEW FORMAT: This is a group, process its entries.
for _, entry := range chatGroup.GetEntries() {
// Convert the ChatEntry into a new Chat object for the flat list.
newChat := convertEntryToChat(entry, filename)
if newChat != nil {
newChat.VerifyUuid()
all.AppendByUuid(newChat)
}
}
} else {
// OLD FORMAT: This is a single, flat Chat message.
// We still process it to handle its external content file correctly.
newChat := convertChatToChat(chatGroup, filename)
if newChat != nil {
newChat.VerifyUuid()
all.AppendByUuid(newChat)
}
}
}
return nil
}
// convertChatToChat handles an old-style Chat message. It creates a clean
// copy and resolves its external content file.
func convertChatToChat(chat *Chat, filename string) *Chat {
if chat == nil {
return nil
}
// Manually create a ChatEntry from the Chat fields to reuse the logic.
entry := &ChatEntry{
From: chat.GetFrom(),
Ctime: chat.GetCtime(),
Content: chat.GetContent(),
Table: chat.GetTable(),
ToolCalls: chat.GetToolCalls(),
ContentFile: chat.GetContentFile(),
Uuid: chat.GetUuid(),
Snippets: chat.GetSnippets(),
}
return convertEntryToChat(entry, filename)
}
// convertEntryToChat creates a new Chat object from a ChatEntry's data
// and resolves its external content file.
func convertEntryToChat(entry *ChatEntry, filename string) *Chat {
if entry == nil {
return nil
}
// Create a new Chat object and copy the fields.
newChat := &Chat{
From: entry.GetFrom(),
Ctime: entry.GetCtime(),
Table: entry.GetTable(),
ToolCalls: entry.GetToolCalls(),
Snippets: entry.GetSnippets(),
Uuid: entry.GetUuid(),
}
// Handle content: prefer content_file, fallback to content.
var content string
if contentFile := entry.GetContentFile(); contentFile != "" {
logDir := filepath.Dir(filename)
contentPath := filepath.Join(logDir, contentFile)
contentBytes, err := os.ReadFile(contentPath)
if err != nil {
content = fmt.Sprintf("--- ERROR: Could not read content file %s: %v ---", contentPath, err)
} else {
content = string(contentBytes)
}
} else {
content = entry.GetContent()
}
newChat.Content = content
return newChat
}
func (chats *Chats) VerifyUuids() bool {
var changed bool
all := chats.SortByUuid()
for all.Scan() {
chat := all.Next()
if chat.Uuid == "" {
chat.Uuid = uuid.New().String()
changed = true
}
}
return changed
}
func (c *Chat) VerifyUuid() bool {
if c.Uuid == "" {
c.Uuid = uuid.New().String()
return true
}
return false
}
func (x *Chats) AppendNew(y *Chat) {
x.Lock()
defer x.Unlock()
var chat *Chat
chat = proto.Clone(y).(*Chat)
x.Chats = append(x.Chats, chat)
}