UPDATE: Chatbot module
All checks were successful
Build and Release / release (push) Successful in 2m13s
All checks were successful
Build and Release / release (push) Successful in 2m13s
This commit is contained in:
60
internal/controllers/chatbotController.go
Normal file
60
internal/controllers/chatbotController.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/services"
|
||||
"history-api/pkg/validator"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type ChatbotController struct {
|
||||
chatbotService services.ChatbotService
|
||||
}
|
||||
|
||||
func NewChatbotController(chatbotService services.ChatbotService) *ChatbotController {
|
||||
return &ChatbotController{
|
||||
chatbotService: chatbotService,
|
||||
}
|
||||
}
|
||||
|
||||
// Chat godoc
|
||||
// @Summary Ask the AI chatbot
|
||||
// @Description Ask a history question based on project context or global knowledge using RAG
|
||||
// @Tags Chatbot
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body request.ChatbotDto true "Chatbot query"
|
||||
// @Success 200 {object} response.CommonResponse{data=string} "Successful response with AI answer"
|
||||
// @Failure 400 {object} response.CommonResponse "Invalid request"
|
||||
// @Failure 500 {object} response.CommonResponse "Internal server error"
|
||||
// @Router /chatbot/chat [post]
|
||||
func (cx *ChatbotController) Chat(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.ChatbotDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Errors: err,
|
||||
})
|
||||
}
|
||||
|
||||
answer, err := cx.chatbotService.Chat(ctx, dto.ProjectID, dto.Question)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: answer,
|
||||
})
|
||||
}
|
||||
6
internal/dtos/request/chatbot.go
Normal file
6
internal/dtos/request/chatbot.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package request
|
||||
|
||||
type ChatbotDto struct {
|
||||
ProjectID *string `json:"project_id"`
|
||||
Question string `json:"question" validate:"required"`
|
||||
}
|
||||
@@ -83,7 +83,7 @@ type WikiSnapshot struct {
|
||||
Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"`
|
||||
Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"`
|
||||
Title string `json:"title" validate:"required"`
|
||||
Doc json.RawMessage `json:"doc,omitempty"`
|
||||
Doc string `json:"doc,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WikiResponse struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Content json.RawMessage `json:"content,omitempty"`
|
||||
ProjectID string `json:"project_id"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty"`
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ProjectID string `json:"project_id"`
|
||||
IsDeleted bool `json:"is_deleted,omitempty"`
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/pgvector/pgvector-go"
|
||||
)
|
||||
|
||||
type Commit struct {
|
||||
@@ -94,6 +95,18 @@ type ProjectMember struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type RagChunk struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
SourceType string `json:"source_type"`
|
||||
SourceID pgtype.UUID `json:"source_id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
ChunkIndex int32 `json:"chunk_index"`
|
||||
Content string `json:"content"`
|
||||
Embedding pgvector.Vector `json:"embedding"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -170,7 +183,7 @@ type Wiki struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
Content []byte `json:"content"`
|
||||
Content pgtype.Text `json:"content"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
|
||||
138
internal/gen/sqlc/rag.sql.go
Normal file
138
internal/gen/sqlc/rag.sql.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: rag.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/pgvector/pgvector-go"
|
||||
)
|
||||
|
||||
const createRagChunk = `-- name: CreateRagChunk :one
|
||||
INSERT INTO rag_chunks (
|
||||
id, source_type, source_id, project_id, chunk_index, content, embedding
|
||||
) VALUES (
|
||||
COALESCE($7::uuid, uuidv7()),
|
||||
$1, $2, $3, $4, $5, $6
|
||||
)
|
||||
RETURNING id, source_type, source_id, project_id, chunk_index, content, embedding, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateRagChunkParams struct {
|
||||
SourceType string `json:"source_type"`
|
||||
SourceID pgtype.UUID `json:"source_id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
ChunkIndex int32 `json:"chunk_index"`
|
||||
Content string `json:"content"`
|
||||
Embedding pgvector.Vector `json:"embedding"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateRagChunk(ctx context.Context, arg CreateRagChunkParams) (RagChunk, error) {
|
||||
row := q.db.QueryRow(ctx, createRagChunk,
|
||||
arg.SourceType,
|
||||
arg.SourceID,
|
||||
arg.ProjectID,
|
||||
arg.ChunkIndex,
|
||||
arg.Content,
|
||||
arg.Embedding,
|
||||
arg.ID,
|
||||
)
|
||||
var i RagChunk
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SourceType,
|
||||
&i.SourceID,
|
||||
&i.ProjectID,
|
||||
&i.ChunkIndex,
|
||||
&i.Content,
|
||||
&i.Embedding,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteRagChunksBySourceIDs = `-- name: DeleteRagChunksBySourceIDs :exec
|
||||
DELETE FROM rag_chunks
|
||||
WHERE source_type = $1 AND source_id = ANY($2::uuid[])
|
||||
`
|
||||
|
||||
type DeleteRagChunksBySourceIDsParams struct {
|
||||
SourceType string `json:"source_type"`
|
||||
Column2 []pgtype.UUID `json:"column_2"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteRagChunksBySourceIDs(ctx context.Context, arg DeleteRagChunksBySourceIDsParams) error {
|
||||
_, err := q.db.Exec(ctx, deleteRagChunksBySourceIDs, arg.SourceType, arg.Column2)
|
||||
return err
|
||||
}
|
||||
|
||||
const searchRagChunks = `-- name: SearchRagChunks :many
|
||||
SELECT
|
||||
id, source_type, source_id, project_id, chunk_index, content,
|
||||
(1 - (embedding <=> $1))::float8 AS similarity
|
||||
FROM rag_chunks
|
||||
WHERE 1=1
|
||||
AND ($2::uuid IS NULL OR project_id = $2::uuid)
|
||||
AND ($3::varchar IS NULL OR source_type = $3::varchar)
|
||||
AND (1 - (embedding <=> $1))::float8 >= $4::float8
|
||||
ORDER BY embedding <=> $1
|
||||
LIMIT $5
|
||||
`
|
||||
|
||||
type SearchRagChunksParams struct {
|
||||
Embedding pgvector.Vector `json:"embedding"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
SourceType pgtype.Text `json:"source_type"`
|
||||
MatchThreshold float64 `json:"match_threshold"`
|
||||
MatchCount int32 `json:"match_count"`
|
||||
}
|
||||
|
||||
type SearchRagChunksRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
SourceType string `json:"source_type"`
|
||||
SourceID pgtype.UUID `json:"source_id"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
ChunkIndex int32 `json:"chunk_index"`
|
||||
Content string `json:"content"`
|
||||
Similarity float64 `json:"similarity"`
|
||||
}
|
||||
|
||||
func (q *Queries) SearchRagChunks(ctx context.Context, arg SearchRagChunksParams) ([]SearchRagChunksRow, error) {
|
||||
rows, err := q.db.Query(ctx, searchRagChunks,
|
||||
arg.Embedding,
|
||||
arg.ProjectID,
|
||||
arg.SourceType,
|
||||
arg.MatchThreshold,
|
||||
arg.MatchCount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SearchRagChunksRow{}
|
||||
for rows.Next() {
|
||||
var i SearchRagChunksRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SourceType,
|
||||
&i.SourceID,
|
||||
&i.ProjectID,
|
||||
&i.ChunkIndex,
|
||||
&i.Content,
|
||||
&i.Similarity,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -77,7 +77,7 @@ RETURNING id, project_id, title, content, is_deleted, created_at, updated_at
|
||||
|
||||
type CreateWikiParams struct {
|
||||
Title pgtype.Text `json:"title"`
|
||||
Content []byte `json:"content"`
|
||||
Content pgtype.Text `json:"content"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
}
|
||||
@@ -312,7 +312,7 @@ RETURNING id, project_id, title, content, is_deleted, created_at, updated_at
|
||||
|
||||
type UpdateWikiParams struct {
|
||||
Title pgtype.Text `json:"title"`
|
||||
Content []byte `json:"content"`
|
||||
Content pgtype.Text `json:"content"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
ID pgtype.UUID `json:"id"`
|
||||
}
|
||||
|
||||
17
internal/models/rag.go
Normal file
17
internal/models/rag.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type RagChunk struct {
|
||||
ID string `json:"id"`
|
||||
SourceType string `json:"source_type"`
|
||||
SourceID string `json:"source_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ChunkIndex int32 `json:"chunk_index"`
|
||||
Content string `json:"content"`
|
||||
Similarity float64 `json:"similarity,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"history-api/internal/dtos/response"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WikiEntity struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
ProjectID string `json:"project_id"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
ProjectID string `json:"project_id"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (w *WikiEntity) ToResponse() *response.WikiResponse {
|
||||
|
||||
94
internal/repositories/ragRepository.go
Normal file
94
internal/repositories/ragRepository.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/convert"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/pgvector/pgvector-go"
|
||||
)
|
||||
|
||||
type RagRepository interface {
|
||||
SaveChunk(ctx context.Context, sourceType string, sourceID string, projectID string, index int, content string, vector []float32) error
|
||||
SearchSimilar(ctx context.Context, projectID *string, vector []float32, limit int, threshold float64) ([]*models.RagChunk, error)
|
||||
DeleteBySourceIDs(ctx context.Context, sourceType string, sourceIDs []string) error
|
||||
WithTx(tx pgx.Tx) RagRepository
|
||||
}
|
||||
|
||||
type ragRepository struct {
|
||||
q *sqlc.Queries
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewRagRepository(db sqlc.DBTX, c cache.Cache) RagRepository {
|
||||
return &ragRepository{q: sqlc.New(db), c: c}
|
||||
}
|
||||
|
||||
func (r *ragRepository) WithTx(tx pgx.Tx) RagRepository {
|
||||
return &ragRepository{q: r.q.WithTx(tx), c: r.c}
|
||||
}
|
||||
|
||||
func (r *ragRepository) SaveChunk(ctx context.Context, sourceType string, sourceID string, projectID string, index int, content string, vector []float32) error {
|
||||
pID, _ := convert.StringToUUID(projectID)
|
||||
sID, _ := convert.StringToUUID(sourceID)
|
||||
|
||||
_, err := r.q.CreateRagChunk(ctx, sqlc.CreateRagChunkParams{
|
||||
SourceType: sourceType,
|
||||
SourceID: sID,
|
||||
ProjectID: pID,
|
||||
ChunkIndex: int32(index),
|
||||
Content: content,
|
||||
Embedding: pgvector.NewVector(vector),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *ragRepository) SearchSimilar(ctx context.Context, projectID *string, vector []float32, limit int, threshold float64) ([]*models.RagChunk, error) {
|
||||
params := sqlc.SearchRagChunksParams{
|
||||
Embedding: pgvector.NewVector(vector),
|
||||
MatchThreshold: threshold,
|
||||
MatchCount: int32(limit),
|
||||
}
|
||||
if projectID != nil && *projectID != "" {
|
||||
pID, _ := convert.StringToUUID(*projectID)
|
||||
params.ProjectID = pID
|
||||
}
|
||||
|
||||
rows, err := r.q.SearchRagChunks(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]*models.RagChunk, len(rows))
|
||||
for i, row := range rows {
|
||||
res[i] = &models.RagChunk{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Content: row.Content,
|
||||
Similarity: row.Similarity,
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *ragRepository) DeleteBySourceIDs(ctx context.Context, sourceType string, sourceIDs []string) error {
|
||||
if len(sourceIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
uids := make([]pgtype.UUID, 0, len(sourceIDs))
|
||||
for _, id := range sourceIDs {
|
||||
uid, err := convert.StringToUUID(id)
|
||||
if err == nil {
|
||||
uids = append(uids, uid)
|
||||
}
|
||||
}
|
||||
|
||||
return r.q.DeleteRagChunksBySourceIDs(ctx, sqlc.DeleteRagChunksBySourceIDsParams{
|
||||
SourceType: sourceType,
|
||||
Column2: uids,
|
||||
})
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string)
|
||||
item := models.WikiEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Title: convert.TextToString(row.Title),
|
||||
Content: json.RawMessage(row.Content),
|
||||
Content: convert.TextToString(row.Content),
|
||||
IsDeleted: row.IsDeleted,
|
||||
ProjectID: convert.UUIDToString(row.ProjectID),
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
@@ -143,7 +143,7 @@ func (r *wikiRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.W
|
||||
wiki = models.WikiEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Title: convert.TextToString(row.Title),
|
||||
Content: json.RawMessage(row.Content),
|
||||
Content: convert.TextToString(row.Content),
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
@@ -172,7 +172,7 @@ func (r *wikiRepository) Search(ctx context.Context, params sqlc.SearchWikisPara
|
||||
wiki := &models.WikiEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Title: convert.TextToString(row.Title),
|
||||
Content: json.RawMessage(row.Content),
|
||||
Content: convert.TextToString(row.Content),
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
@@ -201,7 +201,7 @@ func (r *wikiRepository) Create(ctx context.Context, params sqlc.CreateWikiParam
|
||||
wiki := models.WikiEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Title: convert.TextToString(row.Title),
|
||||
Content: json.RawMessage(row.Content),
|
||||
Content: convert.TextToString(row.Content),
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
@@ -218,7 +218,7 @@ func (r *wikiRepository) Update(ctx context.Context, params sqlc.UpdateWikiParam
|
||||
wiki := models.WikiEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Title: convert.TextToString(row.Title),
|
||||
Content: json.RawMessage(row.Content),
|
||||
Content: convert.TextToString(row.Content),
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
@@ -272,7 +272,7 @@ func (r *wikiRepository) GetByProjectID(ctx context.Context, projectID pgtype.UU
|
||||
wiki := &models.WikiEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Title: convert.TextToString(row.Title),
|
||||
Content: json.RawMessage(row.Content),
|
||||
Content: convert.TextToString(row.Content),
|
||||
IsDeleted: row.IsDeleted,
|
||||
ProjectID: convert.UUIDToString(row.ProjectID),
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
|
||||
16
internal/routes/chatbotRoute.go
Normal file
16
internal/routes/chatbotRoute.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
"history-api/internal/middlewares"
|
||||
"history-api/internal/repositories"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func ChatbotRoutes(app *fiber.App, controller *controllers.ChatbotController, userRepo repositories.UserRepository) {
|
||||
route := app.Group("/chatbot")
|
||||
|
||||
route.Use(middlewares.JwtAccess(userRepo))
|
||||
route.Post("/chat", controller.Chat)
|
||||
}
|
||||
51
internal/services/chatbotService.go
Normal file
51
internal/services/chatbotService.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/ai"
|
||||
)
|
||||
|
||||
type ChatbotService interface {
|
||||
Chat(ctx context.Context, projectID *string, question string) (string, error)
|
||||
}
|
||||
|
||||
type chatbotService struct {
|
||||
repo repositories.RagRepository
|
||||
ragUtils *ai.RagUtils
|
||||
}
|
||||
|
||||
func NewChatbotService(repo repositories.RagRepository, ragUtils *ai.RagUtils) ChatbotService {
|
||||
return &chatbotService{
|
||||
repo: repo,
|
||||
ragUtils: ragUtils,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *chatbotService) Chat(ctx context.Context, projectID *string, question string) (string, error) {
|
||||
qVector, err := s.ragUtils.EmbedQuery(ctx, question)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to embed question: %w", err)
|
||||
}
|
||||
results, err := s.repo.SearchSimilar(ctx, projectID, qVector, 5, 0.65)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to search similar content: %w", err)
|
||||
}
|
||||
|
||||
contextStr := ""
|
||||
for i, res := range results {
|
||||
contextStr += fmt.Sprintf("[Document %d]: %s\n", i+1, res.Content)
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`You are a helpful history assistant. Answer the question based ONLY on the provided context.
|
||||
If the answer is not in the context, say "I don't have enough historical context to answer that."
|
||||
|
||||
Context:
|
||||
%s
|
||||
|
||||
Question: %s
|
||||
Answer:`, contextStr, question)
|
||||
|
||||
return s.ragUtils.GenerateResponse(ctx, prompt)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/ai"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
@@ -36,6 +37,8 @@ type submissionService struct {
|
||||
wikiRepo repositories.WikiRepository
|
||||
geometryRepo repositories.GeometryRepository
|
||||
entityRepo repositories.EntityRepository
|
||||
ragRepo repositories.RagRepository
|
||||
ragUtils *ai.RagUtils
|
||||
db *pgxpool.Pool
|
||||
c cache.Cache
|
||||
}
|
||||
@@ -48,6 +51,8 @@ func NewSubmissionService(
|
||||
wikiRepo repositories.WikiRepository,
|
||||
geometryRepo repositories.GeometryRepository,
|
||||
entityRepo repositories.EntityRepository,
|
||||
ragRepo repositories.RagRepository,
|
||||
ragUtils *ai.RagUtils,
|
||||
db *pgxpool.Pool,
|
||||
c cache.Cache,
|
||||
) SubmissionService {
|
||||
@@ -59,6 +64,8 @@ func NewSubmissionService(
|
||||
wikiRepo: wikiRepo,
|
||||
geometryRepo: geometryRepo,
|
||||
entityRepo: entityRepo,
|
||||
ragRepo: ragRepo,
|
||||
ragUtils: ragUtils,
|
||||
db: db,
|
||||
c: c,
|
||||
}
|
||||
@@ -127,6 +134,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
entityRepo := s.entityRepo.WithTx(tx)
|
||||
geometryRepo := s.geometryRepo.WithTx(tx)
|
||||
wikiRepo := s.wikiRepo.WithTx(tx)
|
||||
ragRepo := s.ragRepo.WithTx(tx)
|
||||
|
||||
submissionUUID, err := convert.StringToUUID(submissionID)
|
||||
if err != nil {
|
||||
@@ -166,8 +174,11 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Commit does not belong to project")
|
||||
}
|
||||
|
||||
listDeleteEntities := make([]pgtype.UUID, 0)
|
||||
listDeleteWikis := make([]pgtype.UUID, 0)
|
||||
listDeleteGeometries := make([]pgtype.UUID, 0)
|
||||
var snapshotData request.CommitSnapshot
|
||||
if status == constants.StatusTypeApproved {
|
||||
var snapshotData request.CommitSnapshot
|
||||
err = json.Unmarshal(commit.SnapshotJson, &snapshotData)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot")
|
||||
@@ -214,7 +225,6 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
persistCurrentItemIDs[item.ID] = struct{}{}
|
||||
}
|
||||
|
||||
listDeleteEntities := make([]pgtype.UUID, 0)
|
||||
for _, e := range currentEntity {
|
||||
if _, ok := persistItemIDs[e.ID]; !ok {
|
||||
itemUUID, err := convert.StringToUUID(e.ID)
|
||||
@@ -226,7 +236,6 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
}
|
||||
}
|
||||
|
||||
listDeleteGeometries := make([]pgtype.UUID, 0)
|
||||
for _, g := range currentGeometry {
|
||||
if _, ok := persistItemIDs[g.ID]; !ok {
|
||||
itemUUID, err := convert.StringToUUID(g.ID)
|
||||
@@ -238,7 +247,6 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
}
|
||||
}
|
||||
|
||||
listDeleteWikis := make([]pgtype.UUID, 0)
|
||||
for _, w := range currentWiki {
|
||||
if _, ok := persistItemIDs[w.ID]; !ok {
|
||||
itemUUID, err := convert.StringToUUID(w.ID)
|
||||
@@ -274,6 +282,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
refEntityIDs = append(refEntityIDs, e.ID)
|
||||
}
|
||||
}
|
||||
|
||||
refEntities, _ := s.entityRepo.GetByIDs(ctx, refEntityIDs)
|
||||
refEntityMap := make(map[string]bool)
|
||||
for _, e := range refEntities {
|
||||
@@ -444,7 +453,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
_, err := wikiRepo.Update(ctx, sqlc.UpdateWikiParams{
|
||||
ID: wikiUUID,
|
||||
Title: convert.StringToText(wiki.Title),
|
||||
Content: wiki.Doc,
|
||||
Content: convert.StringToText(wiki.Doc),
|
||||
ProjectID: projectUUID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -456,7 +465,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
_, err := wikiRepo.Create(ctx, sqlc.CreateWikiParams{
|
||||
ID: wikiUUID,
|
||||
Title: convert.StringToText(wiki.Title),
|
||||
Content: wiki.Doc,
|
||||
Content: convert.StringToText(wiki.Doc),
|
||||
ProjectID: projectUUID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -554,6 +563,58 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
}
|
||||
}
|
||||
|
||||
if status == constants.StatusTypeApproved {
|
||||
wikiDeleteIDs := make([]string, 0)
|
||||
entityDeleteIDs := make([]string, 0)
|
||||
|
||||
for _, id := range listDeleteWikis {
|
||||
wikiDeleteIDs = append(wikiDeleteIDs, convert.UUIDToString(id))
|
||||
}
|
||||
for _, id := range listDeleteEntities {
|
||||
entityDeleteIDs = append(entityDeleteIDs, convert.UUIDToString(id))
|
||||
}
|
||||
|
||||
for _, wiki := range snapshotData.Wikis {
|
||||
if wiki.Operation == "delete" {
|
||||
wikiDeleteIDs = append(wikiDeleteIDs, wiki.ID)
|
||||
}
|
||||
}
|
||||
for _, entity := range snapshotData.Entities {
|
||||
if entity.Operation == "delete" {
|
||||
entityDeleteIDs = append(entityDeleteIDs, entity.ID)
|
||||
}
|
||||
}
|
||||
|
||||
_ = ragRepo.DeleteBySourceIDs(ctx, "wiki", wikiDeleteIDs)
|
||||
_ = ragRepo.DeleteBySourceIDs(ctx, "entity", entityDeleteIDs)
|
||||
|
||||
for _, wiki := range snapshotData.Wikis {
|
||||
if wiki.Source == "inline" {
|
||||
cleanText := s.ragUtils.StripHTML(wiki.Title + "\n" + wiki.Doc)
|
||||
chunks, vectors, err := s.ragUtils.PrepareChunks(ctx, cleanText)
|
||||
if err == nil {
|
||||
_ = ragRepo.DeleteBySourceIDs(ctx, "wiki", []string{wiki.ID})
|
||||
for i, chunk := range chunks {
|
||||
_ = ragRepo.SaveChunk(ctx, "wiki", wiki.ID, commit.ProjectID, i, chunk, vectors[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, entity := range snapshotData.Entities {
|
||||
if entity.Source == "inline" {
|
||||
cleanText := s.ragUtils.StripHTML(entity.Name + "\n" + entity.Description)
|
||||
chunks, vectors, err := s.ragUtils.PrepareChunks(ctx, cleanText)
|
||||
if err == nil {
|
||||
_ = ragRepo.DeleteBySourceIDs(ctx, "entity", []string{entity.ID})
|
||||
for i, chunk := range chunks {
|
||||
_ = ragRepo.SaveChunk(ctx, "entity", entity.ID, commit.ProjectID, i, chunk, vectors[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
arg := sqlc.UpdateSubmissionParams{
|
||||
ID: submissionUUID,
|
||||
Status: pgtype.Int2{Int16: status.Int16(), Valid: true},
|
||||
@@ -563,12 +624,12 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
|
||||
updatedSubmission, err := submissionRepo.Update(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update submission status: " + err.Error())
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update submission status: "+err.Error())
|
||||
}
|
||||
|
||||
err = tx.Commit(ctx)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction: " + err.Error())
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction: "+err.Error())
|
||||
}
|
||||
|
||||
if status == constants.StatusTypeApproved {
|
||||
@@ -579,6 +640,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
|
||||
_ = s.c.DelByPattern(bgCtx, "wiki:search*")
|
||||
}()
|
||||
}
|
||||
|
||||
_ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", submission.ProjectID))
|
||||
|
||||
return updatedSubmission.ToResponse(), nil
|
||||
|
||||
Reference in New Issue
Block a user