mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: user settings & history
This commit is contained in:
+10
-3
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user