$facts chat_on, knowledge_on, has_bot, bot_public, ai (none|demo|real), ai_ready, budget, attached, usable, tested, on_site * @return array{steps: array, current: string, complete: bool, demo: bool} * reason: for a locked step, a code (needs_modules, needs_assistant, needs_ai, needs_budget, needs_knowledge, needs_steps) that SetupWizard turns into text * state: done | demo (the simulated demo answers, no real AI) | optional (knowledge only: none attached, a general-purpose assistant) | todo | locked */ public static function evaluate(array $facts): array { $f = $facts + ['chat_on' => false, 'knowledge_on' => false, 'has_bot' => false, 'bot_public' => false, 'ai' => 'none', 'ai_ready' => false, 'budget' => false, 'attached' => 0, 'usable' => 0, 'tested' => false, 'on_site' => false]; $modules = (bool) $f['chat_on'] && (bool) $f['knowledge_on']; $gate = !$modules ? 'needs_modules' : (!$f['has_bot'] ? 'needs_assistant' : ''); $locked = static fn(string $reason): array => ['state' => 'locked', 'reason' => $reason]; $step = static fn(bool $done): array => ['state' => $done ? 'done' : 'todo', 'reason' => '']; $steps = []; $steps['modules'] = $step($modules); $steps['assistant'] = !$modules ? $locked($gate) : $step((bool) $f['has_bot']); if ($gate !== '') { foreach (['ai', 'budget', 'knowledge', 'test', 'publish'] as $key) { $steps[$key] = $locked($gate); } } else { $steps['ai'] = $f['ai_ready'] ? ['state' => $f['ai'] === 'demo' ? 'demo' : 'done', 'reason' => ''] : ['state' => 'todo', 'reason' => '']; $steps['budget'] = $step((bool) $f['budget']); // Knowledge is what makes an assistant knowledge-grounded. With none attached it is a general-purpose assistant (the chat // service then answers from the AI model and the instruction alone), which is supported: the step is optional, not missing. $steps['knowledge'] = (int) $f['usable'] > 0 ? ['state' => 'done', 'reason' => ''] : ['state' => (int) $f['attached'] > 0 ? 'todo' : 'optional', 'reason' => '']; $steps['test'] = !$f['ai_ready'] ? $locked('needs_ai') : (!$f['budget'] ? $locked('needs_budget') : $step((bool) $f['tested'])); $steps['publish'] = !$f['ai_ready'] ? $locked('needs_ai') : (!$f['budget'] ? $locked('needs_budget') : ((int) $f['attached'] > 0 && (int) $f['usable'] < 1 ? $locked('needs_knowledge') : $step((bool) $f['on_site']))); } // The test is recommended, not required: it is not part of completion. $complete = true; foreach (['modules', 'assistant', 'budget', 'publish'] as $key) { $complete = $complete && $steps[$key]['state'] === 'done'; } $complete = $complete && in_array($steps['ai']['state'], ['done', 'demo'], true) && in_array($steps['knowledge']['state'], ['done', 'optional'], true); $steps['done'] = $complete ? ['state' => 'done', 'reason' => ''] : $locked('needs_steps'); $current = 'modules'; if ($complete) { $current = 'done'; } else { foreach (self::ORDER as $key) { if (in_array($steps[$key]['state'], ['todo', 'optional'], true)) { $current = $key; break; } } } return ['steps' => $steps, 'current' => $current, 'complete' => $complete, 'demo' => $complete && $f['ai'] === 'demo']; } /** * The step to show for a requested address. A request is only a request: an unknown, locked or not yet reachable step falls * back to the first unfinished one, so the address is never evidence that anything is done or allowed. * * @param array{steps: array, current: string} $evaluation */ public static function stepFor(?string $requested, array $evaluation): string { if ($requested !== null && in_array($requested, self::STEPS, true) && $evaluation['steps'][$requested]['state'] !== 'locked') { return $requested; } return $evaluation['current']; } /** * Turns on Chat and Knowledge through the module registry (its prerequisite and dependency checks apply unchanged). Each module * is tried on its own, so one that cannot be enabled never hides that the other is on. Safe to repeat. * * @return array{on: list, failed: list} modules that are on afterwards, and modules that could not be enabled */ public static function enableModules(ModuleRegistry $modules): array { $on = []; $failed = []; foreach (self::MODULES as $id) { try { $modules->enable($id); } catch (\Throwable) { // reported below from the real state, never from the exception text } if ($modules->isEnabled($id)) { $on[] = $id; } else { $failed[] = $id; } } return ['on' => $on, 'failed' => $failed]; } }