feat: user settings & history

This commit is contained in:
Acbox
2026-01-28 15:57:39 +08:00
parent 39215309da
commit 11551b72ab
19 changed files with 1920 additions and 10 deletions
+126 -6
View File
@@ -5,6 +5,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -12,9 +13,11 @@ import (
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/memohai/memoh/internal/db/sqlc"
"github.com/memohai/memoh/internal/memory"
"github.com/memohai/memoh/internal/models"
)
@@ -23,13 +26,14 @@ const defaultMaxContextMinutes = 24 * 60
type Resolver struct {
modelsService *models.Service
queries *sqlc.Queries
memoryService *memory.Service
gatewayBaseURL string
timeout time.Duration
httpClient *http.Client
streamingClient *http.Client
}
func NewResolver(modelsService *models.Service, queries *sqlc.Queries, gatewayBaseURL string, timeout time.Duration) *Resolver {
func NewResolver(modelsService *models.Service, queries *sqlc.Queries, memoryService *memory.Service, gatewayBaseURL string, timeout time.Duration) *Resolver {
if strings.TrimSpace(gatewayBaseURL) == "" {
gatewayBaseURL = "http://127.0.0.1:8081"
}
@@ -40,6 +44,7 @@ func NewResolver(modelsService *models.Service, queries *sqlc.Queries, gatewayBa
return &Resolver{
modelsService: modelsService,
queries: queries,
memoryService: memoryService,
gatewayBaseURL: gatewayBaseURL,
timeout: timeout,
httpClient: &http.Client{
@@ -66,7 +71,18 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
return ChatResponse{}, err
}
messages, err := r.loadHistoryMessages(ctx, req.UserID, req.MaxContextLoadTime)
maxContextLoadTime, language, err := r.loadUserSettings(ctx, req.UserID)
if err != nil {
return ChatResponse{}, err
}
if req.MaxContextLoadTime > 0 {
maxContextLoadTime = req.MaxContextLoadTime
}
if strings.TrimSpace(req.Language) != "" {
language = req.Language
}
messages, err := r.loadHistoryMessages(ctx, req.UserID, maxContextLoadTime)
if err != nil {
return ChatResponse{}, err
}
@@ -82,12 +98,13 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
Locale: req.Locale,
Language: req.Language,
MaxSteps: req.MaxSteps,
MaxContextLoadTime: normalizeMaxContextLoad(req.MaxContextLoadTime),
MaxContextLoadTime: normalizeMaxContextLoad(maxContextLoadTime),
Platforms: req.Platforms,
CurrentPlatform: req.CurrentPlatform,
Messages: messages,
Query: req.Query,
}
payload.Language = language
resp, err := r.postChat(ctx, payload)
if err != nil {
@@ -97,6 +114,9 @@ func (r *Resolver) Chat(ctx context.Context, req ChatRequest) (ChatResponse, err
if err := r.storeHistory(ctx, req.UserID, req.Query, resp.Messages); err != nil {
return ChatResponse{}, err
}
if err := r.storeMemory(ctx, req.UserID, req.Query, resp.Messages); err != nil {
return ChatResponse{}, err
}
return ChatResponse{
Messages: resp.Messages,
@@ -133,7 +153,19 @@ func (r *Resolver) StreamChat(ctx context.Context, req ChatRequest) (<-chan Stre
return
}
messages, err := r.loadHistoryMessages(ctx, req.UserID, req.MaxContextLoadTime)
maxContextLoadTime, language, err := r.loadUserSettings(ctx, req.UserID)
if err != nil {
errChan <- err
return
}
if req.MaxContextLoadTime > 0 {
maxContextLoadTime = req.MaxContextLoadTime
}
if strings.TrimSpace(req.Language) != "" {
language = req.Language
}
messages, err := r.loadHistoryMessages(ctx, req.UserID, maxContextLoadTime)
if err != nil {
errChan <- err
return
@@ -150,12 +182,13 @@ func (r *Resolver) StreamChat(ctx context.Context, req ChatRequest) (<-chan Stre
Locale: req.Locale,
Language: req.Language,
MaxSteps: req.MaxSteps,
MaxContextLoadTime: normalizeMaxContextLoad(req.MaxContextLoadTime),
MaxContextLoadTime: normalizeMaxContextLoad(maxContextLoadTime),
Platforms: req.Platforms,
CurrentPlatform: req.CurrentPlatform,
Messages: messages,
Query: req.Query,
}
payload.Language = language
if err := r.streamChat(ctx, payload, req.UserID, req.Query, chunkChan); err != nil {
errChan <- err
@@ -270,6 +303,9 @@ func (r *Resolver) streamChat(ctx context.Context, payload agentGatewayRequest,
if err := r.storeHistory(ctx, userID, query, parsed.Messages); err != nil {
return err
}
if err := r.storeMemory(ctx, userID, query, parsed.Messages); err != nil {
return err
}
}
if err := scanner.Err(); err != nil {
@@ -345,6 +381,65 @@ func (r *Resolver) storeHistory(ctx context.Context, userID, query string, respo
return err
}
func (r *Resolver) storeMemory(ctx context.Context, userID, query string, responseMessages []GatewayMessage) error {
if r.memoryService == nil {
return nil
}
if strings.TrimSpace(userID) == "" {
return fmt.Errorf("user id is required")
}
if strings.TrimSpace(query) == "" && len(responseMessages) == 0 {
return nil
}
userMessage := GatewayMessage{
"role": "user",
"content": query,
}
messages := append([]GatewayMessage{userMessage}, responseMessages...)
memoryMessages := make([]memory.Message, 0, len(messages))
for _, msg := range messages {
role, content := gatewayMessageToMemory(msg)
if strings.TrimSpace(content) == "" {
continue
}
memoryMessages = append(memoryMessages, memory.Message{
Role: role,
Content: content,
})
}
if len(memoryMessages) == 0 {
return nil
}
_, err := r.memoryService.Add(ctx, memory.AddRequest{
Messages: memoryMessages,
UserID: userID,
})
return err
}
func gatewayMessageToMemory(msg GatewayMessage) (string, string) {
role := "assistant"
if raw, ok := msg["role"].(string); ok && strings.TrimSpace(raw) != "" {
role = raw
}
if raw, ok := msg["content"]; ok {
switch v := raw.(type) {
case string:
return role, v
default:
if encoded, err := json.Marshal(v); err == nil {
return role, string(encoded)
}
}
}
if encoded, err := json.Marshal(msg); err == nil {
return role, string(encoded)
}
return role, ""
}
func (r *Resolver) selectChatModel(ctx context.Context, req ChatRequest) (models.GetResponse, sqlc.LlmProvider, error) {
if r.modelsService == nil {
return models.GetResponse{}, sqlc.LlmProvider{}, fmt.Errorf("models service not configured")
@@ -428,6 +523,32 @@ func normalizeMaxContextLoad(value int) int {
return value
}
func (r *Resolver) loadUserSettings(ctx context.Context, userID string) (int, string, error) {
if r.queries == nil {
return defaultMaxContextMinutes, "Same as user input", nil
}
pgUserID, err := parseUUID(userID)
if err != nil {
return 0, "", err
}
settings, err := r.queries.GetSettingsByUserID(ctx, pgUserID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return defaultMaxContextMinutes, "Same as user input", nil
}
return 0, "", err
}
maxLoad := int(settings.MaxContextLoadTime)
if maxLoad <= 0 {
maxLoad = defaultMaxContextMinutes
}
language := strings.TrimSpace(settings.Language)
if language == "" {
language = "Same as user input"
}
return maxLoad, language, nil
}
func normalizeClientType(clientType string) (string, error) {
switch strings.ToLower(strings.TrimSpace(clientType)) {
case "openai":
@@ -453,4 +574,3 @@ func parseUUID(id string) (pgtype.UUID, error) {
copy(pgID.Bytes[:], parsed[:])
return pgID, nil
}
+76
View File
@@ -71,3 +71,79 @@ func (q *Queries) ListHistoryByUserSince(ctx context.Context, arg ListHistoryByU
}
return items, nil
}
const getHistoryByID = `-- name: GetHistoryByID :one
SELECT id, messages, timestamp, "user"
FROM history
WHERE id = $1
`
func (q *Queries) GetHistoryByID(ctx context.Context, id pgtype.UUID) (History, error) {
row := q.db.QueryRow(ctx, getHistoryByID, id)
var i History
err := row.Scan(
&i.ID,
&i.Messages,
&i.Timestamp,
&i.User,
)
return i, err
}
const listHistoryByUser = `-- name: ListHistoryByUser :many
SELECT id, messages, timestamp, "user"
FROM history
WHERE "user" = $1
ORDER BY timestamp DESC
LIMIT $2
`
type ListHistoryByUserParams struct {
User pgtype.UUID `json:"user"`
Limit int32 `json:"limit"`
}
func (q *Queries) ListHistoryByUser(ctx context.Context, arg ListHistoryByUserParams) ([]History, error) {
rows, err := q.db.Query(ctx, listHistoryByUser, arg.User, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []History
for rows.Next() {
var i History
if err := rows.Scan(
&i.ID,
&i.Messages,
&i.Timestamp,
&i.User,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const deleteHistoryByID = `-- name: DeleteHistoryByID :exec
DELETE FROM history
WHERE id = $1
`
func (q *Queries) DeleteHistoryByID(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteHistoryByID, id)
return err
}
const deleteHistoryByUser = `-- name: DeleteHistoryByUser :exec
DELETE FROM history
WHERE "user" = $1
`
func (q *Queries) DeleteHistoryByUser(ctx context.Context, user pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteHistoryByUser, user)
return err
}
+6
View File
@@ -40,6 +40,12 @@ type History struct {
User pgtype.UUID `json:"user"`
}
type Settings struct {
UserID pgtype.UUID `json:"user_id"`
MaxContextLoadTime int32 `json:"max_context_load_time"`
Language string `json:"language"`
}
type LifecycleEvent struct {
ID string `json:"id"`
ContainerID string `json:"container_id"`
+66
View File
@@ -0,0 +1,66 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: settings.sql
package sqlc
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const getSettingsByUserID = `-- name: GetSettingsByUserID :one
SELECT user_id, max_context_load_time, language
FROM user_settings
WHERE user_id = $1
`
func (q *Queries) GetSettingsByUserID(ctx context.Context, userID pgtype.UUID) (Settings, error) {
row := q.db.QueryRow(ctx, getSettingsByUserID, userID)
var i Settings
err := row.Scan(
&i.UserID,
&i.MaxContextLoadTime,
&i.Language,
)
return i, err
}
const upsertSettings = `-- name: UpsertSettings :one
INSERT INTO user_settings (user_id, max_context_load_time, language)
VALUES ($1, $2, $3)
ON CONFLICT (user_id) DO UPDATE SET
max_context_load_time = EXCLUDED.max_context_load_time,
language = EXCLUDED.language
RETURNING user_id, max_context_load_time, language
`
type UpsertSettingsParams struct {
UserID pgtype.UUID `json:"user_id"`
MaxContextLoadTime int32 `json:"max_context_load_time"`
Language string `json:"language"`
}
func (q *Queries) UpsertSettings(ctx context.Context, arg UpsertSettingsParams) (Settings, error) {
row := q.db.QueryRow(ctx, upsertSettings, arg.UserID, arg.MaxContextLoadTime, arg.Language)
var i Settings
err := row.Scan(
&i.UserID,
&i.MaxContextLoadTime,
&i.Language,
)
return i, err
}
const deleteSettingsByUserID = `-- name: DeleteSettingsByUserID :exec
DELETE FROM user_settings
WHERE user_id = $1
`
func (q *Queries) DeleteSettingsByUserID(ctx context.Context, userID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteSettingsByUserID, userID)
return err
}
+173
View File
@@ -0,0 +1,173 @@
package handlers
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
"github.com/memohai/memoh/internal/auth"
"github.com/memohai/memoh/internal/history"
"github.com/memohai/memoh/internal/identity"
)
type HistoryHandler struct {
service *history.Service
}
func NewHistoryHandler(service *history.Service) *HistoryHandler {
return &HistoryHandler{service: service}
}
func (h *HistoryHandler) Register(e *echo.Echo) {
group := e.Group("/history")
group.POST("", h.Create)
group.GET("", h.List)
group.GET("/:id", h.Get)
group.DELETE("/:id", h.Delete)
group.DELETE("", h.DeleteAll)
}
// Create godoc
// @Summary Create history record
// @Description Create a history record for current user
// @Tags history
// @Param payload body history.CreateRequest true "History payload"
// @Success 201 {object} history.Record
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /history [post]
func (h *HistoryHandler) Create(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
var req history.CreateRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
resp, err := h.service.Create(c.Request().Context(), userID, req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusCreated, resp)
}
// Get godoc
// @Summary Get history record
// @Description Get a history record by ID (must belong to current user)
// @Tags history
// @Param id path string true "History ID"
// @Success 200 {object} history.Record
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /history/{id} [get]
func (h *HistoryHandler) Get(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
record, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if record.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
return c.JSON(http.StatusOK, record)
}
// List godoc
// @Summary List history records
// @Description List history records for current user
// @Tags history
// @Param limit query int false "Limit"
// @Success 200 {object} history.ListResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /history [get]
func (h *HistoryHandler) List(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
limit := 0
if raw := c.QueryParam("limit"); raw != "" {
if _, err := fmt.Sscanf(raw, "%d", &limit); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid limit")
}
}
items, err := h.service.List(c.Request().Context(), userID, limit)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, history.ListResponse{Items: items})
}
// Delete godoc
// @Summary Delete history record
// @Description Delete a history record by ID (must belong to current user)
// @Tags history
// @Param id path string true "History ID"
// @Success 204 "No Content"
// @Failure 400 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /history/{id} [delete]
func (h *HistoryHandler) Delete(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
id := c.Param("id")
if id == "" {
return echo.NewHTTPError(http.StatusBadRequest, "id is required")
}
record, err := h.service.Get(c.Request().Context(), id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
if record.UserID != userID {
return echo.NewHTTPError(http.StatusForbidden, "user mismatch")
}
if err := h.service.Delete(c.Request().Context(), id); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.NoContent(http.StatusNoContent)
}
// DeleteAll godoc
// @Summary Delete all history records
// @Description Delete all history records for current user
// @Tags history
// @Success 204 "No Content"
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /history [delete]
func (h *HistoryHandler) DeleteAll(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
if err := h.service.DeleteByUser(c.Request().Context(), userID); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.NoContent(http.StatusNoContent)
}
func (h *HistoryHandler) requireUserID(c echo.Context) (string, error) {
userID, err := auth.UserIDFromContext(c)
if err != nil {
return "", err
}
if err := identity.ValidateUserID(userID); err != nil {
return "", echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return userID, nil
}
+104
View File
@@ -0,0 +1,104 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/memohai/memoh/internal/auth"
"github.com/memohai/memoh/internal/identity"
"github.com/memohai/memoh/internal/settings"
)
type SettingsHandler struct {
service *settings.Service
}
func NewSettingsHandler(service *settings.Service) *SettingsHandler {
return &SettingsHandler{service: service}
}
func (h *SettingsHandler) Register(e *echo.Echo) {
group := e.Group("/settings")
group.GET("", h.Get)
group.POST("", h.Upsert)
group.PUT("", h.Upsert)
group.DELETE("", h.Delete)
}
// Get godoc
// @Summary Get user settings
// @Description Get agent settings for current user
// @Tags settings
// @Success 200 {object} settings.Settings
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /settings [get]
func (h *SettingsHandler) Get(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
resp, err := h.service.Get(c.Request().Context(), userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, resp)
}
// Upsert godoc
// @Summary Update user settings
// @Description Update or create agent settings for current user
// @Tags settings
// @Param payload body settings.UpsertRequest true "Settings payload"
// @Success 200 {object} settings.Settings
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /settings [put]
// @Router /settings [post]
func (h *SettingsHandler) Upsert(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
var req settings.UpsertRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
resp, err := h.service.Upsert(c.Request().Context(), userID, req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, resp)
}
// Delete godoc
// @Summary Delete user settings
// @Description Remove agent settings for current user
// @Tags settings
// @Success 204 "No Content"
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /settings [delete]
func (h *SettingsHandler) Delete(c echo.Context) error {
userID, err := h.requireUserID(c)
if err != nil {
return err
}
if err := h.service.Delete(c.Request().Context(), userID); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.NoContent(http.StatusNoContent)
}
func (h *SettingsHandler) requireUserID(c echo.Context) (string, error) {
userID, err := auth.UserIDFromContext(c)
if err != nil {
return "", err
}
if err := identity.ValidateUserID(userID); err != nil {
return "", echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return userID, nil
}
+149
View File
@@ -0,0 +1,149 @@
package history
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/memohai/memoh/internal/db/sqlc"
)
const defaultListLimit = 50
type Service struct {
queries *sqlc.Queries
}
func NewService(queries *sqlc.Queries) *Service {
return &Service{queries: queries}
}
func (s *Service) Create(ctx context.Context, userID string, req CreateRequest) (Record, error) {
if len(req.Messages) == 0 {
return Record{}, fmt.Errorf("messages are required")
}
pgID, err := parseUUID(userID)
if err != nil {
return Record{}, err
}
payload, err := json.Marshal(req.Messages)
if err != nil {
return Record{}, err
}
row, err := s.queries.CreateHistory(ctx, sqlc.CreateHistoryParams{
Messages: payload,
Timestamp: pgtype.Timestamptz{
Time: time.Now().UTC(),
Valid: true,
},
User: pgID,
})
if err != nil {
return Record{}, err
}
return toRecord(row)
}
func (s *Service) Get(ctx context.Context, id string) (Record, error) {
pgID, err := parseUUID(id)
if err != nil {
return Record{}, err
}
row, err := s.queries.GetHistoryByID(ctx, pgID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return Record{}, fmt.Errorf("history not found")
}
return Record{}, err
}
return toRecord(row)
}
func (s *Service) List(ctx context.Context, userID string, limit int) ([]Record, error) {
pgID, err := parseUUID(userID)
if err != nil {
return nil, err
}
if limit <= 0 {
limit = defaultListLimit
}
rows, err := s.queries.ListHistoryByUser(ctx, sqlc.ListHistoryByUserParams{
User: pgID,
Limit: int32(limit),
})
if err != nil {
return nil, err
}
items := make([]Record, 0, len(rows))
for _, row := range rows {
record, err := toRecord(row)
if err != nil {
return nil, err
}
items = append(items, record)
}
return items, nil
}
func (s *Service) Delete(ctx context.Context, id string) error {
pgID, err := parseUUID(id)
if err != nil {
return err
}
return s.queries.DeleteHistoryByID(ctx, pgID)
}
func (s *Service) DeleteByUser(ctx context.Context, userID string) error {
pgID, err := parseUUID(userID)
if err != nil {
return err
}
return s.queries.DeleteHistoryByUser(ctx, pgID)
}
func toRecord(row sqlc.History) (Record, error) {
var messages []map[string]interface{}
if len(row.Messages) > 0 {
if err := json.Unmarshal(row.Messages, &messages); err != nil {
return Record{}, err
}
}
record := Record{
Messages: messages,
}
if row.Timestamp.Valid {
record.Timestamp = row.Timestamp.Time
}
if row.ID.Valid {
id, err := uuid.FromBytes(row.ID.Bytes[:])
if err == nil {
record.ID = id.String()
}
}
if row.User.Valid {
uid, err := uuid.FromBytes(row.User.Bytes[:])
if err == nil {
record.UserID = uid.String()
}
}
return record, nil
}
func parseUUID(id string) (pgtype.UUID, error) {
parsed, err := uuid.Parse(strings.TrimSpace(id))
if err != nil {
return pgtype.UUID{}, fmt.Errorf("invalid UUID: %w", err)
}
var pgID pgtype.UUID
pgID.Valid = true
copy(pgID.Bytes[:], parsed[:])
return pgID, nil
}
+19
View File
@@ -0,0 +1,19 @@
package history
import "time"
type Record struct {
ID string `json:"id"`
Messages []map[string]interface{} `json:"messages"`
Timestamp time.Time `json:"timestamp"`
UserID string `json:"user_id"`
}
type CreateRequest struct {
Messages []map[string]interface{} `json:"messages"`
}
type ListResponse struct {
Items []Record `json:"items"`
}
+7 -1
View File
@@ -15,7 +15,7 @@ type Server struct {
addr string
}
func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler, authHandler *handlers.AuthHandler, memoryHandler *handlers.MemoryHandler, embeddingsHandler *handlers.EmbeddingsHandler, chatHandler *handlers.ChatHandler, swaggerHandler *handlers.SwaggerHandler, providersHandler *handlers.ProvidersHandler, modelsHandler *handlers.ModelsHandler, containerdHandler *handlers.ContainerdHandler) *Server {
func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler, authHandler *handlers.AuthHandler, memoryHandler *handlers.MemoryHandler, embeddingsHandler *handlers.EmbeddingsHandler, chatHandler *handlers.ChatHandler, swaggerHandler *handlers.SwaggerHandler, providersHandler *handlers.ProvidersHandler, modelsHandler *handlers.ModelsHandler, settingsHandler *handlers.SettingsHandler, historyHandler *handlers.HistoryHandler, containerdHandler *handlers.ContainerdHandler) *Server {
if addr == "" {
addr = ":8080"
}
@@ -56,6 +56,12 @@ func NewServer(addr string, jwtSecret string, pingHandler *handlers.PingHandler,
if swaggerHandler != nil {
swaggerHandler.Register(e)
}
if settingsHandler != nil {
settingsHandler.Register(e)
}
if historyHandler != nil {
historyHandler.Register(e)
}
if providersHandler != nil {
providersHandler.Register(e)
}
+119
View File
@@ -0,0 +1,119 @@
package settings
import (
"context"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/memohai/memoh/internal/db/sqlc"
)
type Service struct {
queries *sqlc.Queries
}
func NewService(queries *sqlc.Queries) *Service {
return &Service{queries: queries}
}
func (s *Service) Get(ctx context.Context, userID string) (Settings, error) {
pgID, err := parseUUID(userID)
if err != nil {
return Settings{}, err
}
row, err := s.queries.GetSettingsByUserID(ctx, pgID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return Settings{
MaxContextLoadTime: DefaultMaxContextLoadTime,
Language: DefaultLanguage,
}, nil
}
return Settings{}, err
}
settings := Settings{
MaxContextLoadTime: int(row.MaxContextLoadTime),
Language: strings.TrimSpace(row.Language),
}
if settings.MaxContextLoadTime <= 0 {
settings.MaxContextLoadTime = DefaultMaxContextLoadTime
}
if settings.Language == "" {
settings.Language = DefaultLanguage
}
return settings, nil
}
func (s *Service) Upsert(ctx context.Context, userID string, req UpsertRequest) (Settings, error) {
if s.queries == nil {
return Settings{}, fmt.Errorf("settings queries not configured")
}
pgID, err := parseUUID(userID)
if err != nil {
return Settings{}, err
}
current := Settings{
MaxContextLoadTime: DefaultMaxContextLoadTime,
Language: DefaultLanguage,
}
existing, err := s.queries.GetSettingsByUserID(ctx, pgID)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return Settings{}, err
}
if err == nil {
current.MaxContextLoadTime = int(existing.MaxContextLoadTime)
current.Language = strings.TrimSpace(existing.Language)
if current.MaxContextLoadTime <= 0 {
current.MaxContextLoadTime = DefaultMaxContextLoadTime
}
if current.Language == "" {
current.Language = DefaultLanguage
}
}
if req.MaxContextLoadTime != nil && *req.MaxContextLoadTime > 0 {
current.MaxContextLoadTime = *req.MaxContextLoadTime
}
if strings.TrimSpace(req.Language) != "" {
current.Language = strings.TrimSpace(req.Language)
}
_, err = s.queries.UpsertSettings(ctx, sqlc.UpsertSettingsParams{
UserID: pgID,
MaxContextLoadTime: int32(current.MaxContextLoadTime),
Language: current.Language,
})
if err != nil {
return Settings{}, err
}
return current, nil
}
func (s *Service) Delete(ctx context.Context, userID string) error {
if s.queries == nil {
return fmt.Errorf("settings queries not configured")
}
pgID, err := parseUUID(userID)
if err != nil {
return err
}
return s.queries.DeleteSettingsByUserID(ctx, pgID)
}
func parseUUID(id string) (pgtype.UUID, error) {
parsed, err := uuid.Parse(id)
if err != nil {
return pgtype.UUID{}, fmt.Errorf("invalid UUID: %w", err)
}
var pgID pgtype.UUID
pgID.Valid = true
copy(pgID.Bytes[:], parsed[:])
return pgID, nil
}
+17
View File
@@ -0,0 +1,17 @@
package settings
const (
DefaultMaxContextLoadTime = 24 * 60
DefaultLanguage = "Same as user input"
)
type Settings struct {
MaxContextLoadTime int `json:"max_context_load_time"`
Language string `json:"language"`
}
type UpsertRequest struct {
MaxContextLoadTime *int `json:"max_context_load_time,omitempty"`
Language string `json:"language,omitempty"`
}