From 1f87b43346ed42fcf35208884e780bca8e688b20 Mon Sep 17 00:00:00 2001 From: Deng Ming Date: Wed, 17 Jul 2024 22:00:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=20AI=EF=BC=8C=E8=81=94?= =?UTF-8?q?=E8=B0=83=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 + config/config.yaml | 2 +- .../gpt/handler/biz/type.go => error.go} | 13 +- internal/ai/handlers.go | 22 +-- .../ai/internal/domain/{gpt.go => llm.go} | 10 +- .../{module_test.go => llm_service_test.go} | 146 ++++++++++-------- .../ai/internal/integration/startup/wire.go | 32 ++-- .../internal/integration/startup/wire_gen.go | 38 ++--- internal/ai/internal/repository/credit.go | 32 ++-- internal/ai/internal/repository/dao/credit.go | 30 ++-- internal/ai/internal/repository/dao/init.go | 4 +- internal/ai/internal/repository/dao/record.go | 29 ++-- internal/ai/internal/repository/log.go | 20 +-- internal/ai/internal/service/gpt/gpt.go | 28 ---- .../handler/biz/composition_biz.go | 8 +- .../{gpt => llm}/handler/biz/facade.go | 6 +- .../handler/biz/question_examine.go | 14 +- .../{gpt => llm}/handler/config/builder.go | 6 +- .../{gpt => llm}/handler/credit/builder.go | 67 +++++--- .../{gpt => llm}/handler/log/builder.go | 10 +- .../handler/mocks/handler.mock.go | 12 +- .../handler/platform}/zhipu/handler.go | 27 ++-- .../{gpt => llm}/handler/record/builder.go | 14 +- .../service/{gpt => llm}/handler/type.go | 6 +- internal/ai/internal/service/llm/llm.go | 28 ++++ .../ai/mocks/{gpt.mock.go => llm.mock.go} | 14 +- internal/ai/module.go | 2 +- internal/ai/type.go | 8 +- internal/ai/wire.go | 24 +-- internal/ai/wire_gen.go | 30 ++-- .../event/consumer/order_event_consumer.go | 8 +- internal/marketing/internal/event/event.go | 3 + .../internal/integration/module_test.go | 79 ++++++++++ .../service/activity/order/executor.go | 1 + .../activity/order/handler/product_credit.go | 53 +++++++ internal/question/internal/errs/code.go | 2 + .../integration/examine_handler_test.go | 6 +- .../internal/integration/startup/wire_gen.go | 2 +- internal/question/internal/service/examine.go | 30 ++-- .../question/internal/web/examine_hander.go | 19 ++- internal/question/internal/web/examine_vo.go | 1 - internal/question/wire.go | 2 +- internal/question/wire_gen.go | 6 +- 43 files changed, 546 insertions(+), 352 deletions(-) rename internal/ai/{internal/service/gpt/handler/biz/type.go => error.go} (66%) rename internal/ai/internal/domain/{gpt.go => llm.go} (93%) rename internal/ai/internal/integration/{module_test.go => llm_service_test.go} (70%) delete mode 100644 internal/ai/internal/service/gpt/gpt.go rename internal/ai/internal/service/{gpt => llm}/handler/biz/composition_biz.go (89%) rename internal/ai/internal/service/{gpt => llm}/handler/biz/facade.go (75%) rename internal/ai/internal/service/{gpt => llm}/handler/biz/question_examine.go (70%) rename internal/ai/internal/service/{gpt => llm}/handler/config/builder.go (90%) rename internal/ai/internal/service/{gpt => llm}/handler/credit/builder.go (51%) rename internal/ai/internal/service/{gpt => llm}/handler/log/builder.go (74%) rename internal/ai/internal/service/{gpt => llm}/handler/mocks/handler.mock.go (90%) rename internal/ai/internal/service/{gpt/handler/gpt => llm/handler/platform}/zhipu/handler.go (70%) rename internal/ai/internal/service/{gpt => llm}/handler/record/builder.go (77%) rename internal/ai/internal/service/{gpt => llm}/handler/type.go (56%) create mode 100644 internal/ai/internal/service/llm/llm.go rename internal/ai/mocks/{gpt.mock.go => llm.mock.go} (79%) create mode 100644 internal/marketing/internal/service/activity/order/handler/product_credit.go diff --git a/README.md b/README.md index 463612f6..8aa29f80 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,16 @@ > 这里比较蛋疼的是 401 和 403 的语义。所以我也没什么好纠结的,只是做一个简单的区分 ## 商品SPU类别说明 + +从标准的电商结构上来说,类别是一个独立的模块,并且会做比较复杂的关系型数据库的设计。但是目前我们在这个项目里面,并不需要这么复杂的东西,所以只需要搞一个粗糙的二级目录就可以了。 + - category0表示SPU顶级类别,可选值有product表示商品,code表示兑换码 - category1表示SPU次级类别,可选值有member/project等 两者组合语义如下: - category0=product, category1=member 表示会员商品 - category0=code, category1=project 表示项目兑换码 +- category0=product, category1=credit 表示积分(积分本身也可以购买) ## 营销模块说明 diff --git a/config/config.yaml b/config/config.yaml index d14cafc0..2aacb2c2 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -24,7 +24,7 @@ qywechat: zhipu: apikey: '' - knowledgeId: '' + price: 0 mysql: dsn: "webook:webook@tcp(mysql8:3306)/webook?charset=utf8mb4&collation=utf8mb4_general_ci&parseTime=True&loc=Local&timeout=1s&readTimeout=3s&writeTimeout=3s" diff --git a/internal/ai/internal/service/gpt/handler/biz/type.go b/internal/ai/error.go similarity index 66% rename from internal/ai/internal/service/gpt/handler/biz/type.go rename to internal/ai/error.go index 4c083cd2..a723cbe1 100644 --- a/internal/ai/internal/service/gpt/handler/biz/type.go +++ b/internal/ai/error.go @@ -12,15 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package biz +package ai -import ( - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" -) +import "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit" -// GPTBizHandler 近似于标记接口,也就是用于区分专属于业务的,和通用的 Handler -type GPTBizHandler interface { - handler.Handler - // Biz 它处理的业务 - Biz() string -} +var ErrInsufficientCredit = credit.ErrInsufficientCredit diff --git a/internal/ai/handlers.go b/internal/ai/handlers.go index 5ab5d92b..4fec44bc 100644 --- a/internal/ai/handlers.go +++ b/internal/ai/handlers.go @@ -15,13 +15,13 @@ package ai import ( - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/biz" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/config" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/credit" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/gpt/zhipu" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/log" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/record" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/biz" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/config" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/log" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/platform/zhipu" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/record" "github.com/gotomicro/ego/core/econf" ) @@ -51,12 +51,12 @@ func InitZhipu() *zhipu.Handler { func InitQuestionExamineHandler( common []handler.Builder, - // gpt 就是真正的出口 - gpt handler.Handler) *biz.CompositionHandler { - // log -> cfg -> credit -> record -> question_examine -> gpt + // platform 就是真正的出口 + platform handler.Handler) *biz.CompositionHandler { + // log -> cfg -> credit -> record -> question_examine -> platform builder := biz.NewQuestionExamineBizHandlerBuilder() common = append(common, builder) - res := biz.NewCombinedBizHandler("question_examine", common, gpt) + res := biz.NewCombinedBizHandler("question_examine", common, platform) return res } diff --git a/internal/ai/internal/domain/gpt.go b/internal/ai/internal/domain/llm.go similarity index 93% rename from internal/ai/internal/domain/gpt.go rename to internal/ai/internal/domain/llm.go index 08a2266d..422298e1 100644 --- a/internal/ai/internal/domain/gpt.go +++ b/internal/ai/internal/domain/llm.go @@ -2,7 +2,7 @@ package domain const BizQuestionExamine = "question_examine" -type GPTRequest struct { +type LLMRequest struct { Biz string Uid int64 // 请求id @@ -15,12 +15,12 @@ type GPTRequest struct { Config BizConfig } -type GPTResponse struct { +type LLMResponse struct { // 花费的token Tokens int64 // 花费的金额 Amount int64 - // gpt的回答 + // llm 的回答 Answer string } @@ -37,7 +37,7 @@ type BizConfig struct { PromptTemplate string } -type GPTCredit struct { +type LLMCredit struct { Id int64 Tid string Uid int64 @@ -49,7 +49,7 @@ type GPTCredit struct { Utime int64 } -type GPTRecord struct { +type LLMRecord struct { Id int64 Tid string Uid int64 diff --git a/internal/ai/internal/integration/module_test.go b/internal/ai/internal/integration/llm_service_test.go similarity index 70% rename from internal/ai/internal/integration/module_test.go rename to internal/ai/internal/integration/llm_service_test.go index 3f6e3442..d33cee66 100644 --- a/internal/ai/internal/integration/module_test.go +++ b/internal/ai/internal/integration/llm_service_test.go @@ -9,9 +9,9 @@ import ( "time" "github.com/ecodeclub/ekit/sqlx" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt" - gptHandler "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" - hdlmocks "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/mocks" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm" + llmHandler "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" + hdlmocks "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/mocks" "github.com/ecodeclub/webook/internal/ai/internal/domain" "github.com/ecodeclub/webook/internal/ai/internal/integration/startup" @@ -28,23 +28,23 @@ import ( const knowledgeId = "abc" -type GptSuite struct { +type LLMServiceSuite struct { suite.Suite - logDao dao.GPTRecordDAO + logDao dao.LLMRecordDAO db *egorm.Component - svc gpt.Service + svc llm.Service } -func TestGptSuite(t *testing.T) { - suite.Run(t, new(GptSuite)) +func TestLLMServiceSuite(t *testing.T) { + suite.Run(t, new(LLMServiceSuite)) } -func (s *GptSuite) SetupSuite() { +func (s *LLMServiceSuite) SetupSuite() { db := testioc.InitDB() s.db = db err := dao.InitTables(db) require.NoError(s.T(), err) - s.logDao = dao.NewGORMGPTLogDAO(db) + s.logDao = dao.NewGORMLLMLogDAO(db) // 先插入 BizConfig now := time.Now().UnixMilli() @@ -59,30 +59,30 @@ func (s *GptSuite) SetupSuite() { assert.NoError(s.T(), err) } -func (s *GptSuite) TearDownSuite() { +func (s *LLMServiceSuite) TearDownSuite() { err := s.db.Exec("TRUNCATE TABLE `ai_biz_configs`").Error require.NoError(s.T(), err) } -func (s *GptSuite) TearDownTest() { - err := s.db.Exec("TRUNCATE TABLE `gpt_records`").Error +func (s *LLMServiceSuite) TearDownTest() { + err := s.db.Exec("TRUNCATE TABLE `llm_records`").Error require.NoError(s.T(), err) - err = s.db.Exec("TRUNCATE TABLE `gpt_credits`").Error + err = s.db.Exec("TRUNCATE TABLE `llm_credits`").Error require.NoError(s.T(), err) } -func (s *GptSuite) TestService() { +func (s *LLMServiceSuite) TestService() { t := s.T() testCases := []struct { name string - req domain.GPTRequest - before func(t *testing.T, ctrl *gomock.Controller) (gptHandler.Handler, credit.Service) + req domain.LLMRequest + before func(t *testing.T, ctrl *gomock.Controller) (llmHandler.Handler, credit.Service) assertFunc assert.ErrorAssertionFunc - after func(t *testing.T, resp domain.GPTResponse) + after func(t *testing.T, resp domain.LLMResponse) }{ { name: "八股文测试-成功", - req: domain.GPTRequest{ + req: domain.LLMRequest{ Biz: domain.BizQuestionExamine, Uid: 123, Tid: "11", @@ -93,10 +93,10 @@ func (s *GptSuite) TestService() { }, assertFunc: assert.NoError, before: func(t *testing.T, - ctrl *gomock.Controller) (gptHandler.Handler, credit.Service) { - gptHdl := hdlmocks.NewMockHandler(ctrl) - gptHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). - Return(domain.GPTResponse{ + ctrl *gomock.Controller) (llmHandler.Handler, credit.Service) { + llmHdl := hdlmocks.NewMockHandler(ctrl) + llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). + Return(domain.LLMResponse{ Tokens: 100, Amount: 100, Answer: "aians", @@ -105,22 +105,23 @@ func (s *GptSuite) TestService() { creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{ TotalAmount: 1000, }, nil) - creditSvc.EXPECT().AddCredits(gomock.Any(), gomock.Any()).Return(nil) - return gptHdl, creditSvc + creditSvc.EXPECT().TryDeductCredits(gomock.Any(), gomock.Any()).Return(11, nil) + creditSvc.EXPECT().ConfirmDeductCredits(gomock.Any(), int64(123), int64(11)).Return(nil) + return llmHdl, creditSvc }, - after: func(t *testing.T, resp domain.GPTResponse) { + after: func(t *testing.T, resp domain.LLMResponse) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() // 校验response写入的内容是否正确 - assert.Equal(t, domain.GPTResponse{ + assert.Equal(t, domain.LLMResponse{ Tokens: 100, Amount: 100, Answer: "aians", }, resp) - var logModel dao.GPTRecord + var logModel dao.LLMRecord err := s.db.WithContext(ctx).Where("id = ?", 1).First(&logModel).Error require.NoError(t, err) - s.assertLog(dao.GPTRecord{ + s.assertLog(dao.LLMRecord{ Id: 1, Tid: "11", Uid: 123, @@ -140,10 +141,10 @@ func (s *GptSuite) TestService() { Answer: sqlx.NewNullString("aians"), }, logModel) // 校验credit写入的内容是否正确 - var creditLogModel dao.GPTCredit + var creditLogModel dao.LLMCredit err = s.db.WithContext(ctx).Where("id = ?", 1).First(&creditLogModel).Error require.NoError(t, err) - s.assertCreditLog(dao.GPTCredit{ + s.assertCreditLog(dao.LLMCredit{ Id: 1, Tid: "11", Uid: 123, @@ -155,7 +156,7 @@ func (s *GptSuite) TestService() { }, { name: "积分不足", - req: domain.GPTRequest{ + req: domain.LLMRequest{ Biz: domain.BizQuestionExamine, Uid: 124, Tid: "11", @@ -164,21 +165,21 @@ func (s *GptSuite) TestService() { }, }, before: func(t *testing.T, - ctrl *gomock.Controller) (gptHandler.Handler, credit.Service) { - gptHdl := hdlmocks.NewMockHandler(ctrl) + ctrl *gomock.Controller) (llmHandler.Handler, credit.Service) { + llmHdl := hdlmocks.NewMockHandler(ctrl) creditSvc := creditmocks.NewMockService(ctrl) creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{ TotalAmount: 0, }, nil) - return gptHdl, creditSvc + return llmHdl, creditSvc }, - after: func(t *testing.T, resp domain.GPTResponse) { + after: func(t *testing.T, resp domain.LLMResponse) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() - var logModel dao.GPTRecord + var logModel dao.LLMRecord err := s.db.WithContext(ctx).Where("uid = ?", 124).First(&logModel).Error require.NoError(t, err) - s.assertLog(dao.GPTRecord{ + s.assertLog(dao.LLMRecord{ Id: 1, Tid: "11", Uid: 124, @@ -198,8 +199,8 @@ func (s *GptSuite) TestService() { assertFunc: assert.Error, }, { - name: "GPT调用失败", - req: domain.GPTRequest{ + name: "llm 调用失败", + req: domain.LLMRequest{ Biz: domain.BizQuestionExamine, Uid: 125, Tid: "11", @@ -209,23 +210,23 @@ func (s *GptSuite) TestService() { }, }, before: func(t *testing.T, - ctrl *gomock.Controller) (gptHandler.Handler, credit.Service) { - gptHdl := hdlmocks.NewMockHandler(ctrl) - gptHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). - Return(domain.GPTResponse{}, errors.New("调用失败")) + ctrl *gomock.Controller) (llmHandler.Handler, credit.Service) { + llmHdl := hdlmocks.NewMockHandler(ctrl) + llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). + Return(domain.LLMResponse{}, errors.New("调用失败")) creditSvc := creditmocks.NewMockService(ctrl) creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{ TotalAmount: 1000, }, nil) - return gptHdl, creditSvc + return llmHdl, creditSvc }, - after: func(t *testing.T, resp domain.GPTResponse) { + after: func(t *testing.T, resp domain.LLMResponse) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() - var logModel dao.GPTRecord + var logModel dao.LLMRecord err := s.db.WithContext(ctx).Where("uid = ?", 125).First(&logModel).Error require.NoError(t, err) - s.assertLog(dao.GPTRecord{ + s.assertLog(dao.LLMRecord{ Id: 1, Tid: "11", Uid: 125, @@ -245,10 +246,10 @@ func (s *GptSuite) TestService() { Answer: sqlx.NewNullString("aians"), }, logModel) // 校验credit写入的内容是否正确 - var creditLogModel dao.GPTCredit + var creditLogModel dao.LLMCredit err = s.db.WithContext(ctx).Where("id = ?", 1).First(&creditLogModel).Error require.NoError(t, err) - s.assertCreditLog(dao.GPTCredit{ + s.assertCreditLog(dao.LLMCredit{ Id: 1, Tid: "11", Uid: 125, @@ -261,7 +262,7 @@ func (s *GptSuite) TestService() { }, { name: "积分足够,扣款失败", - req: domain.GPTRequest{ + req: domain.LLMRequest{ Biz: domain.BizQuestionExamine, Uid: 126, Tid: "11", @@ -272,10 +273,10 @@ func (s *GptSuite) TestService() { }, assertFunc: assert.Error, before: func(t *testing.T, - ctrl *gomock.Controller) (gptHandler.Handler, credit.Service) { - gptHdl := hdlmocks.NewMockHandler(ctrl) - gptHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). - Return(domain.GPTResponse{ + ctrl *gomock.Controller) (llmHandler.Handler, credit.Service) { + llmHdl := hdlmocks.NewMockHandler(ctrl) + llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). + Return(domain.LLMResponse{ Tokens: 100, Amount: 100, Answer: "aians", @@ -284,22 +285,37 @@ func (s *GptSuite) TestService() { creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{ TotalAmount: 1000, }, nil) - creditSvc.EXPECT().AddCredits(gomock.Any(), gomock.Any()).Return(errors.New("mock db error")) - return gptHdl, creditSvc + creditSvc.EXPECT().TryDeductCredits(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, c credit.Credit) (int64, error) { + c.Logs[0].Key = "" + c.Logs[0].BizId = 0 + assert.Equal(t, credit.Credit{ + Uid: 126, + Logs: []credit.CreditLog{ + { + ChangeAmount: 100, + Uid: 126, + Biz: "ai-llm", + Desc: "ai-llm服务", + }, + }, + }, c) + return 0, errors.New("mock db error") + }) + return llmHdl, creditSvc }, - after: func(t *testing.T, resp domain.GPTResponse) { + after: func(t *testing.T, resp domain.LLMResponse) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() // 校验response写入的内容是否正确 - assert.Equal(t, domain.GPTResponse{ + assert.Equal(t, domain.LLMResponse{ Tokens: 100, Amount: 100, Answer: "aians", }, resp) - var logModel dao.GPTRecord + var logModel dao.LLMRecord err := s.db.WithContext(ctx).Where("uid = ?", 126).First(&logModel).Error require.NoError(t, err) - s.assertLog(dao.GPTRecord{ + s.assertLog(dao.LLMRecord{ Id: 1, Tid: "11", Uid: 126, @@ -319,10 +335,10 @@ func (s *GptSuite) TestService() { Answer: sqlx.NewNullString("aians"), }, logModel) // 校验credit写入的内容是否正确 - var creditLogModel dao.GPTCredit + var creditLogModel dao.LLMCredit err = s.db.WithContext(ctx).Where("id = ?", 1).First(&creditLogModel).Error require.NoError(t, err) - s.assertCreditLog(dao.GPTCredit{ + s.assertCreditLog(dao.LLMCredit{ Id: 1, Tid: "11", Uid: 126, @@ -352,7 +368,7 @@ func (s *GptSuite) TestService() { } } -func (s *GptSuite) assertLog(wantLog dao.GPTRecord, actual dao.GPTRecord) { +func (s *LLMServiceSuite) assertLog(wantLog dao.LLMRecord, actual dao.LLMRecord) { require.True(s.T(), actual.Ctime != 0) require.True(s.T(), actual.Utime != 0) actual.Ctime = 0 @@ -360,7 +376,7 @@ func (s *GptSuite) assertLog(wantLog dao.GPTRecord, actual dao.GPTRecord) { assert.Equal(s.T(), wantLog, actual) } -func (s *GptSuite) assertCreditLog(wantLog dao.GPTCredit, actual dao.GPTCredit) { +func (s *LLMServiceSuite) assertCreditLog(wantLog dao.LLMCredit, actual dao.LLMCredit) { require.True(s.T(), actual.Ctime != 0) require.True(s.T(), actual.Utime != 0) actual.Ctime = 0 diff --git a/internal/ai/internal/integration/startup/wire.go b/internal/ai/internal/integration/startup/wire.go index 9061d236..2a1b69e8 100644 --- a/internal/ai/internal/integration/startup/wire.go +++ b/internal/ai/internal/integration/startup/wire.go @@ -6,13 +6,13 @@ import ( "sync" "github.com/ecodeclub/webook/internal/ai" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/biz" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/config" - aicredit "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/credit" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/log" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/record" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/biz" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/config" + aicredit "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/log" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/record" "github.com/ecodeclub/webook/internal/ai/internal/repository" "github.com/ecodeclub/webook/internal/ai/internal/repository/dao" @@ -26,13 +26,13 @@ func InitModule(db *egorm.Component, hdl handler.Handler, creditSvc *credit.Module) (*ai.Module, error) { wire.Build( - gpt.NewGPTService, - repository.NewGPTLogRepo, - repository.NewGPTCreditLogRepo, + llm.NewLLMService, + repository.NewLLMLogRepo, + repository.NewLLMCreditLogRepo, repository.NewCachedConfigRepository, - InitGPTCreditLogDAO, - dao.NewGORMGPTLogDAO, + InitLLMCreditLogDAO, + dao.NewGORMLLMLogDAO, dao.NewGORMConfigDAO, config.NewBuilder, @@ -49,8 +49,8 @@ func InitModule(db *egorm.Component, return new(ai.Module), nil } -func InitHandlerFacade(common []handler.Builder, gpt handler.Handler) *biz.FacadeHandler { - que := ai.InitQuestionExamineHandler(common, gpt) +func InitHandlerFacade(common []handler.Builder, llm handler.Handler) *biz.FacadeHandler { + que := ai.InitQuestionExamineHandler(common, llm) return biz.NewHandler(map[string]handler.Handler{ que.Biz(): que, }) @@ -67,7 +67,7 @@ func InitTableOnce(db *gorm.DB) { }) } -func InitGPTCreditLogDAO(db *egorm.Component) dao.GPTCreditDAO { +func InitLLMCreditLogDAO(db *egorm.Component) dao.LLMCreditDAO { InitTableOnce(db) - return dao.NewGPTCreditLogDAO(db) + return dao.NewLLMCreditLogDAO(db) } diff --git a/internal/ai/internal/integration/startup/wire_gen.go b/internal/ai/internal/integration/startup/wire_gen.go index 061fcf13..51ed9436 100644 --- a/internal/ai/internal/integration/startup/wire_gen.go +++ b/internal/ai/internal/integration/startup/wire_gen.go @@ -12,13 +12,13 @@ import ( "github.com/ecodeclub/webook/internal/ai" "github.com/ecodeclub/webook/internal/ai/internal/repository" "github.com/ecodeclub/webook/internal/ai/internal/repository/dao" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/biz" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/config" - credit2 "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/credit" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/log" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/record" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/biz" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/config" + credit2 "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/log" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/record" "github.com/ecodeclub/webook/internal/credit" "github.com/ego-component/egorm" "gorm.io/gorm" @@ -32,25 +32,25 @@ func InitModule(db *gorm.DB, hdl handler.Handler, creditSvc *credit.Module) (*ai configRepository := repository.NewCachedConfigRepository(configDAO) configHandlerBuilder := config.NewBuilder(configRepository) service := creditSvc.Svc - gptCreditLogDAO := InitGPTCreditLogDAO(db) - gptCreditLogRepo := repository.NewGPTCreditLogRepo(gptCreditLogDAO) - creditHandlerBuilder := credit2.NewHandlerBuilder(service, gptCreditLogRepo) - gptLogDAO := dao.NewGORMGPTLogDAO(db) - gptLogRepo := repository.NewGPTLogRepo(gptLogDAO) - recordHandlerBuilder := record.NewHandler(gptLogRepo) + llmCreditDAO := InitLLMCreditLogDAO(db) + llmCreditLogRepo := repository.NewLLMCreditLogRepo(llmCreditDAO) + creditHandlerBuilder := credit2.NewHandlerBuilder(service, llmCreditLogRepo) + llmRecordDAO := dao.NewGORMLLMLogDAO(db) + llmLogRepo := repository.NewLLMLogRepo(llmRecordDAO) + recordHandlerBuilder := record.NewHandler(llmLogRepo) v := ai.InitCommonHandlers(handlerBuilder, configHandlerBuilder, creditHandlerBuilder, recordHandlerBuilder) facadeHandler := InitHandlerFacade(v, hdl) - gptService := gpt.NewGPTService(facadeHandler) + llmService := llm.NewLLMService(facadeHandler) module := &ai.Module{ - Svc: gptService, + Svc: llmService, } return module, nil } // wire.go: -func InitHandlerFacade(common []handler.Builder, gpt2 handler.Handler) *biz.FacadeHandler { - que := ai.InitQuestionExamineHandler(common, gpt2) +func InitHandlerFacade(common []handler.Builder, llm2 handler.Handler) *biz.FacadeHandler { + que := ai.InitQuestionExamineHandler(common, llm2) return biz.NewHandler(map[string]handler.Handler{ que.Biz(): que, }) @@ -67,7 +67,7 @@ func InitTableOnce(db *gorm.DB) { }) } -func InitGPTCreditLogDAO(db *egorm.Component) dao.GPTCreditDAO { +func InitLLMCreditLogDAO(db *egorm.Component) dao.LLMCreditDAO { InitTableOnce(db) - return dao.NewGPTCreditLogDAO(db) + return dao.NewLLMCreditLogDAO(db) } diff --git a/internal/ai/internal/repository/credit.go b/internal/ai/internal/repository/credit.go index b0944708..824a388f 100644 --- a/internal/ai/internal/repository/credit.go +++ b/internal/ai/internal/repository/credit.go @@ -7,32 +7,32 @@ import ( "github.com/ecodeclub/webook/internal/ai/internal/repository/dao" ) -type GPTCreditLogRepo interface { - SaveCredit(ctx context.Context, GPTDeductLog domain.GPTCredit) (int64, error) +type LLMCreditLogRepo interface { + SaveCredit(ctx context.Context, l domain.LLMCredit) (int64, error) } -type gptCreditLogRepo struct { - logDao dao.GPTCreditDAO +type llmCreditLogRepo struct { + logDao dao.LLMCreditDAO } -func NewGPTCreditLogRepo(logDao dao.GPTCreditDAO) GPTCreditLogRepo { - return &gptCreditLogRepo{ +func NewLLMCreditLogRepo(logDao dao.LLMCreditDAO) LLMCreditLogRepo { + return &llmCreditLogRepo{ logDao: logDao, } } -func (g *gptCreditLogRepo) creditLogToEntity(gptLog domain.GPTCredit) dao.GPTCredit { - return dao.GPTCredit{ - Id: gptLog.Id, - Tid: gptLog.Tid, - Uid: gptLog.Uid, - Biz: gptLog.Biz, - Amount: gptLog.Amount, - Status: gptLog.Status.ToUint8(), +func (g *llmCreditLogRepo) creditLogToEntity(l domain.LLMCredit) dao.LLMCredit { + return dao.LLMCredit{ + Id: l.Id, + Tid: l.Tid, + Uid: l.Uid, + Biz: l.Biz, + Amount: l.Amount, + Status: l.Status.ToUint8(), } } -func (g *gptCreditLogRepo) SaveCredit(ctx context.Context, gptDeductLog domain.GPTCredit) (int64, error) { - logEntity := g.creditLogToEntity(gptDeductLog) +func (g *llmCreditLogRepo) SaveCredit(ctx context.Context, l domain.LLMCredit) (int64, error) { + logEntity := g.creditLogToEntity(l) return g.logDao.SaveCredit(ctx, logEntity) } diff --git a/internal/ai/internal/repository/dao/credit.go b/internal/ai/internal/repository/dao/credit.go index 1168e691..e74ea413 100644 --- a/internal/ai/internal/repository/dao/credit.go +++ b/internal/ai/internal/repository/dao/credit.go @@ -8,8 +8,8 @@ import ( "gorm.io/gorm/clause" ) -// GPTCredit gpt扣分调用记录表 -type GPTCredit struct { +// LLMCredit llm 扣分调用记录表 +type LLMCredit struct { Id int64 `gorm:"primaryKey;autoIncrement;comment:积分流水表自增ID"` Tid string `gorm:"type:varchar(256);not null;comment:一次请求的Tid,可能有多次"` Uid int64 `gorm:"not null;index:idx_user_id;comment:用户ID"` @@ -20,32 +20,32 @@ type GPTCredit struct { Utime int64 } -func (l GPTCredit) TableName() string { - return "gpt_credits" +func (l LLMCredit) TableName() string { + return "llm_credits" } -type GPTCreditDAO interface { - SaveCredit(ctx context.Context, GPTDeductLog GPTCredit) (int64, error) +type LLMCreditDAO interface { + SaveCredit(ctx context.Context, l LLMCredit) (int64, error) } -type GORMGPTCreditDAO struct { +type GORMLLMCreditDAO struct { db *egorm.Component } -func NewGPTCreditLogDAO(db *egorm.Component) GPTCreditDAO { - return &GORMGPTCreditDAO{ +func NewLLMCreditLogDAO(db *egorm.Component) LLMCreditDAO { + return &GORMLLMCreditDAO{ db: db, } } -func (g *GORMGPTCreditDAO) SaveCredit(ctx context.Context, gptLog GPTCredit) (int64, error) { +func (g *GORMLLMCreditDAO) SaveCredit(ctx context.Context, l LLMCredit) (int64, error) { now := time.Now().UnixMilli() - gptLog.Ctime = now - gptLog.Utime = now - err := g.db.WithContext(ctx).Model(&GPTCredit{}). + l.Ctime = now + l.Utime = now + err := g.db.WithContext(ctx).Model(&LLMCredit{}). Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"status", "utime"}), - }).Create(&gptLog).Error - return gptLog.Id, err + }).Create(&l).Error + return l.Id, err } diff --git a/internal/ai/internal/repository/dao/init.go b/internal/ai/internal/repository/dao/init.go index f33c1526..3e0080b9 100644 --- a/internal/ai/internal/repository/dao/init.go +++ b/internal/ai/internal/repository/dao/init.go @@ -4,8 +4,8 @@ import "github.com/ego-component/egorm" func InitTables(db *egorm.Component) error { return db.AutoMigrate( - &GPTCredit{}, - &GPTRecord{}, + &LLMCredit{}, + &LLMRecord{}, &BizConfig{}, ) } diff --git a/internal/ai/internal/repository/dao/record.go b/internal/ai/internal/repository/dao/record.go index 6781e614..fb2e90f7 100644 --- a/internal/ai/internal/repository/dao/record.go +++ b/internal/ai/internal/repository/dao/record.go @@ -24,23 +24,24 @@ import ( "gorm.io/gorm/clause" ) -type GPTRecordDAO interface { - Save(ctx context.Context, r GPTRecord) (int64, error) +type LLMRecordDAO interface { + Save(ctx context.Context, r LLMRecord) (int64, error) } -type GORMGPTLogDAO struct { +// GORMLLMLogDAO => GORM LLM LogDAO +type GORMLLMLogDAO struct { db *egorm.Component } -func NewGORMGPTLogDAO(db *egorm.Component) GPTRecordDAO { - return &GORMGPTLogDAO{db: db} +func NewGORMLLMLogDAO(db *egorm.Component) LLMRecordDAO { + return &GORMLLMLogDAO{db: db} } -func (g *GORMGPTLogDAO) Save(ctx context.Context, record GPTRecord) (int64, error) { +func (g *GORMLLMLogDAO) Save(ctx context.Context, record LLMRecord) (int64, error) { now := time.Now().UnixMilli() record.Ctime = now record.Utime = now - err := g.db.WithContext(ctx).Model(&GPTRecord{}). + err := g.db.WithContext(ctx).Model(&LLMRecord{}). Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{"status", "utime"}), @@ -48,13 +49,13 @@ func (g *GORMGPTLogDAO) Save(ctx context.Context, record GPTRecord) (int64, erro return record.Id, err } -func (g *GORMGPTLogDAO) FirstLog(ctx context.Context, id int64) (*GPTRecord, error) { - logModel := &GPTRecord{} - err := g.db.WithContext(ctx).Model(&GPTRecord{}).Where("id = ?", id).First(logModel).Error +func (g *GORMLLMLogDAO) FirstLog(ctx context.Context, id int64) (*LLMRecord, error) { + logModel := &LLMRecord{} + err := g.db.WithContext(ctx).Model(&LLMRecord{}).Where("id = ?", id).First(logModel).Error return logModel, err } -type GPTRecord struct { +type LLMRecord struct { Id int64 `gorm:"primaryKey;autoIncrement;comment:积分流水表自增ID"` Tid string `gorm:"type:varchar(256);not null;uniqueIndex:unq_tid;comment:一次请求的Tid只能有一次"` Uid int64 `gorm:"not null;index:idx_user_id;comment:用户ID"` @@ -65,11 +66,11 @@ type GPTRecord struct { Input sqlx.JsonColumn[[]string] `gorm:"type:text;comment:调用请求的参数"` KnowledgeId string `gorm:"type:varchar(256);not null;comment:使用的知识库 ID"` PromptTemplate sql.NullString `gorm:"type:text;comment:PromptTemplate 模板,加上请求参数构成一个完整的 prompt"` - Answer sql.NullString `gorm:"type:text;comment:gpt的回答"` + Answer sql.NullString `gorm:"type:text;comment:llm的回答"` Ctime int64 Utime int64 } -func (l GPTRecord) TableName() string { - return "gpt_records" +func (l LLMRecord) TableName() string { + return "llm_records" } diff --git a/internal/ai/internal/repository/log.go b/internal/ai/internal/repository/log.go index 3da05eec..bc35fda1 100644 --- a/internal/ai/internal/repository/log.go +++ b/internal/ai/internal/repository/log.go @@ -22,28 +22,28 @@ import ( "github.com/ecodeclub/webook/internal/ai/internal/repository/dao" ) -type GPTLogRepo interface { - SaveLog(ctx context.Context, gptLog domain.GPTRecord) (int64, error) +type LLMLogRepo interface { + SaveLog(ctx context.Context, l domain.LLMRecord) (int64, error) } // 调用日志 -type gptLogDAO struct { - logDao dao.GPTRecordDAO +type llmLogDAO struct { + logDao dao.LLMRecordDAO } -func NewGPTLogRepo(logDao dao.GPTRecordDAO) GPTLogRepo { - return &gptLogDAO{ +func NewLLMLogRepo(logDao dao.LLMRecordDAO) LLMLogRepo { + return &llmLogDAO{ logDao: logDao, } } -func (g *gptLogDAO) SaveLog(ctx context.Context, gptLog domain.GPTRecord) (int64, error) { - logEntity := g.toEntity(gptLog) +func (g *llmLogDAO) SaveLog(ctx context.Context, l domain.LLMRecord) (int64, error) { + logEntity := g.toEntity(l) return g.logDao.Save(ctx, logEntity) } -func (g *gptLogDAO) toEntity(r domain.GPTRecord) dao.GPTRecord { - return dao.GPTRecord{ +func (g *llmLogDAO) toEntity(r domain.LLMRecord) dao.LLMRecord { + return dao.LLMRecord{ Id: r.Id, Tid: r.Tid, Uid: r.Uid, diff --git a/internal/ai/internal/service/gpt/gpt.go b/internal/ai/internal/service/gpt/gpt.go deleted file mode 100644 index 73930378..00000000 --- a/internal/ai/internal/service/gpt/gpt.go +++ /dev/null @@ -1,28 +0,0 @@ -package gpt - -import ( - "context" - - "github.com/ecodeclub/webook/internal/ai/internal/domain" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/biz" -) - -//go:generate mockgen -source=./gpt.go -destination=../../../mocks/gpt.mock.go -package=aimocks -typed=true Service -type Service interface { - Invoke(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) -} - -type gptService struct { - // 这边显示依赖 FacadeHandler - handler *biz.FacadeHandler -} - -func NewGPTService(facade *biz.FacadeHandler) Service { - return &gptService{ - handler: facade, - } -} - -func (g *gptService) Invoke(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { - return g.handler.Handle(ctx, req) -} diff --git a/internal/ai/internal/service/gpt/handler/biz/composition_biz.go b/internal/ai/internal/service/llm/handler/biz/composition_biz.go similarity index 89% rename from internal/ai/internal/service/gpt/handler/biz/composition_biz.go rename to internal/ai/internal/service/llm/handler/biz/composition_biz.go index 8537e63f..c24daca9 100644 --- a/internal/ai/internal/service/gpt/handler/biz/composition_biz.go +++ b/internal/ai/internal/service/llm/handler/biz/composition_biz.go @@ -18,7 +18,7 @@ import ( "context" "github.com/ecodeclub/webook/internal/ai/internal/domain" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" ) // CompositionHandler 通过组合 Handler 来完成某个业务 @@ -28,7 +28,7 @@ type CompositionHandler struct { name string } -func (c *CompositionHandler) Handle(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { +func (c *CompositionHandler) Handle(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { return c.root.Handle(ctx, req) } @@ -42,8 +42,8 @@ func (c *CompositionHandler) Biz() string { func NewCombinedBizHandler(name string, common []handler.Builder, - gpt handler.Handler) *CompositionHandler { - root := gpt + l handler.Handler) *CompositionHandler { + root := l for i := len(common) - 1; i >= 0; i-- { current := common[i] root = current.Next(root) diff --git a/internal/ai/internal/service/gpt/handler/biz/facade.go b/internal/ai/internal/service/llm/handler/biz/facade.go similarity index 75% rename from internal/ai/internal/service/gpt/handler/biz/facade.go rename to internal/ai/internal/service/llm/handler/biz/facade.go index 247dc78b..f1577ae9 100644 --- a/internal/ai/internal/service/gpt/handler/biz/facade.go +++ b/internal/ai/internal/service/llm/handler/biz/facade.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/ecodeclub/webook/internal/ai/internal/domain" - handler2 "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" + handler2 "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" ) var ErrUnknownBiz = errors.New("未知的业务") @@ -16,10 +16,10 @@ type FacadeHandler struct { bizMap map[string]handler2.Handler } -func (f *FacadeHandler) Handle(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { +func (f *FacadeHandler) Handle(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { h, ok := f.bizMap[req.Biz] if !ok { - return domain.GPTResponse{}, fmt.Errorf("%w biz: %s", ErrUnknownBiz, req.Biz) + return domain.LLMResponse{}, fmt.Errorf("%w biz: %s", ErrUnknownBiz, req.Biz) } return h.Handle(ctx, req) } diff --git a/internal/ai/internal/service/gpt/handler/biz/question_examine.go b/internal/ai/internal/service/llm/handler/biz/question_examine.go similarity index 70% rename from internal/ai/internal/service/gpt/handler/biz/question_examine.go rename to internal/ai/internal/service/llm/handler/biz/question_examine.go index 8844a451..b998f600 100644 --- a/internal/ai/internal/service/gpt/handler/biz/question_examine.go +++ b/internal/ai/internal/service/llm/handler/biz/question_examine.go @@ -17,9 +17,10 @@ package biz import ( "context" "fmt" + "unicode/utf8" "github.com/ecodeclub/webook/internal/ai/internal/domain" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" ) type QuestionExamineBizHandlerBuilder struct { @@ -30,9 +31,16 @@ func NewQuestionExamineBizHandlerBuilder() *QuestionExamineBizHandlerBuilder { } func (h *QuestionExamineBizHandlerBuilder) Next(next handler.Handler) handler.Handler { - return handler.HandleFunc(func(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { + return handler.HandleFunc(func(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { + title := req.Input[0] + userInput := req.Input[1] + userInputLen := utf8.RuneCount([]byte(userInput)) + + if userInputLen > req.Config.MaxInput { + return domain.LLMResponse{}, fmt.Errorf("输入太长,最常不超过 %d,现有长度 %d", req.Config.MaxInput, userInputLen) + } // 把 input 和 prompt 结合起来 - prompt := fmt.Sprintf(req.Config.PromptTemplate, req.Input[0], req.Input[1]) + prompt := fmt.Sprintf(req.Config.PromptTemplate, title, userInput) req.Prompt = prompt return next.Handle(ctx, req) }) diff --git a/internal/ai/internal/service/gpt/handler/config/builder.go b/internal/ai/internal/service/llm/handler/config/builder.go similarity index 90% rename from internal/ai/internal/service/gpt/handler/config/builder.go rename to internal/ai/internal/service/llm/handler/config/builder.go index eaf2d0ee..5b23cb66 100644 --- a/internal/ai/internal/service/gpt/handler/config/builder.go +++ b/internal/ai/internal/service/llm/handler/config/builder.go @@ -19,7 +19,7 @@ import ( "github.com/ecodeclub/webook/internal/ai/internal/domain" "github.com/ecodeclub/webook/internal/ai/internal/repository" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" ) // HandlerBuilder 改为从数据库中读取 @@ -34,11 +34,11 @@ func NewBuilder(repo repository.ConfigRepository) *HandlerBuilder { } func (b *HandlerBuilder) Next(next handler.Handler) handler.Handler { - return handler.HandleFunc(func(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { + return handler.HandleFunc(func(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { // 读取配置 cfg, err := b.repo.GetConfig(ctx, req.Biz) if err != nil { - return domain.GPTResponse{}, err + return domain.LLMResponse{}, err } req.Config = cfg return next.Handle(ctx, req) diff --git a/internal/ai/internal/service/gpt/handler/credit/builder.go b/internal/ai/internal/service/llm/handler/credit/builder.go similarity index 51% rename from internal/ai/internal/service/gpt/handler/credit/builder.go rename to internal/ai/internal/service/llm/handler/credit/builder.go index f45f1d14..aa03fee2 100644 --- a/internal/ai/internal/service/gpt/handler/credit/builder.go +++ b/internal/ai/internal/service/llm/handler/credit/builder.go @@ -5,16 +5,19 @@ import ( "errors" "fmt" + "github.com/gotomicro/ego/core/elog" + "github.com/ecodeclub/webook/internal/ai/internal/domain" "github.com/ecodeclub/webook/internal/ai/internal/repository" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" "github.com/ecodeclub/webook/internal/credit" uuid "github.com/lithammer/shortuuid/v4" ) type HandlerBuilder struct { creditSvc credit.Service - logRepo repository.GPTCreditLogRepo + logRepo repository.LLMCreditLogRepo + logger *elog.Component } func (h *HandlerBuilder) Name() string { @@ -25,23 +28,24 @@ var ( ErrInsufficientCredit = errors.New("积分不足") ) -func NewHandlerBuilder(creSvc credit.Service, repo repository.GPTCreditLogRepo) *HandlerBuilder { +func NewHandlerBuilder(creSvc credit.Service, repo repository.LLMCreditLogRepo) *HandlerBuilder { return &HandlerBuilder{ creditSvc: creSvc, logRepo: repo, + logger: elog.DefaultLogger, } } func (h *HandlerBuilder) Next(next handler.Handler) handler.Handler { - return handler.HandleFunc(func(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { + return handler.HandleFunc(func(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { cre, err := h.creditSvc.GetCreditsByUID(ctx, req.Uid) if err != nil { - return domain.GPTResponse{}, err + return domain.LLMResponse{}, err } // 如果剩余的积分不足就返回积分不足 ok := h.checkCredit(cre) if !ok { - return domain.GPTResponse{}, fmt.Errorf("%w, 余额非正数,无法继续调用,用户 %d", + return domain.LLMResponse{}, fmt.Errorf("%w, 余额非正数,无法继续调用,用户 %d", ErrInsufficientCredit, req.Uid) } @@ -54,38 +58,57 @@ func (h *HandlerBuilder) Next(next handler.Handler) handler.Handler { // 扣款 id, err := h.logRepo.SaveCredit(ctx, h.newLog(req, resp)) if err != nil { - return domain.GPTResponse{}, err + return domain.LLMResponse{}, err } - err = h.creditSvc.AddCredits(context.Background(), credit.Credit{ + err = h.deductCredit(ctx, credit.Credit{ Uid: req.Uid, Logs: []credit.CreditLog{ { - Key: uuid.New(), - Uid: req.Uid, - Biz: "ai-gpt", - BizId: id, - Desc: "ai-gpt服务", + Key: uuid.New(), + ChangeAmount: resp.Amount, + Uid: req.Uid, + Biz: "ai-llm", + BizId: id, + Desc: "ai-llm服务", }, }, }) if err != nil { - _, _ = h.logRepo.SaveCredit(ctx, domain.GPTCredit{ + _, _ = h.logRepo.SaveCredit(ctx, domain.LLMCredit{ Id: id, Status: domain.CreditStatusFailed, }) - return domain.GPTResponse{}, err - } else { - _, err = h.logRepo.SaveCredit(ctx, domain.GPTCredit{ - Id: id, - Status: domain.CreditStatusSuccess, - }) + return domain.LLMResponse{}, err } + + _, err = h.logRepo.SaveCredit(ctx, domain.LLMCredit{ + Id: id, + Status: domain.CreditStatusSuccess, + }) + return resp, err }) } -func (h *HandlerBuilder) newLog(req domain.GPTRequest, resp domain.GPTResponse) domain.GPTCredit { - return domain.GPTCredit{ +// TODO deductCredit 后面要求 credit 那边提供一个一次性接口,绕开 try-confirm 流程 +func (h *HandlerBuilder) deductCredit(ctx context.Context, c credit.Credit) error { + id, err := h.creditSvc.TryDeductCredits(ctx, c) + if err != nil { + return err + } + err = h.creditSvc.ConfirmDeductCredits(ctx, c.Uid, id) + if err != nil { + err1 := h.creditSvc.CancelDeductCredits(ctx, c.Uid, id) + if err1 != nil { + h.logger.Error("确认扣减积分失败之后试图回滚,也失败了", elog.FieldErr(err1)) + } + err = fmt.Errorf("确认扣减积分失败 %w", err) + } + return err +} + +func (h *HandlerBuilder) newLog(req domain.LLMRequest, resp domain.LLMResponse) domain.LLMCredit { + return domain.LLMCredit{ Tid: req.Tid, Uid: req.Uid, Biz: req.Biz, diff --git a/internal/ai/internal/service/gpt/handler/log/builder.go b/internal/ai/internal/service/llm/handler/log/builder.go similarity index 74% rename from internal/ai/internal/service/gpt/handler/log/builder.go rename to internal/ai/internal/service/llm/handler/log/builder.go index 86fa609b..c87629b7 100644 --- a/internal/ai/internal/service/gpt/handler/log/builder.go +++ b/internal/ai/internal/service/llm/handler/log/builder.go @@ -3,7 +3,7 @@ package log import ( "context" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" "github.com/ecodeclub/webook/internal/ai/internal/domain" "github.com/gotomicro/ego/core/elog" @@ -26,20 +26,20 @@ func (h *HandlerBuilder) Name() string { } func (h *HandlerBuilder) Next(next handler.Handler) handler.Handler { - return handler.HandleFunc(func(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { + return handler.HandleFunc(func(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { logger := h.logger.With(elog.String("tid", req.Tid), elog.Int64("uid", req.Uid), elog.String("biz", req.Biz)) // 记录请求 - logger.Info("请求 GPT") + logger.Info("请求 LLM") resp, err := next.Handle(ctx, req) if err != nil { // 记录错误 - logger.Error("请求gpt服务失败", elog.FieldErr(err)) + logger.Error("请求 LLM 服务失败", elog.FieldErr(err)) return resp, err } // 记录响应 - logger.Info("请求gpt服务响应成功", elog.Int64("tokens", resp.Tokens)) + logger.Info("请求 LLM 服务响应成功", elog.Int64("tokens", resp.Tokens)) return resp, err }) } diff --git a/internal/ai/internal/service/gpt/handler/mocks/handler.mock.go b/internal/ai/internal/service/llm/handler/mocks/handler.mock.go similarity index 90% rename from internal/ai/internal/service/gpt/handler/mocks/handler.mock.go rename to internal/ai/internal/service/llm/handler/mocks/handler.mock.go index 82a85913..5e7648b6 100644 --- a/internal/ai/internal/service/gpt/handler/mocks/handler.mock.go +++ b/internal/ai/internal/service/llm/handler/mocks/handler.mock.go @@ -13,7 +13,7 @@ import ( reflect "reflect" domain "github.com/ecodeclub/webook/internal/ai/internal/domain" - handler "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" + handler "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" gomock "go.uber.org/mock/gomock" ) @@ -41,10 +41,10 @@ func (m *MockHandler) EXPECT() *MockHandlerMockRecorder { } // Handle mocks base method. -func (m *MockHandler) Handle(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { +func (m *MockHandler) Handle(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Handle", ctx, req) - ret0, _ := ret[0].(domain.GPTResponse) + ret0, _ := ret[0].(domain.LLMResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -62,19 +62,19 @@ type HandlerHandleCall struct { } // Return rewrite *gomock.Call.Return -func (c *HandlerHandleCall) Return(arg0 domain.GPTResponse, arg1 error) *HandlerHandleCall { +func (c *HandlerHandleCall) Return(arg0 domain.LLMResponse, arg1 error) *HandlerHandleCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *HandlerHandleCall) Do(f func(context.Context, domain.GPTRequest) (domain.GPTResponse, error)) *HandlerHandleCall { +func (c *HandlerHandleCall) Do(f func(context.Context, domain.LLMRequest) (domain.LLMResponse, error)) *HandlerHandleCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *HandlerHandleCall) DoAndReturn(f func(context.Context, domain.GPTRequest) (domain.GPTResponse, error)) *HandlerHandleCall { +func (c *HandlerHandleCall) DoAndReturn(f func(context.Context, domain.LLMRequest) (domain.LLMResponse, error)) *HandlerHandleCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/ai/internal/service/gpt/handler/gpt/zhipu/handler.go b/internal/ai/internal/service/llm/handler/platform/zhipu/handler.go similarity index 70% rename from internal/ai/internal/service/gpt/handler/gpt/zhipu/handler.go rename to internal/ai/internal/service/llm/handler/platform/zhipu/handler.go index 1fc0f885..00c452ce 100644 --- a/internal/ai/internal/service/gpt/handler/gpt/zhipu/handler.go +++ b/internal/ai/internal/service/llm/handler/platform/zhipu/handler.go @@ -22,7 +22,7 @@ func NewHandler(apikey string, if err != nil { return nil, err } - const model = "glm-4" + const model = "glm-4-0520" svc := client.ChatCompletion(model) return &Handler{ client: client, @@ -33,25 +33,26 @@ func NewHandler(apikey string, } func (h *Handler) Name() string { - return "gpt" + return "zhipu" } -func (h *Handler) Handle(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { +func (h *Handler) Handle(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { // 这边它不会调用 next,因为它是最终的出口 - msg := h.newParams(req.Input) completion, err := h.svc.AddTool(zhipu.ChatCompletionToolRetrieval{ - KnowledgeID: req.Config.KnowledgeId, - PromptTemplate: req.Config.PromptTemplate, - }).AddMessage(msg).Do(ctx) + KnowledgeID: req.Config.KnowledgeId, + }).AddMessage(zhipu.ChatCompletionMessage{ + Role: "user", + Content: req.Prompt, + }).Do(ctx) if err != nil { - return domain.GPTResponse{}, err + return domain.LLMResponse{}, err } tokens := completion.Usage.TotalTokens // 现在的报价都是 N/1k token // 而后向上取整 amt := math.Ceil(float64(tokens) * h.price / 1000) // 金额只有具体的模型才知道怎么算 - resp := domain.GPTResponse{ + resp := domain.LLMResponse{ Tokens: tokens, Amount: int64(amt), } @@ -61,11 +62,3 @@ func (h *Handler) Handle(ctx context.Context, req domain.GPTRequest) (domain.GPT } return resp, nil } - -func (h *Handler) newParams(inputs []string) zhipu.ChatCompletionMessage { - msg := inputs[0] - return zhipu.ChatCompletionMessage{ - Role: "user", - Content: msg, - } -} diff --git a/internal/ai/internal/service/gpt/handler/record/builder.go b/internal/ai/internal/service/llm/handler/record/builder.go similarity index 77% rename from internal/ai/internal/service/gpt/handler/record/builder.go rename to internal/ai/internal/service/llm/handler/record/builder.go index 31950b20..018eca28 100644 --- a/internal/ai/internal/service/gpt/handler/record/builder.go +++ b/internal/ai/internal/service/llm/handler/record/builder.go @@ -3,7 +3,7 @@ package record import ( "context" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" "github.com/gotomicro/ego/core/elog" "github.com/ecodeclub/webook/internal/ai/internal/domain" @@ -11,11 +11,11 @@ import ( ) type HandlerBuilder struct { - repo repository.GPTLogRepo + repo repository.LLMLogRepo logger *elog.Component } -func NewHandler(repo repository.GPTLogRepo) *HandlerBuilder { +func NewHandler(repo repository.LLMLogRepo) *HandlerBuilder { return &HandlerBuilder{ repo: repo, logger: elog.DefaultLogger, @@ -26,8 +26,8 @@ func (h *HandlerBuilder) Name() string { } func (h *HandlerBuilder) Next(next handler.Handler) handler.Handler { - return handler.HandleFunc(func(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { - log := domain.GPTRecord{ + return handler.HandleFunc(func(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { + log := domain.LLMRecord{ Tid: req.Tid, Biz: req.Biz, Uid: req.Uid, @@ -38,13 +38,13 @@ func (h *HandlerBuilder) Next(next handler.Handler) handler.Handler { defer func() { _, err1 := h.repo.SaveLog(ctx, log) if err1 != nil { - h.logger.Error("保存 GPT 访问记录失败", elog.FieldErr(err1)) + h.logger.Error("保存 LLM 访问记录失败", elog.FieldErr(err1)) } }() resp, err := next.Handle(ctx, req) if err != nil { log.Status = domain.RecordStatusFailed - return domain.GPTResponse{}, err + return domain.LLMResponse{}, err } log.Tokens = resp.Tokens log.Amount = resp.Amount diff --git a/internal/ai/internal/service/gpt/handler/type.go b/internal/ai/internal/service/llm/handler/type.go similarity index 56% rename from internal/ai/internal/service/gpt/handler/type.go rename to internal/ai/internal/service/llm/handler/type.go index b2419861..d74e827e 100644 --- a/internal/ai/internal/service/gpt/handler/type.go +++ b/internal/ai/internal/service/llm/handler/type.go @@ -6,15 +6,15 @@ import ( "github.com/ecodeclub/webook/internal/ai/internal/domain" ) -type HandleFunc func(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) +type HandleFunc func(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) -func (f HandleFunc) Handle(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { +func (f HandleFunc) Handle(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { return f(ctx, req) } //go:generate mockgen -source=./type.go -destination=./mocks/handler.mock.go -package=hdlmocks -typed=true Handler type Handler interface { - Handle(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) + Handle(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) } type Builder interface { diff --git a/internal/ai/internal/service/llm/llm.go b/internal/ai/internal/service/llm/llm.go new file mode 100644 index 00000000..e7cb006f --- /dev/null +++ b/internal/ai/internal/service/llm/llm.go @@ -0,0 +1,28 @@ +package llm + +import ( + "context" + + "github.com/ecodeclub/webook/internal/ai/internal/domain" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/biz" +) + +//go:generate mockgen -source=./llm.go -destination=../../../mocks/llm.mock.go -package=aimocks -typed=true Service +type Service interface { + Invoke(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) +} + +type llmService struct { + // 这边显示依赖 FacadeHandler + handler *biz.FacadeHandler +} + +func NewLLMService(facade *biz.FacadeHandler) Service { + return &llmService{ + handler: facade, + } +} + +func (g *llmService) Invoke(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { + return g.handler.Handle(ctx, req) +} diff --git a/internal/ai/mocks/gpt.mock.go b/internal/ai/mocks/llm.mock.go similarity index 79% rename from internal/ai/mocks/gpt.mock.go rename to internal/ai/mocks/llm.mock.go index 83d12882..abd70745 100644 --- a/internal/ai/mocks/gpt.mock.go +++ b/internal/ai/mocks/llm.mock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: ./gpt.go +// Source: ./llm.go // // Generated by this command: // -// mockgen -source=./gpt.go -destination=../../../mocks/gpt.mock.go -package=aimocks -typed=true Service +// mockgen -source=./llm.go -destination=../../../mocks/llm.mock.go -package=aimocks -typed=true Service // // Package aimocks is a generated GoMock package. package aimocks @@ -40,10 +40,10 @@ func (m *MockService) EXPECT() *MockServiceMockRecorder { } // Invoke mocks base method. -func (m *MockService) Invoke(ctx context.Context, req domain.GPTRequest) (domain.GPTResponse, error) { +func (m *MockService) Invoke(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Invoke", ctx, req) - ret0, _ := ret[0].(domain.GPTResponse) + ret0, _ := ret[0].(domain.LLMResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -61,19 +61,19 @@ type ServiceInvokeCall struct { } // Return rewrite *gomock.Call.Return -func (c *ServiceInvokeCall) Return(arg0 domain.GPTResponse, arg1 error) *ServiceInvokeCall { +func (c *ServiceInvokeCall) Return(arg0 domain.LLMResponse, arg1 error) *ServiceInvokeCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *ServiceInvokeCall) Do(f func(context.Context, domain.GPTRequest) (domain.GPTResponse, error)) *ServiceInvokeCall { +func (c *ServiceInvokeCall) Do(f func(context.Context, domain.LLMRequest) (domain.LLMResponse, error)) *ServiceInvokeCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *ServiceInvokeCall) DoAndReturn(f func(context.Context, domain.GPTRequest) (domain.GPTResponse, error)) *ServiceInvokeCall { +func (c *ServiceInvokeCall) DoAndReturn(f func(context.Context, domain.LLMRequest) (domain.LLMResponse, error)) *ServiceInvokeCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/ai/module.go b/internal/ai/module.go index b5d2d8e9..895889f9 100644 --- a/internal/ai/module.go +++ b/internal/ai/module.go @@ -1,5 +1,5 @@ package ai type Module struct { - Svc GPTService + Svc LLMService } diff --git a/internal/ai/type.go b/internal/ai/type.go index 077b0243..6ef8be1c 100644 --- a/internal/ai/type.go +++ b/internal/ai/type.go @@ -2,9 +2,9 @@ package ai import ( "github.com/ecodeclub/webook/internal/ai/internal/domain" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm" ) -type GPTRequest = domain.GPTRequest -type GPTResponse = domain.GPTResponse -type GPTService = gpt.Service +type LLMRequest = domain.LLMRequest +type LLMResponse = domain.LLMResponse +type LLMService = llm.Service diff --git a/internal/ai/wire.go b/internal/ai/wire.go index 1d8a18c4..8d40b96d 100644 --- a/internal/ai/wire.go +++ b/internal/ai/wire.go @@ -5,11 +5,11 @@ package ai import ( "sync" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/config" - aicredit "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/credit" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/log" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/record" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/config" + aicredit "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/log" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/record" "github.com/ecodeclub/webook/internal/ai/internal/repository" "github.com/ecodeclub/webook/internal/ai/internal/repository/dao" @@ -21,13 +21,13 @@ import ( func InitModule(db *egorm.Component, creditSvc *credit.Module) (*Module, error) { wire.Build( - gpt.NewGPTService, - repository.NewGPTLogRepo, - repository.NewGPTCreditLogRepo, + llm.NewLLMService, + repository.NewLLMLogRepo, + repository.NewLLMCreditLogRepo, repository.NewCachedConfigRepository, - InitGPTCreditLogDAO, - dao.NewGORMGPTLogDAO, + InitLLMCreditLogDAO, + dao.NewGORMLLMLogDAO, dao.NewGORMConfigDAO, config.NewBuilder, @@ -56,7 +56,7 @@ func InitTableOnce(db *gorm.DB) { }) } -func InitGPTCreditLogDAO(db *egorm.Component) dao.GPTCreditDAO { +func InitLLMCreditLogDAO(db *egorm.Component) dao.LLMCreditDAO { InitTableOnce(db) - return dao.NewGPTCreditLogDAO(db) + return dao.NewLLMCreditLogDAO(db) } diff --git a/internal/ai/wire_gen.go b/internal/ai/wire_gen.go index c11b861c..8691fc9f 100644 --- a/internal/ai/wire_gen.go +++ b/internal/ai/wire_gen.go @@ -11,11 +11,11 @@ import ( "github.com/ecodeclub/webook/internal/ai/internal/repository" "github.com/ecodeclub/webook/internal/ai/internal/repository/dao" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/config" - credit2 "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/credit" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/log" - "github.com/ecodeclub/webook/internal/ai/internal/service/gpt/handler/record" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/config" + credit2 "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/log" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/record" "github.com/ecodeclub/webook/internal/credit" "github.com/ego-component/egorm" "gorm.io/gorm" @@ -29,18 +29,18 @@ func InitModule(db *gorm.DB, creditSvc *credit.Module) (*Module, error) { configRepository := repository.NewCachedConfigRepository(configDAO) configHandlerBuilder := config.NewBuilder(configRepository) service := creditSvc.Svc - gptCreditDAO := InitGPTCreditLogDAO(db) - gptCreditLogRepo := repository.NewGPTCreditLogRepo(gptCreditDAO) - creditHandlerBuilder := credit2.NewHandlerBuilder(service, gptCreditLogRepo) - gptRecordDAO := dao.NewGORMGPTLogDAO(db) - gptLogRepo := repository.NewGPTLogRepo(gptRecordDAO) - recordHandlerBuilder := record.NewHandler(gptLogRepo) + llmCreditDAO := InitLLMCreditLogDAO(db) + llmCreditLogRepo := repository.NewLLMCreditLogRepo(llmCreditDAO) + creditHandlerBuilder := credit2.NewHandlerBuilder(service, llmCreditLogRepo) + llmRecordDAO := dao.NewGORMLLMLogDAO(db) + llmLogRepo := repository.NewLLMLogRepo(llmRecordDAO) + recordHandlerBuilder := record.NewHandler(llmLogRepo) v := InitCommonHandlers(handlerBuilder, configHandlerBuilder, creditHandlerBuilder, recordHandlerBuilder) handler := InitZhipu() facadeHandler := InitHandlerFacade(v, handler) - gptService := gpt.NewGPTService(facadeHandler) + llmService := llm.NewLLMService(facadeHandler) module := &Module{ - Svc: gptService, + Svc: llmService, } return module, nil } @@ -58,7 +58,7 @@ func InitTableOnce(db *gorm.DB) { }) } -func InitGPTCreditLogDAO(db *egorm.Component) dao.GPTCreditDAO { +func InitLLMCreditLogDAO(db *egorm.Component) dao.LLMCreditDAO { InitTableOnce(db) - return dao.NewGPTCreditLogDAO(db) + return dao.NewLLMCreditLogDAO(db) } diff --git a/internal/marketing/internal/event/consumer/order_event_consumer.go b/internal/marketing/internal/event/consumer/order_event_consumer.go index da824b7c..e8788a05 100644 --- a/internal/marketing/internal/event/consumer/order_event_consumer.go +++ b/internal/marketing/internal/event/consumer/order_event_consumer.go @@ -69,8 +69,14 @@ func (c *OrderEventConsumer) Consume(ctx context.Context) error { return err } + // TODO 删除这个校验。 + // 如果后续找不到对应的 Handler,就直接返回,否则每次都要在这里增加代码 for _, spu := range evt.SPUs { - if !spu.IsMemberProduct() && !spu.IsProjectProduct() && !spu.IsServiceProduct() && !spu.IsCodeCategory() { + if !spu.IsMemberProduct() && + !spu.IsProjectProduct() && + !spu.IsServiceProduct() && + !spu.IsCreditProduct() && + !spu.IsCodeCategory() { return nil } } diff --git a/internal/marketing/internal/event/event.go b/internal/marketing/internal/event/event.go index 1343c933..a02fca51 100644 --- a/internal/marketing/internal/event/event.go +++ b/internal/marketing/internal/event/event.go @@ -55,6 +55,9 @@ func (s SPU) IsMemberProduct() bool { return s.IsProductCategory() && s.Category1 == "member" } +func (s SPU) IsCreditProduct() bool { + return s.IsProductCategory() && s.Category1 == "credit" +} func (s SPU) IsProjectProduct() bool { return s.IsProductCategory() && s.Category1 == "project" } diff --git a/internal/marketing/internal/integration/module_test.go b/internal/marketing/internal/integration/module_test.go index 0c443228..2368a592 100644 --- a/internal/marketing/internal/integration/module_test.go +++ b/internal/marketing/internal/integration/module_test.go @@ -906,6 +906,85 @@ func (s *ModuleTestSuite) TestConsumer_ConsumeOrderEvent() { after: func(t *testing.T, evt event.OrderEvent) {}, }, + // 购买积分 + { + name: "订单成功-增加积分", + newMQFunc: func(t *testing.T, ctrl *gomock.Controller, evt event.OrderEvent) mq.MQ { + t.Helper() + + mockMQ := mocks.NewMockMQ(ctrl) + mockConsumer := mocks.NewMockConsumer(ctrl) + mockConsumer.EXPECT().Consume(gomock.Any()).Return(s.newOrderEventMessage(t, evt), nil).Times(2) + + mockMQ.EXPECT().Consumer(gomock.Any(), gomock.Any()).Return(mockConsumer, nil) + return mockMQ + }, + newSvcFunc: func(t *testing.T, ctrl *gomock.Controller, evt event.OrderEvent, q mq.MQ) service.Service { + t.Helper() + + mockOrderSvc := ordermocks.NewMockService(ctrl) + + const orderId = int64(201) + mockOrderSvc.EXPECT(). + FindUserVisibleOrderByUIDAndSN(gomock.Any(), evt.BuyerID, evt.OrderSN). + Return(order.Order{ + ID: orderId, + SN: evt.OrderSN, + BuyerID: evt.BuyerID, + OriginalTotalAmt: 50000, + RealTotalAmt: 50000, + Status: order.StatusSuccess, + Items: []order.Item{ + { + SPU: order.SPU{ + ID: 5, + Category0: "product", + Category1: "credit", + }, + SKU: order.SKU{ + ID: 16, + SN: "sku-sn-service-product-1", + Attrs: `{"credit": 5000}`, + OriginalPrice: 500 * 100, + RealPrice: 500 * 100, + Quantity: 1, + }, + }, + }, + }, nil).Times(2) + + mockMQ := mocks.NewMockMQ(ctrl) + mockProducer := mocks.NewMockProducer(ctrl) + creditEvent := s.newCreditEventMessage(t, event.CreditIncreaseEvent{ + Key: evt.OrderSN + "_incr", + Uid: evt.BuyerID, + // 在 attrs 里面放着 + Amount: 5000, + Biz: "order", + BizId: orderId, + Action: "购买积分", + }) + mockProducer.EXPECT().Produce(gomock.Any(), creditEvent).Return(&mq.ProducerResult{}, nil).Times(2) + mockMQ.EXPECT().Producer(event.CreditEventName).Return(mockProducer, nil) + creditProducer, err := producer.NewCreditEventProducer(mockMQ) + assert.NoError(t, err) + return service.NewService(nil, mockOrderSvc, nil, nil, nil, nil, creditProducer, nil, nil) + }, + evt: event.OrderEvent{ + OrderSN: "OrderSN-marketing-service-102", + BuyerID: 887655432, + SPUs: []event.SPU{ + { + ID: 5, + Category0: "product", + Category1: "credit", + }, + }, + }, + errRequireFunc: require.NoError, + after: func(t *testing.T, evt event.OrderEvent) {}, + }, + { name: "消费完成订单消息成功_忽略不关心的完成订单消息", newMQFunc: func(t *testing.T, ctrl *gomock.Controller, evt event.OrderEvent) mq.MQ { diff --git a/internal/marketing/internal/service/activity/order/executor.go b/internal/marketing/internal/service/activity/order/executor.go index b824a32a..f6dbb4bb 100644 --- a/internal/marketing/internal/service/activity/order/executor.go +++ b/internal/marketing/internal/service/activity/order/executor.go @@ -44,6 +44,7 @@ func NewOrderActivityExecutor( registry.RegisterOrderHandler("product", "member", handler.NewProductMemberHandler(memberEventProducer, creditEventProducer)) registry.RegisterOrderHandler("product", "project", handler.NewProductProjectHandler(permissionEventProducer, creditEventProducer)) registry.RegisterOrderHandler("product", "service", handler.NewProductServiceHandler(qywechatEventProducer)) + registry.RegisterOrderHandler("product", "credit", handler.NewProductCreditHandler(creditEventProducer)) codeMemberHandler := handler.NewCodeMemberHandler(repo, memberEventProducer, creditEventProducer, redemptionCodeGenerator) registry.RegisterOrderHandler("code", "member", codeMemberHandler) diff --git a/internal/marketing/internal/service/activity/order/handler/product_credit.go b/internal/marketing/internal/service/activity/order/handler/product_credit.go new file mode 100644 index 00000000..568b5c06 --- /dev/null +++ b/internal/marketing/internal/service/activity/order/handler/product_credit.go @@ -0,0 +1,53 @@ +// Copyright 2023 ecodeclub +// +// Licensed 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 handler + +import ( + "context" + + "github.com/ecodeclub/webook/internal/marketing/internal/event" + "github.com/ecodeclub/webook/internal/marketing/internal/event/producer" +) + +var _ OrderHandler = (*ProductCreditHandler)(nil) + +type ProductCreditHandler struct { + producer producer.CreditEventProducer +} + +func NewProductCreditHandler(producer producer.CreditEventProducer) *ProductCreditHandler { + return &ProductCreditHandler{producer: producer} +} + +func (p *ProductCreditHandler) Handle(ctx context.Context, info OrderInfo) error { + type Attrs struct { + Credit uint64 `json:"credit"` + } + var attr Attrs + err := info.Items[0].SKU.UnmarshalAttrs(&attr) + if err != nil { + return err + } + return p.producer.Produce(ctx, event.CreditIncreaseEvent{ + // TODO 当下,我们购买积分的时候也允许用积分,这会导致这个 key 冲突 + // 即购买的时候下单用的也是 SN 作为 key + Key: info.Order.SN + "_incr", + Uid: info.Order.BuyerID, + Amount: attr.Credit, + Biz: Biz, + BizId: info.Order.ID, + Action: "购买积分", + }) +} diff --git a/internal/question/internal/errs/code.go b/internal/question/internal/errs/code.go index dc41f44c..3de10d80 100644 --- a/internal/question/internal/errs/code.go +++ b/internal/question/internal/errs/code.go @@ -2,6 +2,8 @@ package errs var ( SystemError = ErrorCode{Code: 502001, Msg: "系统错误"} + // InsufficientCredit 这个不管说是客户端错误还是服务端错误,都有点勉强,所以随便用一个 5 + InsufficientCredit = ErrorCode{Code: 502002, Msg: "积分不足"} ) type ErrorCode struct { diff --git a/internal/question/internal/integration/examine_handler_test.go b/internal/question/internal/integration/examine_handler_test.go index cd6241fd..0f6e0ae5 100644 --- a/internal/question/internal/integration/examine_handler_test.go +++ b/internal/question/internal/integration/examine_handler_test.go @@ -56,8 +56,8 @@ type ExamineHandlerTest struct { func (s *ExamineHandlerTest) SetupSuite() { ctrl := gomock.NewController(s.T()) aiSvc := aimocks.NewMockService(ctrl) - aiSvc.EXPECT().Invoke(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req ai.GPTRequest) (ai.GPTResponse, error) { - return ai.GPTResponse{ + aiSvc.EXPECT().Invoke(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req ai.LLMRequest) (ai.LLMResponse, error) { + return ai.LLMResponse{ Tokens: req.Uid, Amount: req.Uid, Answer: "评分:15K", @@ -169,7 +169,6 @@ func (s *ExamineHandlerTest) TestExamine() { Data: web.ExamineResult{ Result: domain.ResultBasic.ToUint8(), RawResult: "评分:15K", - Tokens: uid, Amount: uid, }, }, @@ -238,7 +237,6 @@ func (s *ExamineHandlerTest) TestExamine() { Data: web.ExamineResult{ Result: domain.ResultBasic.ToUint8(), RawResult: "评分:15K", - Tokens: uid, Amount: uid, }, }, diff --git a/internal/question/internal/integration/startup/wire_gen.go b/internal/question/internal/integration/startup/wire_gen.go index f56b0f18..3dee45ea 100644 --- a/internal/question/internal/integration/startup/wire_gen.go +++ b/internal/question/internal/integration/startup/wire_gen.go @@ -47,7 +47,7 @@ func InitModule(p event.SyncDataToSearchEventProducer, intrModule *interactive.M examineDAO := dao.NewGORMExamineDAO(db) examineRepository := repository.NewCachedExamineRepository(examineDAO) gptService := aiModule.Svc - examineService := service.NewGPTExamineService(repositoryRepository, examineRepository, gptService) + examineService := service.NewLLMExamineService(repositoryRepository, examineRepository, gptService) service3 := permModule.Svc handler := web.NewHandler(service2, examineService, service3, serviceService) questionSetHandler := web.NewQuestionSetHandler(questionSetService, examineService, service2) diff --git a/internal/question/internal/service/examine.go b/internal/question/internal/service/examine.go index a0b0b46c..2d9bad80 100644 --- a/internal/question/internal/service/examine.go +++ b/internal/question/internal/service/examine.go @@ -25,6 +25,8 @@ import ( "github.com/lithammer/shortuuid/v4" ) +var ErrInsufficientCredit = ai.ErrInsufficientCredit + // ExamineService 测试服务 type ExamineService interface { // Examine 测试服务 @@ -34,43 +36,45 @@ type ExamineService interface { GetResults(ctx context.Context, uid int64, ids []int64) (map[int64]domain.ExamineResult, error) } -var _ ExamineService = &GPTExamineService{} +var _ ExamineService = &LLMExamineService{} -// GPTExamineService 使用 GPT 进行评价的测试服务 -type GPTExamineService struct { +// LLMExamineService 使用 LLM 进行评价的测试服务 +type LLMExamineService struct { queRepo repository.Repository repo repository.ExamineRepository - aiSvc ai.GPTService + aiSvc ai.LLMService } -func (svc *GPTExamineService) GetResults(ctx context.Context, uid int64, ids []int64) (map[int64]domain.ExamineResult, error) { +func (svc *LLMExamineService) GetResults(ctx context.Context, uid int64, ids []int64) (map[int64]domain.ExamineResult, error) { results, err := svc.repo.GetResultsByIds(ctx, uid, ids) return slice.ToMap[domain.ExamineResult, int64](results, func(ele domain.ExamineResult) int64 { return ele.Qid }), err } -func (svc *GPTExamineService) QuestionResult(ctx context.Context, uid, qid int64) (domain.Result, error) { +func (svc *LLMExamineService) QuestionResult(ctx context.Context, uid, qid int64) (domain.Result, error) { return svc.repo.GetResultByUidAndQid(ctx, uid, qid) } -func (svc *GPTExamineService) Examine(ctx context.Context, +func (svc *LLMExamineService) Examine(ctx context.Context, uid int64, qid int64, input string) (domain.ExamineResult, error) { + const biz = "question_examine" // 实际上我们只需要 title,但是懒得写一个新的接口了 que, err := svc.queRepo.GetPubByID(ctx, qid) if err != nil { return domain.ExamineResult{}, err } tid := shortuuid.New() - aiReq := ai.GPTRequest{ + aiReq := ai.LLMRequest{ Uid: uid, Tid: tid, + Biz: biz, Input: []string{que.Title, input}, } aiResp, err := svc.aiSvc.Invoke(ctx, aiReq) if err != nil { - return domain.ExamineResult{}, nil + return domain.ExamineResult{}, err } // 解析结果 parsedRes := svc.parseExamineResult(aiResp.Answer) @@ -86,7 +90,7 @@ func (svc *GPTExamineService) Examine(ctx context.Context, return result, err } -func (svc *GPTExamineService) parseExamineResult(answer string) domain.Result { +func (svc *LLMExamineService) parseExamineResult(answer string) domain.Result { answer = strings.TrimSpace(answer) // 获取第一行 segs := strings.SplitN(answer, "\n", 2) @@ -106,12 +110,12 @@ func (svc *GPTExamineService) parseExamineResult(answer string) domain.Result { } } -func NewGPTExamineService( +func NewLLMExamineService( queRepo repository.Repository, repo repository.ExamineRepository, - aiSvc ai.GPTService, + aiSvc ai.LLMService, ) ExamineService { - return &GPTExamineService{ + return &LLMExamineService{ queRepo: queRepo, repo: repo, aiSvc: aiSvc, diff --git a/internal/question/internal/web/examine_hander.go b/internal/question/internal/web/examine_hander.go index 227ecd19..772fc387 100644 --- a/internal/question/internal/web/examine_hander.go +++ b/internal/question/internal/web/examine_hander.go @@ -15,8 +15,11 @@ package web import ( + "errors" + "github.com/ecodeclub/ginx" "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/question/internal/errs" "github.com/ecodeclub/webook/internal/question/internal/service" "github.com/gin-gonic/gin" ) @@ -38,10 +41,18 @@ func (h *ExamineHandler) MemberRoutes(server *gin.Engine) { func (h *ExamineHandler) Examine(ctx *ginx.Context, req ExamineReq, sess session.Session) (ginx.Result, error) { res, err := h.svc.Examine(ctx, sess.Claims().Uid, req.Qid, req.Input) - if err != nil { + switch { + case errors.Is(err, service.ErrInsufficientCredit): + return ginx.Result{ + Code: errs.InsufficientCredit.Code, + Msg: errs.InsufficientCredit.Msg, + }, nil + + case err == nil: + return ginx.Result{ + Data: newExamineResult(res), + }, nil + default: return systemErrorResult, err } - return ginx.Result{ - Data: newExamineResult(res), - }, nil } diff --git a/internal/question/internal/web/examine_vo.go b/internal/question/internal/web/examine_vo.go index 0f5cff49..65938feb 100644 --- a/internal/question/internal/web/examine_vo.go +++ b/internal/question/internal/web/examine_vo.go @@ -38,7 +38,6 @@ func newExamineResult(r domain.ExamineResult) ExamineResult { Qid: r.Qid, Result: r.Result.ToUint8(), RawResult: r.RawResult, - Tokens: r.Tokens, Amount: r.Amount, } } diff --git a/internal/question/wire.go b/internal/question/wire.go index 70a62a6b..520cb814 100644 --- a/internal/question/wire.go +++ b/internal/question/wire.go @@ -46,7 +46,7 @@ import ( var ExamineHandlerSet = wire.NewSet( web.NewExamineHandler, - service.NewGPTExamineService, + service.NewLLMExamineService, repository.NewCachedExamineRepository, dao.NewGORMExamineDAO) diff --git a/internal/question/wire_gen.go b/internal/question/wire_gen.go index 9bffa55d..1e24d8ad 100644 --- a/internal/question/wire_gen.go +++ b/internal/question/wire_gen.go @@ -50,8 +50,8 @@ func InitModule(db *gorm.DB, intrModule *interactive.Module, ec ecache.Cache, pe service2 := intrModule.Svc examineDAO := dao.NewGORMExamineDAO(db) examineRepository := repository.NewCachedExamineRepository(examineDAO) - gptService := aiModule.Svc - examineService := service.NewGPTExamineService(repositoryRepository, examineRepository, gptService) + llmService := aiModule.Svc + examineService := service.NewLLMExamineService(repositoryRepository, examineRepository, llmService) service3 := perm.Svc handler := web.NewHandler(service2, examineService, service3, serviceService) questionSetHandler := web.NewQuestionSetHandler(questionSetService, examineService, service2) @@ -72,7 +72,7 @@ func InitModule(db *gorm.DB, intrModule *interactive.Module, ec ecache.Cache, pe // wire.go: -var ExamineHandlerSet = wire.NewSet(web.NewExamineHandler, service.NewGPTExamineService, repository.NewCachedExamineRepository, dao.NewGORMExamineDAO) +var ExamineHandlerSet = wire.NewSet(web.NewExamineHandler, service.NewLLMExamineService, repository.NewCachedExamineRepository, dao.NewGORMExamineDAO) var daoOnce = sync.Once{}