Skip to content

Upgrade Notes

Everything you need to review before upgrading to a new version of the bundle. The same notes ship with the package as UPGRADES.md in the repository root.

0.9.0

Generic feedback removed

The generic contact-form feedback feature (subject/type/description, posted to the sulu.ai platform) is gone: FeedbackRequester, the FeedbackController behind /admin/api/feedback, and the sulu.ai.feedback security context. This is unrelated to intelligent-search feedback (the up/down vote and comment on search answers), which stays.

If you granted sulu.ai.feedback to any role, drop it - the permission row is orphaned and no route enforces it anymore. ai-bundle: ^0.9 no longer defines the FEEDBACK_SECURITY_CONTEXT constant on AiAdminExtension.

Internals restructured into DDD/hexagonal layers

ai-platform-bundle and ai-bundle both moved every class under four top-level namespaces: Application/, Domain/, Infrastructure/, UserInterface/. Both bundles must be upgraded together - ai-platform-bundle now requires sulu/ai-bundle: ^0.9. There are no class_alias shims for the old namespaces, so this is a hard break for any code that references an internal class by name.

If you only configure the bundle through sulu_ai_platform.yaml and never referenced an internal class directly, this section does not affect you. It matters if you implemented one of the documented extension interfaces, extended an abstract class, or type-hinted an internal service.

Documented extension points, old namespace to new (all under Sulu\Bundle\AiPlatformBundle\ unless noted):

Old New
Ingestion\Chunker\ContentChunkerInterface Application\Ingestion\Chunker\ContentChunkerInterface
Ingestion\IngestProviderInterface Application\Ingestion\IngestProviderInterface
Store\Api\IngestDocument Application\Ingestion\IngestDocument
Store\Api\IngestChunk Application\Ingestion\IngestChunk
Expert\ExcerptGenerator\ExcerptContextProviderInterface Application\Expert\ExcerptGenerator\ExcerptContextProviderInterface
Expert\SeoGenerator\SeoContextProviderInterface Application\Expert\SeoGenerator\SeoContextProviderInterface
Compatibility\ContentPersister\ContentAdapterInterface Application\Compatibility\ContentPersister\ContentAdapterInterface
IntelligentSearch\Client\IntelligentSearchClientInterface Application\IntelligentSearch\IntelligentSearchClientInterface
IntelligentSearch\Query\IntelligentSearchQuery Application\IntelligentSearch\IntelligentSearchQuery
Features\FullContentTranslation\AbstractFullContentTranslationSubscriber Application\Translation\FullContentTranslation\AbstractFullContentTranslationSubscriber
Compatibility\ContentPersister\Sulu26ContentDecorator Infrastructure\Sulu\Compatibility\ContentPersister\Sulu26ContentDecorator

And in Sulu\Bundle\AiBundle\:

Old New
Expert\ExpertInterface Application\Expert\ExpertInterface
Expert\Translator\TranslatorInterface Application\Translation\TranslatorInterface
Expert\WritingAssistant\WritingAssistantContextProviderInterface Application\Expert\WritingAssistant\WritingAssistantContextProviderInterface
Expert\WritingAssistant\WritingAssistantMessageEnhancerInterface Application\Expert\WritingAssistant\WritingAssistantMessageEnhancerInterface
Compatibility\WebspaceKeyResolverInterface Application\Compatibility\WebspaceKeyResolverInterface

Everything else in both bundles moved too (repositories, entities, controllers, admin classes, platform/store adapters). If phpstan/CI does not catch a reference for you, search your codebase for Sulu\Bundle\AiPlatformBundle\ and Sulu\Bundle\AiBundle\ and resolve each one against the new tree.

A handful of signatures changed along the way, beyond the namespace move:

  • ExpertRepositoryInterface::findBy() returns iterable<Expert> instead of an array; create(), findAll() and the $className constructor argument are gone from the Doctrine implementation.
  • IntelligentSearchFeedbackRepositoryInterface no longer has findOneBy(); use findOneByUuid() or getOneByUuid().
  • PlatformExpert::__construct() now takes an AgentResolverInterface as its first argument.
  • IntelligentSearchClientInterface::searchStream() returns Application\IntelligentSearch\IntelligentSearchStreamInterface instead of the concrete Infrastructure\SuluAi\IntelligentSearch\IntelligentSearchStreamResult. Code that only calls the interface's methods (getAnswerStream(), getAnswer(), getSources(), getUuid()) is unaffected.
  • Application\Store\StoreSearchResult::fromArray() itself now throws \InvalidArgumentException on a malformed row instead of the Infrastructure\SuluAi\Exception\InvalidDatastoreResponseException it threw before; DatastoreClient::search() still catches that and re-throws InvalidDatastoreResponseException, so the exception contract for search() callers is unchanged. Only code calling fromArray() directly sees the new exception type.

