implemented step 14 by Gemini

- added LLMpool, worker script
This commit is contained in:
2026-06-14 14:39:51 +02:00
parent ec698f3f34
commit d07bc89eea
5 changed files with 281 additions and 2 deletions

View File

@ -139,7 +139,8 @@ try {
if (!$projectId) {
sendResponse(false, ['code' => 'MISSING_PROJECT_ID', 'message' => 'Project ID is required.'], 400);
}
sendResponse(true, $taskActions->generateWebsite($userId, $projectId));
$result = $taskActions->generateWebsite($userId, $projectId);
sendResponse(true, $result);
break;
default:

42
scripts/worker.php Normal file
View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* LLMpool Worker CLI Script
* This script processes the AI generation queue.
*/
require_once __DIR__ . '/../vendor/autoload.php';
// Load configuration from root .env
\App\Utils\Config::load(__DIR__ . '/../.env');
use App\Services\LLMpool;
// Ensure this script is only run from CLI
if (php_sapi_name() !== 'cli') {
die("This script can only be run from the command line." . PHP_EOL);
}
$lockFile = sys_get_temp_dir() . '/webwizard_worker.lock';
$fp = fopen($lockFile, 'w');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
die("[" . date('Y-m-d H:i:s') . "] Worker is already running. Skipping." . PHP_EOL);
}
echo "[" . date('Y-m-d H:i:s') . "] Starting LLMpool worker..." . PHP_EOL;
try {
$worker = new LLMpool();
$worker->processQueue();
} catch (Throwable $e) {
echo "[" . date('Y-m-d H:i:s') . "] FATAL ERROR: " . $e->getMessage() . PHP_EOL;
}
echo "[" . date('Y-m-d H:i:s') . "] Finished LLMpool worker." . PHP_EOL;
flock($fp, LOCK_UN);
fclose($fp);
unlink($lockFile);

View File

