From: fomichev Date: Thu, 30 Apr 2026 15:23:56 +0000 (+0300) Subject: feat(ERP-292): AutoMarkService — llmBatchVerify, getPendingUnverifiedCount X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=a9d92d6fc3a94d15ce8aa2261d62aced61af119a;p=erp24_rep%2Fyii-erp24%2F.git feat(ERP-292): AutoMarkService — llmBatchVerify, getPendingUnverifiedCount --- diff --git a/erp24/services/AutoMarkService.php b/erp24/services/AutoMarkService.php index fe5985dc..b7b8c05b 100644 --- a/erp24/services/AutoMarkService.php +++ b/erp24/services/AutoMarkService.php @@ -8,6 +8,7 @@ use Yii; use yii_app\records\Products1c; use yii_app\records\Products1cAutomarkPrediction; use yii_app\records\Products1cNomenclature; +use yii_app\services\automark\LlmVerifier; use yii_app\services\automark\ParseResult; use yii_app\services\automark\RuleBasedParser; use yii_app\services\automark\SimilarityMatcher; @@ -17,6 +18,22 @@ class AutoMarkService public const RULE_THRESHOLD = 0.9; public const SIMILARITY_THRESHOLD = 0.7; + private const EXCLUDED_TYPES = [ + '[букет]', + '[сборка]', + 'Матрица', + 'Авторский букет (вид с сериями)', + 'Маркетплейсы', + 'Не использовать', + 'Статьи затрат', + 'Основные средства', + 'Комиссия', + 'Услуга', + 'Доставка по России', + 'Транспорт Амстердам', + 'Сырье, материалы', + ]; + private RuleBasedParser $ruleParser; private SimilarityMatcher $matcher; @@ -72,9 +89,9 @@ class AutoMarkService $transaction->rollBack(); return false; } - $nomenclature = new Products1cNomenclature(); - $nomenclature->id = $prediction->product_id; - $nomenclature->name = $product->name; + $nomenclature = new Products1cNomenclature(); + $nomenclature->id = $prediction->product_id; + $nomenclature->name = $product->name; $nomenclature->location = ''; $nomenclature->type_num = ''; } @@ -87,7 +104,13 @@ class AutoMarkService $nomenclature->size = $prediction->size ?? $nomenclature->size; $nomenclature->color = $prediction->color ?? $nomenclature->color; - if (!$nomenclature->save()) { + $nomenclature->classification_status = 'classified'; + $nomenclature->confidence = (int) round($prediction->confidence * 100); + $nomenclature->classified_by = $prediction->approved_by; + $nomenclature->classified_at = date('Y-m-d H:i:s'); + + // save(false): location/type_num могут быть '', required-валидатор для форм здесь неприменим + if (!$nomenclature->save(false)) { $transaction->rollBack(); return false; } @@ -130,12 +153,36 @@ class AutoMarkService $corpus = $this->loadCorpus(); $simResult = $this->matcher->findBestMatch($name, $corpus); if ($simResult !== null && $simResult->confidence >= self::SIMILARITY_THRESHOLD) { - return $simResult; + return $this->mergeRuleIntoSim($ruleResult, $simResult); } return $ruleResult ?? $simResult; } + /** + * Заполнить пустые поля similarity-результата данными из rule-парсера. + * Corpus может содержать товары с частично пустой номенклатурой — + * rule-парсер извлекает size/color напрямую из имени нового товара. + */ + private function mergeRuleIntoSim(?ParseResult $rule, ParseResult $sim): ParseResult + { + if ($rule === null) { + return $sim; + } + + return new ParseResult( + category: $sim->category ?? $rule->category, + subcategory: $sim->subcategory ?? $rule->subcategory, + species: $sim->species ?? $rule->species, + sort: $sim->sort ?? $rule->sort, + type: $sim->type ?? $rule->type, + size: ($sim->size === null || $sim->size === 0) ? $rule->size : $sim->size, + color: $sim->color ?? $rule->color, + confidence: $sim->confidence, + method: $sim->method, + ); + } + private function savePrediction(string $productId, ParseResult $result): ?Products1cAutomarkPrediction { $model = new Products1cAutomarkPrediction(); @@ -170,6 +217,33 @@ class AutoMarkService ->leftJoin('products_1c_nomenclature', 'products_1c.id = products_1c_nomenclature.id') ->where(['products_1c_nomenclature.id' => null]) ->andWhere(['products_1c.tip' => Products1c::TYPE_PRODUCTS]) + ->andWhere(['OR', + ['products_1c.type' => null], + ['not in', 'products_1c.type', self::EXCLUDED_TYPES], + ]) ->column(); } + + /** + * Верифицировать pending-предсказания через LLM. + * + * @return int Количество верифицированных предсказаний + */ + public function llmBatchVerify(): int + { + $batchSize = (int)(getenv('LLM_BATCH_SIZE') ?: 20); + $verifier = new LlmVerifier($batchSize); + return $verifier->verifyPending(); + } + + /** + * Количество pending-предсказаний, ещё не прошедших LLM-верификацию. + */ + public function getPendingUnverifiedCount(): int + { + return (int) Products1cAutomarkPrediction::find() + ->where(['status' => Products1cAutomarkPrediction::STATUS_PENDING]) + ->andWhere(['llm_verdict' => null]) + ->count(); + } }