Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3366204
Add feature roadmap design document
DavidLambauer Apr 14, 2026
8e25f93
Add Phase 1 implementation plan
DavidLambauer Apr 14, 2026
0505daf
fix: add explicit Magento module dependencies, remove mirror repo (#22)
DavidLambauer Apr 14, 2026
41871dd
fix: correct temperature label, add range comments to advanced fields…
DavidLambauer Apr 14, 2026
ba77c7f
style: apply PSR-12 code style fixes (#23)
DavidLambauer Apr 14, 2026
9175b7f
feat: add attribute column select renderer for dynamic rows (#27)
DavidLambauer Apr 14, 2026
68eb53c
feat: add dynamic rows field array block for product attributes (#27)
DavidLambauer Apr 14, 2026
636d716
feat: add default dynamic row values for all 5 enrichable attributes …
DavidLambauer Apr 14, 2026
6ba267f
feat: replace static product fields with dynamic rows config (#27)
DavidLambauer Apr 14, 2026
57f87c4
feat: update Config to read attribute prompts from dynamic rows (#27)
DavidLambauer Apr 14, 2026
f06f09d
feat: Enricher reads attribute list from dynamic config (#27)
DavidLambauer Apr 14, 2026
b054e03
feat: add data patch to migrate old product prompts to dynamic rows (…
DavidLambauer Apr 14, 2026
f79ee52
Add Phase 2 implementation plan
DavidLambauer Apr 14, 2026
08c751f
feat: add storeId to Request DTO for store-scoped enrichment (#12)
DavidLambauer Apr 14, 2026
061cce9
feat: Publisher passes storeId to queue messages (#12)
DavidLambauer Apr 14, 2026
9df0899
feat: Consumer sets store scope from queue message (#12)
DavidLambauer Apr 14, 2026
0be6417
feat: SaveAfter observer passes storeId to publisher (#12)
DavidLambauer Apr 14, 2026
573dd99
feat: MassEnrich passes current store scope to publisher (#12)
DavidLambauer Apr 14, 2026
8022545
feat: add enrichment log DB table schema (#48)
DavidLambauer Apr 14, 2026
cfa472d
feat: locale-aware system prompt prepends language instruction (#12)
DavidLambauer Apr 14, 2026
e402540
feat: add Magento_Store to module sequence (#12)
DavidLambauer Apr 14, 2026
fa83a07
feat: add EnrichmentLog model, resource model, and collection (#48)
DavidLambauer Apr 14, 2026
81eb98c
feat: add EnrichmentLogger service for tracking AI-generated attribut…
DavidLambauer Apr 14, 2026
4f74fbf
feat: detect manual edits to enriched attributes, mark as modified (#48)
DavidLambauer Apr 14, 2026
da13f65
feat: Enricher logs enrichment status after successful AI generation …
DavidLambauer Apr 14, 2026
7c09815
feat: add enrichment status UI modifier for product edit form (#48)
DavidLambauer Apr 14, 2026
d635571
fix: update EnricherTest for new EnrichmentLogger constructor param
DavidLambauer Apr 14, 2026
c704d6b
Add Phase 3 implementation plan
DavidLambauer Apr 14, 2026
d8270fc
feat: update config.xml defaults to new ai_integration_enrichment pat…
DavidLambauer Apr 14, 2026
ef2c6c0
feat: move config to AI Services tab (#43)
DavidLambauer Apr 14, 2026
df86407
feat: add data patch to migrate config from catalog_ai to ai_integrat…
DavidLambauer Apr 14, 2026
b1c0d26
feat: update Config paths, ACL, and sensitive config to new section (…
DavidLambauer Apr 14, 2026
ce80f26
feat: add prompt_rule DB table schema (#32)
DavidLambauer Apr 14, 2026
de2da89
feat: add PromptRule model with conditions support (#32)
DavidLambauer Apr 14, 2026
0fb933a
feat: add PromptResolver service with priority-based rule matching (#32)
DavidLambauer Apr 14, 2026
370061e
feat: Enricher uses PromptResolver for rule-based prompt selection (#32)
DavidLambauer Apr 14, 2026
1bfc47a
feat: add prompt rules admin grid with listing and menu (#32)
DavidLambauer Apr 14, 2026
1a90f29
feat: add prompt rule edit form with CRUD controllers (#32)
DavidLambauer Apr 14, 2026
77230cb
feat: add mass delete controller for prompt rules (#32)
DavidLambauer Apr 14, 2026
eaa5571
feat: add preview/test controller for prompt rules (#32)
DavidLambauer Apr 14, 2026
7a84e70
feat: add conditions fieldset modifier for prompt rule form (#32)
DavidLambauer Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->name('*.php');

return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'no_unused_imports' => true,
'single_quote' => true,
'no_trailing_whitespace' => true,
'no_whitespace_in_blank_line' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
])
->setFinder($finder);
25 changes: 25 additions & 0 deletions Api/Data/PromptRuleInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);

namespace MageOS\CatalogDataAI\Api\Data;

interface PromptRuleInterface
{
public const RULE_ID = 'rule_id';
public const NAME = 'name';
public const ATTRIBUTE_CODE = 'attribute_code';
public const STORE_IDS = 'store_ids';
public const CONDITIONS_SERIALIZED = 'conditions_serialized';
public const PROMPT = 'prompt';
public const PRIORITY = 'priority';
public const IS_ACTIVE = 'is_active';

public function getRuleId(): ?int;
public function getName(): string;
public function getAttributeCode(): string;
public function getStoreIds(): string;
public function getConditionsSerialized(): ?string;
public function getPrompt(): string;
public function getPriority(): int;
public function getIsActive(): bool;
}
8 changes: 7 additions & 1 deletion Api/RequestInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
interface RequestInterface
{
/**
* Retrieve products id.
* Retrieve product id.
* @return int
*/
public function getId(): int;
Expand All @@ -16,4 +16,10 @@ public function getId(): int;
* @return bool
*/
public function getOverwrite(): bool;

/**
* Retrieve store id.
* @return int
*/
public function getStoreId(): int;
}
68 changes: 68 additions & 0 deletions Block/Adminhtml/Form/Field/AttributeColumn.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);

namespace MageOS\CatalogDataAI\Block\Adminhtml\Form\Field;

use Magento\Catalog\Api\ProductAttributeRepositoryInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\View\Element\Context;
use Magento\Framework\View\Element\Html\Select;

class AttributeColumn extends Select
{
private array $attributeOptions = [];

public function __construct(
private readonly ProductAttributeRepositoryInterface $attributeRepository,
private readonly SearchCriteriaBuilder $searchCriteriaBuilder,
?Context $context = null,
array $data = []
) {
if ($context !== null) {
parent::__construct($context, $data);
}
}

public function getOptions(): array
{
if (empty($this->attributeOptions)) {
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('frontend_input', ['text', 'textarea'], 'in')
->create();

$attributes = $this->attributeRepository->getList($searchCriteria);

foreach ($attributes->getItems() as $attribute) {
$this->attributeOptions[$attribute->getAttributeCode()] = $attribute->getDefaultFrontendLabel()
?? $attribute->getAttributeCode();
}

asort($this->attributeOptions);
}

return $this->attributeOptions;
}

public function setInputName(string $value): self
{
return $this->setName($value);
}

public function setInputId(string $value): self
{
return $this->setId($value);
}

public function _toHtml(): string
{
if (!$this->getOptions()) {
$this->setOptions($this->getOptions());
}

foreach ($this->getOptions() as $code => $label) {
$this->addOption($code, $label);
}

return parent::_toHtml();
}
}
62 changes: 62 additions & 0 deletions Block/Adminhtml/Form/Field/ProductAttributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);

namespace MageOS\CatalogDataAI\Block\Adminhtml\Form\Field;

use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;
use Magento\Framework\DataObject;
use Magento\Framework\Exception\LocalizedException;

class ProductAttributes extends AbstractFieldArray
{
private ?AttributeColumn $attributeRenderer = null;

protected function _prepareToRender(): void
{
$this->addColumn('attribute', [
'label' => __('Attribute'),
'renderer' => $this->getAttributeRenderer(),
'class' => 'required-entry',
]);
$this->addColumn('prompt', [
'label' => __('Prompt'),
'class' => 'required-entry',
]);
$this->addColumn('enabled', [
'label' => __('Enabled'),
'class' => 'required-entry',
]);

$this->_addAfter = false;
$this->_addButtonLabel = __('Add Attribute');
}

protected function _prepareArrayRow(DataObject $row): void
{
$options = [];
$attribute = $row->getData('attribute');

if ($attribute !== null) {
$key = 'option_' . $this->getAttributeRenderer()->calcOptionHash($attribute);
$options[$key] = 'selected="selected"';
}

$row->setData('option_extra_attrs', $options);
}

/**
* @throws LocalizedException
*/
private function getAttributeRenderer(): AttributeColumn
{
if ($this->attributeRenderer === null) {
$this->attributeRenderer = $this->getLayout()->createBlock(
AttributeColumn::class,
'',
['data' => ['is_render_to_js_template' => true]]
);
}

return $this->attributeRenderer;
}
}
36 changes: 36 additions & 0 deletions Block/Adminhtml/PromptRule/Edit/DeleteButton.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);

namespace MageOS\CatalogDataAI\Block\Adminhtml\PromptRule\Edit;

use Magento\Framework\App\RequestInterface;
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class DeleteButton implements ButtonProviderInterface
{
public function __construct(
private readonly RequestInterface $request,
private readonly UrlInterface $urlBuilder
) {
}

public function getButtonData(): array
{
$ruleId = (int)$this->request->getParam('rule_id');
if (!$ruleId) {
return [];
}

return [
'label' => __('Delete Rule'),
'class' => 'delete',
'on_click' => sprintf(
"deleteConfirm('%s', '%s', {data: {}})",
__('Are you sure you want to delete this rule?'),
$this->urlBuilder->getUrl('*/*/delete', ['rule_id' => $ruleId])
),
'sort_order' => 20,
];
}
}
22 changes: 22 additions & 0 deletions Block/Adminhtml/PromptRule/Edit/SaveButton.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);

namespace MageOS\CatalogDataAI\Block\Adminhtml\PromptRule\Edit;

use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class SaveButton implements ButtonProviderInterface
{
public function getButtonData(): array
{
return [
'label' => __('Save Rule'),
'class' => 'save primary',
'data_attribute' => [
'mage-init' => ['button' => ['event' => 'save']],
'form-role' => 'save',
],
'sort_order' => 90,
];
}
}
13 changes: 8 additions & 5 deletions Controller/Adminhtml/Product/MassEnrich.php
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<?php

declare(strict_types=1);

namespace MageOS\CatalogDataAI\Controller\Adminhtml\Product;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Backend\Model\View\Result\Redirect;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Backend\App\Action;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Exception\LocalizedException;
use Magento\Ui\Component\MassAction\Filter;
use MageOS\CatalogDataAI\Model\Config;
use Magento\Store\Model\StoreManagerInterface;
use MageOS\CatalogDataAI\Model\Product\Publisher;

class MassEnrich extends Action implements HttpPostActionInterface
Expand All @@ -26,6 +28,7 @@ public function __construct(
private readonly Config $config,
private readonly Publisher $publisher,
private readonly ProductRepositoryInterface $productRepository,
private readonly StoreManagerInterface $storeManager,
) {
parent::__construct($context);
}
Expand All @@ -41,11 +44,12 @@ public function execute(): Redirect
$collection = $this->filter->getCollection($this->collectionFactory->create());

$productEnriched = 0;
if($this->config->isEnabled()) {
$storeId = (int)$this->storeManager->getStore()->getId();
if ($this->config->isEnabled()) {
/** @var Product $product */
foreach ($collection->getItems() as $product) {
//@TODO: we hit rate limit, change to batching the request
$this->publisher->execute($product->getId(), $this->overwrite);
$this->publisher->execute($product->getId(), $this->overwrite, $storeId);
$productEnriched++;
}

Expand All @@ -54,8 +58,7 @@ public function execute(): Redirect
__('A total of %1 record(s) are scheduled to get data enriched.', $productEnriched)
);
}
}
else {
} else {
$this->messageManager->addErrorMessage(
__('Data enrichment is disabled. Please enable it in the configuration.')
);
Expand Down
3 changes: 1 addition & 2 deletions Controller/Adminhtml/Product/MassEnrichSafe.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<?php

declare(strict_types=1);

namespace MageOS\CatalogDataAI\Controller\Adminhtml\Product;

use MageOS\CatalogDataAI\Controller\Adminhtml\Product\MassEnrich;

class MassEnrichSafe extends MassEnrich
{
protected $overwrite = false;
Expand Down
51 changes: 51 additions & 0 deletions Controller/Adminhtml/PromptRule/Delete.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);

namespace MageOS\CatalogDataAI\Controller\Adminhtml\PromptRule;

use Magento\Backend\App\Action;
use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\Controller\Result\Redirect;
use MageOS\CatalogDataAI\Model\PromptRuleFactory;
use MageOS\CatalogDataAI\Model\ResourceModel\PromptRule as PromptRuleResource;

class Delete extends Action implements HttpGetActionInterface
{
public const ADMIN_RESOURCE = 'MageOS_CatalogDataAI::prompt_rules';

public function __construct(
Action\Context $context,
private readonly PromptRuleFactory $ruleFactory,
private readonly PromptRuleResource $ruleResource
) {
parent::__construct($context);
}

public function execute(): Redirect
{
$ruleId = (int)$this->getRequest()->getParam('rule_id');
$redirect = $this->resultRedirectFactory->create()->setPath('*/*/');

if (!$ruleId) {
$this->messageManager->addErrorMessage(__('Rule ID is required.'));
return $redirect;
}

$rule = $this->ruleFactory->create();
$this->ruleResource->load($rule, $ruleId);

if (!$rule->getRuleId()) {
$this->messageManager->addErrorMessage(__('This rule no longer exists.'));
return $redirect;
}

try {
$this->ruleResource->delete($rule);
$this->messageManager->addSuccessMessage(__('The rule has been deleted.'));
} catch (\Exception $e) {
$this->messageManager->addErrorMessage($e->getMessage());
}

return $redirect;
}
}
Loading