0.8.0

contact_email is required

sulu_ai_platform.contact_email is a new required option, so an application that does not set it fails to boot after the upgrade. It is the address editors are offered when the platform refuses a request for account reasons - an expired subscription or exhausted credits - and the subscription page links it as well.

sulu_ai_platform:
    contact_email: 'ai-admin@my-agency.example'

Point it at whoever owns the sulu.ai account for that installation, usually the implementation partner rather than an editor.

Searches are persisted by the client, not by the controller

IntelligentSearchClient dispatches IntelligentSearchCompletedEvent or IntelligentSearchFailedEvent and a listener writes the feedback row, so a search is recorded wherever it is performed. Previously only IntelligentSearchController wrote it. See the feedback documentation.

  • IntelligentSearchClient::__construct() takes an IntelligentSearchOutcomeReporter as its second argument, before $defaultIdentifier.
  • Own implementations of IntelligentSearchClientInterface no longer get persistence from the controller. Compose IntelligentSearchOutcomeReporter and call completed() / failed().
  • sulu:ai:intelligent-search:search records searches now. --no-persist opts out, as does intelligent_search.feedback.persist: false or IntelligentSearchQuery::create(...)->withoutPersistence(). Neither stops the events.
  • IntelligentSearchResult::__construct(), IntelligentSearchResult::fromArray() and IntelligentSearchStreamResult::__construct() require the $uuid that used to default to an empty string. It is the id the feedback endpoint expects, so an empty one left a visitor unable to vote on the answer they just got.

Feedback comments are capped and no longer overwritten by a plain re-vote

The public feedback endpoint rejects a comment longer than 2000 characters with a 400. The limit is configurable:

sulu_ai_platform:
    intelligent_search:
        feedback:
            max_comment_length: 2000

A request that carries no comment now leaves an existing one untouched. Previously any vote overwrote the field, so a visitor who down-voted with a detailed comment and then clicked up-vote silently destroyed it. Send comment: '' to clear a comment deliberately.

Redundant index on ai_search_feedback.uuid removed

The entity declared an explicit index on uuid next to the column's unique constraint, which already provides one - so every generated migration created two indexes over the same column. The explicit one is gone.

Existing installations keep idx_search_feedback_uuid until you drop it. Nothing breaks if you leave it, but doctrine:schema:validate reports the difference. To clean it up, generate a migration after upgrading:

php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate

The index on created_at is unaffected.

New security context for the subscription page

The new Sulu.ai → Subscription admin page is guarded by the new sulu.ai.subscription security context (category Sulu AI). After upgrading, grant the VIEW permission to the roles that should see the page under Settings → User Roles — without it, neither the navigation entry nor the page is available. The page also requires a Sulu release that ships the subscription view — the bundle now requires Sulu ^2.6.26 (2.6 line) or ^3.0.9 (3.0 line). See the subscription documentation.

Full content translation failures no longer abort the request

An error while translating a copied locale no longer turns the copy-locale request into a 500. The copied locale stays in place untranslated and the failure is logged. If you relied on the request failing, override AbstractFullContentTranslationSubscriber::onTranslationFailure() and rethrow.

setLogger() on full content translation subscribers

The bundle now calls setLogger() on every AbstractFullContentTranslationSubscriber. Subclasses declaring a setLogger() of their own have to accept a Psr\Log\LoggerInterface.

0.7.0

Sulu minimum version raised

The bundles now require Sulu ^2.6.25 (2.6 line) or ^3.0.8 (3.0 line). These versions ship the admin support for the configurable AI field types described below. Update your Sulu installation before upgrading the bundles.

symfony/ai packages upgraded to 0.10

Both bundles now require the symfony/ai-* packages in version ^0.10. If your application requires any symfony/ai-* package directly (for example symfony/ai-bundle or symfony/ai-agent), update its constraint to ^0.10 before upgrading.

New optional configuration

