From 32da9bb48fa71b1bfed95e9f948d272682f1c7ed Mon Sep 17 00:00:00 2001 From: Simon Kuehn Date: Mon, 18 May 2026 10:20:17 +0000 Subject: [PATCH] refactor: make prompt templates DB-only system prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove all hardcoded defaults from PromptTemplateService — the DB record is now mandatory and render() throws if a key is missing. Admin UI disables new/delete and makes the key field read-only so system prompts cannot be renamed or removed. Co-Authored-By: Claude Sonnet 4.6 --- .../AI/PromptTemplateService.php | 74 +------------------ .../Admin/PromptTemplateCrudController.php | 13 ++-- 2 files changed, 11 insertions(+), 76 deletions(-) diff --git a/src/Infrastructure/AI/PromptTemplateService.php b/src/Infrastructure/AI/PromptTemplateService.php index 05e3c95..743017e 100644 --- a/src/Infrastructure/AI/PromptTemplateService.php +++ b/src/Infrastructure/AI/PromptTemplateService.php @@ -8,68 +8,6 @@ use App\Domain\AI\Repository\PromptTemplateRepositoryInterface; final class PromptTemplateService { - /** @var array */ - private const array DEFAULTS = [ - 'specs_research' => <<<'PROMPT' -You are a hardware specifications expert. Extract the technical specifications for the {{articleType}}: "{{subject}}". - -Web search results: -{{searchResults}} - -Based on the search results above, list all technical specifications including: -processor, RAM, storage variants, display size and resolution, GPU, battery capacity, -ports, connectivity, weight, dimensions, OS, and any other relevant specs. -Be specific and accurate. If a spec is not found in the search results, omit it rather than guessing. -PROMPT, - - 'ebay_title' => <<<'PROMPT' -Create a concise eBay listing title (max 80 characters) for this {{typeName}}. -Device: {{deviceLabel}} -Use the most important specifications. Include condition if not "new". -Condition: {{condition}} -{{specsSection}} -Return ONLY the title text, no quotes, no explanation. -PROMPT, - - 'ebay_description' => <<<'PROMPT' -Create a professional eBay listing description in German for this {{typeName}}. -Device: {{deviceLabel}} -Include all available specifications in a clear, structured format. -Mention the condition: {{condition}}. -{{conditionNotes}} -{{specsSection}} -Use HTML formatting (ul, li, strong tags). Max 2000 characters. -PROMPT, - - 'vision_analyze' => <<<'PROMPT' -Look at this nameplate/label photo of IT hardware. -Extract the manufacturer, any model identifier (name or number), and serial number visible on the label. -If the label shows both a product name (e.g. "ThinkPad T490s") and a separate part/product code (e.g. "20NXS0BA00"), put the product name in MODEL_NAME and the code in MODEL_NUMBER. -If only one model field is visible, put it in MODEL_NAME and leave MODEL_NUMBER completely empty. -MODEL_NUMBER must never contain the serial number. -Do not guess or add information not visible on the label. -Respond in exactly this format: -MANUFACTURER: Lenovo -MODEL_NAME: ThinkBook 14 G6 IRL -MODEL_NUMBER: -SERIAL: PNV09SJZ -Use empty string (nothing after the colon) when a field is not visible. -PROMPT, - - 'json_coding' => <<<'PROMPT' -Convert the following hardware specifications to a JSON object. -The JSON must use these exact keys (UUIDs) and follow the indicated value formats: - -{{schema}} -{{missingHint}} -Specifications text: -{{specsText}} - -Return ONLY valid JSON. No explanation. No markdown. No extra text. -JSON: -PROMPT, - ]; - public function __construct( private readonly PromptTemplateRepositoryInterface $repository, ) { @@ -83,9 +21,11 @@ PROMPT, public function render(string $key, array $variables = []): string { $template = $this->repository->findByKey($key); - $body = $template?->getBody() ?? self::DEFAULTS[$key] - ?? throw new \InvalidArgumentException("Unknown prompt template key: {$key}"); + if (null === $template) { + throw new \RuntimeException("System prompt '{$key}' not found in database."); + } + $body = $template->getBody(); $search = array_map(static fn (string $k) => '{{'.$k.'}}', array_keys($variables)); return str_replace($search, array_values($variables), $body); @@ -106,10 +46,4 @@ PROMPT, 'json_coding' => ['schema', 'missingHint', 'specsText'], ]; } - - /** @return string */ - public static function defaultFor(string $key): string - { - return self::DEFAULTS[$key] ?? ''; - } } diff --git a/src/Infrastructure/Http/Controller/Admin/PromptTemplateCrudController.php b/src/Infrastructure/Http/Controller/Admin/PromptTemplateCrudController.php index 314e696..11fcd9f 100644 --- a/src/Infrastructure/Http/Controller/Admin/PromptTemplateCrudController.php +++ b/src/Infrastructure/Http/Controller/Admin/PromptTemplateCrudController.php @@ -6,13 +6,15 @@ namespace App\Infrastructure\Http\Controller\Admin; use App\Domain\AI\PromptTemplate; use App\Infrastructure\AI\PromptTemplateService; -use Symfony\Component\Translation\TranslatableMessage; +use EasyCorp\Bundle\EasyAdminBundle\Config\Action; +use EasyCorp\Bundle\EasyAdminBundle\Config\Actions; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController; use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField; use EasyCorp\Bundle\EasyAdminBundle\Field\IdField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField; use EasyCorp\Bundle\EasyAdminBundle\Field\TextField; +use Symfony\Component\Translation\TranslatableMessage; /** @extends AbstractCrudController */ final class PromptTemplateCrudController extends AbstractCrudController @@ -30,17 +32,17 @@ final class PromptTemplateCrudController extends AbstractCrudController ->setDefaultSort(['key' => 'ASC']); } - public function createEntity(string $entityFqcn): PromptTemplate + public function configureActions(Actions $actions): Actions { - return new PromptTemplate('', ''); + return $actions->disable(Action::NEW, Action::DELETE); } public function configureFields(string $pageName): iterable { yield IdField::new('id')->hideOnForm()->hideOnIndex(); yield TextField::new('key', new TranslatableMessage('field.prompt_key', [], 'admin')) - ->setHelp(new TranslatableMessage('field.prompt_key_help', [], 'admin')) - ->setColumns(4); + ->setColumns(4) + ->setFormTypeOption('disabled', true); yield TextareaField::new('body', new TranslatableMessage('field.prompt_body', [], 'admin')) ->setHelp($this->buildVariableHelp()) ->setNumOfRows(18) @@ -64,7 +66,6 @@ final class PromptTemplateCrudController extends AbstractCrudController $lines[] = "
  • {$key}: {$varList}
  • "; } $lines[] = ''; - $lines[] = 'If no DB entry exists for a key, the built-in default is used automatically.'; return implode('', $lines); }