ModelRegistry.php

Code Hygiene Score: 78

Keine Issues gefunden.

Dependencies 4

Klassen 1

Funktionen 17

Verwendet von 9

Versionen 5

Code

<?php

declare(strict_types=1);

namespace Infrastructure\AI;

// @responsibility: Zentrale Modell-Registry aus ki_dev.ai_models

use Domain\Service\ModelRegistryInterface;
use PDO;

final class ModelRegistry implements ModelRegistryInterface
{
    private static ?array $cache = null;
    private static ?self $instance = null;

    public function __construct(
        private PDO $pdo
    ) {
    }

    /**
     * Get singleton instance (for static facade compatibility).
     * Must be initialized via setInstance() from DI container first.
     *
     * @throws \RuntimeException if not initialized
     */
    public static function getInstance(): self
    {
        if (self::$instance === null) {
            throw new \RuntimeException(
                'ModelRegistry not initialized. Use DI container or call setInstance() first.'
            );
        }

        return self::$instance;
    }

    /**
     * Set the singleton instance (called by DI container).
     */
    public static function setInstance(self $instance): void
    {
        self::$instance = $instance;
    }

    /**
     * Clear cache (e.g., after Ollama sync).
     */
    public static function clearCache(): void
    {
        self::$cache = null;
    }

    /**
     * Get all available chat models.
     *
     * @return array<string, string> [full_key => display_name]
     */
    public function getChatModels(): array
    {
        return $this->getModels(chat: true);
    }

    /**
     * Get all vision-capable models.
     *
     * @return array<string, string> [full_key => display_name]
     */
    public function getVisionModels(): array
    {
        return $this->getModels(vision: true);
    }

    /**
     * Get all embedding models.
     *
     * @return array<string, string> [full_key => display_name]
     */
    public function getEmbeddingModels(): array
    {
        return $this->getModels(embedding: true);
    }

    /**
     * Get models with optional filters.
     *
     * @return array<string, string> [full_key => display_name]
     */
    public function getModels(
        ?bool $chat = null,
        ?bool $vision = null,
        ?bool $embedding = null,
        ?string $provider = null
    ): array {
        $allModels = $this->loadAllModels();
        $result = [];

        foreach ($allModels as $model) {
            if (!$model['is_available']) {
                continue;
            }
            if ($chat !== null && (bool) $model['is_chat'] !== $chat) {
                continue;
            }
            if ($vision !== null && (bool) $model['is_vision'] !== $vision) {
                continue;
            }
            if ($embedding !== null && (bool) $model['is_embedding'] !== $embedding) {
                continue;
            }
            if ($provider !== null && $model['provider'] !== $provider) {
                continue;
            }

            $result[$model['full_key']] = $model['display_name'];
        }

        return $result;
    }

    /**
     * Get a single model by full_key.
     */
    public function getModel(string $fullKey): ?array
    {
        $allModels = $this->loadAllModels();

        foreach ($allModels as $model) {
            if ($model['full_key'] === $fullKey) {
                return $model;
            }
        }

        return null;
    }

    /**
     * Get display label for a model.
     */
    public function getLabel(string $fullKey): string
    {
        $model = $this->getModel($fullKey);

        return $model['display_name'] ?? $fullKey;
    }

    /**
     * Check if model exists and is available.
     */
    public function isValid(string $fullKey): bool
    {
        $model = $this->getModel($fullKey);

        return $model !== null && $model['is_available'];
    }

    /**
     * Check if model is local (Ollama).
     */
    public function isLocal(string $fullKey): bool
    {
        return str_starts_with($fullKey, 'ollama:');
    }

    /**
     * Get default chat model (first available by priority).
     */
    public function getDefaultChatModel(): string
    {
        $chatModels = $this->getChatModels();

        return array_key_first($chatModels) ?? 'ollama:mistral:latest';
    }

    /**
     * Get default vision model.
     */
    public function getDefaultVisionModel(): string
    {
        $visionModels = $this->getVisionModels();
        // Prefer local vision model
        foreach (array_keys($visionModels) as $key) {
            if (str_starts_with($key, 'ollama:')) {
                return $key;
            }
        }

        return array_key_first($visionModels) ?? 'ollama:minicpm-v:latest';
    }

    /**
     * Load all models from database (with caching).
     */
    private function loadAllModels(): array
    {
        if (self::$cache !== null) {
            return self::$cache;
        }

        $stmt = $this->pdo->query(
            'SELECT id, provider, model_id, display_name, full_key,
                    is_available, is_chat, is_embedding, is_vision,
                    context_length, parameters, priority
             FROM ai_models
             WHERE is_available = 1
             ORDER BY priority ASC'
        );

        self::$cache = $stmt->fetchAll(PDO::FETCH_ASSOC);

        return self::$cache;
    }

    /**
     * Sync models from Ollama (call after `ollama pull`).
     * Updates is_available and last_seen_at for Ollama models.
     */
    public function syncFromOllama(): array
    {
        $output = [];
        exec('ollama list 2>/dev/null', $output, $returnCode);

        if ($returnCode !== 0) {
            return ['error' => 'Could not run ollama list'];
        }

        $ollamaModels = [];
        foreach ($output as $line) {
            if (preg_match('/^(\S+)\s+/', $line, $matches)) {
                $modelName = $matches[1];
                if ($modelName !== 'NAME') {
                    $ollamaModels[] = $modelName;
                }
            }
        }

        // Mark all Ollama models as unavailable first
        $this->pdo->exec(
            "UPDATE ai_models SET is_available = 0 WHERE provider = 'ollama'"
        );

        $added = [];
        $updated = [];

        foreach ($ollamaModels as $modelId) {
            // Check if model exists
            $stmt = $this->pdo->prepare(
                'SELECT id FROM ai_models WHERE provider = ? AND model_id = ?'
            );
            $stmt->execute(['ollama', $modelId]);
            $existing = $stmt->fetch();

            if ($existing) {
                // Update existing
                $stmt = $this->pdo->prepare(
                    'UPDATE ai_models SET is_available = 1, last_seen_at = NOW()
                     WHERE provider = ? AND model_id = ?'
                );
                $stmt->execute(['ollama', $modelId]);
                $updated[] = $modelId;
            } else {
                // Insert new
                $displayName = $this->generateDisplayName($modelId);
                $isEmbedding = str_contains($modelId, 'embed');
                $isVision = str_contains($modelId, 'vision') || str_contains($modelId, 'minicpm-v');

                $stmt = $this->pdo->prepare(
                    'INSERT INTO ai_models
                     (provider, model_id, display_name, is_available, is_chat, is_embedding, is_vision, last_seen_at, priority)
                     VALUES (?, ?, ?, 1, ?, ?, ?, NOW(), 90)'
                );
                $stmt->execute([
                    'ollama',
                    $modelId,
                    $displayName,
                    $isEmbedding ? 0 : 1,
                    $isEmbedding ? 1 : 0,
                    $isVision ? 1 : 0,
                ]);
                $added[] = $modelId;
            }
        }

        self::clearCache();

        return [
            'ollama_models' => $ollamaModels,
            'added' => $added,
            'updated' => $updated,
        ];
    }

    /**
     * Generate display name from model ID.
     */
    private function generateDisplayName(string $modelId): string
    {
        // Remove version tags
        $name = preg_replace('/:latest$/', '', $modelId);

        // Capitalize and format
        $parts = explode(':', $name);
        $baseName = ucfirst($parts[0]);

        if (isset($parts[1])) {
            $baseName .= ' ' . strtoupper($parts[1]);
        }

        return $baseName . ' (lokal)';
    }
}
← Übersicht Graph