150 lines
4.1 KiB
PHP
150 lines
4.1 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace Core\Agentic\Jobs;
|
||
|
|
|
||
|
|
use Core\Agentic\Services\AgenticManager;
|
||
|
|
use Mod\Content\Models\ContentTask;
|
||
|
|
use Mod\Content\Services\ContentProcessingService;
|
||
|
|
use Core\Mod\Tenant\Services\EntitlementService;
|
||
|
|
use Illuminate\Bus\Queueable;
|
||
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||
|
|
use Illuminate\Queue\InteractsWithQueue;
|
||
|
|
use Illuminate\Queue\SerializesModels;
|
||
|
|
use Throwable;
|
||
|
|
|
||
|
|
class ProcessContentTask implements ShouldQueue
|
||
|
|
{
|
||
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||
|
|
|
||
|
|
public int $tries = 3;
|
||
|
|
|
||
|
|
public int $backoff = 60;
|
||
|
|
|
||
|
|
public int $timeout = 300;
|
||
|
|
|
||
|
|
public function __construct(
|
||
|
|
public ContentTask $task
|
||
|
|
) {
|
||
|
|
$this->onQueue('ai');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function handle(
|
||
|
|
AgenticManager $ai,
|
||
|
|
ContentProcessingService $processor,
|
||
|
|
EntitlementService $entitlements
|
||
|
|
): void {
|
||
|
|
$this->task->markProcessing();
|
||
|
|
|
||
|
|
$prompt = $this->task->prompt;
|
||
|
|
|
||
|
|
if (! $prompt) {
|
||
|
|
$this->task->markFailed('Prompt not found');
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check AI credits entitlement
|
||
|
|
$workspace = $this->task->workspace;
|
||
|
|
|
||
|
|
if ($workspace) {
|
||
|
|
$result = $entitlements->can($workspace, 'ai.credits');
|
||
|
|
|
||
|
|
if ($result->isDenied()) {
|
||
|
|
$this->task->markFailed("Entitlement denied: {$result->message}");
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$provider = $ai->provider($prompt->model);
|
||
|
|
|
||
|
|
if (! $provider->isAvailable()) {
|
||
|
|
$this->task->markFailed("AI provider '{$prompt->model}' is not configured");
|
||
|
|
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Interpolate variables into the user template
|
||
|
|
$userPrompt = $this->interpolateVariables(
|
||
|
|
$prompt->user_template,
|
||
|
|
$this->task->input_data ?? []
|
||
|
|
);
|
||
|
|
|
||
|
|
$response = $provider->generate(
|
||
|
|
$prompt->system_prompt,
|
||
|
|
$userPrompt,
|
||
|
|
$prompt->model_config ?? []
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->task->markCompleted($response->content, [
|
||
|
|
'tokens_input' => $response->inputTokens,
|
||
|
|
'tokens_output' => $response->outputTokens,
|
||
|
|
'model' => $response->model,
|
||
|
|
'duration_ms' => $response->durationMs,
|
||
|
|
'estimated_cost' => $response->estimateCost(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Record AI usage
|
||
|
|
if ($workspace) {
|
||
|
|
$entitlements->recordUsage(
|
||
|
|
$workspace,
|
||
|
|
'ai.credits',
|
||
|
|
quantity: 1,
|
||
|
|
metadata: [
|
||
|
|
'task_id' => $this->task->id,
|
||
|
|
'prompt_id' => $prompt->id,
|
||
|
|
'model' => $response->model,
|
||
|
|
'tokens_input' => $response->inputTokens,
|
||
|
|
'tokens_output' => $response->outputTokens,
|
||
|
|
'estimated_cost' => $response->estimateCost(),
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If this task has a target, process the output
|
||
|
|
if ($this->task->target_type && $this->task->target_id) {
|
||
|
|
$this->processOutput($response->content, $processor);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function failed(Throwable $exception): void
|
||
|
|
{
|
||
|
|
$this->task->markFailed($exception->getMessage());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Interpolate template variables.
|
||
|
|
*/
|
||
|
|
private function interpolateVariables(string $template, array $data): string
|
||
|
|
{
|
||
|
|
foreach ($data as $key => $value) {
|
||
|
|
if (is_string($value)) {
|
||
|
|
$template = str_replace("{{{$key}}}", $value, $template);
|
||
|
|
} elseif (is_array($value)) {
|
||
|
|
$template = str_replace("{{{$key}}}", json_encode($value), $template);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $template;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Process the AI output based on target type.
|
||
|
|
*/
|
||
|
|
private function processOutput(string $content, ContentProcessingService $processor): void
|
||
|
|
{
|
||
|
|
$target = $this->task->target;
|
||
|
|
|
||
|
|
if (! $target) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle different target types
|
||
|
|
// This can be extended for different content types
|
||
|
|
// For now, just log that processing occurred
|
||
|
|
}
|
||
|
|
}
|