---
title: Modulo IA — Generador de contenido con OpenAI/LLM
section: examples
slug: ai-content-generator
description: "Ejemplo completo de modulo de IA para PrestaShop: cliente LLM, generacion AJAX de contenido, tab en producto, batch cron, logs y configuracion multi-proveedor."
last_updated: 2026-04
source_url: "https://ayudaprestashop.es/examples/ai-content-generator"
---

# Modulo IA — Generador de contenido con OpenAI/LLM

> Ejemplo completo de modulo de IA para PrestaShop: cliente LLM, generacion AJAX de contenido, tab en producto, batch cron, logs y configuracion multi-proveedor.

> **[I] Ejemplo basado en codigo real**
>
> Este ejemplo esta basado en el modulo ecom_omnimind, un modulo de produccion que usa OpenAI/OpenRouter para generar descripciones de producto, meta tags SEO, FAQs, bullet points y traducciones automaticas.

## Patron: modulo de IA para ecommerce

Los modulos de IA en PrestaShop siguen un patron comun: conectarse a una API de LLM (OpenAI, Anthropic, OpenRouter...), enviar el contexto del producto, y recibir contenido generado. Este patron incluye:


## Arquitectura del modulo

*Estructura de archivos*

```text
ecom_omnimind/
├── ecom_omnimind.php            # Clase principal + hooks
├── classes/
│   ├── LLMClient.php            # Cliente API generico
│   ├── PromptBuilder.php        # Construye prompts con contexto PS
│   ├── ContentLog.php           # ObjectModel para logs
│   └── TokenCounter.php         # Conteo de tokens
├── controllers/admin/
│   ├── AdminOmniMindDashboardController.php
│   ├── AdminOmniMindProductTabController.php
│   └── AdminOmniMindAjaxController.php  # AJAX endpoint
├── views/
│   ├── templates/admin/
│   │   ├── product_tab.tpl      # Tab en ficha producto
│   │   └── dashboard.tpl
│   └── js/
│       └── product-ai.js        # JS para generacion AJAX
└── config.xml
```

## Cliente API para OpenAI / LLM

*classes/LLMClient.php*

```php
<?php
if (!defined('_PS_VERSION_')) { exit; }

class LLMClient
{
    private string $apiKey;
    private string $apiUrl;
    private string $model;
    private float $temperature;

    public function __construct()
    {
        $this->apiKey     = Configuration::get('OMNIMIND_API_KEY');
        $this->apiUrl     = Configuration::get('OMNIMIND_API_URL') ?: 'https://api.openai.com/v1';
        $this->model      = Configuration::get('OMNIMIND_MODEL') ?: 'gpt-4o-mini';
        $this->temperature = (float) (Configuration::get('OMNIMIND_TEMPERATURE') ?: 0.7);
    }

    /**
     * Enviar prompt al LLM y obtener respuesta
     */
    public function generate(string $systemPrompt, string $userPrompt, ?int $maxTokens = 2000): ?string
    {
        $payload = [
            'model'       => $this->model,
            'messages'    => [
                ['role' => 'system', 'content' => $systemPrompt],
                ['role' => 'user',   'content' => $userPrompt],
            ],
            'temperature' => $this->temperature,
            'max_tokens'  => $maxTokens,
        ];

        $ch = curl_init($this->apiUrl . '/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->apiKey,
            ],
            CURLOPT_TIMEOUT        => 60, // LLMs pueden tardar
            CURLOPT_CONNECTTIMEOUT => 10,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error    = curl_error($ch);
        curl_close($ch);

        if ($httpCode !== 200 || $error) {
            PrestaShopLogger::addLog(
                'LLMClient error: HTTP ' . $httpCode . ' - ' . $error,
                3, null, 'LLMClient'
            );
            return null;
        }

        $data = json_decode($response, true);
        return $data['choices'][0]['message']['content'] ?? null;
    }

    /**
     * Generar con retry (para rate limits)
     */
    public function generateWithRetry(string $system, string $user, int $retries = 3): ?string
    {
        for ($i = 0; $i < $retries; $i++) {
            $result = $this->generate($system, $user);
            if ($result !== null) return $result;
            if ($i < $retries - 1) sleep(2 * ($i + 1)); // Backoff: 2s, 4s
        }
        return null;
    }
}
```

