initial commit
This commit is contained in:
10
backend/internal/store/errors.go
Normal file
10
backend/internal/store/errors.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrChatNotFound = errors.New("chat not found")
|
||||
ErrNilChatID = errors.New("chat id cannot be nil")
|
||||
)
|
||||
18
backend/internal/store/interface.go
Normal file
18
backend/internal/store/interface.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
// Settings Methods
|
||||
SaveSettings(*Settings) error
|
||||
GetSettings() (*Settings, error)
|
||||
|
||||
// Chat Methods
|
||||
GetChat(chatID uuid.UUID) (*Chat, error)
|
||||
DeleteChat(chatID uuid.UUID) error
|
||||
ListChats() ([]*Chat, error)
|
||||
SaveChat(*Chat) error
|
||||
SaveChatMessage(*Message) error
|
||||
}
|
||||
126
backend/internal/store/memory.go
Normal file
126
backend/internal/store/memory.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"reichard.io/aethera/pkg/slices"
|
||||
)
|
||||
|
||||
var _ Store = (*InMemoryStore)(nil)
|
||||
|
||||
// InMemoryStore implements Store interface using in-memory storage
|
||||
type InMemoryStore struct {
|
||||
mu sync.RWMutex
|
||||
chats map[uuid.UUID]*Chat
|
||||
settings *Settings
|
||||
}
|
||||
|
||||
// NewInMemoryStore creates a new InMemoryStore
|
||||
func NewInMemoryStore() *InMemoryStore {
|
||||
return &InMemoryStore{
|
||||
chats: make(map[uuid.UUID]*Chat),
|
||||
}
|
||||
}
|
||||
|
||||
// SaveChat creates or updates a chat
|
||||
func (s *InMemoryStore) SaveChat(c *Chat) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
c.ensureDefaults()
|
||||
s.chats[c.ID] = c
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetChat retrieves a chat by ID
|
||||
func (s *InMemoryStore) GetChat(chatID uuid.UUID) (*Chat, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
chat, exists := s.chats[chatID]
|
||||
if !exists {
|
||||
return nil, ErrChatNotFound
|
||||
}
|
||||
|
||||
// Return a copy to avoid concurrent modification
|
||||
return chat, nil
|
||||
}
|
||||
|
||||
// DeleteChat removes a chat by ID
|
||||
func (s *InMemoryStore) DeleteChat(chatID uuid.UUID) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.chats[chatID]; !exists {
|
||||
return ErrChatNotFound
|
||||
}
|
||||
|
||||
delete(s.chats, chatID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListChats returns all chat
|
||||
func (s *InMemoryStore) ListChats() ([]*Chat, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
// Convert Map
|
||||
var chats []*Chat
|
||||
for _, chat := range s.chats {
|
||||
chats = append(chats, chat)
|
||||
}
|
||||
|
||||
return chats, nil
|
||||
}
|
||||
|
||||
// SaveChatMessage creates or updates a chat message to a chat
|
||||
func (s *InMemoryStore) SaveChatMessage(m *Message) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if m.ChatID == uuid.Nil {
|
||||
return ErrNilChatID
|
||||
}
|
||||
m.ensureDefaults()
|
||||
|
||||
// Get Chat
|
||||
chat, exists := s.chats[m.ChatID]
|
||||
if !exists {
|
||||
return ErrChatNotFound
|
||||
}
|
||||
|
||||
// Find Existing
|
||||
existingMsg, found := slices.FindFirst(chat.Messages, func(item *Message) bool {
|
||||
return item.ID == m.ID
|
||||
})
|
||||
|
||||
// Upsert
|
||||
if found {
|
||||
*existingMsg = *m
|
||||
} else {
|
||||
chat.Messages = append(chat.Messages, m)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveSettings saves settings to in-memory storage
|
||||
func (s *InMemoryStore) SaveSettings(settings *Settings) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.settings = settings
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSettings retrieves settings from in-memory storage
|
||||
func (s *InMemoryStore) GetSettings() (*Settings, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if s.settings == nil {
|
||||
return &Settings{}, nil
|
||||
}
|
||||
return s.settings, nil
|
||||
}
|
||||
190
backend/internal/store/storage.go
Normal file
190
backend/internal/store/storage.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"reichard.io/aethera/pkg/slices"
|
||||
)
|
||||
|
||||
var _ Store = (*FileStore)(nil)
|
||||
|
||||
// Settings represents the application settings
|
||||
type Settings struct {
|
||||
APIEndpoint string `json:"api_endpoint,omitempty"`
|
||||
ImageEditSelector string `json:"image_edit_selector,omitempty"`
|
||||
ImageGenerationSelector string `json:"image_generation_selector,omitempty"`
|
||||
TextGenerationSelector string `json:"text_generation_selector,omitempty"`
|
||||
}
|
||||
|
||||
// FileStore implements the Store interface using a file-based storage
|
||||
type FileStore struct {
|
||||
filePath string
|
||||
chatDir string
|
||||
}
|
||||
|
||||
// NewFileStore creates a new FileStore with the specified file path
|
||||
func NewFileStore(filePath string) (*FileStore, error) {
|
||||
// Derive Chat Directory
|
||||
chatDir := filepath.Join(filepath.Dir(filePath), "chats")
|
||||
|
||||
// Ensure Chat Directory Exists
|
||||
if err := os.MkdirAll(chatDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
return &FileStore{
|
||||
filePath: filePath,
|
||||
chatDir: chatDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSettings reads and returns the settings from the file
|
||||
func (fs *FileStore) GetSettings() (*Settings, error) {
|
||||
data, err := os.ReadFile(fs.filePath)
|
||||
if err != nil {
|
||||
return &Settings{}, nil
|
||||
}
|
||||
|
||||
var settings Settings
|
||||
err = json.Unmarshal(data, &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// SaveSettings saves the settings to the file
|
||||
func (fs *FileStore) SaveSettings(settings *Settings) error {
|
||||
data, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(fs.filePath, data, 0644)
|
||||
}
|
||||
|
||||
// GetChat retrieves a chat from disk
|
||||
func (fs *FileStore) GetChat(chatID uuid.UUID) (*Chat, error) {
|
||||
filePath := filepath.Join(fs.chatDir, chatID.String()+".json")
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrChatNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read chat: %w", err)
|
||||
}
|
||||
|
||||
var chat Chat
|
||||
if err := json.Unmarshal(data, &chat); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal chat: %w", err)
|
||||
}
|
||||
|
||||
return &chat, nil
|
||||
}
|
||||
|
||||
// SaveChat creates or updates a chat and persists it to disk
|
||||
func (fs *FileStore) SaveChat(c *Chat) error {
|
||||
c.ensureDefaults()
|
||||
return fs.saveChatSession(c)
|
||||
}
|
||||
|
||||
// DeleteChat removes a chat from disk
|
||||
func (fs *FileStore) DeleteChat(chatID uuid.UUID) error {
|
||||
filePath := filepath.Join(fs.chatDir, chatID.String()+".json")
|
||||
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
return ErrChatNotFound
|
||||
}
|
||||
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
return fmt.Errorf("failed to delete chat: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListChats returns all persisted chats
|
||||
func (fs *FileStore) ListChats() ([]*Chat, error) {
|
||||
// Read Files
|
||||
entries, err := os.ReadDir(fs.chatDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read chat directory: %w", err)
|
||||
}
|
||||
|
||||
var chats []*Chat
|
||||
for _, entry := range entries {
|
||||
// Ensure JSON
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract Chat ID
|
||||
rawChatID := strings.TrimSuffix(entry.Name(), ".json")
|
||||
chatID, err := uuid.Parse(rawChatID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid chat id %s", err, rawChatID)
|
||||
}
|
||||
|
||||
// Read & Parse Chat
|
||||
chat, err := fs.GetChat(chatID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: failed to read chat id %s", err, rawChatID)
|
||||
}
|
||||
|
||||
chats = append(chats, chat)
|
||||
}
|
||||
|
||||
return chats, nil
|
||||
}
|
||||
|
||||
// SaveChatMessage creates or updates a chat message to a chat and persists it to disk
|
||||
func (fs *FileStore) SaveChatMessage(m *Message) error {
|
||||
if m.ChatID == uuid.Nil {
|
||||
return ErrNilChatID
|
||||
}
|
||||
m.ensureDefaults()
|
||||
|
||||
// Get Chat
|
||||
chat, err := fs.GetChat(m.ChatID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find Existing
|
||||
existingMsg, found := slices.FindFirst(chat.Messages, func(item *Message) bool {
|
||||
return item.ID == m.ID
|
||||
})
|
||||
|
||||
// Upsert
|
||||
if found {
|
||||
*existingMsg = *m
|
||||
} else {
|
||||
chat.Messages = append(chat.Messages, m)
|
||||
}
|
||||
|
||||
// Save
|
||||
return fs.saveChatSession(chat)
|
||||
}
|
||||
|
||||
// saveChatSession is a helper method to save a chat to disk
|
||||
func (fs *FileStore) saveChatSession(session *Chat) error {
|
||||
filePath := filepath.Join(fs.chatDir, session.ID.String()+".json")
|
||||
|
||||
data, err := json.MarshalIndent(session, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal chat: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write chat file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
39
backend/internal/store/types.go
Normal file
39
backend/internal/store/types.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"reichard.io/aethera/internal/types"
|
||||
)
|
||||
|
||||
type baseModel struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (b *baseModel) ensureDefaults() {
|
||||
if b.ID == uuid.Nil {
|
||||
b.ID = uuid.New()
|
||||
}
|
||||
if b.CreatedAt.IsZero() {
|
||||
b.CreatedAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
type Chat struct {
|
||||
baseModel
|
||||
|
||||
Title string `json:"title"`
|
||||
Messages []*Message `json:"messages"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
baseModel
|
||||
|
||||
ChatID uuid.UUID `json:"chat_id"`
|
||||
Role string `json:"role"`
|
||||
Thinking string `json:"thinking"`
|
||||
Content string `json:"content"`
|
||||
Stats *types.MessageStats `json:"stats,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user