Skip to content

Latest commit

 

History

History
326 lines (246 loc) · 11.8 KB

INTEGRATION.md

File metadata and controls

326 lines (246 loc) · 11.8 KB

Integrating with Frosh AI

This document describes the public API for other Shopware plugins that want to use the merchant’s configured AI providers.

Public surface

Symbol Purpose
Frosh\AI\Client\AiClient Main facade — inject this
frosh_ai.client Stable DI alias (decoration)
Frosh\AI\Client\PromptBuilder Fluent prompt API
Frosh\AI\Contract\AiProviderInterface Implement only if you add a provider
Frosh\AI\Contract\LlmClientInterface Low-level transport (rare)
Frosh\AI\Contract\ModelCatalogInterface Model list / capabilities
ChatRequest, ChatResponse, ChatMessage, ModelPreference Value objects
Capability, MessageRole, FinishReason Enums
AiException hierarchy Errors
AiChatBeforeEvent, AiChatAfterEvent, AiChatFailedEvent Hooks

Do not depend on: OpenAI HTTP clients, config keys (FroshAI.config.*), device-auth internals, or admin Vue routes.

Composer

{
  "require": {
    "frosh/ai": "^0.1"
  }
}

Feature consumer (typical)

<?php declare(strict_types=1);

namespace Acme\Seo;

use Frosh\AI\Client\AiClient;
use Frosh\AI\Exception\NoProviderAvailableException;

final class ProductTitleSuggester
{
    public function __construct(
        private readonly AiClient $ai,
    ) {
    }

    public function suggest(string $description): ?string
    {
        if (!$this->ai->isAvailable()) {
            return null;
        }

        try {
            return $this->ai->prompt($description)
                ->system('Suggest a concise product title. Return only the title.')
                ->prefer('openai', 'gpt-4o')
                ->prefer('anthropic', 'claude-sonnet-4-6')
                ->temperature(0.3)
                ->maxTokens(64)
                ->withMetadata([
                    'extension' => 'AcmeSeo',
                    'feature' => 'product-title',
                ])
                ->generateText();
        } catch (NoProviderAvailableException) {
            return null;
        }
    }
}
<service id="Acme\Seo\ProductTitleSuggester">
    <argument type="service" id="Frosh\AI\Client\AiClient"/>
    <!-- or: id="frosh_ai.client" -->
</service>

Resolution rules

Order at runtime:

  1. Explicit usingProvider() / usingModel() on the request
  2. Ordered prefer(provider, model) pairs that are configured and match the required capability
  3. Shop default models (Settings → Default models): text vs image
  4. First configured provider that advertises the capability

generateText() always requires text_generation. Image generation:

use Frosh\AI\Enum\Capability;

$image = $this->ai->prompt('Studio product photo of a red sneaker on white background')
    ->prefer('openai', 'gpt-image-1')
    ->imageSize('1024x1024')
    ->withMetadata([
        'extension' => 'AcmeMedia',
        'feature' => 'product-image',
    ])
    ->generateImage();

$b64 = $image->first()?->b64Json;
// or $image->first()?->url / dataUri()

Requires an OpenAI API key (Images API). ChatGPT device login alone cannot generate images; you may keep device auth for chat and still set an API key for images.

If nothing matches → NoProviderAvailableException.
If the provider has no Images transport → ImageGenerationNotSupportedException.

Capability defaults (merchant UI)

Capability Config keys
text_generation defaultTextProvider + defaultTextModel (legacy fallback: openAiDefaultModel on OpenAI)
image_generation defaultImageProvider + defaultImageModel

Provider-level “default model” on OpenAI / compatible endpoints is a text fallback for that provider only.

Metadata convention

->withMetadata([
    'extension' => 'AcmeSeo',
    'feature' => 'meta-description',
    'entity' => 'product',
    'entityId' => $productId,
])

Events

use Frosh\AI\Event\AiChatAfterEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class AiUsageSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [AiChatAfterEvent::class => 'onAfter'];
    }

    public function onAfter(AiChatAfterEvent $event): void
    {
        $usage = $event->getResponse()->usage;
        // log $event->getDurationMs(), tokens, metadata…
    }
}
  • AiChatBeforeEvent — setRequest() to inject system guidelines
  • AiChatAfterEvent — success metrics
  • AiChatFailedEvent — failure metrics (exception is still re-thrown)

OpenAI-compatible custom endpoints

Merchants can add any Chat Completions-compatible server in the admin (Providers → Add compatible API):

Field Example
Name Ollama
Technical id ollama → provider id compat.ollama
Base URL http://127.0.0.1:11434/v1
API key optional (empty for many local servers)
Default model llama3.2 (text_generation only)

Compatible endpoints are treated as text providers. They do not appear in the image default picker.

Extensions prefer them like any other provider:

->prefer('compat.ollama', 'llama3.2')
->prefer('compat.openrouter', 'openai/gpt-4o')
->prefer('openai', 'gpt-4o')

Provider plugin (optional)

<service id="Acme\AI\AnthropicProvider">
    <tag name="frosh_ai.provider"/>
</service>

Implement AiProviderInterface including getDescriptor() and getAdminStatus().
Optional admin form: register Vue component frosh-ai-provider-{id}.

Admin UI

  • Providers — list + per-provider detail
  • Playground — exercises the same AiClient path as extensions