## Admin AJAX Controller — generar contenido

*controllers/admin/AdminOmniMindAjaxController.php*

```php
<?php
class AdminOmniMindAjaxController extends ModuleAdminController
{
    public function __construct()
    {
        parent::__construct();
        $this->ajax = true; // Deshabilitar layout
    }

    public function ajaxProcessGenerateDescription()
    {
        $idProduct = (int) Tools::getValue('id_product');
        $idLang    = (int) Tools::getValue('id_lang', $this->context->language->id);
        $type      = Tools::getValue('type', 'description'); // description, meta_title, meta_description, features

        if (!$idProduct) {
            die(json_encode(['success' => false, 'error' => 'Product ID required']));
        }

        // Cargar datos del producto
        $product = new Product($idProduct, true, $idLang);
        if (!Validate::isLoadedObject($product)) {
            die(json_encode(['success' => false, 'error' => 'Product not found']));
        }

        // Construir prompt con contexto del producto
        $promptBuilder = new PromptBuilder();
        $systemPrompt = $promptBuilder->getSystemPrompt($type, $idLang);
        $userPrompt   = $promptBuilder->buildProductPrompt($product, $type, $idLang);

        // Llamar al LLM
        $client = new LLMClient();
        $content = $client->generateWithRetry($systemPrompt, $userPrompt);

        if ($content === null) {
            die(json_encode(['success' => false, 'error' => 'LLM generation failed']));
        }

        // Guardar log
        ContentLog::logGeneration($idProduct, $type, $content);

        die(json_encode([
            'success' => true,
            'content' => $content,
            'type'    => $type,
            'tokens'  => TokenCounter::estimate($content),
        ]));
    }

    public function ajaxProcessSaveContent()
    {
        $idProduct = (int) Tools::getValue('id_product');
        $idLang    = (int) Tools::getValue('id_lang');
        $type      = Tools::getValue('type');
        $content   = Tools::getValue('content', false); // false = no strip tags

        $product = new Product($idProduct);
        if (!Validate::isLoadedObject($product)) {
            die(json_encode(['success' => false, 'error' => 'Product not found']));
        }

        switch ($type) {
            case 'description':
                $product->description[$idLang] = $content;
                break;
            case 'description_short':
                $product->description_short[$idLang] = $content;
                break;
            case 'meta_title':
                $product->meta_title[$idLang] = pSQL($content);
                break;
            case 'meta_description':
                $product->meta_description[$idLang] = pSQL($content);
                break;
        }

        $success = $product->update();
        die(json_encode(['success' => $success]));
    }
}
```

## Tab en la ficha de producto (BO)

*Hook displayAdminProductsExtra — tab en producto*

```php
// En la clase principal del modulo:
public function hookDisplayAdminProductsExtra($params)
{
    $idProduct = (int) ($params['id_product'] ?? Tools::getValue('id_product'));
    if (!$idProduct) return '';

    $product = new Product($idProduct, true, $this->context->language->id);
    $logs = ContentLog::getByProduct($idProduct, 10); // Ultimos 10 logs

    $this->context->smarty->assign([
        'product'       => $product,
        'id_product'    => $idProduct,
        'id_lang'       => $this->context->language->id,
        'languages'     => Language::getLanguages(true),
        'logs'          => $logs,
        'ajax_url'      => $this->context->link->getAdminLink('AdminOmniMindAjax'),
        'module_dir'    => $this->_path,
    ]);

    return $this->display(__FILE__, 'views/templates/admin/product_tab.tpl');
}

// Para PS 8.1+ con Product V2, usar el hook moderno:
public function hookActionProductFormBuilderModifier(array $params): void
{
    // Ver guia de Product Form V2 para la implementacion completa
}
```

