ChatPromptLoader.php

Code Hygiene Score: 90

Keine Issues gefunden.

Dependencies 2

Klassen 1

Funktionen 9

Verwendet von 3

Versionen 1

Code

<?php

declare(strict_types=1);

namespace UseCases\Chat;

// @responsibility: Lädt Chat-Prompts aus ContentConfig-Repository

use Domain\Repository\ContentConfigRepositoryInterface;

final class ChatPromptLoader
{
    public function __construct(
        private ContentConfigRepositoryInterface $configRepo
    ) {
    }

    /**
     * Load style prompt from author profile.
     *
     * @throws \RuntimeException When profile cannot be parsed
     */
    public function getStylePrompt(int $profileId): ?string
    {
        if ($profileId === 0) {
            return null;
        }

        $profile = $this->configRepo->getAuthorProfile($profileId);
        if ($profile === null) {
            return null;
        }

        $config = json_decode($profile['content'] ?? '{}', true);
        if ($config === null) {
            return null;
        }

        $parts = [];

        if (isset($config['stimme']['ton'])) {
            $parts[] = 'Ton: ' . $config['stimme']['ton'];
        }
        if (isset($config['stimme']['perspektive'])) {
            $parts[] = 'Perspektive: ' . $config['stimme']['perspektive'];
        }
        if (isset($config['stil']['fachsprache']) && $config['stil']['fachsprache']) {
            $parts[] = 'Verwende Fachsprache';
        }
        if (isset($config['stil']['beispiele']) && $config['stil']['beispiele'] === 'häufig') {
            $parts[] = 'Nutze häufig Beispiele';
        }
        if (isset($config['stil']['listen']) && $config['stil']['listen'] === 'bevorzugt') {
            $parts[] = 'Bevorzuge Listen und Bullet-Points';
        }
        if (isset($config['tabus']) && is_array($config['tabus'])) {
            $parts[] = 'Vermeide: ' . implode(', ', $config['tabus']);
        }

        if ($parts === []) {
            return null;
        }

        return 'Schreibstil (' . ($profile['name'] ?? 'Profil') . '): ' . implode('. ', $parts) . '.';
    }

    /**
     * Load system prompt by ID.
     *
     * @throws \RuntimeException When prompt cannot be parsed
     */
    public function getSystemPrompt(int $promptId): ?string
    {
        if ($promptId === 0) {
            return null;
        }

        $prompt = $this->configRepo->getSystemPrompt($promptId);
        if ($prompt === null) {
            return null;
        }

        $content = json_decode($prompt['content'] ?? '{}', true);

        return $content['prompt'] ?? null;
    }

    /**
     * Load structure prompt.
     *
     * @throws \RuntimeException When structure cannot be parsed
     */
    public function getStructurePrompt(int $structureId): ?string
    {
        if ($structureId === 0) {
            return null;
        }

        $structure = $this->configRepo->getStructure($structureId);
        if ($structure === null) {
            return null;
        }

        $name = $structure['name'] ?? 'Struktur';
        $config = json_decode($structure['content'] ?? '{}', true);
        if ($config === null) {
            return null;
        }

        // Complex structure (Blog-Beitrag, LinkedIn-Beitrag etc.)
        if (isset($config['ausgabe']) || isset($config['aufbau']) || isset($config['gesamtaufbau'])) {
            return $this->buildComplexStructurePrompt($name, $config);
        }

        // Simple structure (legacy format)
        $parts = ["Formatiere deine Antwort als: {$name}"];

        if (isset($config['sections']) && is_array($config['sections'])) {
            $parts[] = 'Struktur: ' . implode(' → ', $config['sections']);
        }
        if (isset($config['max_chars'])) {
            $parts[] = 'Maximale Länge: ' . $config['max_chars'] . ' Zeichen';
        }
        if (isset($config['min_words'])) {
            $parts[] = 'Mindestens ' . $config['min_words'] . ' Wörter';
        }
        if (isset($config['max_words'])) {
            $parts[] = 'Maximal ' . $config['max_words'] . ' Wörter';
        }
        if (isset($config['format']) && $config['format'] === 'qa') {
            $min = $config['min_questions'] ?? 3;
            $parts[] = "Formatiere als FAQ mit mindestens {$min} Frage-Antwort-Paaren";
        }
        if (isset($config['hashtags']) && $config['hashtags']) {
            $parts[] = 'Füge passende Hashtags hinzu';
        }
        if (isset($config['cta']) && $config['cta']) {
            $parts[] = 'Schließe mit einem Call-to-Action ab';
        }

        return implode('. ', $parts) . '.';
    }

    /**
     * Build prompt from complex structure config.
     *
     * @param array<string, mixed> $config
     */
    private function buildComplexStructurePrompt(string $name, array $config): string
    {
        $prompt = "## Ausgabeformat: {$name}\n\n";
        $prompt .= "Befolge diese Struktur-Vorgaben exakt:\n\n";
        $prompt .= $this->configToYaml($config, 0);

        return $prompt;
    }

    /**
     * Convert config array to readable YAML-like format.
     *
     * @param array<string, mixed> $config
     */
    private function configToYaml(array $config, int $indent): string
    {
        $result = '';
        $prefix = str_repeat('  ', $indent);

        foreach ($config as $key => $value) {
            if (is_array($value)) {
                if ($this->isIndexedArray($value)) {
                    $result .= "{$prefix}{$key}:\n";
                    foreach ($value as $item) {
                        if (is_array($item)) {
                            $result .= "{$prefix}- " . trim($this->configToYaml($item, 0)) . "\n";
                        } else {
                            $result .= "{$prefix}- {$item}\n";
                        }
                    }
                } else {
                    $result .= "{$prefix}{$key}:\n";
                    $result .= $this->configToYaml($value, $indent + 1);
                }
            } else {
                $result .= "{$prefix}{$key}: {$value}\n";
            }
        }

        return $result;
    }

    /**
     * Check if array is indexed (list) vs associative.
     *
     * @param array<mixed> $arr
     */
    private function isIndexedArray(array $arr): bool
    {
        if ($arr === []) {
            return true;
        }

        return array_keys($arr) === range(0, count($arr) - 1);
    }

    /**
     * Get structure name by ID.
     */
    public function getStructureName(int $structureId): ?string
    {
        if ($structureId === 0) {
            return null;
        }

        $structure = $this->configRepo->getStructure($structureId);

        return $structure['name'] ?? null;
    }

    /**
     * Compose effective system prompt from components.
     */
    public function composeSystemPrompt(
        int $systemPromptId,
        int $structureId,
        int $authorProfileId,
        bool $hasContext,
        string $defaultRagPrompt,
        string $defaultNoContextPrompt
    ): string {
        $systemPrompt = $this->getSystemPrompt($systemPromptId);
        $structurePrompt = $this->getStructurePrompt($structureId);
        $stylePrompt = $this->getStylePrompt($authorProfileId);

        // Add structure to system prompt
        if ($structurePrompt !== null) {
            $systemPrompt = ($systemPrompt ?? '') . "\n\n" . $structurePrompt;
        }

        // Use default if no system prompt
        $effectivePrompt = $systemPrompt ?? ($hasContext ? $defaultRagPrompt : $defaultNoContextPrompt);

        // Add style prompt
        if ($stylePrompt !== null && $stylePrompt !== '') {
            $effectivePrompt .= "\n\n" . $stylePrompt;
        }

        return $effectivePrompt;
    }
}
← Übersicht Graph