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;
public const RULE_THRESHOLD = 0.9;
public const SIMILARITY_THRESHOLD = 0.7;
+ private const EXCLUDED_TYPES = [
+ '[букет]',
+ '[сборка]',
+ 'Матрица',
+ 'Авторский букет (вид с сериями)',
+ 'Маркетплейсы',
+ 'Не использовать',
+ 'Статьи затрат',
+ 'Основные средства',
+ 'Комиссия',
+ 'Услуга',
+ 'Доставка по России',
+ 'Транспорт Амстердам',
+ 'Сырье, материалы',
+ ];
+
private RuleBasedParser $ruleParser;
private SimilarityMatcher $matcher;
$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 = '';
}
$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;
}
$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();
->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();
+ }
}