Files
wehub-resource-sync 39dbe3a57d
CI / Check and lint (push) Failing after 1s
Lint / Lint (ubuntu-latest) (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:22:32 +08:00

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
}