382 lines
12 KiB
Go
382 lines
12 KiB
Go
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
package ai_conversation
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/apache/answer/internal/base/pager"
|
|
"github.com/apache/answer/internal/base/reason"
|
|
"github.com/apache/answer/internal/entity"
|
|
"github.com/apache/answer/internal/repo/ai_conversation"
|
|
"github.com/apache/answer/internal/schema"
|
|
usercommon "github.com/apache/answer/internal/service/user_common"
|
|
"github.com/segmentfault/pacman/errors"
|
|
"github.com/segmentfault/pacman/log"
|
|
)
|
|
|
|
// AIConversationService
|
|
type AIConversationService interface {
|
|
CreateConversation(ctx context.Context, userID, conversationID, topic string) error
|
|
SaveConversationRecords(ctx context.Context, conversationID, chatcmplID string, records []*ConversationMessage) error
|
|
GetConversationList(ctx context.Context, req *schema.AIConversationListReq) (*pager.PageModel, error)
|
|
GetConversationDetail(ctx context.Context, req *schema.AIConversationDetailReq) (resp *schema.AIConversationDetailResp, exist bool, err error)
|
|
VoteRecord(ctx context.Context, req *schema.AIConversationVoteReq) error
|
|
GetConversationListForAdmin(ctx context.Context, req *schema.AIConversationAdminListReq) (*pager.PageModel, error)
|
|
GetConversationDetailForAdmin(ctx context.Context, req *schema.AIConversationAdminDetailReq) (*schema.AIConversationAdminDetailResp, error)
|
|
DeleteConversationForAdmin(ctx context.Context, req *schema.AIConversationAdminDeleteReq) error
|
|
}
|
|
|
|
// ConversationMessage
|
|
type ConversationMessage struct {
|
|
ChatCompletionID string `json:"chat_completion_id"`
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
|
}
|
|
|
|
// aiConversationService
|
|
type aiConversationService struct {
|
|
aiConversationRepo ai_conversation.AIConversationRepo
|
|
userCommon *usercommon.UserCommon
|
|
}
|
|
|
|
// NewAIConversationService
|
|
func NewAIConversationService(
|
|
aiConversationRepo ai_conversation.AIConversationRepo,
|
|
userCommon *usercommon.UserCommon,
|
|
) AIConversationService {
|
|
return &aiConversationService{
|
|
aiConversationRepo: aiConversationRepo,
|
|
userCommon: userCommon,
|
|
}
|
|
}
|
|
|
|
// CreateConversation
|
|
func (s *aiConversationService) CreateConversation(ctx context.Context, userID, conversationID, topic string) error {
|
|
conversation := &entity.AIConversation{
|
|
ConversationID: conversationID,
|
|
Topic: topic,
|
|
UserID: userID,
|
|
}
|
|
err := s.aiConversationRepo.CreateConversation(ctx, conversation)
|
|
if err != nil {
|
|
log.Errorf("create conversation failed: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SaveConversationRecords
|
|
func (s *aiConversationService) SaveConversationRecords(ctx context.Context, conversationID, chatcmplID string, records []*ConversationMessage) error {
|
|
conversation, exist, err := s.aiConversationRepo.GetConversation(ctx, conversationID)
|
|
if err != nil {
|
|
return errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
if !exist {
|
|
return errors.BadRequest(reason.ObjectNotFound)
|
|
}
|
|
|
|
content := strings.Builder{}
|
|
reasoning := strings.Builder{}
|
|
|
|
for _, record := range records {
|
|
if len(record.ChatCompletionID) > 0 {
|
|
continue
|
|
}
|
|
if record.Role == "user" {
|
|
aiRecord := &entity.AIConversationRecord{
|
|
ConversationID: conversationID,
|
|
ChatCompletionID: chatcmplID,
|
|
Role: "user",
|
|
Content: record.Content,
|
|
}
|
|
|
|
err = s.aiConversationRepo.CreateRecord(ctx, aiRecord)
|
|
if err != nil {
|
|
log.Errorf("create conversation record failed: %v", err)
|
|
return errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
content.WriteString(record.Content)
|
|
content.WriteString("\n")
|
|
if record.ReasoningContent != "" {
|
|
reasoning.WriteString(record.ReasoningContent)
|
|
reasoning.WriteString("\n")
|
|
}
|
|
}
|
|
aiRecord := &entity.AIConversationRecord{
|
|
ConversationID: conversationID,
|
|
ChatCompletionID: chatcmplID,
|
|
Role: "assistant",
|
|
Content: content.String(),
|
|
ReasoningContent: reasoning.String(),
|
|
Helpful: 0,
|
|
Unhelpful: 0,
|
|
}
|
|
|
|
err = s.aiConversationRepo.CreateRecord(ctx, aiRecord)
|
|
if err != nil {
|
|
log.Errorf("create conversation record failed: %v", err)
|
|
return errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
|
|
conversation.UpdatedAt = time.Now()
|
|
err = s.aiConversationRepo.UpdateConversation(ctx, conversation)
|
|
if err != nil {
|
|
log.Errorf("update conversation failed: %v", err)
|
|
return errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetConversationList
|
|
func (s *aiConversationService) GetConversationList(ctx context.Context, req *schema.AIConversationListReq) (*pager.PageModel, error) {
|
|
conversations, total, err := s.aiConversationRepo.GetConversationsPage(ctx, req.Page, req.PageSize, &entity.AIConversation{UserID: req.UserID})
|
|
if err != nil {
|
|
return nil, errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
|
|
list := make([]schema.AIConversationListItem, 0, len(conversations))
|
|
for _, conversation := range conversations {
|
|
list = append(list, schema.AIConversationListItem{
|
|
ConversationID: conversation.ConversationID,
|
|
CreatedAt: conversation.CreatedAt.Unix(),
|
|
Topic: conversation.Topic,
|
|
})
|
|
}
|
|
|
|
return pager.NewPageModel(total, list), nil
|
|
}
|
|
|
|
// GetConversationDetail
|
|
func (s *aiConversationService) GetConversationDetail(ctx context.Context, req *schema.AIConversationDetailReq) (
|
|
resp *schema.AIConversationDetailResp, exist bool, err error) {
|
|
conversation, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID)
|
|
if err != nil {
|
|
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
if !exist || conversation.UserID != req.UserID {
|
|
return nil, false, nil
|
|
}
|
|
|
|
records, err := s.aiConversationRepo.GetRecordsByConversationID(ctx, req.ConversationID)
|
|
if err != nil {
|
|
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
|
|
recordList := make([]*schema.AIConversationRecord, 0, len(records))
|
|
for i, record := range records {
|
|
if i == 0 {
|
|
record.Content = conversation.Topic
|
|
}
|
|
recordList = append(recordList, &schema.AIConversationRecord{
|
|
ChatCompletionID: record.ChatCompletionID,
|
|
Role: record.Role,
|
|
Content: record.Content,
|
|
ReasoningContent: record.ReasoningContent,
|
|
Helpful: record.Helpful,
|
|
Unhelpful: record.Unhelpful,
|
|
CreatedAt: record.CreatedAt.Unix(),
|
|
})
|
|
}
|
|
|
|
return &schema.AIConversationDetailResp{
|
|
ConversationID: conversation.ConversationID,
|
|
Topic: conversation.Topic,
|
|
Records: recordList,
|
|
CreatedAt: conversation.CreatedAt.Unix(),
|
|
UpdatedAt: conversation.UpdatedAt.Unix(),
|
|
}, true, nil
|
|
}
|
|
|
|
// VoteRecord
|
|
func (s *aiConversationService) VoteRecord(ctx context.Context, req *schema.AIConversationVoteReq) error {
|
|
record, exist, err := s.aiConversationRepo.GetRecordByChatCompletionID(ctx, "assistant", req.ChatCompletionID)
|
|
if err != nil {
|
|
return errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
if !exist {
|
|
return errors.BadRequest(reason.ObjectNotFound)
|
|
}
|
|
|
|
conversation, exist, err := s.aiConversationRepo.GetConversation(ctx, record.ConversationID)
|
|
if err != nil {
|
|
return errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
if !exist {
|
|
return errors.BadRequest(reason.ObjectNotFound)
|
|
}
|
|
|
|
if conversation.UserID != req.UserID {
|
|
return errors.Forbidden(reason.UnauthorizedError)
|
|
}
|
|
|
|
if record.Role != "assistant" {
|
|
return errors.BadRequest("Only AI responses can be voted")
|
|
}
|
|
|
|
if req.VoteType == "helpful" {
|
|
if req.Cancel {
|
|
record.Helpful = 0
|
|
} else {
|
|
record.Helpful = 1
|
|
record.Unhelpful = 0
|
|
}
|
|
} else {
|
|
if req.Cancel {
|
|
record.Unhelpful = 0
|
|
} else {
|
|
record.Unhelpful = 1
|
|
record.Helpful = 0
|
|
}
|
|
}
|
|
|
|
err = s.aiConversationRepo.UpdateRecordVote(ctx, record)
|
|
if err != nil {
|
|
return errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetConversationListForAdmin
|
|
func (s *aiConversationService) GetConversationListForAdmin(
|
|
ctx context.Context, req *schema.AIConversationAdminListReq) (*pager.PageModel, error) {
|
|
conversations, total, err := s.aiConversationRepo.GetConversationsForAdmin(ctx, req.Page, req.PageSize, &entity.AIConversation{})
|
|
if err != nil {
|
|
return nil, errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
|
|
list := make([]*schema.AIConversationAdminListItem, 0, len(conversations))
|
|
for _, conversation := range conversations {
|
|
userInfo, err := s.getUserInfo(ctx, conversation.UserID)
|
|
if err != nil {
|
|
log.Errorf("get user info failed for user %s: %v", conversation.UserID, err)
|
|
continue
|
|
}
|
|
|
|
helpful, unhelpful, err := s.aiConversationRepo.GetConversationWithVoteStats(ctx, conversation.ConversationID)
|
|
if err != nil {
|
|
log.Errorf("get conversation vote stats failed for conversation %s: %v", conversation.ConversationID, err)
|
|
continue
|
|
}
|
|
|
|
list = append(list, &schema.AIConversationAdminListItem{
|
|
ID: conversation.ConversationID,
|
|
Topic: conversation.Topic,
|
|
UserInfo: userInfo,
|
|
HelpfulCount: helpful,
|
|
UnhelpfulCount: unhelpful,
|
|
CreatedAt: conversation.CreatedAt.Unix(),
|
|
})
|
|
}
|
|
|
|
return pager.NewPageModel(total, list), nil
|
|
}
|
|
|
|
// GetConversationDetailForAdmin
|
|
func (s *aiConversationService) GetConversationDetailForAdmin(ctx context.Context, req *schema.AIConversationAdminDetailReq) (*schema.AIConversationAdminDetailResp, error) {
|
|
conversation, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID)
|
|
if err != nil {
|
|
return nil, errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
if !exist {
|
|
return nil, errors.BadRequest(reason.ObjectNotFound)
|
|
}
|
|
|
|
userInfo, err := s.getUserInfo(ctx, conversation.UserID)
|
|
if err != nil {
|
|
return nil, errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
|
|
records, err := s.aiConversationRepo.GetRecordsByConversationID(ctx, req.ConversationID)
|
|
if err != nil {
|
|
return nil, errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
|
|
recordList := make([]schema.AIConversationRecord, 0, len(records))
|
|
for i, record := range records {
|
|
if i == 0 {
|
|
record.Content = conversation.Topic
|
|
}
|
|
recordList = append(recordList, schema.AIConversationRecord{
|
|
ChatCompletionID: record.ChatCompletionID,
|
|
Role: record.Role,
|
|
Content: record.Content,
|
|
ReasoningContent: record.ReasoningContent,
|
|
Helpful: record.Helpful,
|
|
Unhelpful: record.Unhelpful,
|
|
CreatedAt: record.CreatedAt.Unix(),
|
|
})
|
|
}
|
|
|
|
return &schema.AIConversationAdminDetailResp{
|
|
ConversationID: conversation.ConversationID,
|
|
Topic: conversation.Topic,
|
|
UserInfo: userInfo,
|
|
Records: recordList,
|
|
CreatedAt: conversation.CreatedAt.Unix(),
|
|
}, nil
|
|
}
|
|
|
|
// getUserInfo
|
|
func (s *aiConversationService) getUserInfo(ctx context.Context, userID string) (schema.AIConversationUserInfo, error) {
|
|
userInfo := schema.AIConversationUserInfo{}
|
|
|
|
user, exist, err := s.userCommon.GetUserBasicInfoByID(ctx, userID)
|
|
if err != nil {
|
|
return userInfo, err
|
|
}
|
|
if !exist {
|
|
return userInfo, errors.BadRequest(reason.ObjectNotFound)
|
|
}
|
|
|
|
userInfo.ID = user.ID
|
|
userInfo.Username = user.Username
|
|
userInfo.DisplayName = user.DisplayName
|
|
userInfo.Avatar = user.Avatar
|
|
userInfo.Rank = user.Rank
|
|
return userInfo, nil
|
|
}
|
|
|
|
// DeleteConversationForAdmin
|
|
func (s *aiConversationService) DeleteConversationForAdmin(ctx context.Context, req *schema.AIConversationAdminDeleteReq) error {
|
|
_, exist, err := s.aiConversationRepo.GetConversation(ctx, req.ConversationID)
|
|
if err != nil {
|
|
return errors.InternalServer(reason.DatabaseError).WithError(err)
|
|
}
|
|
if !exist {
|
|
return errors.BadRequest(reason.ObjectNotFound)
|
|
}
|
|
|
|
if err := s.aiConversationRepo.DeleteConversation(ctx, req.ConversationID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|