feat: implement chat and conversation history management with database schema and API endpoints
All checks were successful
Build and Release / release (push) Successful in 1m30s
All checks were successful
Build and Release / release (push) Successful in 1m30s
This commit is contained in:
@@ -97,6 +97,7 @@ func (s *FiberServer) SetupServer(
|
|||||||
raguRepo := repositories.NewRagRepository(poolPg, redis)
|
raguRepo := repositories.NewRagRepository(poolPg, redis)
|
||||||
usageRepo := repositories.NewUsageRepository(redis)
|
usageRepo := repositories.NewUsageRepository(redis)
|
||||||
statisticRepo := repositories.NewStatisticRepository(poolPg, redis)
|
statisticRepo := repositories.NewStatisticRepository(poolPg, redis)
|
||||||
|
chatRepo := repositories.NewChatRepository(poolPg, redis)
|
||||||
|
|
||||||
// service setup
|
// service setup
|
||||||
authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis, poolPg)
|
authService := services.NewAuthService(userRepo, roleRepo, tokenRepo, redis, poolPg)
|
||||||
@@ -116,7 +117,7 @@ func (s *FiberServer) SetupServer(
|
|||||||
userRepo, wikiRepo, geometryRepo, entityRepo,
|
userRepo, wikiRepo, geometryRepo, entityRepo,
|
||||||
raguRepo, raguUtils, poolPg, redis,
|
raguRepo, raguUtils, poolPg, redis,
|
||||||
)
|
)
|
||||||
chatbotService := services.NewChatbotService(raguRepo, usageRepo, raguUtils)
|
chatbotService := services.NewChatbotService(raguRepo, usageRepo, chatRepo, raguUtils)
|
||||||
statisticService := services.NewStatisticService(statisticRepo)
|
statisticService := services.NewStatisticService(statisticRepo)
|
||||||
|
|
||||||
// controller setup
|
// controller setup
|
||||||
|
|||||||
3
db/migrations/0000015_chat_and_support.down.sql
Normal file
3
db/migrations/0000015_chat_and_support.down.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE IF EXISTS chatbot_histories CASCADE;
|
||||||
|
DROP TABLE IF EXISTS messages CASCADE;
|
||||||
|
DROP TABLE IF EXISTS conversations CASCADE;
|
||||||
34
db/migrations/0000015_chat_and_support.up.sql
Normal file
34
db/migrations/0000015_chat_and_support.up.sql
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS conversations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
mod_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
status SMALLINT NOT NULL DEFAULT 1, -- 1: waiting, 2: active, 3: resolved
|
||||||
|
closed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_conversations_user_id ON conversations (user_id);
|
||||||
|
CREATE INDEX idx_conversations_mod_id ON conversations (mod_id);
|
||||||
|
CREATE INDEX idx_conversations_status ON conversations (status);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
|
sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_messages_conversation_id ON messages (conversation_id);
|
||||||
|
CREATE INDEX idx_messages_created_at ON messages (created_at DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS chatbot_histories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
question TEXT NOT NULL,
|
||||||
|
answer TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_chatbot_histories_user_id ON chatbot_histories (user_id, created_at DESC);
|
||||||
46
db/query/chat.sql
Normal file
46
db/query/chat.sql
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
-- name: CreateConversation :one
|
||||||
|
INSERT INTO conversations (user_id, status)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateConversationStatus :one
|
||||||
|
UPDATE conversations
|
||||||
|
SET status = $2, mod_id = COALESCE($3, mod_id), closed_at = $4, updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: CreateMessage :one
|
||||||
|
INSERT INTO messages (conversation_id, sender_id, content)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetMessagesByConversation :many
|
||||||
|
SELECT * FROM messages
|
||||||
|
WHERE conversation_id = $1
|
||||||
|
AND (sqlc.narg('cursor_id')::uuid IS NULL OR id < sqlc.narg('cursor_id')::uuid)
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT sqlc.arg('limit');
|
||||||
|
|
||||||
|
-- name: GetConversationsByIDs :many
|
||||||
|
SELECT * FROM conversations WHERE id = ANY($1::uuid[]);
|
||||||
|
|
||||||
|
-- name: GetMessagesByIDs :many
|
||||||
|
SELECT * FROM messages WHERE id = ANY($1::uuid[]);
|
||||||
|
|
||||||
|
-- name: CreateChatbotHistory :one
|
||||||
|
INSERT INTO chatbot_histories (user_id, question, answer)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetChatbotHistory :many
|
||||||
|
SELECT * FROM (
|
||||||
|
SELECT * FROM chatbot_histories
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND (sqlc.narg('cursor_id')::uuid IS NULL OR id < sqlc.narg('cursor_id')::uuid)
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT sqlc.arg('limit')
|
||||||
|
) sub
|
||||||
|
ORDER BY created_at ASC;
|
||||||
|
|
||||||
|
-- name: GetChatbotHistoriesByIDs :many
|
||||||
|
SELECT * FROM chatbot_histories WHERE id = ANY($1::uuid[]);
|
||||||
@@ -208,3 +208,30 @@ CREATE TABLE IF NOT EXISTS system_statistics (
|
|||||||
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS conversations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
mod_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
status SMALLINT NOT NULL DEFAULT 1,
|
||||||
|
closed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||||
|
sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS chatbot_histories (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuidv7(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
question TEXT NOT NULL,
|
||||||
|
answer TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
|||||||
123
docs/docs.go
123
docs/docs.go
@@ -462,6 +462,72 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/chatbot/history": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get chatbot history for the current user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Chatbot"
|
||||||
|
],
|
||||||
|
"summary": "Get chatbot history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Cursor ID for pagination",
|
||||||
|
"name": "cursor",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit number of items returned, default 10",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.GetChatbotHistoryResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/entities": {
|
"/entities": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Search entities with cursor pagination",
|
"description": "Search entities with cursor pagination",
|
||||||
@@ -4208,12 +4274,6 @@ const docTemplate = `{
|
|||||||
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
|
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity_wikis": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"geometries": {
|
"geometries": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -4378,8 +4438,7 @@ const docTemplate = `{
|
|||||||
"history-api_internal_dtos_request.EntitySnapshot": {
|
"history-api_internal_dtos_request.EntitySnapshot": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id"
|
||||||
"name"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"base_hash": {
|
"base_hash": {
|
||||||
@@ -4452,7 +4511,8 @@ const docTemplate = `{
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"reference",
|
"reference",
|
||||||
"delete"
|
"delete",
|
||||||
|
"binding"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"wiki_id": {
|
"wiki_id": {
|
||||||
@@ -4586,14 +4646,21 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"geometry_id": {
|
"geometry_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"reference",
|
||||||
|
"delete",
|
||||||
|
"binding"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"history-api_internal_dtos_request.GeometrySnapshot": {
|
"history-api_internal_dtos_request.GeometrySnapshot": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id"
|
||||||
"type"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"base_hash": {
|
"base_hash": {
|
||||||
@@ -4926,6 +4993,26 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"history-api_internal_dtos_response.ChatbotHistoryDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"answer": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"history-api_internal_dtos_response.CommonResponse": {
|
"history-api_internal_dtos_response.CommonResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4939,6 +5026,20 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"history-api_internal_dtos_response.GetChatbotHistoryResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.ChatbotHistoryDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pre_cursor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"history-api_internal_dtos_response.PaginatedResponse": {
|
"history-api_internal_dtos_response.PaginatedResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -455,6 +455,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/chatbot/history": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get chatbot history for the current user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Chatbot"
|
||||||
|
],
|
||||||
|
"summary": "Get chatbot history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Cursor ID for pagination",
|
||||||
|
"name": "cursor",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Limit number of items returned, default 10",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.GetChatbotHistoryResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/entities": {
|
"/entities": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Search entities with cursor pagination",
|
"description": "Search entities with cursor pagination",
|
||||||
@@ -4201,12 +4267,6 @@
|
|||||||
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
|
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity_wikis": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"geometries": {
|
"geometries": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -4371,8 +4431,7 @@
|
|||||||
"history-api_internal_dtos_request.EntitySnapshot": {
|
"history-api_internal_dtos_request.EntitySnapshot": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id"
|
||||||
"name"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"base_hash": {
|
"base_hash": {
|
||||||
@@ -4445,7 +4504,8 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"reference",
|
"reference",
|
||||||
"delete"
|
"delete",
|
||||||
|
"binding"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"wiki_id": {
|
"wiki_id": {
|
||||||
@@ -4579,14 +4639,21 @@
|
|||||||
},
|
},
|
||||||
"geometry_id": {
|
"geometry_id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"reference",
|
||||||
|
"delete",
|
||||||
|
"binding"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"history-api_internal_dtos_request.GeometrySnapshot": {
|
"history-api_internal_dtos_request.GeometrySnapshot": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id"
|
||||||
"type"
|
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"base_hash": {
|
"base_hash": {
|
||||||
@@ -4919,6 +4986,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"history-api_internal_dtos_response.ChatbotHistoryDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"answer": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"history-api_internal_dtos_response.CommonResponse": {
|
"history-api_internal_dtos_response.CommonResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -4932,6 +5019,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"history-api_internal_dtos_response.GetChatbotHistoryResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/history-api_internal_dtos_response.ChatbotHistoryDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pre_cursor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"history-api_internal_dtos_response.PaginatedResponse": {
|
"history-api_internal_dtos_response.PaginatedResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -80,10 +80,6 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
$ref: '#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot'
|
$ref: '#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot'
|
||||||
type: array
|
type: array
|
||||||
entity_wikis:
|
|
||||||
items:
|
|
||||||
$ref: '#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot'
|
|
||||||
type: array
|
|
||||||
geometries:
|
geometries:
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/history-api_internal_dtos_request.GeometrySnapshot'
|
$ref: '#/definitions/history-api_internal_dtos_request.GeometrySnapshot'
|
||||||
@@ -232,7 +228,6 @@ definitions:
|
|||||||
type: number
|
type: number
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- name
|
|
||||||
type: object
|
type: object
|
||||||
history-api_internal_dtos_request.EntityWikiLinkSnapshot:
|
history-api_internal_dtos_request.EntityWikiLinkSnapshot:
|
||||||
properties:
|
properties:
|
||||||
@@ -247,6 +242,7 @@ definitions:
|
|||||||
enum:
|
enum:
|
||||||
- reference
|
- reference
|
||||||
- delete
|
- delete
|
||||||
|
- binding
|
||||||
type: string
|
type: string
|
||||||
wiki_id:
|
wiki_id:
|
||||||
type: string
|
type: string
|
||||||
@@ -338,6 +334,12 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
geometry_id:
|
geometry_id:
|
||||||
type: string
|
type: string
|
||||||
|
operation:
|
||||||
|
enum:
|
||||||
|
- reference
|
||||||
|
- delete
|
||||||
|
- binding
|
||||||
|
type: string
|
||||||
required:
|
required:
|
||||||
- entity_id
|
- entity_id
|
||||||
- geometry_id
|
- geometry_id
|
||||||
@@ -380,7 +382,6 @@ definitions:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- type
|
|
||||||
type: object
|
type: object
|
||||||
history-api_internal_dtos_request.MediaBulkDeleteDto:
|
history-api_internal_dtos_request.MediaBulkDeleteDto:
|
||||||
properties:
|
properties:
|
||||||
@@ -577,6 +578,19 @@ definitions:
|
|||||||
- id
|
- id
|
||||||
- title
|
- title
|
||||||
type: object
|
type: object
|
||||||
|
history-api_internal_dtos_response.ChatbotHistoryDto:
|
||||||
|
properties:
|
||||||
|
answer:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
question:
|
||||||
|
type: string
|
||||||
|
user_id:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
history-api_internal_dtos_response.CommonResponse:
|
history-api_internal_dtos_response.CommonResponse:
|
||||||
properties:
|
properties:
|
||||||
data: {}
|
data: {}
|
||||||
@@ -586,6 +600,15 @@ definitions:
|
|||||||
status:
|
status:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
|
history-api_internal_dtos_response.GetChatbotHistoryResponse:
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.ChatbotHistoryDto'
|
||||||
|
type: array
|
||||||
|
pre_cursor:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
history-api_internal_dtos_response.PaginatedResponse:
|
history-api_internal_dtos_response.PaginatedResponse:
|
||||||
properties:
|
properties:
|
||||||
data: {}
|
data: {}
|
||||||
@@ -962,6 +985,45 @@ paths:
|
|||||||
summary: Ask the AI chatbot
|
summary: Ask the AI chatbot
|
||||||
tags:
|
tags:
|
||||||
- Chatbot
|
- Chatbot
|
||||||
|
/chatbot/history:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get chatbot history for the current user
|
||||||
|
parameters:
|
||||||
|
- description: Cursor ID for pagination
|
||||||
|
in: query
|
||||||
|
name: cursor
|
||||||
|
type: string
|
||||||
|
- description: Limit number of items returned, default 10
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Successful response
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.GetChatbotHistoryResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Invalid request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Get chatbot history
|
||||||
|
tags:
|
||||||
|
- Chatbot
|
||||||
/entities:
|
/entities:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"history-api/internal/dtos/request"
|
"history-api/internal/dtos/request"
|
||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
|
"history-api/internal/models"
|
||||||
"history-api/internal/services"
|
"history-api/internal/services"
|
||||||
"history-api/pkg/validator"
|
"history-api/pkg/validator"
|
||||||
"time"
|
"time"
|
||||||
@@ -67,3 +68,51 @@ func (cx *ChatbotController) Chat(c fiber.Ctx) error {
|
|||||||
Data: answer,
|
Data: answer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHistory godoc
|
||||||
|
// @Summary Get chatbot history
|
||||||
|
// @Description Get chatbot history for the current user
|
||||||
|
// @Tags Chatbot
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Param cursor query string false "Cursor ID for pagination"
|
||||||
|
// @Param limit query int false "Limit number of items returned, default 10"
|
||||||
|
// @Success 200 {object} response.CommonResponse{data=response.GetChatbotHistoryResponse} "Successful response"
|
||||||
|
// @Failure 400 {object} response.CommonResponse "Invalid request"
|
||||||
|
// @Failure 500 {object} response.CommonResponse "Internal server error"
|
||||||
|
// @Router /chatbot/history [get]
|
||||||
|
func (cx *ChatbotController) GetHistory(c fiber.Ctx) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dto := &request.GetChatbotHistoryDto{}
|
||||||
|
if err := validator.ValidateQueryDto(c, dto); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Errors: err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.Locals("uid").(string)
|
||||||
|
histories, err := cx.chatbotService.GetHistory(ctx, uid, dto)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||||
|
Status: false,
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var preCursor string
|
||||||
|
if len(histories) > 0 {
|
||||||
|
preCursor = histories[0].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
|
Status: true,
|
||||||
|
Data: response.GetChatbotHistoryResponse{
|
||||||
|
Items: models.ChatbotHistoryEntitiesToResponse(histories),
|
||||||
|
PreCursor: preCursor,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,3 +4,8 @@ type ChatbotDto struct {
|
|||||||
ProjectID *string `json:"project_id"`
|
ProjectID *string `json:"project_id"`
|
||||||
Question string `json:"question" validate:"required"`
|
Question string `json:"question" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetChatbotHistoryDto struct {
|
||||||
|
Cursor *string `json:"cursor" query:"cursor" validate:"omitempty,uuid"`
|
||||||
|
Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|||||||
18
internal/dtos/response/chatbot.go
Normal file
18
internal/dtos/response/chatbot.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatbotHistoryDto struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Question string `json:"question"`
|
||||||
|
Answer string `json:"answer"`
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetChatbotHistoryResponse struct {
|
||||||
|
Items []*ChatbotHistoryDto `json:"items"`
|
||||||
|
PreCursor string `json:"pre_cursor,omitempty"`
|
||||||
|
}
|
||||||
297
internal/gen/sqlc/chat.sql.go
Normal file
297
internal/gen/sqlc/chat.sql.go
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: chat.sql
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createChatbotHistory = `-- name: CreateChatbotHistory :one
|
||||||
|
INSERT INTO chatbot_histories (user_id, question, answer)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, user_id, question, answer, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateChatbotHistoryParams struct {
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
Question string `json:"question"`
|
||||||
|
Answer string `json:"answer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateChatbotHistory(ctx context.Context, arg CreateChatbotHistoryParams) (ChatbotHistory, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createChatbotHistory, arg.UserID, arg.Question, arg.Answer)
|
||||||
|
var i ChatbotHistory
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Question,
|
||||||
|
&i.Answer,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createConversation = `-- name: CreateConversation :one
|
||||||
|
INSERT INTO conversations (user_id, status)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, user_id, mod_id, status, closed_at, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateConversationParams struct {
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
Status int16 `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateConversation(ctx context.Context, arg CreateConversationParams) (Conversation, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createConversation, arg.UserID, arg.Status)
|
||||||
|
var i Conversation
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.ModID,
|
||||||
|
&i.Status,
|
||||||
|
&i.ClosedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const createMessage = `-- name: CreateMessage :one
|
||||||
|
INSERT INTO messages (conversation_id, sender_id, content)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, conversation_id, sender_id, content, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateMessageParams struct {
|
||||||
|
ConversationID pgtype.UUID `json:"conversation_id"`
|
||||||
|
SenderID pgtype.UUID `json:"sender_id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createMessage, arg.ConversationID, arg.SenderID, arg.Content)
|
||||||
|
var i Message
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ConversationID,
|
||||||
|
&i.SenderID,
|
||||||
|
&i.Content,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChatbotHistoriesByIDs = `-- name: GetChatbotHistoriesByIDs :many
|
||||||
|
SELECT id, user_id, question, answer, created_at FROM chatbot_histories WHERE id = ANY($1::uuid[])
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetChatbotHistoriesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]ChatbotHistory, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getChatbotHistoriesByIDs, dollar_1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ChatbotHistory{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ChatbotHistory
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Question,
|
||||||
|
&i.Answer,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getChatbotHistory = `-- name: GetChatbotHistory :many
|
||||||
|
SELECT id, user_id, question, answer, created_at FROM (
|
||||||
|
SELECT id, user_id, question, answer, created_at FROM chatbot_histories
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND ($2::uuid IS NULL OR id < $2::uuid)
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $3
|
||||||
|
) sub
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetChatbotHistoryParams struct {
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
CursorID pgtype.UUID `json:"cursor_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetChatbotHistory(ctx context.Context, arg GetChatbotHistoryParams) ([]ChatbotHistory, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getChatbotHistory, arg.UserID, arg.CursorID, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ChatbotHistory{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ChatbotHistory
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Question,
|
||||||
|
&i.Answer,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getConversationsByIDs = `-- name: GetConversationsByIDs :many
|
||||||
|
SELECT id, user_id, mod_id, status, closed_at, created_at, updated_at FROM conversations WHERE id = ANY($1::uuid[])
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetConversationsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Conversation, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getConversationsByIDs, dollar_1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []Conversation{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i Conversation
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.ModID,
|
||||||
|
&i.Status,
|
||||||
|
&i.ClosedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMessagesByConversation = `-- name: GetMessagesByConversation :many
|
||||||
|
SELECT id, conversation_id, sender_id, content, created_at FROM messages
|
||||||
|
WHERE conversation_id = $1
|
||||||
|
AND ($2::uuid IS NULL OR id < $2::uuid)
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetMessagesByConversationParams struct {
|
||||||
|
ConversationID pgtype.UUID `json:"conversation_id"`
|
||||||
|
CursorID pgtype.UUID `json:"cursor_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetMessagesByConversation(ctx context.Context, arg GetMessagesByConversationParams) ([]Message, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getMessagesByConversation, arg.ConversationID, arg.CursorID, arg.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []Message{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i Message
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ConversationID,
|
||||||
|
&i.SenderID,
|
||||||
|
&i.Content,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMessagesByIDs = `-- name: GetMessagesByIDs :many
|
||||||
|
SELECT id, conversation_id, sender_id, content, created_at FROM messages WHERE id = ANY($1::uuid[])
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetMessagesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Message, error) {
|
||||||
|
rows, err := q.db.Query(ctx, getMessagesByIDs, dollar_1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []Message{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i Message
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ConversationID,
|
||||||
|
&i.SenderID,
|
||||||
|
&i.Content,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateConversationStatus = `-- name: UpdateConversationStatus :one
|
||||||
|
UPDATE conversations
|
||||||
|
SET status = $2, mod_id = COALESCE($3, mod_id), closed_at = $4, updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, user_id, mod_id, status, closed_at, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateConversationStatusParams struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
Status int16 `json:"status"`
|
||||||
|
ModID pgtype.UUID `json:"mod_id"`
|
||||||
|
ClosedAt pgtype.Timestamptz `json:"closed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateConversationStatus(ctx context.Context, arg UpdateConversationStatusParams) (Conversation, error) {
|
||||||
|
row := q.db.QueryRow(ctx, updateConversationStatus,
|
||||||
|
arg.ID,
|
||||||
|
arg.Status,
|
||||||
|
arg.ModID,
|
||||||
|
arg.ClosedAt,
|
||||||
|
)
|
||||||
|
var i Conversation
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.ModID,
|
||||||
|
&i.Status,
|
||||||
|
&i.ClosedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@@ -11,6 +11,14 @@ import (
|
|||||||
"github.com/pgvector/pgvector-go"
|
"github.com/pgvector/pgvector-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ChatbotHistory struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
Question string `json:"question"`
|
||||||
|
Answer string `json:"answer"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type Commit struct {
|
type Commit struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
ProjectID pgtype.UUID `json:"project_id"`
|
ProjectID pgtype.UUID `json:"project_id"`
|
||||||
@@ -22,6 +30,16 @@ type Commit struct {
|
|||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Conversation struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
|
ModID pgtype.UUID `json:"mod_id"`
|
||||||
|
Status int16 `json:"status"`
|
||||||
|
ClosedAt pgtype.Timestamptz `json:"closed_at"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type Entity struct {
|
type Entity struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
ProjectID pgtype.UUID `json:"project_id"`
|
ProjectID pgtype.UUID `json:"project_id"`
|
||||||
@@ -74,6 +92,14 @@ type Media struct {
|
|||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
ConversationID pgtype.UUID `json:"conversation_id"`
|
||||||
|
SenderID pgtype.UUID `json:"sender_id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
|||||||
53
internal/models/chat.go
Normal file
53
internal/models/chat.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"history-api/internal/dtos/response"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConversationEntity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
ModID *string `json:"mod_id,omitempty"`
|
||||||
|
Status int16 `json:"status"`
|
||||||
|
ClosedAt *time.Time `json:"closed_at,omitempty"`
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageEntity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
ConversationID string `json:"conversation_id"`
|
||||||
|
SenderID string `json:"sender_id"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatbotHistoryEntity struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Question string `json:"question"`
|
||||||
|
Answer string `json:"answer"`
|
||||||
|
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ChatbotHistoryEntity) ToResponse() *response.ChatbotHistoryDto {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &response.ChatbotHistoryDto{
|
||||||
|
ID: c.ID,
|
||||||
|
UserID: c.UserID,
|
||||||
|
Question: c.Question,
|
||||||
|
Answer: c.Answer,
|
||||||
|
CreatedAt: c.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChatbotHistoryEntitiesToResponse(entities []*ChatbotHistoryEntity) []*response.ChatbotHistoryDto {
|
||||||
|
res := make([]*response.ChatbotHistoryDto, 0, len(entities))
|
||||||
|
for _, e := range entities {
|
||||||
|
res = append(res, e.ToResponse())
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
379
internal/repositories/chatRepository.go
Normal file
379
internal/repositories/chatRepository.go
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"history-api/internal/gen/sqlc"
|
||||||
|
"history-api/internal/models"
|
||||||
|
"history-api/pkg/cache"
|
||||||
|
"history-api/pkg/constants"
|
||||||
|
"history-api/pkg/convert"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatRepository interface {
|
||||||
|
CreateConversation(ctx context.Context, params sqlc.CreateConversationParams) (*models.ConversationEntity, error)
|
||||||
|
UpdateConversationStatus(ctx context.Context, params sqlc.UpdateConversationStatusParams) (*models.ConversationEntity, error)
|
||||||
|
CreateMessage(ctx context.Context, params sqlc.CreateMessageParams) (*models.MessageEntity, error)
|
||||||
|
GetMessagesByConversation(ctx context.Context, params sqlc.GetMessagesByConversationParams) ([]*models.MessageEntity, error)
|
||||||
|
CreateChatbotHistory(ctx context.Context, params sqlc.CreateChatbotHistoryParams) (*models.ChatbotHistoryEntity, error)
|
||||||
|
GetChatbotHistory(ctx context.Context, params sqlc.GetChatbotHistoryParams) ([]*models.ChatbotHistoryEntity, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatRepository struct {
|
||||||
|
db *pgxpool.Pool
|
||||||
|
q *sqlc.Queries
|
||||||
|
c cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatRepository(db *pgxpool.Pool, c cache.Cache) ChatRepository {
|
||||||
|
return &chatRepository{
|
||||||
|
db: db,
|
||||||
|
q: sqlc.New(db),
|
||||||
|
c: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) generateQueryKey(prefix string, params any) string {
|
||||||
|
b, _ := json.Marshal(params)
|
||||||
|
hash := fmt.Sprintf("%x", md5.Sum(b))
|
||||||
|
return fmt.Sprintf("%s:query:%s", prefix, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) getConversationsByIDsWithFallback(ctx context.Context, ids []string) ([]*models.ConversationEntity, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []*models.ConversationEntity{}, nil
|
||||||
|
}
|
||||||
|
keys := make([]string, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
keys[i] = fmt.Sprintf("conversation:id:%s", id)
|
||||||
|
}
|
||||||
|
raws := r.c.MGet(ctx, keys...)
|
||||||
|
|
||||||
|
var results []*models.ConversationEntity
|
||||||
|
missingToCache := make(map[string]any)
|
||||||
|
|
||||||
|
var missingPgIds []pgtype.UUID
|
||||||
|
for i, b := range raws {
|
||||||
|
if len(b) == 0 {
|
||||||
|
pgId := pgtype.UUID{}
|
||||||
|
err := pgId.Scan(ids[i])
|
||||||
|
if err == nil {
|
||||||
|
missingPgIds = append(missingPgIds, pgId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbMap := make(map[string]*models.ConversationEntity)
|
||||||
|
if len(missingPgIds) > 0 {
|
||||||
|
dbRows, err := r.q.GetConversationsByIDs(ctx, missingPgIds)
|
||||||
|
if err == nil {
|
||||||
|
for _, row := range dbRows {
|
||||||
|
item := models.ConversationEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
UserID: convert.UUIDToString(row.UserID),
|
||||||
|
ModID: convert.UUIDToStringPtr(row.ModID),
|
||||||
|
Status: row.Status,
|
||||||
|
ClosedAt: convert.TimeToPtr(row.ClosedAt),
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
|
}
|
||||||
|
dbMap[item.ID] = &item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, b := range raws {
|
||||||
|
if len(b) > 0 {
|
||||||
|
var c models.ConversationEntity
|
||||||
|
if err := json.Unmarshal(b, &c); err == nil {
|
||||||
|
results = append(results, &c)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if item, ok := dbMap[ids[i]]; ok {
|
||||||
|
results = append(results, item)
|
||||||
|
missingToCache[keys[i]] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingToCache) > 0 {
|
||||||
|
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) getMessagesByIDsWithFallback(ctx context.Context, ids []string) ([]*models.MessageEntity, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []*models.MessageEntity{}, nil
|
||||||
|
}
|
||||||
|
keys := make([]string, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
keys[i] = fmt.Sprintf("message:id:%s", id)
|
||||||
|
}
|
||||||
|
raws := r.c.MGet(ctx, keys...)
|
||||||
|
|
||||||
|
var results []*models.MessageEntity
|
||||||
|
missingToCache := make(map[string]any)
|
||||||
|
|
||||||
|
var missingPgIds []pgtype.UUID
|
||||||
|
for i, b := range raws {
|
||||||
|
if len(b) == 0 {
|
||||||
|
pgId := pgtype.UUID{}
|
||||||
|
err := pgId.Scan(ids[i])
|
||||||
|
if err == nil {
|
||||||
|
missingPgIds = append(missingPgIds, pgId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbMap := make(map[string]*models.MessageEntity)
|
||||||
|
if len(missingPgIds) > 0 {
|
||||||
|
dbRows, err := r.q.GetMessagesByIDs(ctx, missingPgIds)
|
||||||
|
if err == nil {
|
||||||
|
for _, row := range dbRows {
|
||||||
|
item := models.MessageEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
ConversationID: convert.UUIDToString(row.ConversationID),
|
||||||
|
SenderID: convert.UUIDToString(row.SenderID),
|
||||||
|
Content: row.Content,
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
}
|
||||||
|
dbMap[item.ID] = &item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, b := range raws {
|
||||||
|
if len(b) > 0 {
|
||||||
|
var c models.MessageEntity
|
||||||
|
if err := json.Unmarshal(b, &c); err == nil {
|
||||||
|
results = append(results, &c)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if item, ok := dbMap[ids[i]]; ok {
|
||||||
|
results = append(results, item)
|
||||||
|
missingToCache[keys[i]] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingToCache) > 0 {
|
||||||
|
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) getChatbotHistoriesByIDsWithFallback(ctx context.Context, ids []string) ([]*models.ChatbotHistoryEntity, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []*models.ChatbotHistoryEntity{}, nil
|
||||||
|
}
|
||||||
|
keys := make([]string, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
keys[i] = fmt.Sprintf("chatbot_history:id:%s", id)
|
||||||
|
}
|
||||||
|
raws := r.c.MGet(ctx, keys...)
|
||||||
|
|
||||||
|
var results []*models.ChatbotHistoryEntity
|
||||||
|
missingToCache := make(map[string]any)
|
||||||
|
|
||||||
|
var missingPgIds []pgtype.UUID
|
||||||
|
for i, b := range raws {
|
||||||
|
if len(b) == 0 {
|
||||||
|
pgId := pgtype.UUID{}
|
||||||
|
err := pgId.Scan(ids[i])
|
||||||
|
if err == nil {
|
||||||
|
missingPgIds = append(missingPgIds, pgId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbMap := make(map[string]*models.ChatbotHistoryEntity)
|
||||||
|
if len(missingPgIds) > 0 {
|
||||||
|
dbRows, err := r.q.GetChatbotHistoriesByIDs(ctx, missingPgIds)
|
||||||
|
if err == nil {
|
||||||
|
for _, row := range dbRows {
|
||||||
|
item := models.ChatbotHistoryEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
UserID: convert.UUIDToString(row.UserID),
|
||||||
|
Question: row.Question,
|
||||||
|
Answer: row.Answer,
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
}
|
||||||
|
dbMap[item.ID] = &item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, b := range raws {
|
||||||
|
if len(b) > 0 {
|
||||||
|
var c models.ChatbotHistoryEntity
|
||||||
|
if err := json.Unmarshal(b, &c); err == nil {
|
||||||
|
results = append(results, &c)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if item, ok := dbMap[ids[i]]; ok {
|
||||||
|
results = append(results, item)
|
||||||
|
missingToCache[keys[i]] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingToCache) > 0 {
|
||||||
|
_ = r.c.MSet(ctx, missingToCache, constants.NormalCacheDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) CreateConversation(ctx context.Context, params sqlc.CreateConversationParams) (*models.ConversationEntity, error) {
|
||||||
|
row, err := r.q.CreateConversation(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entity := &models.ConversationEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
UserID: convert.UUIDToString(row.UserID),
|
||||||
|
ModID: convert.UUIDToStringPtr(row.ModID),
|
||||||
|
Status: row.Status,
|
||||||
|
ClosedAt: convert.TimeToPtr(row.ClosedAt),
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
|
}
|
||||||
|
_ = r.c.Set(ctx, fmt.Sprintf("conversation:id:%s", entity.ID), entity, constants.NormalCacheDuration)
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) UpdateConversationStatus(ctx context.Context, params sqlc.UpdateConversationStatusParams) (*models.ConversationEntity, error) {
|
||||||
|
row, err := r.q.UpdateConversationStatus(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entity := &models.ConversationEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
UserID: convert.UUIDToString(row.UserID),
|
||||||
|
ModID: convert.UUIDToStringPtr(row.ModID),
|
||||||
|
Status: row.Status,
|
||||||
|
ClosedAt: convert.TimeToPtr(row.ClosedAt),
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||||
|
}
|
||||||
|
_ = r.c.Set(ctx, fmt.Sprintf("conversation:id:%s", entity.ID), entity, constants.NormalCacheDuration)
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) CreateMessage(ctx context.Context, params sqlc.CreateMessageParams) (*models.MessageEntity, error) {
|
||||||
|
row, err := r.q.CreateMessage(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entity := &models.MessageEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
ConversationID: convert.UUIDToString(row.ConversationID),
|
||||||
|
SenderID: convert.UUIDToString(row.SenderID),
|
||||||
|
Content: row.Content,
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
}
|
||||||
|
_ = r.c.Set(ctx, fmt.Sprintf("message:id:%s", entity.ID), entity, constants.NormalCacheDuration)
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) GetMessagesByConversation(ctx context.Context, params sqlc.GetMessagesByConversationParams) ([]*models.MessageEntity, error) {
|
||||||
|
queryKey := r.generateQueryKey("message:conversation", params)
|
||||||
|
var cachedIDs []string
|
||||||
|
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||||
|
return r.getMessagesByIDsWithFallback(ctx, cachedIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.q.GetMessagesByConversation(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []*models.MessageEntity
|
||||||
|
var ids []string
|
||||||
|
toCache := make(map[string]any)
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
item := &models.MessageEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
ConversationID: convert.UUIDToString(row.ConversationID),
|
||||||
|
SenderID: convert.UUIDToString(row.SenderID),
|
||||||
|
Content: row.Content,
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
}
|
||||||
|
ids = append(ids, item.ID)
|
||||||
|
results = append(results, item)
|
||||||
|
toCache[fmt.Sprintf("message:id:%s", item.ID)] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toCache) > 0 {
|
||||||
|
_ = r.c.MSet(ctx, toCache, constants.NormalCacheDuration)
|
||||||
|
}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) CreateChatbotHistory(ctx context.Context, params sqlc.CreateChatbotHistoryParams) (*models.ChatbotHistoryEntity, error) {
|
||||||
|
row, err := r.q.CreateChatbotHistory(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
entity := &models.ChatbotHistoryEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
UserID: convert.UUIDToString(row.UserID),
|
||||||
|
Question: row.Question,
|
||||||
|
Answer: row.Answer,
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
}
|
||||||
|
_ = r.c.Set(ctx, fmt.Sprintf("chatbot_history:id:%s", entity.ID), entity, constants.NormalCacheDuration)
|
||||||
|
return entity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *chatRepository) GetChatbotHistory(ctx context.Context, params sqlc.GetChatbotHistoryParams) ([]*models.ChatbotHistoryEntity, error) {
|
||||||
|
queryKey := r.generateQueryKey("chatbot_history:user", params)
|
||||||
|
var cachedIDs []string
|
||||||
|
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||||
|
return r.getChatbotHistoriesByIDsWithFallback(ctx, cachedIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.q.GetChatbotHistory(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []*models.ChatbotHistoryEntity
|
||||||
|
var ids []string
|
||||||
|
toCache := make(map[string]any)
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
item := &models.ChatbotHistoryEntity{
|
||||||
|
ID: convert.UUIDToString(row.ID),
|
||||||
|
UserID: convert.UUIDToString(row.UserID),
|
||||||
|
Question: row.Question,
|
||||||
|
Answer: row.Answer,
|
||||||
|
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||||
|
}
|
||||||
|
ids = append(ids, item.ID)
|
||||||
|
results = append(results, item)
|
||||||
|
toCache[fmt.Sprintf("chatbot_history:id:%s", item.ID)] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toCache) > 0 {
|
||||||
|
_ = r.c.MSet(ctx, toCache, constants.NormalCacheDuration)
|
||||||
|
}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
@@ -13,4 +13,5 @@ func ChatbotRoutes(app *fiber.App, controller *controllers.ChatbotController, us
|
|||||||
|
|
||||||
route.Use(middlewares.JwtAccess(userRepo))
|
route.Use(middlewares.JwtAccess(userRepo))
|
||||||
route.Post("/chat", controller.Chat)
|
route.Post("/chat", controller.Chat)
|
||||||
|
route.Get("/history", controller.GetHistory)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,34 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"history-api/internal/dtos/request"
|
||||||
|
"history-api/internal/gen/sqlc"
|
||||||
|
"history-api/internal/models"
|
||||||
"history-api/internal/repositories"
|
"history-api/internal/repositories"
|
||||||
"history-api/pkg/ai"
|
"history-api/pkg/ai"
|
||||||
"history-api/pkg/constants"
|
"history-api/pkg/constants"
|
||||||
|
"history-api/pkg/convert"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChatbotService interface {
|
type ChatbotService interface {
|
||||||
Chat(ctx context.Context, userID string, projectID *string, question string) (string, error)
|
Chat(ctx context.Context, userID string, projectID *string, question string) (string, error)
|
||||||
|
GetHistory(ctx context.Context, userID string, dto *request.GetChatbotHistoryDto) ([]*models.ChatbotHistoryEntity, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type chatbotService struct {
|
type chatbotService struct {
|
||||||
repo repositories.RagRepository
|
repo repositories.RagRepository
|
||||||
usageRepo repositories.UsageRepository
|
usageRepo repositories.UsageRepository
|
||||||
|
chatRepo repositories.ChatRepository
|
||||||
ragUtils *ai.RagUtils
|
ragUtils *ai.RagUtils
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChatbotService(repo repositories.RagRepository, usageRepo repositories.UsageRepository, ragUtils *ai.RagUtils) ChatbotService {
|
func NewChatbotService(repo repositories.RagRepository, usageRepo repositories.UsageRepository, chatRepo repositories.ChatRepository, ragUtils *ai.RagUtils) ChatbotService {
|
||||||
return &chatbotService{
|
return &chatbotService{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
usageRepo: usageRepo,
|
usageRepo: usageRepo,
|
||||||
|
chatRepo: chatRepo,
|
||||||
ragUtils: ragUtils,
|
ragUtils: ragUtils,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,15 +60,38 @@ func (s *chatbotService) Chat(ctx context.Context, userID string, projectID *str
|
|||||||
contextStr += fmt.Sprintf("[Document %d]: %s\n", i+1, res.Content)
|
contextStr += fmt.Sprintf("[Document %d]: %s\n", i+1, res.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pgUserID, err := convert.StringToUUID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid user id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
histories, err := s.chatRepo.GetChatbotHistory(ctx, sqlc.GetChatbotHistoryParams{
|
||||||
|
UserID: pgUserID,
|
||||||
|
Limit: 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: failed to get chatbot history: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
historyStr := ""
|
||||||
|
for _, h := range histories {
|
||||||
|
historyStr += fmt.Sprintf("User: %s\nAssistant: %s\n\n", h.Question, h.Answer)
|
||||||
|
}
|
||||||
|
|
||||||
var prompt string
|
var prompt string
|
||||||
if contextStr == "" {
|
if contextStr == "" {
|
||||||
prompt = fmt.Sprintf(`You are a friendly history assistant chatbot. The user said: "%s"
|
prompt = fmt.Sprintf(`You are a friendly history assistant chatbot.
|
||||||
|
|
||||||
|
Recent Chat History:
|
||||||
|
%s
|
||||||
|
|
||||||
|
The user said: "%s"
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
- If it is a greeting (like "hello", "hi", "xin chào"), respond with a friendly greeting and briefly introduce yourself.
|
- If it is a greeting (like "hello", "hi", "xin chào"), respond with a friendly greeting and briefly introduce yourself.
|
||||||
- If it is a history question, say that you don't have relevant documents to answer.
|
- If it is a history question, say that you don't have relevant documents to answer.
|
||||||
- You MUST wrap your final response inside <answer> tags. Example: <answer>Hello!</answer>
|
- You MUST wrap your final response inside <answer> tags. Example: <answer>Hello!</answer>
|
||||||
- Do NOT show your reasoning outside or inside the tags if possible, but the final answer MUST be in <answer> tags.`, question)
|
- Do NOT show your reasoning outside or inside the tags if possible, but the final answer MUST be in <answer> tags.`, historyStr, question)
|
||||||
} else {
|
} else {
|
||||||
prompt = fmt.Sprintf(`You are a helpful history assistant. Answer the question using ONLY the provided context.
|
prompt = fmt.Sprintf(`You are a helpful history assistant. Answer the question using ONLY the provided context.
|
||||||
|
|
||||||
@@ -71,7 +103,10 @@ Rules:
|
|||||||
Context:
|
Context:
|
||||||
%s
|
%s
|
||||||
|
|
||||||
Question: %s`, contextStr, question)
|
Recent Chat History:
|
||||||
|
%s
|
||||||
|
|
||||||
|
Question: %s`, contextStr, historyStr, question)
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := s.ragUtils.GenerateResponse(ctx, prompt)
|
response, err := s.ragUtils.GenerateResponse(ctx, prompt)
|
||||||
@@ -80,5 +115,40 @@ Question: %s`, contextStr, question)
|
|||||||
}
|
}
|
||||||
_, _ = s.usageRepo.IncrementAIUsage(ctx, userID)
|
_, _ = s.usageRepo.IncrementAIUsage(ctx, userID)
|
||||||
|
|
||||||
|
_, err = s.chatRepo.CreateChatbotHistory(ctx, sqlc.CreateChatbotHistoryParams{
|
||||||
|
UserID: pgUserID,
|
||||||
|
Question: question,
|
||||||
|
Answer: response,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: failed to save chatbot history: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *chatbotService) GetHistory(ctx context.Context, userID string, dto *request.GetChatbotHistoryDto) ([]*models.ChatbotHistoryEntity, error) {
|
||||||
|
pgUserID, err := convert.StringToUUID(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid user id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pgCursorID pgtype.UUID
|
||||||
|
if dto.Cursor != nil {
|
||||||
|
if err := pgCursorID.Scan(*dto.Cursor); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid cursor id: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pgCursorID.Valid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if dto.Limit <= 0 {
|
||||||
|
dto.Limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.chatRepo.GetChatbotHistory(ctx, sqlc.GetChatbotHistoryParams{
|
||||||
|
UserID: pgUserID,
|
||||||
|
CursorID: pgCursorID,
|
||||||
|
Limit: int32(dto.Limit),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user