aiConfiguration->makeDriver($companyId); if ($driver === null) { throw new AiException('AI is not enabled for this company', 'ai_disabled'); } $resolved = $this->aiConfiguration->resolveForCompany($companyId); if (empty($resolved['text_generation_enabled'])) { throw new AiException('Text generation is not enabled for this company', 'text_generation_disabled'); } $model = (string) ($resolved['ai_text_generation_model'] ?? ''); if ($model === '') { throw new AiException('No text generation model configured', 'missing_model'); } $fullPrompt = $this->buildPrompt($prompt, $context); return trim($driver->textCompletion($fullPrompt, $model)); } /** * Compose the final prompt sent to the model. * * Keep the framing terse — text-generation output should not be padded * with "Here is the text you requested:" preambles. The instruction is * always placed last so the model gives it the most weight. */ protected function buildPrompt(string $prompt, ?string $context): string { $system = "You are a helpful writing assistant embedded in an invoicing application. Generate text based on the user's instruction. Rules:\n" .'- Return only the requested text. No preamble, no explanation, no markdown code fences.'."\n" .'- Match the tone and language implied by the instruction.'."\n" .'- Be concise unless explicitly asked for length.'; if ($context !== null && trim($context) !== '') { return $system."\n\n" ."Context (current content the user is working with):\n" .trim($context) ."\n\n" ."Instruction: {$prompt}"; } return $system."\n\nInstruction: {$prompt}"; } }