No action is required — the defaults match the previous behavior:

  • sulu_ai_platform.text_field_types (default ['text_line', 'text_area']) and sulu_ai_platform.html_field_types (default ['text_editor']) control which property types the AI features treat as plain-text respectively HTML content. Projects with custom property types containing translatable text should add them here — see the translator documentation.
  • sulu_ai_platform.intelligent_search.identifier optionally targets a specific intelligent search by UUID. When omitted, the project default is used.

0.6.0

New Sulu AI security contexts

The ai-platform-bundle now registers its own Sulu AI security category in the Sulu admin permission system.

Bundle users should review their roles and grant the new VIEW permissions for the AI features they want to expose:

  • sulu.ai.writing_assistant
  • sulu.ai.translate
  • sulu.ai.seo_generator
  • sulu.ai.excerpt_generator
  • sulu.ai.media_metadata_generator
  • sulu.ai.feedback

Without these permissions, the corresponding AI UI actions are hidden and the backend endpoints return 403 Access Denied.

Translation permissions are combined

The following translation-related permissions were combined into a single permission:

  • sulu.ai.translate_text
  • sulu.ai.media_metadata_translate
  • sulu.ai.category_translate
  • sulu.ai.full_content_translate

Use sulu.ai.translate instead.

SQL helper for existing roles

If you want to grant all new Sulu AI VIEW permissions to an existing role directly in SQL, you can use the following statement. Replace :ROLE_ID with the numeric role id from se_roles.id:

INSERT INTO se_permissions (`context`, `module`, `permissions`, `idRoles`)
VALUES
    ('sulu.ai.writing_assistant', NULL, 64, :ROLE_ID),
    ('sulu.ai.translate', NULL, 64, :ROLE_ID),
    ('sulu.ai.seo_generator', NULL, 64, :ROLE_ID),
    ('sulu.ai.excerpt_generator', NULL, 64, :ROLE_ID),
    ('sulu.ai.media_metadata_generator', NULL, 64, :ROLE_ID),
    ('sulu.ai.feedback', NULL, 64, :ROLE_ID)
ON DUPLICATE KEY UPDATE
    `permissions` = `permissions` | VALUES(`permissions`);

64 is the Sulu bitmask for VIEW. The ON DUPLICATE KEY UPDATE clause preserves any existing permission bits and only adds VIEW where needed.

0.5.0

Context objects replace individual parameters

All expert interfaces now use dedicated context objects instead of individual $locale and $resourceKey parameters. These context objects bundle locale, resourceKey, and the new webspaceKey together, enabling the platform to resolve profile metadata per webspace.

ai-bundle: WritingAssistantGeneratorInterface

The $locale parameter was replaced by WritingAssistantContext:

// Before
public function optimize(
    string $chatId,
    string $text,
    string $message,
    ?string $expertUuid,
    string $locale,
): WritingAssistantResponse;

// After
use Sulu\Bundle\AiBundle\Expert\WritingAssistant\WritingAssistantContext;

public function optimize(
    string $chatId,
    string $text,
    string $message,
    ?string $expertUuid,
    WritingAssistantContext $context,
): WritingAssistantResponse;

The WritingAssistantContext provides locale, resourceId, resourceKey, schemeAndHttpHost, optional webspaceKey, and optional content data (data, dataPath).

ai-bundle: WritingAssistantContextProviderInterface

The $locale parameter was removed and parameters were reordered. The context object now carries the locale:

// Before
public function generateContext(
    string $text,
    string $message,
    string $locale,
    ?string $expertUuid,
): iterable;

// After
use Sulu\Bundle\AiBundle\Expert\WritingAssistant\WritingAssistantContext;

public function generateContext(
    string $text,
    string $message,
    ?string $expertUuid,
    WritingAssistantContext $context,
): iterable;

ai-platform-bundle: SeoGeneratorInterface

The $locale and $resourceKey parameters were replaced by SeoGeneratorContext, and $expertUuid was moved:

// Before
public function generate(
    string $id,
    string $locale,
    string $expertUuid,
    ?array $currentSeoData,
    string $resourceKey,
): SeoGeneratorResponse;

// After
use Sulu\Bundle\AiPlatformBundle\Expert\SeoGenerator\SeoGeneratorContext;

public function generate(
    string $id,
    string $expertUuid,
    ?array $currentSeoData,
    SeoGeneratorContext $context,
): SeoGeneratorResponse;

ai-platform-bundle: SeoContextProviderInterface

The $locale parameter was replaced by SeoGeneratorContext, and $expertUuid was moved:

// Before
public function generateContext(
    ContentAdapterInterface $content,
    string $locale,
    string $expertUuid,
): iterable;

