From bdf74652b3e62d76bf846952e6b4e8d07a71ea7d Mon Sep 17 00:00:00 2001 From: huang <1724659546@qq.com> Date: Tue, 2 Dec 2025 15:51:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0ai?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yml | 2 +- docs/docs.go | 79 ++++++++++++ docs/swagger.json | 79 ++++++++++++ docs/swagger.yaml | 47 +++++++ go.mod | 34 +++-- go.sum | 85 +++++++++--- internal/app/api/router.go | 1 + .../app/controller/feed/recipe_controller.go | 46 +++++++ internal/app/dto/feed_dto.go | 13 ++ internal/app/service/recipe_service.go | 17 +++ internal/core/component_initializers.go | 11 ++ internal/domain/recipe/recipe_service.go | 122 ++++++++++++++++++ internal/infra/ai/ai.go | 19 +++ internal/infra/ai/gemini.go | 73 +++++++++++ internal/infra/config/config.go | 13 +- internal/infra/logs/logs.go | 4 +- internal/infra/models/models.go | 6 + 17 files changed, 619 insertions(+), 32 deletions(-) create mode 100644 internal/infra/ai/ai.go create mode 100644 internal/infra/ai/gemini.go diff --git a/config/config.yml b/config/config.yml index 8bcf55f..83298e9 100644 --- a/config/config.yml +++ b/config/config.yml @@ -108,5 +108,5 @@ alarm_notification: ai: gemini: api_key: "AIzaSyAJdXUmoN07LIswDac6YxPeRnvXlR73OO8" # 替换为你的 Gemini API Key - model_name: "gemini-2.5-flash" # Gemini 模型名称,例如 "gemini-pro" + model_name: "gemini-2.0-flash" # Gemini 模型名称,例如 "gemini-pro" timeout: 30 # AI 请求超时时间 (秒) diff --git a/docs/docs.go b/docs/docs.go index 0e42018..5794ac5 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -3371,6 +3371,59 @@ const docTemplate = `{ } } }, + "/api/v1/feed/recipes/{id}/ai-diagnose": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "使用AI对指定配方进行点评,并针对目标猪类型给出建议。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "AI点评配方", + "parameters": [ + { + "type": "integer", + "description": "配方ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "猪类型ID", + "name": "pig_type_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表AI点评成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ReviewRecipeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/inventory/stock/adjust": { "post": { "security": [ @@ -9165,6 +9218,23 @@ const docTemplate = `{ } } }, + "dto.ReviewRecipeResponse": { + "type": "object", + "properties": { + "ai_model": { + "description": "使用的 AI 模型", + "allOf": [ + { + "$ref": "#/definitions/models.AIModel" + } + ] + }, + "review_message": { + "description": "点评内容", + "type": "string" + } + } + }, "dto.SellPigsRequest": { "type": "object", "required": [ @@ -10084,6 +10154,15 @@ const docTemplate = `{ } } }, + "models.AIModel": { + "type": "string", + "enum": [ + "Gemini" + ], + "x-enum-varnames": [ + "AI_MODEL_GEMINI" + ] + }, "models.AlarmCode": { "type": "string", "enum": [ diff --git a/docs/swagger.json b/docs/swagger.json index f6e3d7b..e97232d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -3363,6 +3363,59 @@ } } }, + "/api/v1/feed/recipes/{id}/ai-diagnose": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "使用AI对指定配方进行点评,并针对目标猪类型给出建议。", + "produces": [ + "application/json" + ], + "tags": [ + "饲料管理-配方" + ], + "summary": "AI点评配方", + "parameters": [ + { + "type": "integer", + "description": "配方ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "猪类型ID", + "name": "pig_type_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "业务码为200代表AI点评成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/controller.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/dto.ReviewRecipeResponse" + } + } + } + ] + } + } + } + } + }, "/api/v1/inventory/stock/adjust": { "post": { "security": [ @@ -9157,6 +9210,23 @@ } } }, + "dto.ReviewRecipeResponse": { + "type": "object", + "properties": { + "ai_model": { + "description": "使用的 AI 模型", + "allOf": [ + { + "$ref": "#/definitions/models.AIModel" + } + ] + }, + "review_message": { + "description": "点评内容", + "type": "string" + } + } + }, "dto.SellPigsRequest": { "type": "object", "required": [ @@ -10076,6 +10146,15 @@ } } }, + "models.AIModel": { + "type": "string", + "enum": [ + "Gemini" + ], + "x-enum-varnames": [ + "AI_MODEL_GEMINI" + ] + }, "models.AlarmCode": { "type": "string", "enum": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8fd239e..69bde1e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1648,6 +1648,16 @@ definitions: - quantity - treatment_location type: object + dto.ReviewRecipeResponse: + properties: + ai_model: + allOf: + - $ref: '#/definitions/models.AIModel' + description: 使用的 AI 模型 + review_message: + description: 点评内容 + type: string + type: object dto.SellPigsRequest: properties: pen_id: @@ -2274,6 +2284,12 @@ definitions: weight: type: number type: object + models.AIModel: + enum: + - Gemini + type: string + x-enum-varnames: + - AI_MODEL_GEMINI models.AlarmCode: enum: - 温度阈值 @@ -4755,6 +4771,37 @@ paths: summary: 更新配方 tags: - 饲料管理-配方 + /api/v1/feed/recipes/{id}/ai-diagnose: + get: + description: 使用AI对指定配方进行点评,并针对目标猪类型给出建议。 + parameters: + - description: 配方ID + in: path + name: id + required: true + type: integer + - description: 猪类型ID + in: query + name: pig_type_id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 业务码为200代表AI点评成功 + schema: + allOf: + - $ref: '#/definitions/controller.Response' + - properties: + data: + $ref: '#/definitions/dto.ReviewRecipeResponse' + type: object + security: + - BearerAuth: [] + summary: AI点评配方 + tags: + - 饲料管理-配方 /api/v1/feed/recipes/generate-from-all-materials/{pig_type_id}: post: description: 根据指定的猪类型ID,使用系统中所有可用的原料,自动计算并创建一个成本最优的配方。 diff --git a/go.mod b/go.mod index f2a4bd9..48c7998 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/go-openapi/swag v0.25.1 github.com/go-openapi/validate v0.24.0 github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/generative-ai-go v0.20.1 github.com/google/uuid v1.6.0 github.com/labstack/echo/v4 v4.13.4 github.com/panjf2000/ants/v2 v2.11.3 @@ -20,7 +21,8 @@ require ( go.uber.org/zap v1.27.0 golang.org/x/crypto v0.43.0 gonum.org/v1/gonum v0.16.0 - google.golang.org/protobuf v1.36.9 + google.golang.org/api v0.256.0 + google.golang.org/protobuf v1.36.10 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 gorm.io/datatypes v1.2.6 @@ -29,11 +31,18 @@ require ( ) require ( + cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go/ai v0.8.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/longrunning v0.5.7 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect @@ -52,7 +61,9 @@ require ( github.com/go-openapi/swag/typeutils v0.25.1 // indirect github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect - github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect @@ -72,18 +83,25 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.46.0 // indirect - golang.org/x/sync v0.17.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.30.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.38.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/grpc v1.76.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/mysql v1.5.6 // indirect gorm.io/driver/sqlite v1.6.0 // indirect diff --git a/go.sum b/go.sum index d2b2334..c0ec608 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,38 @@ +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w= +cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE= +cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= +cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= @@ -67,10 +88,20 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ= +github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -107,6 +138,8 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -138,14 +171,22 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= @@ -160,21 +201,33 @@ golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= +google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/app/api/router.go b/internal/app/api/router.go index b741782..fd60b70 100644 --- a/internal/app/api/router.go +++ b/internal/app/api/router.go @@ -261,6 +261,7 @@ func (a *API) setupRoutes() { feedGroup.GET("/recipes", a.recipeController.ListRecipes) feedGroup.POST("/recipes/generate-from-all-materials/:pig_type_id", a.recipeController.GenerateFromAllMaterials) feedGroup.POST("/recipes/generate-prioritized-stock/:pig_type_id", a.recipeController.GenerateRecipeWithPrioritizedStockRawMaterials) + feedGroup.GET("/recipes/:id/ai-diagnose", a.recipeController.AIDiagnoseRecipe) } logger.Debug("饲料管理相关接口注册成功 (需要认证和审计)") diff --git a/internal/app/controller/feed/recipe_controller.go b/internal/app/controller/feed/recipe_controller.go index 6374454..d5e7ca5 100644 --- a/internal/app/controller/feed/recipe_controller.go +++ b/internal/app/controller/feed/recipe_controller.go @@ -3,6 +3,7 @@ package feed import ( "context" "errors" + "fmt" "strconv" "git.huangwc.com/pig/pig-farm-controller/internal/app/controller" @@ -256,3 +257,48 @@ func (c *RecipeController) GenerateRecipeWithPrioritizedStockRawMaterials(ctx ec logger.Infof("%s: 配方生成成功, 新配方ID: %d", actionType, resp.ID) return controller.SendSuccessWithAudit(ctx, controller.CodeCreated, "配方生成成功", resp, actionType, "配方生成成功", resp) } + +// AIDiagnoseRecipe godoc +// @Summary AI点评配方 +// @Description 使用AI对指定配方进行点评,并针对目标猪类型给出建议。 +// @Tags 饲料管理-配方 +// @Security BearerAuth +// @Produce json +// @Param id path int true "配方ID" +// @Param pig_type_id query int true "猪类型ID" +// @Success 200 {object} controller.Response{data=dto.ReviewRecipeResponse} "业务码为200代表AI点评成功" +// @Router /api/v1/feed/recipes/{id}/ai-diagnose [get] +func (c *RecipeController) AIDiagnoseRecipe(ctx echo.Context) error { + reqCtx, logger := logs.Trace(ctx.Request().Context(), c.ctx, "AIDiagnoseRecipe") + const actionType = "AI点评配方" + fmt.Println("xxx") + // 从路径参数中获取配方ID + recipeIDStr := ctx.Param("id") + recipeID, err := strconv.ParseUint(recipeIDStr, 10, 32) + if err != nil { + logger.Errorf("%s: 配方ID格式错误: %v, ID: %s", actionType, err, recipeIDStr) + return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的配方ID格式", actionType, "配方ID格式错误", recipeIDStr) + } + + // 从查询参数中获取猪类型ID + pigTypeIDStr := ctx.QueryParam("pig_type_id") + pigTypeID, err := strconv.ParseUint(pigTypeIDStr, 10, 32) + if err != nil { + logger.Errorf("%s: 猪类型ID格式错误: %v, ID: %s", actionType, err, pigTypeIDStr) + return controller.SendErrorWithAudit(ctx, controller.CodeBadRequest, "无效的猪类型ID格式", actionType, "猪类型ID格式错误", pigTypeIDStr) + } + + // 调用应用服务进行AI点评 + reviewResponse, err := c.recipeService.AIDiagnoseRecipe(reqCtx, uint32(recipeID), uint32(pigTypeID)) + if err != nil { + logger.Errorf("%s: 服务层AI点评失败: %v, RecipeID: %d, PigTypeID: %d", actionType, err, recipeID, pigTypeID) + if errors.Is(err, service.ErrRecipeNotFound) { + return controller.SendErrorWithAudit(ctx, controller.CodeNotFound, err.Error(), actionType, "配方或猪类型不存在", map[string]uint32{"recipe_id": uint32(recipeID), "pig_type_id": uint32(pigTypeID)}) + } + // 对于其他错误,统一返回内部服务器错误 + return controller.SendErrorWithAudit(ctx, controller.CodeInternalError, "AI点评失败: "+err.Error(), actionType, "服务层AI点评失败", map[string]uint32{"recipe_id": uint32(recipeID), "pig_type_id": uint32(pigTypeID)}) + } + + logger.Infof("%s: AI点评成功, RecipeID: %d, PigTypeID: %d", actionType, recipeID, pigTypeID) + return controller.SendSuccessWithAudit(ctx, controller.CodeSuccess, "AI点评成功", reviewResponse, actionType, "AI点评成功", reviewResponse) +} diff --git a/internal/app/dto/feed_dto.go b/internal/app/dto/feed_dto.go index ef1a777..1480ec9 100644 --- a/internal/app/dto/feed_dto.go +++ b/internal/app/dto/feed_dto.go @@ -1,5 +1,7 @@ package dto +import "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + // ============================================================================================================= // 营养种类 (Nutrient) 相关 DTO // ============================================================================================================= @@ -335,3 +337,14 @@ type GenerateRecipeResponse struct { Name string `json:"name"` // 新生成的配方名称 Description string `json:"description"` // 新生成的配方描述 } + +// ReviewRecipeRequest 定义了点评配方的请求体 +type ReviewRecipeRequest struct { + PigTypeID uint32 `json:"pig_type_id" binding:"required"` // 猪类型ID +} + +// ReviewRecipeResponse 定义了点评配方的响应体 +type ReviewRecipeResponse struct { + ReviewMessage string `json:"review_message"` // 点评内容 + AIModel models.AIModel `json:"ai_model"` // 使用的 AI 模型 +} diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index 1e86f46..9ce1fa4 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -29,6 +29,8 @@ type RecipeService interface { GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) // GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料 GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) + // AIDiagnoseRecipe 智能诊断配方, 返回智能诊断结果和使用的AI模型 + AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (*dto.ReviewRecipeResponse, error) } // recipeServiceImpl 是 RecipeService 接口的实现 @@ -175,3 +177,18 @@ func (s *recipeServiceImpl) ListRecipes(ctx context.Context, req *dto.ListRecipe return dto.ConvertRecipeListToDTO(recipes, total, req.Page, req.PageSize), nil } + +// AIDiagnoseRecipe 实现智能诊断配方的方法 +func (s *recipeServiceImpl) AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (*dto.ReviewRecipeResponse, error) { + serviceCtx := logs.AddFuncName(ctx, s.ctx, "AIDiagnoseRecipe") + + reviewMessage, aiModel, err := s.recipeSvc.AIDiagnoseRecipe(serviceCtx, recipeID, pigTypeID) + if err != nil { + return nil, fmt.Errorf("AI 诊断配方失败: %w", err) + } + + return &dto.ReviewRecipeResponse{ + ReviewMessage: reviewMessage, + AIModel: aiModel, + }, nil +} diff --git a/internal/core/component_initializers.go b/internal/core/component_initializers.go index bb29f09..3bd7de4 100644 --- a/internal/core/component_initializers.go +++ b/internal/core/component_initializers.go @@ -15,6 +15,7 @@ import ( "git.huangwc.com/pig/pig-farm-controller/internal/domain/plan" "git.huangwc.com/pig/pig-farm-controller/internal/domain/recipe" "git.huangwc.com/pig/pig-farm-controller/internal/domain/task" + infra_ai "git.huangwc.com/pig/pig-farm-controller/internal/infra/ai" "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" "git.huangwc.com/pig/pig-farm-controller/internal/infra/database" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" @@ -34,6 +35,7 @@ type Infrastructure struct { storage database.Storage repos *Repositories lora *LoraComponents + aiManager infra_ai.AI tokenGenerator token.Generator } @@ -53,10 +55,17 @@ func initInfrastructure(ctx context.Context, cfg *config.Config) (*Infrastructur tokenGenerator := token.NewTokenGenerator([]byte(cfg.App.JWTSecret)) + // 初始化 AI + aiManager, err := infra_ai.NewGeminiAI(logs.AddCompName(ctx, "GeminiAI"), &cfg.AI) + if err != nil { + return nil, fmt.Errorf("初始化 AI 管理器失败: %w", err) + } + return &Infrastructure{ storage: storage, repos: repos, lora: lora, + aiManager: aiManager, tokenGenerator: tokenGenerator, }, nil } @@ -238,6 +247,8 @@ func initDomainServices(ctx context.Context, cfg *config.Config, infra *Infrastr pigTypeService, recipeCoreService, recipeGenerateManager, + infra.repos.recipeRepo, + infra.aiManager, ) return &DomainServices{ diff --git a/internal/domain/recipe/recipe_service.go b/internal/domain/recipe/recipe_service.go index 04056a9..17a83af 100644 --- a/internal/domain/recipe/recipe_service.go +++ b/internal/domain/recipe/recipe_service.go @@ -2,8 +2,11 @@ package recipe import ( "context" + "encoding/json" "fmt" + "strings" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/ai" "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" "git.huangwc.com/pig/pig-farm-controller/internal/infra/repository" @@ -22,6 +25,8 @@ type Service interface { GenerateRecipeWithAllRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) // GenerateRecipeWithPrioritizedStockRawMaterials 生成新配方,优先使用有库存的原料 GenerateRecipeWithPrioritizedStockRawMaterials(ctx context.Context, pigTypeID uint32) (*models.Recipe, error) + // AIDiagnoseRecipe 智能诊断配方, 返回智能诊断结果 + AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (string, models.AIModel, error) } // recipeServiceImpl 是 Service 的实现,通过组合各个子服务来实现 @@ -34,6 +39,9 @@ type recipeServiceImpl struct { PigTypeService RecipeCoreService RecipeGenerateManager + + recipeRepo repository.RecipeRepository + ai ai.AI } // NewRecipeService 创建一个新的 Service 实例 @@ -46,6 +54,8 @@ func NewRecipeService( pigTypeService PigTypeService, recipeCoreService RecipeCoreService, recipeGenerateManager RecipeGenerateManager, + recipeRepo repository.RecipeRepository, + ai ai.AI, ) Service { return &recipeServiceImpl{ ctx: ctx, @@ -56,6 +66,8 @@ func NewRecipeService( PigTypeService: pigTypeService, RecipeCoreService: recipeCoreService, RecipeGenerateManager: recipeGenerateManager, + recipeRepo: recipeRepo, + ai: ai, } } @@ -225,3 +237,113 @@ func (r *recipeServiceImpl) GenerateRecipeWithPrioritizedStockRawMaterials(ctx c // 7. 返回创建的配方 return recipe, nil } + +// AIDiagnoseRecipe 使用 AI 为指定食谱生成诊断。 +func (s *recipeServiceImpl) AIDiagnoseRecipe(ctx context.Context, recipeID uint32, pigTypeID uint32) (string, models.AIModel, error) { + serviceCtx, logger := logs.Trace(ctx, context.Background(), "AIDiagnoseRecipe") + + // 1. 根据 recipeID 获取配方详情 + recipe, err := s.recipeRepo.GetRecipeByID(serviceCtx, recipeID) + if err != nil { + logger.Errorf("获取配方详情失败: %v", err) + return "", s.ai.AIModel(), fmt.Errorf("获取配方详情失败: %w", err) + } + if recipe == nil { + logger.Warnf("未找到配方,ID: %d", recipeID) + return "", s.ai.AIModel(), fmt.Errorf("未找到配方,ID: %d", recipeID) + } + + // 2. 获取目标猪只类型信息 + pigType, err := s.GetPigTypeByID(serviceCtx, pigTypeID) + if err != nil { + logger.Errorf("获取猪只类型信息失败: %v", err) + return "", s.ai.AIModel(), fmt.Errorf("获取猪只类型信息失败: %w", err) + } + if pigType == nil { + logger.Warnf("未找到猪只类型,ID: %d", pigTypeID) + return "", s.ai.AIModel(), fmt.Errorf("未找到猪只类型,ID: %d", pigTypeID) + } + + // 3. 定义 AI 输入结构体 + type ingredientNutrient struct { + NutrientName string `json:"nutrient_name"` + Value float32 `json:"value"` + } + + type recipeIngredient struct { + RawMaterialName string `json:"raw_material_name"` + Percentage float32 `json:"percentage"` + Nutrients []ingredientNutrient `json:"nutrients"` + } + + type aiDiagnosisInput struct { + RecipeName string `json:"recipe_name"` + TargetPigType struct { + Name string `json:"name"` + } `json:"target_pig_type"` + Ingredients []recipeIngredient `json:"ingredients"` + } + + // 4. 填充 AI 输入结构体 + input := aiDiagnosisInput{ + RecipeName: recipe.Name, + } + + input.TargetPigType.Name = fmt.Sprintf("%s-%s", pigType.Breed.Name, pigType.AgeStage.Name) + + for _, ingredient := range recipe.RecipeIngredients { + if ingredient.RawMaterial.ID == 0 { + logger.Warnf("配方成分中存在未加载的原料信息,RecipeIngredientID: %d", ingredient.ID) + continue + } + ing := recipeIngredient{ + RawMaterialName: ingredient.RawMaterial.Name, + Percentage: ingredient.Percentage, + } + for _, rmn := range ingredient.RawMaterial.RawMaterialNutrients { + if rmn.Nutrient.ID == 0 { + logger.Warnf("原料营养成分中存在未加载的营养素信息,RawMaterialNutrientID: %d", rmn.ID) + continue + } + ing.Nutrients = append(ing.Nutrients, ingredientNutrient{ + NutrientName: rmn.Nutrient.Name, + Value: rmn.Value, + }) + } + input.Ingredients = append(input.Ingredients, ing) + } + + // 5. 序列化为 JSON 字符串 + jsonBytes, err := json.Marshal(input) + if err != nil { + logger.Errorf("序列化配方和猪只类型信息为 JSON 失败: %v", err) + return "", s.ai.AIModel(), fmt.Errorf("序列化数据失败: %w", err) + } + jsonString := string(jsonBytes) + + // 6. 构建 AI Prompt + var promptBuilder strings.Builder + promptBuilder.WriteString(` + 你是一个专业的动物营养师。请根据以下猪饲料配方数据,生成一份详细的、对养殖户友好的说明报告。 + 说明报告应包括以下部分: + 1. 诊断猪只配方是否合理,如合理需要说明为什么合理, 如不合理需给出详细的改进建议。 + 2. 关键成分分析:分析主要原料和营养成分的作用 + 3. 使用建议:提供使用此配方的最佳实践和注意事项。 + \n`) + promptBuilder.WriteString("```") + promptBuilder.WriteString(jsonString) + promptBuilder.WriteString("```") + prompt := promptBuilder.String() + + logger.Debugf("生成的 AI 诊断 Prompt: \n%s", prompt) + + // 7. 调用 AI Manager 进行诊断 + diagnosisResult, err := s.ai.GenerateReview(serviceCtx, prompt) + if err != nil { + logger.Errorf("调用 AI Manager 诊断配方失败: %v", err) + return "", s.ai.AIModel(), fmt.Errorf("AI 诊断失败: %w", err) + } + + logger.Infof("成功对配方 ID: %d (目标猪只类型 ID: %d) 进行 AI 诊断。", recipeID, pigTypeID) + return diagnosisResult, s.ai.AIModel(), nil +} diff --git a/internal/infra/ai/ai.go b/internal/infra/ai/ai.go new file mode 100644 index 0000000..2c8c986 --- /dev/null +++ b/internal/infra/ai/ai.go @@ -0,0 +1,19 @@ +package ai + +import ( + "context" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" +) + +// AI 定义了通用的 AI 管理接口。 +// 它可以用于处理各种 AI 相关的任务,例如文本生成、内容审核等。 +type AI interface { + // GenerateReview 根据提供的文本内容生成评论。 + // prompt: 用于生成评论的输入文本。 + // 返回生成的评论字符串和可能发生的错误。 + GenerateReview(ctx context.Context, prompt string) (string, error) + + // AIModel 返回当前使用的 AI 模型。 + AIModel() models.AIModel +} diff --git a/internal/infra/ai/gemini.go b/internal/infra/ai/gemini.go new file mode 100644 index 0000000..4881eef --- /dev/null +++ b/internal/infra/ai/gemini.go @@ -0,0 +1,73 @@ +package ai + +import ( + "context" + "fmt" + "time" + + "git.huangwc.com/pig/pig-farm-controller/internal/infra/config" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/logs" + "git.huangwc.com/pig/pig-farm-controller/internal/infra/models" + + "github.com/google/generative-ai-go/genai" + "google.golang.org/api/option" +) + +// geminiImpl 是 Gemini AI 服务的实现。 +type geminiImpl struct { + client *genai.GenerativeModel + cfg *config.Gemini +} + +// NewGeminiAI 创建一个新的 geminiImpl 实例。 +func NewGeminiAI(ctx context.Context, cfg *config.AIConfig) (AI, error) { + // 检查 API Key 是否存在 + if cfg.Gemini.APIKey == "" { + return nil, fmt.Errorf("Gemini API Key 未配置") + } + + // 创建 Gemini 客户端 + genaiClient, err := genai.NewClient(ctx, option.WithAPIKey(cfg.Gemini.APIKey)) + if err != nil { + return nil, fmt.Errorf("创建 Gemini 客户端失败: %w", err) + } + + return &geminiImpl{ + client: genaiClient.GenerativeModel(cfg.Gemini.ModelName), + cfg: &cfg.Gemini, + }, nil +} + +// GenerateReview 根据提供的文本内容生成评论。 +func (g *geminiImpl) GenerateReview(ctx context.Context, prompt string) (string, error) { + serviceCtx, logger := logs.Trace(ctx, context.Background(), "GenerateReview") + logger.Debugf("开始调用 Gemini 生成评论,prompt: %s", prompt) + + timeoutCtx, cancel := context.WithTimeout(serviceCtx, time.Duration(g.cfg.Timeout)*time.Second) + defer cancel() + + resp, err := g.client.GenerateContent(timeoutCtx, genai.Text(prompt)) + if err != nil { + logger.Errorf("调用 Gemini API 失败: %v", err) + return "", fmt.Errorf("调用 Gemini API 失败: %w", err) + } + + if resp == nil || len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 { + logger.Warn("Gemini API 返回空内容或无候选评论") + return "", fmt.Errorf("Gemini API 返回空内容或无候选评论") + } + + var review string + for _, part := range resp.Candidates[0].Content.Parts { + if txt, ok := part.(genai.Text); ok { + review += string(txt) + } + } + + logger.Debugf("成功从 Gemini 生成评论: %s", review) + return review, nil +} + +func (g *geminiImpl) AIModel() models.AIModel { + return models.AI_MODEL_GEMINI +} diff --git a/internal/infra/config/config.go b/internal/infra/config/config.go index 2ac4cd1..a05ebbc 100644 --- a/internal/infra/config/config.go +++ b/internal/infra/config/config.go @@ -236,11 +236,14 @@ type AlarmNotificationConfig struct { // AIConfig AI 服务配置 type AIConfig struct { - Gemini struct { - APIKey string `yaml:"api_key"` // Gemini API Key - ModelName string `yaml:"model_name"` // Gemini 模型名称,例如 "gemini-pro" - Timeout int `yaml:"timeout"` // AI 请求超时时间 (秒) - } `yaml:"gemini"` + Gemini Gemini `yaml:"gemini"` +} + +// Gemini 代表 Gemini AI 服务的配置 +type Gemini struct { + APIKey string `yaml:"api_key"` // Gemini API Key + ModelName string `yaml:"model_name"` // Gemini 模型名称,例如 "gemini-pro" + Timeout int `yaml:"timeout"` // AI 请求超时时间 (秒) } // NewConfig 创建并返回一个新的配置实例 diff --git a/internal/infra/logs/logs.go b/internal/infra/logs/logs.go index 683bbb2..f991dcf 100644 --- a/internal/infra/logs/logs.go +++ b/internal/infra/logs/logs.go @@ -66,8 +66,8 @@ func NewLogger(cfg config.LogConfig) *Logger { // 5. 构建 Logger // zap.AddCaller() 会记录调用日志的代码行 - // zap.AddCallerSkip(1) 可以向上跳一层调用栈,如果我们将 logger.Info 等方法再封装一层,这个选项会很有用 - zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)) + // zap.AddCallerSkip(2) 可以向上跳两层调用栈,因为我们的日志方法被封装了两层 (Logger.Info -> Logger.logWithTrace) + zapLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(2)) return &Logger{sl: zapLogger.Sugar()} } diff --git a/internal/infra/models/models.go b/internal/infra/models/models.go index 3caeb56..28e61c6 100644 --- a/internal/infra/models/models.go +++ b/internal/infra/models/models.go @@ -12,6 +12,12 @@ import ( "gorm.io/gorm" ) +type AIModel string + +const ( + AI_MODEL_GEMINI AIModel = "Gemini" +) + // Model 用于代替gorm.Model, 使用uint32以节约空间 type Model struct { ID uint32 `gorm:"primarykey"`