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()returnsiterable<Expert>instead of an array;create(),findAll()and the$classNameconstructor argument are gone from the Doctrine implementation.IntelligentSearchFeedbackRepositoryInterfaceno longer hasfindOneBy(); usefindOneByUuid()orgetOneByUuid().PlatformExpert::__construct()now takes anAgentResolverInterfaceas its first argument.IntelligentSearchClientInterface::searchStream()returnsApplication\IntelligentSearch\IntelligentSearchStreamInterfaceinstead of the concreteInfrastructure\SuluAi\IntelligentSearch\IntelligentSearchStreamResult. Code that only calls the interface's methods (getAnswerStream(),getAnswer(),getSources(),getUuid()) is unaffected.Application\Store\StoreSearchResult::fromArray()itself now throws\InvalidArgumentExceptionon a malformed row instead of theInfrastructure\SuluAi\Exception\InvalidDatastoreResponseExceptionit threw before;DatastoreClient::search()still catches that and re-throwsInvalidDatastoreResponseException, so the exception contract forsearch()callers is unchanged. Only code callingfromArray()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 anIntelligentSearchOutcomeReporteras its second argument, before$defaultIdentifier.- Own implementations of
IntelligentSearchClientInterfaceno longer get persistence from the controller. ComposeIntelligentSearchOutcomeReporterand callcompleted()/failed(). sulu:ai:intelligent-search:searchrecords searches now.--no-persistopts out, as doesintelligent_search.feedback.persist: falseorIntelligentSearchQuery::create(...)->withoutPersistence(). Neither stops the events.IntelligentSearchResult::__construct(),IntelligentSearchResult::fromArray()andIntelligentSearchStreamResult::__construct()require the$uuidthat 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']) andsulu_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.identifieroptionally 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_assistantsulu.ai.translatesulu.ai.seo_generatorsulu.ai.excerpt_generatorsulu.ai.media_metadata_generatorsulu.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_textsulu.ai.media_metadata_translatesulu.ai.category_translatesulu.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\WritingAssistantContextFactorySulu\Bundle\AiPlatformBundle\Expert\SeoGenerator\SeoGeneratorContextFactorySulu\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)