// After
use Sulu\Bundle\AiPlatformBundle\Expert\SeoGenerator\SeoGeneratorContext;

public function generateContext(
    ContentAdapterInterface $content,
    string $expertUuid,
    SeoGeneratorContext $context,
): iterable;

ai-platform-bundle: ExcerptGeneratorInterface

Same pattern as SeoGeneratorInterface:

// Before
public function generate(
    string $id,
    string $locale,
    string $expertUuid,
    ?array $currentExcerptData,
    string $resourceKey,
): ExcerptGeneratorResponse;

// After
use Sulu\Bundle\AiPlatformBundle\Expert\ExcerptGenerator\ExcerptGeneratorContext;

public function generate(
    string $id,
    string $expertUuid,
    ?array $currentExcerptData,
    ExcerptGeneratorContext $context,
): ExcerptGeneratorResponse;

ai-platform-bundle: ExcerptContextProviderInterface

Same pattern as SeoContextProviderInterface:

// Before
public function generateContext(
    ContentAdapterInterface $content,
    string $locale,
    string $expertUuid,
): iterable;

// After
use Sulu\Bundle\AiPlatformBundle\Expert\ExcerptGenerator\ExcerptGeneratorContext;

public function generateContext(
    ContentAdapterInterface $content,
    string $expertUuid,
    ExcerptGeneratorContext $context,
): iterable;

ai-bundle: TranslatorInterface new parameter

A new optional $webspaceKey parameter was added to translate():

// Before
public function translate(
    string $translateId,
    array $texts,
    string $targetLanguage,
    ?string $sourceLanguage = null,
    bool $html = true,
): TranslatorResponse;

// After
public function translate(
    string $translateId,
    array $texts,
    string $targetLanguage,
    ?string $sourceLanguage = null,
    bool $html = true,
    ?string $webspaceKey = null,
): TranslatorResponse;

Existing callers are unaffected. Implementations of this interface must add the new parameter.

New context factories

Context objects are now created via factories that handle webspace key resolution for articles:

  • Sulu\Bundle\AiBundle\Expert\WritingAssistant\WritingAssistantContextFactory
  • Sulu\Bundle\AiPlatformBundle\Expert\SeoGenerator\SeoGeneratorContextFactory
  • Sulu\Bundle\AiPlatformBundle\Expert\ExcerptGenerator\ExcerptGeneratorContextFactory

If you extend WritingAssistantController, SeoGeneratorController, or ExcerptGeneratorController, note that their constructors now require the corresponding context factory as an additional parameter.

New WebspaceKeyResolverInterface

A new Sulu\Bundle\AiBundle\Compatibility\WebspaceKeyResolverInterface has been introduced. The ai-platform-bundle provides implementations for both Sulu 2.6 (Sulu26WebspaceKeyResolver) and Sulu 3.0 (Sulu3WebspaceKeyResolver) that resolve the webspaceKey for articles based on their main webspace.

New WritingAssistantMessageEnhancerInterface

A new WritingAssistantMessageEnhancerInterface has been introduced for enhancing the locale instruction message with additional content parts (e.g., media images). Implementations are tagged with sulu_ai.writing_assistant_message_enhancer.


0.4.0

Migration from ModelflowAi to Symfony/AI

Quick Navigation: - For Bundle Users - If you're using AI features in Sulu admin - For Advanced Users - If you implement custom experts or extend the bundles


For Bundle Users

If you're using the AI features in Sulu admin, follow these steps:

1. Update Dependencies

composer update sulu/ai-platform-bundle -W

Note: This bundle requires Symfony 7.3 or higher due to the Symfony AI component dependencies.

2. Update Bundle Configuration

You have to enable the Symfony AI bundle in your application while removing the ModelflowAi Bundle.

Update config/bundles.php:

return [
    // Remove this line:
    // ModelflowAi\Integration\Symfony\ModelflowAiBundle::class => ['all' => true],

    // Add Symfony AI bundle
    Symfony\AI\AiBundle\AiBundle::class => ['all' => true],

    // Keep these existing bundles:
    Sulu\Bundle\AiPlatformBundle\SuluAiPlatformBundle::class => ['all' => true],
    Sulu\Bundle\AiBundle\SuluAiBundle::class => ['all' => true],
];


For Advanced Users

If you implement custom experts or extend the bundles:

ExpertInterface Changes

Only relevant if you implement ExpertInterface yourself:

