implemented step 14 by Gemini
- added LLMpool, worker script
This commit is contained in:
@ -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
42
scripts/worker.php
Normal 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);
|
||||
@ -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
164
src/Services/LLMpool.php
Normal 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
72
tests/test_llmpool.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user