*views/js/product-ai.js — AJAX generation*

```javascript
document.addEventListener('DOMContentLoaded', function() {
    const ajaxUrl = typeof omnimindAjaxUrl !== 'undefined' ? omnimindAjaxUrl : '';

    document.querySelectorAll('[data-generate-ai]').forEach(function(btn) {
        btn.addEventListener('click', async function(e) {
            e.preventDefault();
            const type = this.dataset.generateAi;
            const idProduct = this.dataset.productId;
            const idLang = document.getElementById('ai-lang-select')?.value || 1;
            const resultDiv = document.getElementById('ai-result-' + type);

            btn.disabled = true;
            btn.textContent = 'Generando...';
            resultDiv.innerHTML = '<div class="spinner"></div>';

            try {
                const response = await fetch(ajaxUrl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: new URLSearchParams({
                        ajax: 1,
                        action: 'GenerateDescription',
                        id_product: idProduct,
                        id_lang: idLang,
                        type: type,
                    }),
                });
                const data = await response.json();

                if (data.success) {
                    resultDiv.innerHTML = '<textarea class="form-control" rows="8">' +
                        data.content.replace(/</g, '&lt;') + '</textarea>' +
                        '<button class="btn btn-primary mt-2" data-save-ai="' + type + '">' +
                        'Guardar en producto</button>' +
                        '<small class="text-muted">~' + data.tokens + ' tokens</small>';
                } else {
                    resultDiv.innerHTML = '<div class="alert alert-danger">' + data.error + '</div>';
                }
            } catch (err) {
                resultDiv.innerHTML = '<div class="alert alert-danger">Error de conexion</div>';
            }

            btn.disabled = false;
            btn.textContent = 'Generar con IA';
        });
    });
});
```

## Procesamiento batch via cron

*Cron endpoint para generar contenido masivo*

```php
// controllers/front/batch.php
class EcomOmnimindBatchModuleFrontController extends ModuleFrontController
{
    public function initContent()
    {
        $token = Tools::getValue('token');
        if ($token !== Configuration::get('OMNIMIND_CRON_TOKEN')) {
            die(json_encode(['error' => 'Unauthorized']));
        }

        $type   = Tools::getValue('type', 'description');
        $limit  = (int) Tools::getValue('limit', 10);
        $idLang = (int) Tools::getValue('id_lang', Configuration::get('PS_LANG_DEFAULT'));

        // Obtener productos sin descripcion
        $products = Db::getInstance()->executeS('
            SELECT p.id_product, pl.name
            FROM ' . _DB_PREFIX_ . 'product p
            JOIN ' . _DB_PREFIX_ . 'product_lang pl ON pl.id_product = p.id_product
                AND pl.id_lang = ' . $idLang . '
                AND pl.id_shop = ' . (int) Context::getContext()->shop->id . '
            WHERE (pl.description IS NULL OR pl.description = "")
                AND p.active = 1
            LIMIT ' . $limit
        );

        $client = new LLMClient();
        $promptBuilder = new PromptBuilder();
        $results = [];

        foreach ($products as $row) {
            $product = new Product((int) $row['id_product'], true, $idLang);
            $system = $promptBuilder->getSystemPrompt($type, $idLang);
            $user   = $promptBuilder->buildProductPrompt($product, $type, $idLang);

            $content = $client->generateWithRetry($system, $user);
            if ($content) {
                $product->description[$idLang] = $content;
                $product->update();
                ContentLog::logGeneration((int) $row['id_product'], $type, $content);
                $results[] = ['id' => $row['id_product'], 'name' => $row['name'], 'status' => 'ok'];
            } else {
                $results[] = ['id' => $row['id_product'], 'name' => $row['name'], 'status' => 'error'];
            }

            sleep(1); // Rate limiting
        }

        die(json_encode(['processed' => count($results), 'results' => $results]));
    }
}

// Cron: wget 'https://tienda.com/module/ecom_omnimind/batch?token=XXX&type=description&limit=20'
```