// Before
public function createThread(?string $expertUuid): ThreadInterface;
public function getVariants(): array;  // Returns objects

// After
public function createThread(?string $variantUuid = null): ThreadInterface;
public function getVariants(): array;  // Returns arrays

Variant access:

// Before: Objects
$variant->getUuid()

// After: Arrays
$variant['uuid']

Service Names

Only relevant if you inject expert services:

Expert services have been split into two distinct services following a consistent naming pattern:

Old Service ID New Service ID Type
modelflow_ai.experts.writing_assistant sulu_ai.writing_assistant_expert Expert (ExpertInterface)
sulu_ai.experts.writing_assistant sulu_ai.writing_assistant_generator WritingAssistantGenerator implementation
modelflow_ai.experts.seo_generator sulu_ai_platform.seo_expert Expert (ExpertInterface)
sulu_ai_platform.experts.seo_generator sulu_ai_platform.seo_generator SeoGenerator implementation
modelflow_ai.experts.excerpt_generator sulu_ai_platform.excerpt_expert Expert (ExpertInterface)
sulu_ai_platform.experts.excerpt_generator sulu_ai_platform.excerpt_generator ExcerptGenerator implementation
modelflow_ai.experts.media_metadata_generator sulu_ai_platform.media_metadata_expert Expert (ExpertInterface)
sulu_ai_platform.experts.media_metadata_generator sulu_ai_platform.media_metadata_generator MediaMetadataGenerator implementation
sulu_ai_platform.experts.translator sulu_ai_platform.translator Translator

Naming Pattern: - Services ending in _expert: Return ExpertVariant arrays for expert definitions - Services ending in _generator: Implementation services for programmatic use

Expert/Thread Architecture Refactoring

Only relevant if you extend our generators (WritingAssistantGenerator, SeoGenerator, ExcerptGenerator, MediaMetadataGenerator):

We've simplified the generators to use the Expert/Thread pattern. If you're extending any of the generators, please check your implementation.

Context Provider Interface Change

Only relevant if you implement a custom SeoContextProviderInterface or ExcerptContextProviderInterface:

The $document parameter of generateContext() changed from object to ConentAdapterInterface:

// Before
public function generateContext(
    StructureBehavior $document,
    string $locale,
    string $expertUuid,
): iterable;

// After
use Sulu\Bundle\AiPlatformBundle\Compatibility\ContentPersister\ConentAdapterInterface;

public function generateContext(
    ConentAdapterInterface $content,
    string $locale,
    string $expertUuid,
): iterable;

Update your implementations to use ConentAdapterInterface and its methods (getTemplateData(), getSeoData(), getExcerptData(), etc.) instead of checking for version-specific types.

Class renamings

Following classes have been renamed:

Old Class New Class
Sulu\Bundle\AiBundle\Expert\WritingAssistant\WritingAssistant Sulu\Bundle\AiBundle\Expert\WritingAssistant\WritingAssistantGenerator
Sulu\Bundle\AiBundle\Expert\WritingAssistant\WritingAssistantInterface Sulu\Bundle\AiBundle\Expert\WritingAssistant\WritingAssistantGeneratorInterface

Testing

Only relevant if you test AI features:

// Before
use Sulu\Bundle\AiBundle\Testing\TestAdapter\TestChatAdapter;
$adapter = new TestChatAdapter([...]);

// After
use Sulu\Bundle\AiBundle\Testing\MockPlatform;
$mockPlatform = new MockPlatform(/*...*/);
$mockPlatform->addMockResponse(fn($m, $msg) => true, '{"result": "test"}');

Removed Classes

Unlikely you were using these, but they're removed: - Criteria\ExpertCriteria, Expert\ExpertFactory, Expert\ExpertFactoryInterface - Testing\TestAdapter\TestChatAdapter


Quick Checklist

For all users: - [ ] Update Symfony to ^7.3 - [ ] Update dependencies: composer update sulu/ai-platform-bundle -W - [ ] Remove ModelflowAi\Integration\Symfony\ModelflowAiBundle from bundles.php - [ ] Add Symfony\AI\AiBundle\AiBundle to bundles.php

For advanced users only: - [ ] Update custom expert implementations (if you have any) - [ ] Update service injection (if you inject expert services) - [ ] Update test code (if you test AI features) - [ ] Update generator extensions (if you extend generators) - [ ] Update custom context provider implementations (if you implement SeoContextProviderInterface or ExcerptContextProviderInterface)