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
+10 -3
View File
@@ -15,10 +15,12 @@ import (
dbsqlc "github.com/memohai/memoh/internal/db/sqlc"
"github.com/memohai/memoh/internal/embeddings"
"github.com/memohai/memoh/internal/handlers"
"github.com/memohai/memoh/internal/history"
"github.com/memohai/memoh/internal/mcp"
"github.com/memohai/memoh/internal/memory"
"github.com/memohai/memoh/internal/models"
"github.com/memohai/memoh/internal/providers"
"github.com/memohai/memoh/internal/settings"
"github.com/memohai/memoh/internal/server"
)
@@ -71,8 +73,8 @@ func main() {
authHandler := handlers.NewAuthHandler(conn, cfg.Auth.JWTSecret, jwtExpiresIn)
// Initialize chat resolver for both chat and memory operations
chatResolver := chat.NewResolver(modelsService, queries, cfg.AgentGateway.BaseURL(), 30*time.Second)
// Initialize chat resolver after memory service is configured.
var chatResolver *chat.Resolver
// Create LLM client for memory operations (deferred model/provider selection).
var llmClient memory.LLM = &lazyLLMClient{
@@ -140,6 +142,7 @@ func main() {
memoryService = memory.NewService(llmClient, textEmbedder, store, resolver, textModel.ModelID, multimodalModel.ModelID)
memoryHandler = handlers.NewMemoryHandler(memoryService)
}
chatResolver = chat.NewResolver(modelsService, queries, memoryService, cfg.AgentGateway.BaseURL(), 30*time.Second)
embeddingsHandler := handlers.NewEmbeddingsHandler(modelsService, queries)
swaggerHandler := handlers.NewSwaggerHandler()
chatHandler := handlers.NewChatHandler(chatResolver)
@@ -148,7 +151,11 @@ func main() {
providersService := providers.NewService(queries)
providersHandler := handlers.NewProvidersHandler(providersService)
modelsHandler := handlers.NewModelsHandler(modelsService)
srv := server.NewServer(addr, cfg.Auth.JWTSecret, pingHandler, authHandler, memoryHandler, embeddingsHandler, chatHandler, swaggerHandler, providersHandler, modelsHandler, containerdHandler)
settingsService := settings.NewService(queries)
settingsHandler := handlers.NewSettingsHandler(settingsService)
historyService := history.NewService(queries)
historyHandler := handlers.NewHistoryHandler(historyService)
srv := server.NewServer(addr, cfg.Auth.JWTSecret, pingHandler, authHandler, memoryHandler, embeddingsHandler, chatHandler, swaggerHandler, providersHandler, modelsHandler, settingsHandler, historyHandler, containerdHandler)
if err := srv.Start(); err != nil {
log.Fatalf("server failed: %v", err)
+3
View File
@@ -1,6 +1,9 @@
DROP TABLE IF EXISTS user_settings;
DROP TABLE IF EXISTS history;
DROP TABLE IF EXISTS lifecycle_events;
DROP TABLE IF EXISTS container_versions;
DROP TABLE IF EXISTS models;
DROP TABLE IF EXISTS llm_providers;
DROP TABLE IF EXISTS snapshots;
DROP TABLE IF EXISTS containers;
DROP TABLE IF EXISTS users;
+6
View File
@@ -140,3 +140,9 @@ CREATE TABLE IF NOT EXISTS history (
CREATE INDEX IF NOT EXISTS idx_history_user ON history("user");
CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history(timestamp);
CREATE TABLE IF NOT EXISTS user_settings (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
max_context_load_time INTEGER NOT NULL DEFAULT 1440,
language TEXT NOT NULL DEFAULT 'Same as user input'
);
+20
View File
@@ -9,3 +9,23 @@ FROM history
WHERE "user" = $1 AND timestamp >= $2
ORDER BY timestamp ASC;
-- name: GetHistoryByID :one
SELECT id, messages, timestamp, "user"
FROM history
WHERE id = $1;
-- name: ListHistoryByUser :many
SELECT id, messages, timestamp, "user"
FROM history
WHERE "user" = $1
ORDER BY timestamp DESC
LIMIT $2;
-- name: DeleteHistoryByID :exec
DELETE FROM history
WHERE id = $1;
-- name: DeleteHistoryByUser :exec
DELETE FROM history
WHERE "user" = $1;
+17
View File
@@ -0,0 +1,17 @@
-- name: GetSettingsByUserID :one
SELECT user_id, max_context_load_time, language
FROM user_settings
WHERE user_id = $1;
-- 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;
-- name: DeleteSettingsByUserID :exec
DELETE FROM user_settings
WHERE user_id = $1;
+377
View File
@@ -203,6 +203,188 @@ const docTemplate = `{
}
}
},
"/history": {
"get": {
"description": "List history records for current user",
"tags": [
"history"
],
"summary": "List history records",
"parameters": [
{
"type": "integer",
"description": "Limit",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history.ListResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a history record for current user",
"tags": [
"history"
],
"summary": "Create history record",
"parameters": [
{
"description": "History payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/history.CreateRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/history.Record"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete all history records for current user",
"tags": [
"history"
],
"summary": "Delete all history records",
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/history/{id}": {
"get": {
"description": "Get a history record by ID (must belong to current user)",
"tags": [
"history"
],
"summary": "Get history record",
"parameters": [
{
"type": "string",
"description": "History ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history.Record"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete a history record by ID (must belong to current user)",
"tags": [
"history"
],
"summary": "Delete history record",
"parameters": [
{
"type": "string",
"description": "History ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/mcp/containers": {
"post": {
"tags": [
@@ -1429,6 +1611,135 @@ const docTemplate = `{
}
}
}
},
"/settings": {
"get": {
"description": "Get agent settings for current user",
"tags": [
"settings"
],
"summary": "Get user settings",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/settings.Settings"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Update or create agent settings for current user",
"tags": [
"settings"
],
"summary": "Update user settings",
"parameters": [
{
"description": "Settings payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/settings.UpsertRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/settings.Settings"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"description": "Update or create agent settings for current user",
"tags": [
"settings"
],
"summary": "Update user settings",
"parameters": [
{
"description": "Settings payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/settings.UpsertRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/settings.Settings"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Remove agent settings for current user",
"tags": [
"settings"
],
"summary": "Delete user settings",
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
}
},
"definitions": {
@@ -1666,6 +1977,50 @@ const docTemplate = `{
}
}
},
"history.CreateRequest": {
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
},
"history.ListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/history.Record"
}
}
}
},
"history.Record": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"timestamp": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"memory.AddRequest": {
"type": "object",
"properties": {
@@ -2116,6 +2471,28 @@ const docTemplate = `{
"type": "string"
}
}
},
"settings.Settings": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"max_context_load_time": {
"type": "integer"
}
}
},
"settings.UpsertRequest": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"max_context_load_time": {
"type": "integer"
}
}
}
}
}`
+377
View File
@@ -192,6 +192,188 @@
}
}
},
"/history": {
"get": {
"description": "List history records for current user",
"tags": [
"history"
],
"summary": "List history records",
"parameters": [
{
"type": "integer",
"description": "Limit",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history.ListResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"description": "Create a history record for current user",
"tags": [
"history"
],
"summary": "Create history record",
"parameters": [
{
"description": "History payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/history.CreateRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/history.Record"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete all history records for current user",
"tags": [
"history"
],
"summary": "Delete all history records",
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/history/{id}": {
"get": {
"description": "Get a history record by ID (must belong to current user)",
"tags": [
"history"
],
"summary": "Get history record",
"parameters": [
{
"type": "string",
"description": "History ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/history.Record"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Delete a history record by ID (must belong to current user)",
"tags": [
"history"
],
"summary": "Delete history record",
"parameters": [
{
"type": "string",
"description": "History ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
},
"/mcp/containers": {
"post": {
"tags": [
@@ -1418,6 +1600,135 @@
}
}
}
},
"/settings": {
"get": {
"description": "Get agent settings for current user",
"tags": [
"settings"
],
"summary": "Get user settings",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/settings.Settings"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"put": {
"description": "Update or create agent settings for current user",
"tags": [
"settings"
],
"summary": "Update user settings",
"parameters": [
{
"description": "Settings payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/settings.UpsertRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/settings.Settings"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"post": {
"description": "Update or create agent settings for current user",
"tags": [
"settings"
],
"summary": "Update user settings",
"parameters": [
{
"description": "Settings payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/settings.UpsertRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/settings.Settings"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
},
"delete": {
"description": "Remove agent settings for current user",
"tags": [
"settings"
],
"summary": "Delete user settings",
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handlers.ErrorResponse"
}
}
}
}
}
},
"definitions": {
@@ -1655,6 +1966,50 @@
}
}
},
"history.CreateRequest": {
"type": "object",
"properties": {
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
}
},
"history.ListResponse": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/history.Record"
}
}
}
},
"history.Record": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"messages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
},
"timestamp": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"memory.AddRequest": {
"type": "object",
"properties": {
@@ -2105,6 +2460,28 @@
"type": "string"
}
}
},
"settings.Settings": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"max_context_load_time": {
"type": "integer"
}
}
},
"settings.UpsertRequest": {
"type": "object",
"properties": {
"language": {
"type": "string"
},
"max_context_load_time": {
"type": "integer"
}
}
}
}
}
+248
View File
@@ -151,6 +151,35 @@ definitions:
username:
type: string
type: object
history.CreateRequest:
properties:
messages:
items:
additionalProperties: true
type: object
type: array
type: object
history.ListResponse:
properties:
items:
items:
$ref: '#/definitions/history.Record'
type: array
type: object
history.Record:
properties:
id:
type: string
messages:
items:
additionalProperties: true
type: object
type: array
timestamp:
type: string
user_id:
type: string
type: object
memory.AddRequest:
properties:
agent_id:
@@ -454,6 +483,20 @@ definitions:
name:
type: string
type: object
settings.Settings:
properties:
language:
type: string
max_context_load_time:
type: integer
type: object
settings.UpsertRequest:
properties:
language:
type: string
max_context_load_time:
type: integer
type: object
info:
contact: {}
paths:
@@ -582,6 +625,126 @@ paths:
summary: Create embeddings
tags:
- embeddings
/history:
delete:
description: Delete all history records for current user
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Delete all history records
tags:
- history
get:
description: List history records for current user
parameters:
- description: Limit
in: query
name: limit
type: integer
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history.ListResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: List history records
tags:
- history
post:
description: Create a history record for current user
parameters:
- description: History payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/history.CreateRequest'
responses:
"201":
description: Created
schema:
$ref: '#/definitions/history.Record'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Create history record
tags:
- history
/history/{id}:
delete:
description: Delete a history record by ID (must belong to current user)
parameters:
- description: History ID
in: path
name: id
required: true
type: string
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Delete history record
tags:
- history
get:
description: Get a history record by ID (must belong to current user)
parameters:
- description: History ID
in: path
name: id
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/history.Record'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Get history record
tags:
- history
/mcp/containers:
post:
parameters:
@@ -1394,4 +1557,89 @@ paths:
summary: Get provider by name
tags:
- providers
/settings:
delete:
description: Remove agent settings for current user
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Delete user settings
tags:
- settings
get:
description: Get agent settings for current user
responses:
"200":
description: OK
schema:
$ref: '#/definitions/settings.Settings'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Get user settings
tags:
- settings
post:
description: Update or create agent settings for current user
parameters:
- description: Settings payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/settings.UpsertRequest'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/settings.Settings'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Update user settings
tags:
- settings
put:
description: Update or create agent settings for current user
parameters:
- description: Settings payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/settings.UpsertRequest'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/settings.Settings'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handlers.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handlers.ErrorResponse'
summary: Update user settings
tags:
- settings
swagger: "2.0"
+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"`
}