@ -9,7 +9,7 @@ use Exception;
class DAIClient
{
private string $apiUrl;
private int $timeout = 60;
private int $timeout = 120;
public function __construct(?string $apiUrl = null)
{

164
src/Services/LLMpool.php Normal file
View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Actions\ProjectActions;
use App\Prompts\ContentPrompt;
use Exception;
use Throwable;
class LLMpool
{
private FileStorage $storage;
private DAIClient $daiClient;
private ContentPrompt $promptGenerator;
public function __construct()
{
$this->storage = new FileStorage();
$this->daiClient = new DAIClient();
$this->promptGenerator = new ContentPrompt();
}
/**
* Processes all queued tasks.
*/
public function processQueue(): void
{
$tasksDir = realpath(__DIR__ . '/../../data/llm');
$files = glob($tasksDir . '/t_*.json');
foreach ($files as $file) {
$this->processTask(basename($file));
}
}
/**
* Processes a single task by its filename.
*/
public function processTask(string $taskFilename): bool
{
$taskPath = "llm/$taskFilename";
$task = $this->storage->get($taskPath);
if (!$task || $task['status'] !== 'queued') {
return false;
}
// 1. Lock task (atomic update to 'generating')
$task['status'] = 'generating';
$task['attempt_count']++;
$task['started_at'] = date('c');
$this->storage->put($taskPath, $task);
$projectId = $task['project_id'];
// Update project status to 'generating'
$projectData = $this->storage->get("projects/{$projectId}.json");
if ($projectData) {
$projectData['status'] = 'generating';
$this->storage->put("projects/{$projectId}.json", $projectData);
}
$projectActions = new ProjectActions();
try {
// 2. Generate Prompt
$prompt = $this->promptGenerator->generate($task['wizard_data']);
// 3. Call AI
$aiResponse = $this->daiClient->sendRequest($prompt);
if ($aiResponse === null) {
throw new Exception("AI API returned no answer.");
}
// Update project status to 'rendering'
if ($projectData) {
$projectData['status'] = 'rendering';
$this->storage->put("projects/{$projectId}.json", $projectData);
}
// 4. Validate and Parse
$content = $this->validateAndParseResponse($aiResponse);
// 5. Update Project
$projectData = $this->storage->get("projects/{$projectId}.json");
if (!$projectData) {
throw new Exception("Project $projectId not found during processing.");
}
$projectData['content']['generated'] = $content;
$projectData['status'] = 'ready'; // Transition to ready
$projectData['current_step'] = 7; // Automatically ready for preview
$projectData['updated_at'] = date('c');
$this->storage->put("projects/{$projectId}.json", $projectData);
// 6. Finish Task (Delete or Archive)
$this->storage->delete($taskPath);
error_log("LLMpool: Task $taskFilename processed successfully for project $projectId.");
return true;
} catch (Throwable $e) {
error_log("LLMpool Error processing task $taskFilename: " . $e->getMessage());
$task['status'] = 'queued'; // Back to queue for retry
$task['last_error'] = $e->getMessage();
if ($task['attempt_count'] >= $task['max_attempts']) {
$task['status'] = 'failed';
// Update project status to failed as well
$projectData = $this->storage->get("projects/{$projectId}.json");
if ($projectData) {
$projectData['status'] = 'failed';
$projectData['last_error'] = $e->getMessage();
$this->storage->put("projects/{$projectId}.json", $projectData);
}
}
$this->storage->put($taskPath, $task);
return false;
}
}
/**
* Validates AI output: must be JSON, no HTML, has required sections.
*/
private function validateAndParseResponse(string $rawResponse): array
{
// Strip potential markdown code blocks if AI included them
$jsonStr = trim($rawResponse);
if (strpos($jsonStr, '```json') === 0) {
$jsonStr = substr($jsonStr, 7);
}
if (str_ends_with($jsonStr, '```')) {
$jsonStr = substr($jsonStr, 0, -3);
}
$jsonStr = trim($jsonStr);
$data = json_decode($jsonStr, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("Invalid JSON format from AI: " . json_last_error_msg());
}
// Check for HTML tags
if (preg_match('/<[a-z\/][^>]*>/i', $jsonStr)) {
throw new Exception("AI response contains prohibited HTML tags.");
}
// Check required sections
$required = ['seo', 'hero', 'about', 'services'];
foreach ($required as $section) {
if (!isset($data[$section])) {
throw new Exception("AI response is missing required section: $section");
}
}
return $data;
}
}

72
tests/test_llmpool.php Normal file
View File

@ -0,0 +1,72 @@
<?php
require 'vendor/autoload.php';
// Load configuration
\App\Utils\Config::load(__DIR__ . '/../.env');
use App\Services\LLMpool;
use App\Services\FileStorage;
use App\Actions\ProjectActions;
echo "Testing LLMpool Worker (Step 14)..." . PHP_EOL . PHP_EOL;
$storage = new FileStorage();
$projectActions = new ProjectActions();
// 1. Create a dummy project
$userId = 'u_test_worker';
$userData = [
'user_id' => $userId,
'created_at' => date('c'),
'projects' => []
];
$storage->put("users/{$userId}.json", $userData);
$project = $projectActions->createProject($userId);
$projectId = $project['project_id'];
// Mock some wizard data
$project['wizard_data'] = [
'business_category' => ['group' => 'gastro', 'subcategory' => 'cafe'],
'identity' => ['business_name' => 'Kaviareň u Robota', 'tagline' => 'Káva s dušou'],
'services' => ['items' => [['name' => 'Espresso', 'description' => 'Silná káva', 'price_from' => '2']]],
'smart_answers' => ['terrace' => true]
];
$storage->put("projects/{$projectId}.json", $project);
// 2. Create a dummy task
$taskId = 't_test_worker';
$taskData = [
'task_id' => $taskId,
'project_id' => $projectId,
'status' => 'queued',
'attempt_count' => 0,
'max_attempts' => 3,
'created_at' => date('c'),
'wizard_data' => $project['wizard_data']
];
$storage->put("llm/{$taskId}.json", $taskData);
echo "[INFO] Task created: $taskId for project $projectId" . PHP_EOL;
// 3. Run Worker
$worker = new LLMpool();
echo "[PROCESS] Running worker for task $taskId.json..." . PHP_EOL;
// Since we call real AI, it might take time
$success = $worker->processTask($taskId . '.json');
if ($success) {
echo "[SUCCESS] Worker finished successfully." . PHP_EOL;
$updatedProject = $storage->get("projects/{$projectId}.json");
echo "[INFO] Project status: " . $updatedProject['status'] . PHP_EOL;
echo "[INFO] Generated content keys: " . implode(', ', array_keys($updatedProject['content']['generated'])) . PHP_EOL;
} else {
echo "[FAIL] Worker failed to process task." . PHP_EOL;
$task = $storage->get("llm/{$taskId}.json");
if ($task) {
echo "[DEBUG] Task status: " . $task['status'] . PHP_EOL;
echo "[DEBUG] Last error: " . ($task['last_error'] ?? 'N/A') . PHP_EOL;
}
}