## Sistema de logs y auditoria

*classes/ContentLog.php — ObjectModel de logs*

```php
<?php
class ContentLog extends ObjectModel
{
    public $id_product;
    public $type;         // description, meta_title, features, faqs...
    public $content;
    public $tokens_used;
    public $model_used;
    public $date_add;

    public static $definition = [
        'table'   => 'omnimind_logs',
        'primary' => 'id_log',
        'fields'  => [
            'id_product'  => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
            'type'        => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 50],
            'content'     => ['type' => self::TYPE_HTML, 'validate' => 'isCleanHtml'],
            'tokens_used' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt'],
            'model_used'  => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 100],
            'date_add'    => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
        ],
    ];

    public static function logGeneration(int $idProduct, string $type, string $content): void
    {
        $log = new self();
        $log->id_product  = $idProduct;
        $log->type        = $type;
        $log->content     = $content;
        $log->tokens_used = TokenCounter::estimate($content);
        $log->model_used  = Configuration::get('OMNIMIND_MODEL') ?: 'gpt-4o-mini';
        $log->date_add    = date('Y-m-d H:i:s');
        $log->add();
    }

    public static function getByProduct(int $idProduct, int $limit = 20): array
    {
        return Db::getInstance()->executeS('
            SELECT * FROM ' . _DB_PREFIX_ . 'omnimind_logs
            WHERE id_product = ' . $idProduct . '
            ORDER BY date_add DESC LIMIT ' . $limit
        );
    }
}
```

## Configuracion del proveedor LLM

*HelperForm para configuracion de IA*

```php
// Configuracion multi-proveedor
$providerOptions = [
    ['id' => 'openai',     'name' => 'OpenAI (GPT-4o, GPT-4o-mini)'],
    ['id' => 'anthropic',  'name' => 'Anthropic (Claude 4)'],
    ['id' => 'openrouter', 'name' => 'OpenRouter (Multi-modelo)'],
];

$modelOptions = [
    ['id' => 'gpt-4o-mini',        'name' => 'GPT-4o Mini (rapido, barato)'],
    ['id' => 'gpt-4o',             'name' => 'GPT-4o (equilibrado)'],
    ['id' => 'claude-sonnet-4-6',  'name' => 'Claude Sonnet 4.6 (rapido)'],
    ['id' => 'claude-opus-4-6',    'name' => 'Claude Opus 4.6 (mejor calidad)'],
];

$fields = [
    ['type' => 'select', 'label' => 'Proveedor', 'name' => 'OMNIMIND_PROVIDER',
     'options' => ['query' => $providerOptions, 'id' => 'id', 'name' => 'name']],
    ['type' => 'text', 'label' => 'API Key', 'name' => 'OMNIMIND_API_KEY',
     'desc' => 'Tu API key del proveedor seleccionado', 'size' => 60],
    ['type' => 'text', 'label' => 'API URL', 'name' => 'OMNIMIND_API_URL',
     'desc' => 'URL base de la API. Por defecto: https://api.openai.com/v1'],
    ['type' => 'select', 'label' => 'Modelo', 'name' => 'OMNIMIND_MODEL',
     'options' => ['query' => $modelOptions, 'id' => 'id', 'name' => 'name']],
    ['type' => 'text', 'label' => 'Temperatura', 'name' => 'OMNIMIND_TEMPERATURE',
     'desc' => '0.0 = determinista, 1.0 = creativo. Recomendado: 0.7', 'size' => 5],
    ['type' => 'textarea', 'label' => 'System Prompt custom', 'name' => 'OMNIMIND_SYSTEM_PROMPT',
     'desc' => 'Prompt de sistema personalizado. Dejar vacio para usar el por defecto.',
     'rows' => 6, 'cols' => 60],
];
```


---

*Fuente: [https://ayudaprestashop.es/examples/ai-content-generator](https://ayudaprestashop.es/examples/ai-content-generator). Version Markdown generada automaticamente para consumo por LLMs.*