Admin HTTP (/_action/frosh-ai/*) is for admin UIs. Storefront and other plugins’ business logic should call PHP AiClient, not those routes.

Usage tracking

Every chat and image call is logged via AiChat* / AiImage* events into frosh_ai_usage_log:

  • provider, model, capability, duration, tokens (chat)
  • feature / extension / entity from request metadata
  • success vs failed (+ truncated error)
  • Prompts and response bodies are not stored

Admin: Settings → Frosh AI → Usage (stats, feature/provider filters, expandable errors, CSV export, purge all / older-than-N).

Endpoint Method Notes
/api/_action/frosh-ai/usage GET Paginated list. Query: page, limit, status, type, feature, provider. Response includes stats, facets (distinct features/providers), retentionDays.
/api/_action/frosh-ai/usage/stats GET Aggregates + facets + retention.
/api/_action/frosh-ai/usage/export GET CSV (max 10 000 rows), same filter query params. UTF-8 BOM for Excel.
/api/_action/frosh-ai/usage DELETE Purge all, or ?olderThanDays=N for aged rows only.

Retention: plugin config usageRetentionDays (default 90, 0 = keep forever). Daily scheduled task frosh_ai.cleanup_usage_logs deletes rows older than that.

Run plugin migrations after update so the table exists. Register scheduled tasks after install/update (bin/console scheduled-task:register or plugin lifecycle).

Feature flags

Master switch + per-ability toggles (Settings → Frosh AI → General):

Id Controls
(master) featuresEnabled All abilities
product_description Product description generation
product_title Product title (name) generation
product_seo Meta title/description, keywords, Open Graph text
image_generation Image generation (playground + AiClient::generateImage)

A feature is effective only when master AND the ability flag are on. Missing ability keys default to on.

PHP: Frosh\AI\Feature\FeatureFlags / FeatureId.
Admin API: GET|POST /api/_action/frosh-ai/features (also on status.featureFlags).

Built-in features: product copy (title / description / SEO)

Catalog product form + SEO tab show a single Generate with AI button that opens a modal:

  • Field checkboxes (title, description, meta title/description, keywords, Open Graph)
  • Only fill empty fields (default on); overwrite requires confirm
  • Per-field progress (queued / generating / done / skipped / failed)
  • Last field selection, tone, and fill-empty preference remembered per screen (localStorage)
  • Shared tone presets + optional extra instructions
  • Language = product content language (admin language switcher), not the admin UI locale
  • Uses Default models → Text
Flag API
product_title POST …/product/generate-title → title
product_description POST …/product/generate-description → description (HTML)
product_seo POST …/product/generate-seo → metaTitle, metaDescription, keywords, ogTitle, ogDescription

Common payload: product facts, tone, languageId / language / languageName, optional extraInstructions.

  • Tones: GET|POST /api/_action/frosh-ai/product/tones, reset via …/tones/reset
  • Status: GET …/product/description-status → enabled, titleEnabled, seoEnabled, tones
  • Admin component: frosh-ai-product-generate-modal
  • PHP: ProductDescriptionGenerator, ProductTitleGenerator, ProductSeoGenerator, ProductCopySupport, ProductDescriptionToneStore

Draft fields are filled in admin only; the merchant must save the product.

Built-in provider: Google Gemini

Provider id: gemini (models.dev catalog key: google).

Area Details
Auth AI Studio API key (FroshAI.config.geminiApiKey)
Chat POST …/v1beta/models/{model}:generateContent
Image Native gemini-*-image via generateContent + responseModalities: [TEXT, IMAGE]; Imagen models via :predict
Defaults geminiDefaultModel (text), geminiDefaultImageModel (image)
Admin Settings → Frosh AI → Providers → Google Gemini

Example:

$text = $ai->prompt('Write a short tagline for sneakers.')
    ->prefer('gemini', 'gemini-2.5-flash')
    ->generateText();

$images = $ai->prefer('gemini', 'gemini-2.5-flash-image')
    ->generateImage(new \Frosh\AI\Request\ImageRequest(
        prompt: 'Product photo of red running shoes on white background',
    ));

Shop-wide: set Default models → Text/Image provider to gemini when it should be the global default.

Image editing

Edit existing pictures with a text instruction (image + prompt → image).

Flag image_edit (FroshAI.config.featureImageEdit)
API Same AiClient::generateImage() when sourceImages is non-empty; alias editImage()
Gemini Native image models: generateContent with inlineData + text + responseModalities: [TEXT, IMAGE]
OpenAI POST /v1/images/edits (multipart; first source image; optional mask)
Admin Playground → Image editing (upload + instruction); Save to media library
HTTP POST /api/_action/frosh-ai/playground/image with sourceImages: [{ dataUri|b64, mimeType? }]; POST …/media/save-image
use Frosh\AI\Request\ImageInput;

$result = $ai->prompt('Remove background, pure white studio, keep product sharp')
    ->prefer('gemini', 'gemini-2.5-flash-image')
    ->withSourceImage($base64Png, 'image/png')
    ->editImage();

// Or:
$ai->generateImage(new \Frosh\AI\Request\ImageRequest(
    prompt: 'Soft lifestyle background',
    provider: 'openai',
    model: 'gpt-image-1',
    sourceImages: [ImageInput::fromBase64($b64, 'image/png')],
));

Usage log: type=image, metadata.mode=edit|generate, sourceImageCount — no image bytes stored.

Soft dependency

If your plugin must run without Frosh AI installed, define your own interface and bind it only when AiClient exists, or make Frosh AI an optional suggest requirement and guard features with isAvailable().