diff --git a/.gitignore b/.gitignore index eb5b1f3e..4fdd6a86 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,8 @@ cover.out /internal/cases/internal/integration/logs /internal/skill/internal/integration/logs /internal/feedback/internal/integration/logs +/internal/ai/internal/integration/logs +/internal/resume/internal/integration/logs /config/cert/ local_test.go diff --git a/.run/webook.run.xml b/.run/webook.run.xml index 62190a45..003a6c57 100644 --- a/.run/webook.run.xml +++ b/.run/webook.run.xml @@ -4,6 +4,9 @@ + + + diff --git a/.script/setup.sh b/.script/setup.sh index 75bc09c5..d8953228 100644 --- a/.script/setup.sh +++ b/.script/setup.sh @@ -28,7 +28,7 @@ test -x $TARGET_PUSH || chmod +x $TARGET_PUSH test -x $TARGET_COMMIT || chmod +x $TARGET_COMMIT echo "安装 golangci-lint..." -go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.52.2 +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 echo "安装 goimports..." go install golang.org/x/tools/cmd/goimports@latest \ No newline at end of file diff --git a/internal/ai/internal/domain/jd.go b/internal/ai/internal/domain/jd.go new file mode 100644 index 00000000..b82bcf73 --- /dev/null +++ b/internal/ai/internal/domain/jd.go @@ -0,0 +1,19 @@ +package domain + +const ( + AnalysisJDTech = "analysis_jd_tech" + AnalysisJDBiz = "analysis_jd_biz" + AnalysisJDPosition = "analysis_jd_position" +) + +type JDEvaluation struct { + Score float64 `json:"score"` + Analysis string `json:"analysis"` +} + +type JD struct { + Amount int64 + TechScore *JDEvaluation + BizScore *JDEvaluation + PosScore *JDEvaluation +} diff --git a/internal/ai/internal/domain/llm.go b/internal/ai/internal/domain/llm.go index 1f145ab6..ea55f011 100644 --- a/internal/ai/internal/domain/llm.go +++ b/internal/ai/internal/domain/llm.go @@ -43,6 +43,8 @@ type LLMResponse struct { } type BizConfig struct { + Id int64 + Biz string // 使用的模型 Model string // 多少分钱/1000 token diff --git a/internal/ai/internal/errs/code.go b/internal/ai/internal/errs/code.go new file mode 100644 index 00000000..bc763b59 --- /dev/null +++ b/internal/ai/internal/errs/code.go @@ -0,0 +1,11 @@ +package errs + +var ( + SystemError = ErrorCode{Code: 516001, Msg: "系统错误"} + InsufficientCredit = ErrorCode{Code: 516002, Msg: "积分不足"} +) + +type ErrorCode struct { + Code int + Msg string +} diff --git a/internal/ai/internal/integration/llm_config_test.go b/internal/ai/internal/integration/llm_config_test.go new file mode 100644 index 00000000..9039def2 --- /dev/null +++ b/internal/ai/internal/integration/llm_config_test.go @@ -0,0 +1,306 @@ +package integration + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/ai/internal/integration/startup" + "github.com/ecodeclub/webook/internal/ai/internal/repository/dao" + "github.com/ecodeclub/webook/internal/ai/internal/web" + "github.com/ecodeclub/webook/internal/credit" + "github.com/ecodeclub/webook/internal/test" + testioc "github.com/ecodeclub/webook/internal/test/ioc" + "github.com/gin-gonic/gin" + "github.com/gotomicro/ego/core/econf" + "github.com/gotomicro/ego/server/egin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "gorm.io/gorm" +) + +type ConfigSuite struct { + suite.Suite + db *gorm.DB + adminHandler *web.AdminHandler + server *egin.Component +} + +func (s *ConfigSuite) SetupSuite() { + db := testioc.InitDB() + s.db = db + err := dao.InitTables(db) + s.NoError(err) + // 先插入 BizConfig + mou, err := startup.InitModule(s.db, nil, &credit.Module{}) + require.NoError(s.T(), err) + s.adminHandler = mou.AdminHandler + econf.Set("server", map[string]any{"contextTimeout": "1s"}) + server := egin.Load("server").Build() + server.Use(func(ctx *gin.Context) { + ctx.Set("_session", session.NewMemorySession(session.Claims{ + Uid: 123, + })) + }) + s.adminHandler.RegisterRoutes(server.Engine) + s.server = server +} + +func (s *ConfigSuite) TestConfig_Save() { + testCases := []struct { + name string + config web.ConfigRequest + before func(t *testing.T) + after func(t *testing.T, id int64) + wantCode int + id int64 + }{ + { + name: "新增", + config: web.ConfigRequest{ + Config: web.Config{ + Biz: "test", + MaxInput: 10, + Model: "testModel", + Price: 100, + Temperature: 0.5, + TopP: 0.5, + SystemPrompt: "testPrompt", + PromptTemplate: "testTemplate", + KnowledgeId: "testKnowledgeId", + }, + }, + before: func(t *testing.T) { + + }, + wantCode: 200, + id: 1, + after: func(t *testing.T, id int64) { + var conf dao.BizConfig + err := s.db.WithContext(context.Background()). + Where("id = ?", id).First(&conf).Error + require.NoError(t, err) + s.assertBizConfig(dao.BizConfig{ + Id: 1, + Biz: "test", + MaxInput: 10, + Model: "testModel", + Price: 100, + Temperature: 0.5, + TopP: 0.5, + SystemPrompt: "testPrompt", + PromptTemplate: "testTemplate", + KnowledgeId: "testKnowledgeId", + }, conf) + }, + }, + { + name: "更新", + config: web.ConfigRequest{ + Config: web.Config{ + Id: 2, + Biz: "2_test", + MaxInput: 102, + Model: "2_testModel", + Price: 102, + Temperature: 2.5, + TopP: 2.5, + SystemPrompt: "testPrompt2", + PromptTemplate: "testTemplate2", + KnowledgeId: "testKnowledgeId2", + }, + }, + before: func(t *testing.T) { + err := s.db.WithContext(context.Background()). + Table("ai_biz_configs"). + Create(dao.BizConfig{ + Id: 2, + Biz: "test_2", + MaxInput: 100, + Model: "testModel", + Price: 100, + Temperature: 0.5, + TopP: 0.5, + SystemPrompt: "testPrompt", + PromptTemplate: "testTemplate", + KnowledgeId: "testKnowledgeId", + Ctime: 11, + Utime: 22, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T, id int64) { + var conf dao.BizConfig + err := s.db.WithContext(context.Background()). + Where("id = ?", id). + Model(&dao.BizConfig{}). + First(&conf).Error + require.NoError(t, err) + s.assertBizConfig(dao.BizConfig{ + Id: 2, + Biz: "2_test", + MaxInput: 102, + Model: "2_testModel", + Price: 102, + Temperature: 2.5, + TopP: 2.5, + SystemPrompt: "testPrompt2", + PromptTemplate: "testTemplate2", + KnowledgeId: "testKnowledgeId2", + }, conf) + }, + wantCode: 200, + id: 2, + }, + } + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/ai/config/save", iox.NewJSONReader(tc.config)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[int64]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + id := recorder.MustScan().Data + assert.Equal(t, tc.id, id) + tc.after(t, id) + err = s.db.Exec("TRUNCATE TABLE `ai_biz_configs`").Error + require.NoError(s.T(), err) + }) + } +} + +func (s *ConfigSuite) TestConfig_List() { + configs := make([]dao.BizConfig, 0, 32) + for i := 1; i < 10; i++ { + cfg := dao.BizConfig{ + Id: int64(i), + Biz: fmt.Sprintf("biz_%d", i), + MaxInput: 100, + Model: fmt.Sprintf("test_model_%d", i), + Price: 1000, + Temperature: 37.5, + TopP: 0.8, + SystemPrompt: "test_prompt", + PromptTemplate: "test_template", + KnowledgeId: "test_knowledge", + } + configs = append(configs, cfg) + } + err := s.db.WithContext(context.Background()).Create(&configs).Error + require.NoError(s.T(), err) + req, err := http.NewRequest(http.MethodGet, + "/ai/config/list", iox.NewJSONReader(nil)) + req.Header.Set("content-type", "application/json") + require.NoError(s.T(), err) + recorder := test.NewJSONResponseRecorder[[]web.Config]() + s.server.ServeHTTP(recorder, req) + require.Equal(s.T(), 200, recorder.Code) + confs := recorder.MustScan().Data + assert.Equal(s.T(), getWantConfigs(), confs) + err = s.db.Exec("TRUNCATE TABLE `ai_biz_configs`").Error + require.NoError(s.T(), err) +} + +func (s *ConfigSuite) Test_Detail() { + testcases := []struct { + name string + req web.ConfigInfoReq + before func(t *testing.T) + wantCode int + wantData web.Config + }{ + { + name: "获取配置", + wantCode: 200, + req: web.ConfigInfoReq{ + Id: 3, + }, + before: func(t *testing.T) { + err := s.db.WithContext(context.Background()). + Table("ai_biz_configs"). + Create(dao.BizConfig{ + Id: 3, + Biz: "test_3", + MaxInput: 100, + Model: "testModel", + Price: 100, + Temperature: 0.5, + TopP: 0.5, + SystemPrompt: "testPrompt", + PromptTemplate: "testTemplate", + KnowledgeId: "testKnowledgeId", + Ctime: 11, + Utime: 22, + }).Error + require.NoError(t, err) + }, + wantData: web.Config{ + Id: 3, + Biz: "test_3", + MaxInput: 100, + Model: "testModel", + Price: 100, + Temperature: 0.5, + TopP: 0.5, + SystemPrompt: "testPrompt", + PromptTemplate: "testTemplate", + KnowledgeId: "testKnowledgeId", + }, + }, + } + for _, tc := range testcases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/ai/config/detail", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.Config]() + s.server.ServeHTTP(recorder, req) + require.Equal(s.T(), 200, recorder.Code) + conf := recorder.MustScan().Data + assert.Equal(t, tc.wantData, conf) + err = s.db.Exec("TRUNCATE TABLE `ai_biz_configs`").Error + require.NoError(s.T(), err) + }) + } +} + +func getWantConfigs() []web.Config { + configs := make([]web.Config, 0, 32) + for i := 9; i >= 1; i-- { + cfg := web.Config{ + Id: int64(i), + Biz: fmt.Sprintf("biz_%d", i), + MaxInput: 100, + Model: fmt.Sprintf("test_model_%d", i), + Price: 1000, + Temperature: 37.5, + TopP: 0.8, + SystemPrompt: "test_prompt", + PromptTemplate: "test_template", + KnowledgeId: "test_knowledge", + } + configs = append(configs, cfg) + } + return configs +} + +func (s *ConfigSuite) assertBizConfig(wantConfig dao.BizConfig, actualConfig dao.BizConfig) { + assert.True(s.T(), actualConfig.Ctime > 0) + assert.True(s.T(), actualConfig.Utime > 0) + actualConfig.Ctime = 0 + actualConfig.Utime = 0 + assert.Equal(s.T(), wantConfig, actualConfig) +} + +func TestConfigSuite(t *testing.T) { + suite.Run(t, new(ConfigSuite)) +} diff --git a/internal/ai/internal/integration/llm_service_test.go b/internal/ai/internal/integration/llm_service_test.go index 74a4539c..d285f574 100644 --- a/internal/ai/internal/integration/llm_service_test.go +++ b/internal/ai/internal/integration/llm_service_test.go @@ -5,9 +5,18 @@ package integration import ( "context" "errors" + "net/http" "testing" "time" + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/ai/internal/web" + "github.com/ecodeclub/webook/internal/test" + "github.com/gin-gonic/gin" + "github.com/gotomicro/ego/core/econf" + "github.com/gotomicro/ego/server/egin" + "github.com/ecodeclub/ekit/sqlx" "github.com/ecodeclub/webook/internal/ai/internal/service/llm" hdlmocks "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/mocks" @@ -65,6 +74,37 @@ func (s *LLMServiceSuite) SetupSuite() { Utime: now, }).Error s.NoError(err) + + err = s.db.Create(&dao.BizConfig{ + Biz: domain.AnalysisJDBiz, + MaxInput: 100, + PromptTemplate: "这是岗位描述 %s", + KnowledgeId: knowledgeId, + Ctime: now, + Utime: now, + }).Error + s.NoError(err) + + err = s.db.Create(&dao.BizConfig{ + Biz: domain.AnalysisJDTech, + MaxInput: 100, + PromptTemplate: "这是岗位描述tech %s", + KnowledgeId: knowledgeId, + Ctime: now, + Utime: now, + }).Error + s.NoError(err) + + err = s.db.Create(&dao.BizConfig{ + Biz: domain.AnalysisJDPosition, + MaxInput: 100, + PromptTemplate: "这是岗位描述position %s", + KnowledgeId: knowledgeId, + Ctime: now, + Utime: now, + }).Error + s.NoError(err) + } func (s *LLMServiceSuite) TearDownSuite() { @@ -461,6 +501,233 @@ func (s *LLMServiceSuite) TestService() { } } +func (s *LLMServiceSuite) TestHandler_Ask() { + testCases := []struct { + name string + req web.LLMRequest + before func(t *testing.T, ctrl *gomock.Controller) (*hdlmocks.MockHandler, credit.Service) + assertFunc assert.ErrorAssertionFunc + after func(t *testing.T, resp web.LLMResponse) + wantCode int + }{ + { + name: "八股文web-成功", + req: web.LLMRequest{ + Biz: domain.BizQuestionExamine, + Input: []string{ + "问题1", + "问题1内容", + "用户输入1", + }, + }, + assertFunc: assert.NoError, + before: func(t *testing.T, + ctrl *gomock.Controller) (*hdlmocks.MockHandler, credit.Service) { + llmHdl := hdlmocks.NewMockHandler(ctrl) + llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). + Return(domain.LLMResponse{ + Tokens: 100, + Amount: 100, + Answer: "aians", + }, nil) + creditSvc := creditmocks.NewMockService(ctrl) + creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{ + TotalAmount: 1000, + }, nil) + 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 web.LLMResponse) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + // 校验response写入的内容是否正确 + assert.Equal(t, web.LLMResponse{ + Amount: 100, + RawResult: "aians", + }, resp) + + var logModel dao.LLMRecord + err := s.db.WithContext(ctx).Where("id = ?", 1).First(&logModel).Error + require.NoError(t, err) + assert.True(t, logModel.Tid != "") + s.assertLog(dao.LLMRecord{ + Id: 1, + Uid: 123, + Tid: logModel.Tid, + Biz: domain.BizQuestionExamine, + Tokens: 100, + Amount: 100, + KnowledgeId: knowledgeId, + Input: sqlx.JsonColumn[[]string]{ + Valid: true, + Val: []string{ + "问题1", + "问题1内容", + "用户输入1", + }, + }, + Status: 1, + PromptTemplate: sqlx.NewNullString("这是问题 %s,这是问题内容 %s,这是用户输入 %s"), + Answer: sqlx.NewNullString("aians"), + }, logModel) + // 校验credit写入的内容是否正确 + var creditLogModel dao.LLMCredit + err = s.db.WithContext(ctx).Where("id = ?", 1).First(&creditLogModel).Error + require.NoError(t, err) + assert.True(t, logModel.Tid != "") + + s.assertCreditLog(dao.LLMCredit{ + Id: 1, + Tid: logModel.Tid, + Uid: 123, + Biz: domain.BizQuestionExamine, + Amount: 100, + Status: 1, + }, creditLogModel) + }, + wantCode: 200, + }, + } + for _, tc := range testCases { + tc := tc + s.T().Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockHdl, mockCredit := tc.before(t, ctrl) + mou, err := startup.InitModule(s.db, mockHdl, &credit.Module{Svc: mockCredit}) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPost, + "/ai/ask", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + econf.Set("server", map[string]any{"contextTimeout": "1s"}) + + server := egin.Load("server").Build() + server.Use(func(ctx *gin.Context) { + ctx.Set(session.CtxSessionKey, + session.NewMemorySession(session.Claims{ + Uid: 123, + })) + }) + mou.Hdl.MemberRoutes(server.Engine) + recorder := test.NewJSONResponseRecorder[web.LLMResponse]() + server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + tc.after(t, recorder.MustScan().Data) + }) + } +} + +func (s *LLMServiceSuite) TestHandler_AnalysisJD() { + testCases := []struct { + name string + req web.JDRequest + before func(t *testing.T, ctrl *gomock.Controller) (*hdlmocks.MockHandler, credit.Service) + assertFunc assert.ErrorAssertionFunc + after func(t *testing.T, resp web.JDResponse) + wantCode int + }{ + { + name: "岗位测评", + req: web.JDRequest{ + JD: "我们招聘一个go工程师", + }, + assertFunc: assert.NoError, + before: func(t *testing.T, + ctrl *gomock.Controller) (*hdlmocks.MockHandler, credit.Service) { + llmHdl := hdlmocks.NewMockHandler(ctrl) + llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, request domain.LLMRequest) (domain.LLMResponse, error) { + if request.Biz == "analysis_jd_tech" { + return domain.LLMResponse{ + Tokens: 1000, + Amount: 100, + Answer: `score: 6 +这是技术前景`, + }, nil + } + if request.Biz == "analysis_jd_biz" { + return domain.LLMResponse{ + Tokens: 100, + Amount: 200, + Answer: `score: 7 +这是业务前景`, + }, nil + } + if request.Biz == "analysis_jd_position" { + return domain.LLMResponse{ + Tokens: 100, + Amount: 100, + Answer: `score: 8 +这是公司地位`, + }, nil + } + return domain.LLMResponse{}, errors.New("unknown biz") + }).AnyTimes() + creditSvc := creditmocks.NewMockService(ctrl) + creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{ + TotalAmount: 200000, + }, nil).AnyTimes() + creditSvc.EXPECT().TryDeductCredits(gomock.Any(), gomock.Any()).Return(11, nil).AnyTimes() + creditSvc.EXPECT().ConfirmDeductCredits(gomock.Any(), int64(123), int64(11)).Return(nil).AnyTimes() + return llmHdl, creditSvc + }, + after: func(t *testing.T, resp web.JDResponse) { + // 校验response写入的内容是否正确 + assert.Equal(t, web.JDResponse{ + Amount: 400, + TechScore: &web.JDEvaluation{ + Score: 6, + Analysis: "这是技术前景", + }, + BizScore: &web.JDEvaluation{ + Score: 7, + Analysis: "这是业务前景", + }, + PosScore: &web.JDEvaluation{ + Score: 8, + Analysis: "这是公司地位", + }, + }, resp) + + }, + wantCode: 200, + }, + } + for _, tc := range testCases { + tc := tc + s.T().Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockHdl, mockCredit := tc.before(t, ctrl) + mou, err := startup.InitModule(s.db, mockHdl, &credit.Module{Svc: mockCredit}) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPost, + "/ai/analysis_jd", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + econf.Set("server", map[string]any{"contextTimeout": "1s"}) + server := egin.Load("server").Build() + server.Use(func(ctx *gin.Context) { + ctx.Set(session.CtxSessionKey, + session.NewMemorySession(session.Claims{ + Uid: 123, + })) + }) + mou.Hdl.MemberRoutes(server.Engine) + recorder := test.NewJSONResponseRecorder[web.JDResponse]() + server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + tc.after(t, recorder.MustScan().Data) + err = s.db.Exec("TRUNCATE TABLE `llm_records`").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE `llm_credits`").Error + require.NoError(s.T(), err) + }) + } +} + func (s *LLMServiceSuite) assertLog(wantLog dao.LLMRecord, actual dao.LLMRecord) { require.True(s.T(), actual.Ctime != 0) require.True(s.T(), actual.Utime != 0) diff --git a/internal/ai/internal/integration/startup/wire.go b/internal/ai/internal/integration/startup/wire.go index 1146c0e3..49349210 100644 --- a/internal/ai/internal/integration/startup/wire.go +++ b/internal/ai/internal/integration/startup/wire.go @@ -5,6 +5,9 @@ package startup import ( "sync" + "github.com/ecodeclub/webook/internal/ai/internal/service" + "github.com/ecodeclub/webook/internal/ai/internal/web" + hdlmocks "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/mocks" "github.com/ecodeclub/webook/internal/ai" @@ -43,7 +46,11 @@ func InitModule(db *egorm.Component, ai.InitCommonHandlers, InitRootHandler, - + service.NewGeneralService, + service.NewJDService, + service.NewConfigService, + web.NewHandler, + web.NewAdminHandler, wire.Struct(new(ai.Module), "*"), wire.FieldsOf(new(*credit.Module), "Svc"), ) diff --git a/internal/ai/internal/integration/startup/wire_gen.go b/internal/ai/internal/integration/startup/wire_gen.go index d9129c10..8adcb6ec 100644 --- a/internal/ai/internal/integration/startup/wire_gen.go +++ b/internal/ai/internal/integration/startup/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -12,6 +12,7 @@ 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" "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/config" @@ -19,6 +20,7 @@ import ( "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/log" hdlmocks "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/mocks" "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/record" + "github.com/ecodeclub/webook/internal/ai/internal/web" "github.com/ecodeclub/webook/internal/credit" "github.com/ego-component/egorm" "gorm.io/gorm" @@ -31,18 +33,25 @@ func InitModule(db *gorm.DB, hdl *hdlmocks.MockHandler, creditSvc *credit.Module configDAO := dao.NewGORMConfigDAO(db) configRepository := repository.NewCachedConfigRepository(configDAO) configHandlerBuilder := config.NewBuilder(configRepository) - service := creditSvc.Svc + serviceService := creditSvc.Svc llmCreditDAO := InitLLMCreditLogDAO(db) llmCreditLogRepo := repository.NewLLMCreditLogRepo(llmCreditDAO) - creditHandlerBuilder := credit2.NewHandlerBuilder(service, llmCreditLogRepo) + creditHandlerBuilder := credit2.NewHandlerBuilder(serviceService, llmCreditLogRepo) llmRecordDAO := dao.NewGORMLLMLogDAO(db) llmLogRepo := repository.NewLLMLogRepo(llmRecordDAO) recordHandlerBuilder := record.NewHandler(llmLogRepo) v := ai.InitCommonHandlers(handlerBuilder, configHandlerBuilder, creditHandlerBuilder, recordHandlerBuilder) handler := InitRootHandler(v, hdl) llmService := llm.NewLLMService(handler) + generalService := service.NewGeneralService(llmService) + jdService := service.NewJDService(llmService) + webHandler := web.NewHandler(generalService, jdService) + configService := service.NewConfigService(configRepository) + adminHandler := web.NewAdminHandler(configService) module := &ai.Module{ - Svc: llmService, + Svc: llmService, + Hdl: webHandler, + AdminHandler: adminHandler, } return module, nil } diff --git a/internal/ai/internal/repository/config.go b/internal/ai/internal/repository/config.go index f8ddcd01..526a2f4d 100644 --- a/internal/ai/internal/repository/config.go +++ b/internal/ai/internal/repository/config.go @@ -17,12 +17,16 @@ package repository import ( "context" + "github.com/ecodeclub/ekit/slice" "github.com/ecodeclub/webook/internal/ai/internal/domain" "github.com/ecodeclub/webook/internal/ai/internal/repository/dao" ) type ConfigRepository interface { GetConfig(ctx context.Context, biz string) (domain.BizConfig, error) + Save(ctx context.Context, cfg domain.BizConfig) (int64, error) + List(ctx context.Context) ([]domain.BizConfig, error) + GetById(ctx context.Context, id int64) (domain.BizConfig, error) } // CachedConfigRepository 这个是一定要搞缓存的 @@ -35,6 +39,61 @@ func NewCachedConfigRepository(dao dao.ConfigDAO) ConfigRepository { return &CachedConfigRepository{dao: dao} } +// Save 保存配置 +func (r *CachedConfigRepository) Save(ctx context.Context, cfg domain.BizConfig) (int64, error) { + return r.dao.Save(ctx, dao.BizConfig{ + Id: cfg.Id, + Biz: cfg.Biz, + MaxInput: cfg.MaxInput, + Model: cfg.Model, + Price: cfg.Price, + Temperature: cfg.Temperature, + TopP: cfg.TopP, + SystemPrompt: cfg.SystemPrompt, + PromptTemplate: cfg.PromptTemplate, + KnowledgeId: cfg.KnowledgeId, + }) +} +func (r *CachedConfigRepository) List(ctx context.Context) ([]domain.BizConfig, error) { + configs, err := r.dao.List(ctx) + if err != nil { + return nil, err + } + return slice.Map(configs, func(idx int, src dao.BizConfig) domain.BizConfig { + return domain.BizConfig{ + Id: src.Id, + Biz: src.Biz, + Model: src.Model, + Price: src.Price, + Temperature: src.Temperature, + TopP: src.TopP, + SystemPrompt: src.SystemPrompt, + MaxInput: src.MaxInput, + KnowledgeId: src.KnowledgeId, + PromptTemplate: src.PromptTemplate, + } + }), nil +} + +func (r *CachedConfigRepository) GetById(ctx context.Context, id int64) (domain.BizConfig, error) { + cfg, err := r.dao.GetById(ctx, id) + if err != nil { + return domain.BizConfig{}, err + } + + return domain.BizConfig{ + Id: cfg.Id, + Biz: cfg.Biz, + Model: cfg.Model, + Price: cfg.Price, + Temperature: cfg.Temperature, + TopP: cfg.TopP, + SystemPrompt: cfg.SystemPrompt, + MaxInput: cfg.MaxInput, + KnowledgeId: cfg.KnowledgeId, + PromptTemplate: cfg.PromptTemplate, + }, nil +} func (repo *CachedConfigRepository) GetConfig(ctx context.Context, biz string) (domain.BizConfig, error) { res, err := repo.dao.GetConfig(ctx, biz) if err != nil { diff --git a/internal/ai/internal/repository/dao/config.go b/internal/ai/internal/repository/dao/config.go index c7d88b53..6bd858ee 100644 --- a/internal/ai/internal/repository/dao/config.go +++ b/internal/ai/internal/repository/dao/config.go @@ -16,12 +16,17 @@ package dao import ( "context" + "time" "github.com/ego-component/egorm" + "gorm.io/gorm/clause" ) type ConfigDAO interface { GetConfig(ctx context.Context, biz string) (BizConfig, error) + Save(ctx context.Context, cfg BizConfig) (int64, error) + List(ctx context.Context) ([]BizConfig, error) + GetById(ctx context.Context, id int64) (BizConfig, error) } type GORMConfigDAO struct { @@ -32,11 +37,35 @@ func NewGORMConfigDAO(db *egorm.Component) ConfigDAO { return &GORMConfigDAO{db: db} } +func (dao *GORMConfigDAO) GetById(ctx context.Context, id int64) (BizConfig, error) { + var config BizConfig + err := dao.db.WithContext(ctx).Where("id = ?", id).First(&config).Error + return config, err +} + +func (dao *GORMConfigDAO) List(ctx context.Context) ([]BizConfig, error) { + var configs []BizConfig + err := dao.db.WithContext(ctx). + Model(&BizConfig{}). + Order("id desc"). + Find(&configs).Error + return configs, err +} func (dao *GORMConfigDAO) GetConfig(ctx context.Context, biz string) (BizConfig, error) { var res BizConfig err := dao.db.WithContext(ctx).Where("biz = ?", biz).First(&res).Error return res, err } +func (dao *GORMConfigDAO) Save(ctx context.Context, cfg BizConfig) (int64, error) { + now := time.Now().UnixMilli() + cfg.Utime = now + cfg.Ctime = now + err := dao.db.WithContext(ctx).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "id"}}, + DoUpdates: clause.AssignmentColumns([]string{"biz", "max_input", "model", "price", "temperature", "top_p", "system_prompt", "prompt_template", "knowledge_id", "utime"}), + }).Create(&cfg).Error + return cfg.Id, err +} type BizConfig struct { Id int64 `gorm:"primaryKey;autoIncrement;comment:AI biz 配置表ID"` diff --git a/internal/ai/internal/service/config_service.go b/internal/ai/internal/service/config_service.go new file mode 100644 index 00000000..4c1863c0 --- /dev/null +++ b/internal/ai/internal/service/config_service.go @@ -0,0 +1,46 @@ +package service + +import ( + "context" + "fmt" + + "github.com/ecodeclub/webook/internal/ai/internal/domain" + "github.com/ecodeclub/webook/internal/ai/internal/repository" +) + +// ConfigService 定义配置服务的接口 +type ConfigService interface { + Save(ctx context.Context, cfg domain.BizConfig) (int64, error) + List(ctx context.Context) ([]domain.BizConfig, error) + GetById(ctx context.Context, id int64) (domain.BizConfig, error) +} + +// configService 具体实现 +type configService struct { + repo repository.ConfigRepository +} + +// NewConfigService 创建 ConfigService 实例 +func NewConfigService(repo repository.ConfigRepository) ConfigService { + return &configService{ + repo: repo, + } +} + +// Save 保存配置 +func (s *configService) Save(ctx context.Context, cfg domain.BizConfig) (int64, error) { + return s.repo.Save(ctx, cfg) +} + +// List 获取所有配置列表 +func (s *configService) List(ctx context.Context) ([]domain.BizConfig, error) { + return s.repo.List(ctx) +} + +// GetById 根据ID获取配置 +func (s *configService) GetById(ctx context.Context, id int64) (domain.BizConfig, error) { + if id <= 0 { + return domain.BizConfig{}, fmt.Errorf("无效的ID") + } + return s.repo.GetById(ctx, id) +} diff --git a/internal/ai/internal/service/general_service.go b/internal/ai/internal/service/general_service.go new file mode 100644 index 00000000..49ad00e1 --- /dev/null +++ b/internal/ai/internal/service/general_service.go @@ -0,0 +1,35 @@ +package service + +import ( + "context" + + "github.com/ecodeclub/webook/internal/ai/internal/domain" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm" + "github.com/lithammer/shortuuid/v4" +) + +type GeneralService interface { + // LLMAsk 通用询问ai的接口 + LLMAsk(ctx context.Context, uid int64, biz string, input []string) (domain.LLMResponse, error) +} + +func NewGeneralService(aiSvc llm.Service) GeneralService { + return &generalSvc{ + aiSvc: aiSvc, + } +} + +type generalSvc struct { + aiSvc llm.Service +} + +func (g *generalSvc) LLMAsk(ctx context.Context, uid int64, biz string, input []string) (domain.LLMResponse, error) { + tid := shortuuid.New() + aiReq := domain.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: biz, + Input: input, + } + return g.aiSvc.Invoke(ctx, aiReq) +} diff --git a/internal/ai/internal/service/jd_service.go b/internal/ai/internal/service/jd_service.go new file mode 100644 index 00000000..093e947b --- /dev/null +++ b/internal/ai/internal/service/jd_service.go @@ -0,0 +1,102 @@ +package service + +import ( + "context" + "errors" + "strconv" + "strings" + "sync/atomic" + + "github.com/ecodeclub/webook/internal/ai/internal/domain" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm" + "github.com/lithammer/shortuuid/v4" + "golang.org/x/sync/errgroup" +) + +type JDService interface { + // Evaluate 测评 + Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, error) +} + +type jdSvc struct { + aiSvc llm.Service +} + +func NewJDService(aiSvc llm.Service) JDService { + return &jdSvc{ + aiSvc: aiSvc, + } +} + +func (j *jdSvc) Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, error) { + var techJD, bizJD, positionJD *domain.JDEvaluation + var amount int64 + var eg errgroup.Group + eg.Go(func() error { + var err error + var techAmount int64 + techAmount, techJD, err = j.analysisJd(ctx, uid, domain.AnalysisJDTech, jd) + if err != nil { + return err + } + atomic.AddInt64(&amount, techAmount) + return nil + }) + eg.Go(func() error { + var err error + var bizAmount int64 + bizAmount, bizJD, err = j.analysisJd(ctx, uid, domain.AnalysisJDBiz, jd) + if err != nil { + return err + } + atomic.AddInt64(&amount, bizAmount) + return nil + }) + eg.Go(func() error { + var err error + var positionAmount int64 + positionAmount, positionJD, err = j.analysisJd(ctx, uid, domain.AnalysisJDPosition, jd) + if err != nil { + return err + } + atomic.AddInt64(&amount, positionAmount) + return nil + }) + if err := eg.Wait(); err != nil { + return domain.JD{}, err + } + return domain.JD{ + Amount: amount, + TechScore: techJD, + BizScore: bizJD, + PosScore: positionJD, + }, nil +} + +func (j *jdSvc) analysisJd(ctx context.Context, uid int64, biz string, jd string) (int64, *domain.JDEvaluation, error) { + tid := shortuuid.New() + aiReq := domain.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: biz, + Input: []string{jd}, + } + resp, err := j.aiSvc.Invoke(ctx, aiReq) + if err != nil { + return 0, nil, err + } + answer := strings.SplitN(resp.Answer, "\n", 2) + if len(answer) != 2 { + return 0, nil, errors.New("不符合预期的大模型响应") + } + score := answer[0] + scoreNum, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimPrefix(score, "score:")), 64) + if err != nil { + return 0, nil, errors.New("分数返回的数据不对") + } + + return resp.Amount, &domain.JDEvaluation{ + Score: scoreNum, + Analysis: strings.TrimSpace(strings.TrimPrefix(answer[1], "analysis:")), + }, nil +} diff --git a/internal/ai/internal/web/admin_handler.go b/internal/ai/internal/web/admin_handler.go new file mode 100644 index 00000000..70a130d7 --- /dev/null +++ b/internal/ai/internal/web/admin_handler.go @@ -0,0 +1,89 @@ +package web + +import ( + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/webook/internal/ai/internal/domain" + "github.com/ecodeclub/webook/internal/ai/internal/service" + "github.com/gin-gonic/gin" +) + +type AdminHandler struct { + svc service.ConfigService +} + +func NewAdminHandler(svc service.ConfigService) *AdminHandler { + return &AdminHandler{ + svc: svc, + } +} + +func (h *AdminHandler) RegisterRoutes(server *gin.Engine) { + // 管理员路由组 + admin := server.Group("/ai/config") + admin.POST("/save", ginx.B[ConfigRequest](h.Save)) + admin.GET("/list", ginx.W(h.List)) + admin.POST("/detail", ginx.B[ConfigInfoReq](h.GetById)) +} + +func (h *AdminHandler) Save(ctx *ginx.Context, req ConfigRequest) (ginx.Result, error) { + id, err := h.svc.Save(ctx, domain.BizConfig{ + Id: req.Config.Id, + Biz: req.Config.Biz, + MaxInput: req.Config.MaxInput, + Model: req.Config.Model, + Price: req.Config.Price, + Temperature: req.Config.Temperature, + TopP: req.Config.TopP, + SystemPrompt: req.Config.SystemPrompt, + PromptTemplate: req.Config.PromptTemplate, + KnowledgeId: req.Config.KnowledgeId, + }) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: id, + }, nil +} + +// List 获取配置列表 +func (h *AdminHandler) List(ctx *ginx.Context) (ginx.Result, error) { + configs, err := h.svc.List(ctx) + if err != nil { + return systemErrorResult, err + } + + return ginx.Result{ + Data: slice.Map(configs, func(idx int, c domain.BizConfig) Config { + return h.domainToConfig(c) + }), + }, nil +} + +func (h *AdminHandler) GetById(ctx *ginx.Context, req ConfigInfoReq) (ginx.Result, error) { + id := req.Id + config, err := h.svc.GetById(ctx, id) + if err != nil { + return systemErrorResult, err + } + + return ginx.Result{ + Data: h.domainToConfig(config), + }, nil +} + +func (h *AdminHandler) domainToConfig(cfg domain.BizConfig) Config { + return Config{ + Id: cfg.Id, + Biz: cfg.Biz, + MaxInput: cfg.MaxInput, + Model: cfg.Model, + Price: cfg.Price, + Temperature: cfg.Temperature, + TopP: cfg.TopP, + SystemPrompt: cfg.SystemPrompt, + PromptTemplate: cfg.PromptTemplate, + KnowledgeId: cfg.KnowledgeId, + } +} diff --git a/internal/ai/internal/web/handler.go b/internal/ai/internal/web/handler.go new file mode 100644 index 00000000..40d0d403 --- /dev/null +++ b/internal/ai/internal/web/handler.go @@ -0,0 +1,81 @@ +package web + +import ( + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/ai/internal/domain" + "github.com/ecodeclub/webook/internal/ai/internal/errs" + "github.com/ecodeclub/webook/internal/ai/internal/service" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler/credit" + "github.com/gin-gonic/gin" + "github.com/pkg/errors" +) + +type Handler struct { + generalSvc service.GeneralService + jdSvc service.JDService +} + +func NewHandler(generalSvc service.GeneralService, jdSvc service.JDService) *Handler { + return &Handler{ + generalSvc: generalSvc, + jdSvc: jdSvc, + } +} + +func (h *Handler) MemberRoutes(server *gin.Engine) { + server.POST("/ai/ask", ginx.BS(h.LLMAsk)) + server.POST("/ai/analysis_jd", ginx.BS(h.AnalysisJd)) +} + +func (h *Handler) LLMAsk(ctx *ginx.Context, req LLMRequest, sess session.Session) (ginx.Result, error) { + uid := sess.Claims().Uid + resp, err := h.generalSvc.LLMAsk(ctx, uid, req.Biz, req.Input) + switch { + case errors.Is(err, credit.ErrInsufficientCredit): + return ginx.Result{ + Code: errs.InsufficientCredit.Code, + Msg: errs.InsufficientCredit.Msg, + }, nil + case err == nil: + return ginx.Result{ + Data: LLMResponse{ + Amount: resp.Amount, + RawResult: resp.Answer, + }, + }, nil + default: + return systemErrorResult, err + } +} + +func (h *Handler) AnalysisJd(ctx *ginx.Context, req JDRequest, sess session.Session) (ginx.Result, error) { + uid := sess.Claims().Uid + resp, err := h.jdSvc.Evaluate(ctx, uid, req.JD) + switch { + case errors.Is(err, credit.ErrInsufficientCredit): + return ginx.Result{ + Code: errs.InsufficientCredit.Code, + Msg: errs.InsufficientCredit.Msg, + }, nil + case err == nil: + return ginx.Result{ + Data: JDResponse{ + Amount: resp.Amount, + TechScore: h.newJD(resp.TechScore), + BizScore: h.newJD(resp.BizScore), + PosScore: h.newJD(resp.PosScore), + }, + }, nil + default: + return systemErrorResult, err + } + +} + +func (h *Handler) newJD(jd *domain.JDEvaluation) *JDEvaluation { + return &JDEvaluation{ + Score: jd.Score, + Analysis: jd.Analysis, + } +} diff --git a/internal/ai/internal/web/result.go b/internal/ai/internal/web/result.go new file mode 100644 index 00000000..055c36f9 --- /dev/null +++ b/internal/ai/internal/web/result.go @@ -0,0 +1,13 @@ +package web + +import ( + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/webook/internal/ai/internal/errs" +) + +var ( + systemErrorResult = ginx.Result{ + Code: errs.SystemError.Code, + Msg: errs.SystemError.Msg, + } +) diff --git a/internal/ai/internal/web/vo.go b/internal/ai/internal/web/vo.go new file mode 100644 index 00000000..4735c6b5 --- /dev/null +++ b/internal/ai/internal/web/vo.go @@ -0,0 +1,46 @@ +package web + +type LLMRequest struct { + Biz string `json:"biz"` + Input []string `json:"input"` +} + +type LLMResponse struct { + Amount int64 `json:"amount"` + RawResult string `json:"rawResult"` +} + +type JDRequest struct { + JD string `json:"jd"` +} + +type JDResponse struct { + Amount int64 `json:"amount"` + TechScore *JDEvaluation `json:"techScore"` + BizScore *JDEvaluation `json:"bizScore"` + PosScore *JDEvaluation `json:"posScore"` +} + +type JDEvaluation struct { + Score float64 `json:"score"` + Analysis string `json:"analysis"` +} + +type Config struct { + Id int64 `json:"id"` + Biz string `json:"biz"` + MaxInput int `json:"maxInput"` + Model string `json:"model"` + Price int64 `json:"price"` + Temperature float64 `json:"temperature"` + TopP float64 `json:"topP"` + SystemPrompt string `json:"systemPrompt"` + PromptTemplate string `json:"promptTemplate"` + KnowledgeId string `json:"knowledgeId"` +} +type ConfigRequest struct { + Config Config `json:"config"` +} +type ConfigInfoReq struct { + Id int64 `json:"id"` +} diff --git a/internal/ai/module.go b/internal/ai/module.go index 895889f9..941289c7 100644 --- a/internal/ai/module.go +++ b/internal/ai/module.go @@ -1,5 +1,7 @@ package ai type Module struct { - Svc LLMService + Svc LLMService + Hdl *LLMHandler + AdminHandler *AdminHandler } diff --git a/internal/ai/type.go b/internal/ai/type.go index 6ef8be1c..66a813b9 100644 --- a/internal/ai/type.go +++ b/internal/ai/type.go @@ -3,8 +3,11 @@ package ai import ( "github.com/ecodeclub/webook/internal/ai/internal/domain" "github.com/ecodeclub/webook/internal/ai/internal/service/llm" + "github.com/ecodeclub/webook/internal/ai/internal/web" ) type LLMRequest = domain.LLMRequest type LLMResponse = domain.LLMResponse type LLMService = llm.Service +type AdminHandler = web.AdminHandler +type LLMHandler = web.Handler diff --git a/internal/ai/wire.go b/internal/ai/wire.go index 5304bd54..38640e9f 100644 --- a/internal/ai/wire.go +++ b/internal/ai/wire.go @@ -5,6 +5,9 @@ package ai import ( "sync" + "github.com/ecodeclub/webook/internal/ai/internal/service" + "github.com/ecodeclub/webook/internal/ai/internal/web" + "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" @@ -38,7 +41,11 @@ func InitModule(db *egorm.Component, creditSvc *credit.Module) (*Module, error) InitCompositionHandlerUsingZhipu, InitCommonHandlers, InitZhipu, - + service.NewGeneralService, + service.NewJDService, + service.NewConfigService, + web.NewHandler, + web.NewAdminHandler, wire.Struct(new(Module), "*"), wire.FieldsOf(new(*credit.Module), "Svc"), ) diff --git a/internal/ai/wire_gen.go b/internal/ai/wire_gen.go index aba23935..360f42ef 100644 --- a/internal/ai/wire_gen.go +++ b/internal/ai/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -11,11 +11,13 @@ 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" "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/ai/internal/web" "github.com/ecodeclub/webook/internal/credit" "github.com/ego-component/egorm" "gorm.io/gorm" @@ -28,10 +30,10 @@ func InitModule(db *gorm.DB, creditSvc *credit.Module) (*Module, error) { configDAO := dao.NewGORMConfigDAO(db) configRepository := repository.NewCachedConfigRepository(configDAO) configHandlerBuilder := config.NewBuilder(configRepository) - service := creditSvc.Svc + serviceService := creditSvc.Svc llmCreditDAO := InitLLMCreditLogDAO(db) llmCreditLogRepo := repository.NewLLMCreditLogRepo(llmCreditDAO) - creditHandlerBuilder := credit2.NewHandlerBuilder(service, llmCreditLogRepo) + creditHandlerBuilder := credit2.NewHandlerBuilder(serviceService, llmCreditLogRepo) llmRecordDAO := dao.NewGORMLLMLogDAO(db) llmLogRepo := repository.NewLLMLogRepo(llmRecordDAO) recordHandlerBuilder := record.NewHandler(llmLogRepo) @@ -39,8 +41,15 @@ func InitModule(db *gorm.DB, creditSvc *credit.Module) (*Module, error) { handler := InitZhipu() handlerHandler := InitCompositionHandlerUsingZhipu(v, handler) llmService := llm.NewLLMService(handlerHandler) + generalService := service.NewGeneralService(llmService) + jdService := service.NewJDService(llmService) + webHandler := web.NewHandler(generalService, jdService) + configService := service.NewConfigService(configRepository) + adminHandler := web.NewAdminHandler(configService) module := &Module{ - Svc: llmService, + Svc: llmService, + Hdl: webHandler, + AdminHandler: adminHandler, } return module, nil } diff --git a/internal/resume/internal/domain/analysis.go b/internal/resume/internal/domain/analysis.go new file mode 100644 index 00000000..7de71b4f --- /dev/null +++ b/internal/resume/internal/domain/analysis.go @@ -0,0 +1,21 @@ +package domain + +const ( + BizResumeSkillKeyPoints = "biz_resume_skill_keypoints" + BizSkillsRewrite = "biz_resume_skill_rewrite" + // BizResumeProjectEvaluation 评价项目经历 + BizResumeProjectEvaluation = "biz_resume_project_evaluation" + BizResumeProjectRewrite = "biz_resume_project_rewrite" + //BizResumeJobsKeyPoints = "biz_resume_job_keypoints" + BizResumeJobsRewrite = "biz_resume_job_rewrite" +) + +type ResumeAnalysis struct { + Amount int64 + // 技能 + RewriteSkills string + // 项目 + RewriteProject string + // 工作经历 + RewriteJobs string +} diff --git a/internal/resume/internal/errs/code.go b/internal/resume/internal/errs/code.go index 5a7b4438..e2ba1e77 100644 --- a/internal/resume/internal/errs/code.go +++ b/internal/resume/internal/errs/code.go @@ -2,6 +2,8 @@ package errs var ( SystemError = ErrorCode{Code: 515001, Msg: "系统错误"} + // InsufficientCredit 这个不管说是客户端错误还是服务端错误,都有点勉强,所以随便用一个 5 + InsufficientCredit = ErrorCode{Code: 515002, Msg: "积分不足"} ) type ErrorCode struct { diff --git a/internal/resume/internal/integration/analysis_handler_test.go b/internal/resume/internal/integration/analysis_handler_test.go new file mode 100644 index 00000000..8009c407 --- /dev/null +++ b/internal/resume/internal/integration/analysis_handler_test.go @@ -0,0 +1,129 @@ +package integration + +import ( + "context" + "errors" + "fmt" + "net/http" + "testing" + + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/ai" + aimocks "github.com/ecodeclub/webook/internal/ai/mocks" + "github.com/ecodeclub/webook/internal/cases" + "github.com/ecodeclub/webook/internal/resume/internal/domain" + "github.com/ecodeclub/webook/internal/resume/internal/integration/startup" + "github.com/ecodeclub/webook/internal/resume/internal/web" + "github.com/ecodeclub/webook/internal/test" + "github.com/gin-gonic/gin" + "github.com/gotomicro/ego/core/econf" + "github.com/gotomicro/ego/server/egin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" +) + +type AnalysisTestSuite struct { + suite.Suite + server *egin.Component +} + +func (a *AnalysisTestSuite) SetupSuite() { + ctrl := gomock.NewController(a.T()) + aiSvc := aimocks.NewMockService(ctrl) + aiSvc.EXPECT().Invoke(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req ai.LLMRequest) (ai.LLMResponse, error) { + switch req.Biz { + case domain.BizResumeSkillKeyPoints: + return ai.LLMResponse{ + Tokens: 100, + Amount: 100, + Answer: fmt.Sprintf("skill的keypoints %s", req.Input[0]), + }, nil + case domain.BizSkillsRewrite: + return ai.LLMResponse{ + Tokens: 200, + Amount: 200, + Answer: fmt.Sprintf("%s:%s", req.Input[0], req.Input[1]), + }, nil + case domain.BizResumeProjectEvaluation: + return ai.LLMResponse{ + Tokens: 150, + Amount: 150, + Answer: fmt.Sprintf("project的评估 %s", req.Input[0]), + }, nil + case domain.BizResumeProjectRewrite: + return ai.LLMResponse{ + Tokens: 220, + Amount: 220, + Answer: fmt.Sprintf("project的重写 %s", req.Input[0]), + }, nil + case domain.BizResumeJobsRewrite: + return ai.LLMResponse{ + Tokens: 400, + Amount: 400, + Answer: fmt.Sprintf("工作经历重写%s", req.Input[0]), + }, nil + default: + return ai.LLMResponse{}, errors.New("mock Err") + } + }).AnyTimes() + module := startup.InitModule(&cases.Module{}, &ai.Module{Svc: aiSvc}) + + hdl := module.AnalysisHandler + econf.Set("server", map[string]any{"contextTimeout": "1s"}) + server := egin.Load("server").Build() + server.Use(func(ctx *gin.Context) { + ctx.Set(session.CtxSessionKey, + session.NewMemorySession(session.Claims{ + Uid: uid, + })) + }) + hdl.MemberRoutes(server.Engine) + a.server = server +} + +func (a *AnalysisTestSuite) TestAnalysis() { + testCases := []struct { + name string + + req web.AnalysisReq + + wantCode int + wantResp test.Result[web.AnalysisResp] + }{ + { + name: "分析简历", + req: web.AnalysisReq{ + Resume: "resume", + }, + wantCode: 200, + wantResp: test.Result[web.AnalysisResp]{ + Data: web.AnalysisResp{ + Amount: 1070, + RewriteSkills: ":skill的keypoints resume", + RewriteJobs: "工作经历重写resume", + RewriteProject: "project的重写 resume\n## 综合评价\nproject的评估 project的重写 resume", + }, + }, + }, + } + for _, tc := range testCases { + a.T().Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, + "/resume/analysis", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.AnalysisResp]() + a.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + data := recorder.MustScan() + assert.Equal(t, tc.wantResp, data) + }) + } +} + +func TestAnalysisModule(t *testing.T) { + suite.Run(t, new(AnalysisTestSuite)) +} diff --git a/internal/resume/internal/integration/experience_handler_test.go b/internal/resume/internal/integration/experience_handler_test.go index 8989849d..a10010f8 100644 --- a/internal/resume/internal/integration/experience_handler_test.go +++ b/internal/resume/internal/integration/experience_handler_test.go @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/ekit/iox" "github.com/ecodeclub/ekit/sqlx" "github.com/ecodeclub/webook/internal/resume/internal/domain" @@ -62,7 +64,9 @@ func (s *ExperienceTestSuite) SetupSuite() { module := startup.InitModule(&cases.Module{ ExamineSvc: examSvc, - }) + }, + &ai.Module{}, + ) econf.Set("server", map[string]any{"contextTimeout": "1s"}) server := egin.Load("server").Build() diff --git a/internal/resume/internal/integration/project_handler_test.go b/internal/resume/internal/integration/project_handler_test.go index 42e41f7b..3dc466a5 100644 --- a/internal/resume/internal/integration/project_handler_test.go +++ b/internal/resume/internal/integration/project_handler_test.go @@ -9,6 +9,8 @@ import ( "strconv" "testing" + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/ekit/iox" "github.com/ecodeclub/ekit/slice" "github.com/ecodeclub/ginx/session" @@ -82,7 +84,8 @@ func (s *ProjectTestSuite) SetupSuite() { module := startup.InitModule(&cases.Module{ ExamineSvc: examSvc, Svc: caseSvc, - }) + }, + &ai.Module{}) econf.Set("server", map[string]any{"contextTimeout": "1s"}) server := egin.Load("server").Build() server.Use(func(ctx *gin.Context) { @@ -1052,7 +1055,7 @@ func (s *ProjectTestSuite) TestResumeInfo() { } } -func (s *ProjectTestSuite) TestReaumeList() { +func (s *ProjectTestSuite) TestResumeList() { for i := 1; i < 4; i++ { _, err := s.pdao.Upsert(context.Background(), dao.ResumeProject{ ID: int64(i), @@ -1113,8 +1116,8 @@ func (s *ProjectTestSuite) TestReaumeList() { }) req, err := http.NewRequest(http.MethodPost, "/resume/project/list", iox.NewJSONReader(nil)) - req.Header.Set("content-type", "application/json") require.NoError(s.T(), err) + req.Header.Set("content-type", "application/json") recorder := test.NewJSONResponseRecorder[[]web.Project]() s.server.ServeHTTP(recorder, req) diff --git a/internal/resume/internal/integration/startup/wire.go b/internal/resume/internal/integration/startup/wire.go index 099d04fd..434fc142 100644 --- a/internal/resume/internal/integration/startup/wire.go +++ b/internal/resume/internal/integration/startup/wire.go @@ -3,6 +3,7 @@ package startup import ( + "github.com/ecodeclub/webook/internal/ai" "github.com/ecodeclub/webook/internal/cases" "github.com/ecodeclub/webook/internal/resume" "github.com/ecodeclub/webook/internal/resume/internal/repository" @@ -13,7 +14,7 @@ import ( "github.com/google/wire" ) -func InitModule(caModule *cases.Module) *resume.Module { +func InitModule(caModule *cases.Module, aiModule *ai.Module) *resume.Module { wire.Build( testioc.InitDB, dao.NewResumeProjectDAO, @@ -22,10 +23,13 @@ func InitModule(caModule *cases.Module) *resume.Module { repository.NewExperience, service.NewService, service.NewExperienceService, + service.NewAnalysisService, wire.FieldsOf(new(*cases.Module), "ExamineSvc"), wire.FieldsOf(new(*cases.Module), "Svc"), + wire.FieldsOf(new(*ai.Module), "Svc"), web.NewHandler, web.NewExperienceHandler, + web.NewAnalysisHandler, wire.Struct(new(resume.Module), "*"), ) return new(resume.Module) diff --git a/internal/resume/internal/integration/startup/wire_gen.go b/internal/resume/internal/integration/startup/wire_gen.go index b4558380..594fe2dc 100644 --- a/internal/resume/internal/integration/startup/wire_gen.go +++ b/internal/resume/internal/integration/startup/wire_gen.go @@ -7,6 +7,7 @@ package startup import ( + "github.com/ecodeclub/webook/internal/ai" "github.com/ecodeclub/webook/internal/cases" "github.com/ecodeclub/webook/internal/resume" "github.com/ecodeclub/webook/internal/resume/internal/repository" @@ -18,7 +19,7 @@ import ( // Injectors from wire.go: -func InitModule(caModule *cases.Module) *resume.Module { +func InitModule(caModule *cases.Module, aiModule *ai.Module) *resume.Module { db := testioc.InitDB() resumeProjectDAO := dao.NewResumeProjectDAO(db) resumeProjectRepo := repository.NewResumeProjectRepo(resumeProjectDAO) @@ -30,9 +31,13 @@ func InitModule(caModule *cases.Module) *resume.Module { experience := repository.NewExperience(experienceDAO) experienceService := service.NewExperienceService(experience) experienceHandler := web.NewExperienceHandler(experienceService) + llmService := aiModule.Svc + analysisService := service.NewAnalysisService(llmService) + analysisHandler := web.NewAnalysisHandler(analysisService) module := &resume.Module{ - PrjHdl: projectHandler, - ExperienceHdl: experienceHandler, + PrjHdl: projectHandler, + ExperienceHdl: experienceHandler, + AnalysisHandler: analysisHandler, } return module } diff --git a/internal/resume/internal/service/analysis.go b/internal/resume/internal/service/analysis.go new file mode 100644 index 00000000..781b3091 --- /dev/null +++ b/internal/resume/internal/service/analysis.go @@ -0,0 +1,185 @@ +package service + +import ( + "context" + "fmt" + "sync/atomic" + + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/webook/internal/resume/internal/domain" + "github.com/lithammer/shortuuid/v4" + "golang.org/x/sync/errgroup" +) + +var ErrInsufficientCredit = ai.ErrInsufficientCredit + +type AnalysisService interface { + Analysis(ctx context.Context, uid int64, resume string) (domain.ResumeAnalysis, error) +} + +type analysisService struct { + aiSvc ai.LLMService +} + +func NewAnalysisService(aiSvc ai.LLMService) AnalysisService { + return &analysisService{ + aiSvc: aiSvc, + } +} + +func (r *analysisService) Analysis(ctx context.Context, uid int64, resume string) (domain.ResumeAnalysis, error) { + tid := shortuuid.New() + var eg errgroup.Group + + var amount int64 + var rewriteSKills, rewriteProject, rewriteJobs string + // 重写技能 + eg.Go(func() error { + keyPointsAmount, keyPoints, err := r.skillKeypoint(ctx, uid, fmt.Sprintf("%s_skills_get_keypoints", tid), resume) + if err != nil { + return err + } + atomic.AddInt64(&amount, keyPointsAmount) + // 暂时不需要传入原始简历,不然会严重超时,并且上下文太长,搞崩系统 + rewriteSkillsAmount, ans, err := r.rewriteSkills(ctx, uid, fmt.Sprintf("%s_skills_rewrite", tid), keyPoints, "") + if err != nil { + return err + } + atomic.AddInt64(&amount, rewriteSkillsAmount) + rewriteSKills = ans + return nil + }) + // 重写项目 + eg.Go(func() error { + rewriteProjectAmount, ans, err := r.rewriteProject(ctx, uid, fmt.Sprintf("%s_project_rewrite", tid), resume) + if err != nil { + return err + } + atomic.AddInt64(&amount, rewriteProjectAmount) + evaluationAmt, evaluation, err := r.evaluatePrj(ctx, uid, + domain.BizResumeProjectEvaluation, + fmt.Sprintf("%s_project_get_evaludation", tid), ans) + if err != nil { + return err + } + atomic.AddInt64(&amount, evaluationAmt) + rewriteProject = fmt.Sprintf("%s\n## 综合评价\n%s", ans, evaluation) + return nil + }) + // 重写工作经历 + eg.Go(func() error { + // 暂时还不需要提取关键字 + //keyPointsAmount, keyPoints, err := r.evaluatePrj(ctx, uid, domain.BizResumeJobsKeyPoints, fmt.Sprintf("%s_jobs_get_keypoints", tid), resume) + //if err != nil { + // return err + //} + //atomic.AddInt64(&amount, keyPointsAmount) + rewriteJobsAmount, ans, err := r.rewriteJobs(ctx, uid, fmt.Sprintf("%s_jobs_rewrite", tid), "", resume) + if err != nil { + return err + } + atomic.AddInt64(&amount, rewriteJobsAmount) + rewriteJobs = ans + return nil + }) + + if err := eg.Wait(); err != nil { + return domain.ResumeAnalysis{}, err + } + + return domain.ResumeAnalysis{ + Amount: amount, + RewriteSkills: rewriteSKills, + RewriteProject: rewriteProject, + RewriteJobs: rewriteJobs, + }, nil + +} + +func (r *analysisService) evaluatePrj(ctx context.Context, uid int64, biz, tid, rewritePrj string) (int64, string, error) { + aiReq := ai.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: biz, + // 标题,标准答案,输入 + Input: []string{rewritePrj}, + } + resp, err := r.aiSvc.Invoke(ctx, aiReq) + if err != nil { + return 0, "", err + } + return resp.Amount, resp.Answer, nil +} + +// 提取关键字 +func (r *analysisService) skillKeypoint(ctx context.Context, uid int64, tid, resume string) (int64, string, error) { + aiReq := ai.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: domain.BizResumeSkillKeyPoints, + Input: []string{resume}, + } + resp, err := r.aiSvc.Invoke(ctx, aiReq) + if err != nil { + return 0, "", err + } + return resp.Amount, resp.Answer, nil +} + +// 重写技能 +func (r *analysisService) rewriteSkills(ctx context.Context, uid int64, tid, keyPoints, resume string) (int64, string, error) { + aiReq := ai.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: domain.BizSkillsRewrite, + // 标题,标准答案,输入 + Input: []string{ + // 简历 + resume, + // 前一步提取的关键字 + keyPoints, + }, + } + resp, err := r.aiSvc.Invoke(ctx, aiReq) + if err != nil { + return 0, "", err + } + return resp.Amount, resp.Answer, nil +} + +// 重写项目 +func (r *analysisService) rewriteProject(ctx context.Context, uid int64, tid, resume string) (int64, string, error) { + aiReq := ai.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: domain.BizResumeProjectRewrite, + // 标题,标准答案,输入 + Input: []string{ + resume, + }, + } + resp, err := r.aiSvc.Invoke(ctx, aiReq) + if err != nil { + return 0, "", err + } + return resp.Amount, resp.Answer, nil +} + +// 重写工作经历 +func (r *analysisService) rewriteJobs(ctx context.Context, uid int64, tid, keyPoints, resume string) (int64, string, error) { + aiReq := ai.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: domain.BizResumeJobsRewrite, + // 标题,标准答案,输入 + Input: []string{ + resume, + keyPoints, + }, + } + resp, err := r.aiSvc.Invoke(ctx, aiReq) + if err != nil { + return 0, "", err + } + return resp.Amount, resp.Answer, nil +} diff --git a/internal/resume/internal/web/analysis.go b/internal/resume/internal/web/analysis.go new file mode 100644 index 00000000..7e403b61 --- /dev/null +++ b/internal/resume/internal/web/analysis.go @@ -0,0 +1,49 @@ +package web + +import ( + "errors" + + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/resume/internal/errs" + "github.com/ecodeclub/webook/internal/resume/internal/service" + "github.com/gin-gonic/gin" +) + +type AnalysisHandler struct { + svc service.AnalysisService +} + +func NewAnalysisHandler(svc service.AnalysisService) *AnalysisHandler { + return &AnalysisHandler{ + svc: svc, + } +} + +func (h *AnalysisHandler) MemberRoutes(server *gin.Engine) { + g := server.Group("/resume/analysis") + g.POST("", ginx.BS(h.Analysis)) +} + +func (h *AnalysisHandler) Analysis(ctx *ginx.Context, req AnalysisReq, sess session.Session) (ginx.Result, error) { + analysis, err := h.svc.Analysis(ctx, sess.Claims().Uid, req.Resume) + 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: AnalysisResp{ + Amount: analysis.Amount, + RewriteProject: analysis.RewriteProject, + RewriteSkills: analysis.RewriteSkills, + RewriteJobs: analysis.RewriteJobs, + }, + }, nil + default: + return systemErrorResult, err + } +} diff --git a/internal/resume/internal/web/project.go b/internal/resume/internal/web/project.go index 3a1bb552..e8405203 100644 --- a/internal/resume/internal/web/project.go +++ b/internal/resume/internal/web/project.go @@ -113,7 +113,7 @@ func (h *ProjectHandler) ProjectInfo(ctx *ginx.Context, req IDItem, sess session func (h *ProjectHandler) ProjectList(ctx *ginx.Context, sess session.Session) (ginx.Result, error) { uid := sess.Claims().Uid - projects, err := h.svc.FindProjects(ctx.Request.Context(), uid) + projects, err := h.svc.FindProjects(ctx, uid) if err != nil { return systemErrorResult, err } @@ -184,13 +184,14 @@ func (h *ProjectHandler) getCaMap(ctx *ginx.Context, uid int64, cids []int64) (m caMap map[int64]cases.Case eg errgroup.Group ) + pctx := ctx.Request.Context() eg.Go(func() error { var eerr error - resMap, eerr = h.examSvc.GetResults(ctx, uid, cids) + resMap, eerr = h.examSvc.GetResults(pctx, uid, cids) return eerr }) eg.Go(func() error { - cas, eerr := h.caseSvc.GetPubByIDs(ctx, cids) + cas, eerr := h.caseSvc.GetPubByIDs(pctx, cids) if eerr != nil { return eerr } diff --git a/internal/resume/internal/web/vo.go b/internal/resume/internal/web/vo.go index 9370f800..c98344c6 100644 --- a/internal/resume/internal/web/vo.go +++ b/internal/resume/internal/web/vo.go @@ -70,6 +70,16 @@ type Difficulty struct { // 适合用作难点的基本方案 Case Case `json:"case"` } +type AnalysisReq struct { + Resume string `json:"resume"` +} + +type AnalysisResp struct { + Amount int64 `json:"amount"` + RewriteSkills string `json:"rewriteSkills"` + RewriteProject string `json:"rewriteProject"` + RewriteJobs string `json:"rewriteJobs"` +} func newProject(project domain.Project, examMap map[int64]cases.ExamineResult, caseMap map[int64]cases.Case) Project { return Project{ diff --git a/internal/resume/module.go b/internal/resume/module.go index cf933542..db4fefb6 100644 --- a/internal/resume/module.go +++ b/internal/resume/module.go @@ -18,8 +18,10 @@ import "github.com/ecodeclub/webook/internal/resume/internal/web" type ExperienceHandler = web.ExperienceHandler type ProjectHandler = web.ProjectHandler +type AnalysisHandler = web.AnalysisHandler type Module struct { - PrjHdl *ProjectHandler - ExperienceHdl *ExperienceHandler + PrjHdl *ProjectHandler + ExperienceHdl *ExperienceHandler + AnalysisHandler *AnalysisHandler } diff --git a/internal/resume/wire.go b/internal/resume/wire.go index be789787..c867a6ca 100644 --- a/internal/resume/wire.go +++ b/internal/resume/wire.go @@ -19,6 +19,8 @@ package resume import ( "sync" + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/webook/internal/cases" "github.com/ecodeclub/webook/internal/resume/internal/repository" "github.com/ecodeclub/webook/internal/resume/internal/repository/dao" @@ -28,14 +30,21 @@ import ( "github.com/google/wire" ) -func InitModule(db *egorm.Component, caModule *cases.Module) *Module { +func InitModule(db *egorm.Component, caModule *cases.Module, aiModule *ai.Module) *Module { wire.Build( initResumeProjectDAOOnce, + dao.NewExperienceDAO, repository.NewResumeProjectRepo, + repository.NewExperience, + service.NewExperienceService, service.NewService, wire.FieldsOf(new(*cases.Module), "ExamineSvc"), wire.FieldsOf(new(*cases.Module), "Svc"), + wire.FieldsOf(new(*ai.Module), "Svc"), + service.NewAnalysisService, web.NewHandler, + web.NewAnalysisHandler, + web.NewExperienceHandler, wire.Struct(new(Module), "*"), ) return new(Module) diff --git a/internal/resume/wire_gen.go b/internal/resume/wire_gen.go index 82e31b4f..3f29acd2 100644 --- a/internal/resume/wire_gen.go +++ b/internal/resume/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -9,6 +9,7 @@ package resume import ( "sync" + "github.com/ecodeclub/webook/internal/ai" "github.com/ecodeclub/webook/internal/cases" "github.com/ecodeclub/webook/internal/resume/internal/repository" "github.com/ecodeclub/webook/internal/resume/internal/repository/dao" @@ -20,15 +21,24 @@ import ( // Injectors from wire.go: -func InitModule(db *gorm.DB, caModule *cases.Module) *Module { +func InitModule(db *gorm.DB, caModule *cases.Module, aiModule *ai.Module) *Module { daoResumeProjectDAO := initResumeProjectDAOOnce(db) resumeProjectRepo := repository.NewResumeProjectRepo(daoResumeProjectDAO) serviceService := service.NewService(resumeProjectRepo) examineService := caModule.ExamineSvc service2 := caModule.Svc projectHandler := web.NewHandler(serviceService, examineService, service2) + experienceDAO := dao.NewExperienceDAO(db) + experience := repository.NewExperience(experienceDAO) + experienceService := service.NewExperienceService(experience) + experienceHandler := web.NewExperienceHandler(experienceService) + llmService := aiModule.Svc + analysisService := service.NewAnalysisService(llmService) + analysisHandler := web.NewAnalysisHandler(analysisService) module := &Module{ - PrjHdl: projectHandler, + PrjHdl: projectHandler, + ExperienceHdl: experienceHandler, + AnalysisHandler: analysisHandler, } return module } diff --git a/ioc/admin.go b/ioc/admin.go index cb750ee4..aba8d06c 100644 --- a/ioc/admin.go +++ b/ioc/admin.go @@ -18,6 +18,8 @@ import ( "net/http" "strings" + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/webook/internal/cases" baguwen "github.com/ecodeclub/webook/internal/question" @@ -43,7 +45,9 @@ func InitAdminServer(prj *project.AdminHandler, queSet *baguwen.AdminQuestionSetHandler, caseHdl *cases.AdminCaseHandler, caseSetHdl *cases.AdminCaseSetHandler, - mark *marketing.AdminHandler) AdminServer { + mark *marketing.AdminHandler, + aiHdl *ai.AdminHandler, +) AdminServer { res := egin.Load("admin").Build() res.Use(cors.New(cors.Config{ ExposeHeaders: []string{"X-Refresh-Token", "X-Access-Token"}, @@ -73,6 +77,7 @@ func InitAdminServer(prj *project.AdminHandler, que.PrivateRoutes(res.Engine) caseHdl.PrivateRoutes(res.Engine) caseSetHdl.PrivateRoutes(res.Engine) + aiHdl.RegisterRoutes(res.Engine) return res } diff --git a/ioc/gin.go b/ioc/gin.go index 64e024bf..2b3a80c6 100644 --- a/ioc/gin.go +++ b/ioc/gin.go @@ -18,6 +18,8 @@ import ( "net/http" "strings" + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/webook/internal/resume" "github.com/ecodeclub/webook/internal/bff" @@ -86,6 +88,8 @@ func initGinxServer(sp session.Provider, csHdl *cases.CaseSetHandler, caseExamineHdl *cases.ExamineHandler, resumePrjHdl *resume.ProjectHandler, + resumeAnaHdl *resume.AnalysisHandler, + aiHdl *ai.LLMHandler, ) *egin.Component { session.SetDefaultProvider(sp) res := egin.Load("web").Build() @@ -159,5 +163,7 @@ func initGinxServer(sp session.Provider, skillHdl.MemberRoutes(res.Engine) caseExamineHdl.MemberRoutes(res.Engine) resumePrjHdl.MemberRoutes(res.Engine) + resumeAnaHdl.MemberRoutes(res.Engine) + aiHdl.MemberRoutes(res.Engine) return res } diff --git a/ioc/wire.go b/ioc/wire.go index 67114711..2bf39031 100644 --- a/ioc/wire.go +++ b/ioc/wire.go @@ -91,7 +91,9 @@ func InitApp() (*App, error) { bff.InitModule, wire.FieldsOf(new(*bff.Module), "Hdl"), resume.InitModule, - wire.FieldsOf(new(*resume.Module), "PrjHdl"), + wire.FieldsOf(new(*resume.Module), "PrjHdl", "AnalysisHandler"), + wire.FieldsOf(new(*ai.Module), "Hdl", "AdminHandler"), + initLocalActiveLimiterBuilder, initCronJobs, // 这两个顺序不要换 diff --git a/ioc/wire_gen.go b/ioc/wire_gen.go index 16bf3130..9234ffe3 100644 --- a/ioc/wire_gen.go +++ b/ioc/wire_gen.go @@ -131,9 +131,11 @@ func InitApp() (*App, error) { handler16 := bffModule.Hdl caseSetHandler := casesModule.CsHdl webExamineHandler := casesModule.ExamineHdl - resumeModule := resume.InitModule(db, casesModule) + resumeModule := resume.InitModule(db, casesModule, aiModule) projectHandler := resumeModule.PrjHdl - component := initGinxServer(provider, checkMembershipMiddlewareBuilder, localActiveLimit, checkPermissionMiddlewareBuilder, handler, examineHandler, questionSetHandler, webHandler, handler2, handler3, handler4, handler5, handler6, handler7, handler8, handler9, handler10, handler11, handler12, handler13, handler14, handler15, handler16, caseSetHandler, webExamineHandler, projectHandler) + analysisHandler := resumeModule.AnalysisHandler + handler17 := aiModule.Hdl + component := initGinxServer(provider, checkMembershipMiddlewareBuilder, localActiveLimit, checkPermissionMiddlewareBuilder, handler, examineHandler, questionSetHandler, webHandler, handler2, handler3, handler4, handler5, handler6, handler7, handler8, handler9, handler10, handler11, handler12, handler13, handler14, handler15, handler16, caseSetHandler, webExamineHandler, projectHandler, analysisHandler, handler17) adminHandler := projectModule.AdminHdl webAdminHandler := roadmapModule.AdminHdl adminHandler2 := baguwenModule.AdminHdl @@ -141,7 +143,8 @@ func InitApp() (*App, error) { adminCaseHandler := casesModule.AdminHandler adminCaseSetHandler := casesModule.AdminSetHandler adminHandler3 := marketingModule.AdminHdl - adminServer := InitAdminServer(adminHandler, webAdminHandler, adminHandler2, adminQuestionSetHandler, adminCaseHandler, adminCaseSetHandler, adminHandler3) + adminHandler4 := aiModule.AdminHandler + adminServer := InitAdminServer(adminHandler, webAdminHandler, adminHandler2, adminQuestionSetHandler, adminCaseHandler, adminCaseSetHandler, adminHandler3, adminHandler4) closeTimeoutOrdersJob := orderModule.CloseTimeoutOrdersJob closeTimeoutLockedCreditsJob := creditModule.CloseTimeoutLockedCreditsJob syncWechatOrderJob := paymentModule.SyncWechatOrderJob