feat: implement AI styling foundation and billing scope rework
This commit is contained in:
148
app/Jobs/PollAiEditRequest.php
Normal file
148
app/Jobs/PollAiEditRequest.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\AiEditOutput;
|
||||
use App\Models\AiEditRequest;
|
||||
use App\Models\AiProviderRun;
|
||||
use App\Services\AiEditing\AiEditingRuntimeConfig;
|
||||
use App\Services\AiEditing\AiImageProviderManager;
|
||||
use App\Services\AiEditing\AiUsageLedgerService;
|
||||
use App\Services\AiEditing\Safety\AiSafetyPolicyService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class PollAiEditRequest implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
/**
|
||||
* @var array<int, int>
|
||||
*/
|
||||
public array $backoff = [20, 60, 120];
|
||||
|
||||
public int $timeout = 60;
|
||||
|
||||
public function __construct(
|
||||
private readonly int $requestId,
|
||||
private readonly string $providerTaskId,
|
||||
private readonly int $pollAttempt = 1,
|
||||
) {
|
||||
$this->onQueue((string) config('ai-editing.queue.name', 'default'));
|
||||
}
|
||||
|
||||
public function handle(
|
||||
AiImageProviderManager $providers,
|
||||
AiSafetyPolicyService $safetyPolicy,
|
||||
AiEditingRuntimeConfig $runtimeConfig,
|
||||
AiUsageLedgerService $usageLedger
|
||||
): void {
|
||||
$request = AiEditRequest::query()->with('outputs')->find($this->requestId);
|
||||
if (! $request || $request->status !== AiEditRequest::STATUS_PROCESSING) {
|
||||
return;
|
||||
}
|
||||
|
||||
$run = AiProviderRun::query()->create([
|
||||
'request_id' => $request->id,
|
||||
'provider' => $request->provider,
|
||||
'attempt' => ((int) $request->providerRuns()->max('attempt')) + 1,
|
||||
'provider_task_id' => $this->providerTaskId,
|
||||
'status' => AiProviderRun::STATUS_RUNNING,
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$result = $providers->forProvider($request->provider)->poll($request, $this->providerTaskId);
|
||||
|
||||
$run->forceFill([
|
||||
'status' => $result->status === 'succeeded' ? AiProviderRun::STATUS_SUCCEEDED : ($result->status === 'processing' ? AiProviderRun::STATUS_RUNNING : AiProviderRun::STATUS_FAILED),
|
||||
'http_status' => $result->httpStatus,
|
||||
'finished_at' => $result->status === 'processing' ? null : now(),
|
||||
'duration_ms' => $run->started_at ? (int) max(0, $run->started_at->diffInMilliseconds(now())) : null,
|
||||
'cost_usd' => $result->costUsd,
|
||||
'request_payload' => $result->requestPayload,
|
||||
'response_payload' => $result->responsePayload,
|
||||
'error_message' => $result->failureMessage,
|
||||
])->save();
|
||||
|
||||
if ($result->status === 'succeeded') {
|
||||
$outputDecision = $safetyPolicy->evaluateProviderOutput($result);
|
||||
if ($outputDecision->blocked) {
|
||||
$request->forceFill([
|
||||
'status' => AiEditRequest::STATUS_BLOCKED,
|
||||
'safety_state' => $outputDecision->state,
|
||||
'safety_reasons' => $outputDecision->reasonCodes,
|
||||
'failure_code' => $outputDecision->failureCode ?? 'output_policy_blocked',
|
||||
'failure_message' => $outputDecision->failureMessage,
|
||||
'completed_at' => now(),
|
||||
])->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($result->outputs as $output) {
|
||||
AiEditOutput::query()->updateOrCreate(
|
||||
[
|
||||
'request_id' => $request->id,
|
||||
'provider_asset_id' => (string) Arr::get($output, 'provider_asset_id', $this->providerTaskId),
|
||||
],
|
||||
[
|
||||
'provider_url' => Arr::get($output, 'provider_url'),
|
||||
'mime_type' => Arr::get($output, 'mime_type'),
|
||||
'width' => Arr::get($output, 'width'),
|
||||
'height' => Arr::get($output, 'height'),
|
||||
'is_primary' => true,
|
||||
'safety_state' => 'passed',
|
||||
'safety_reasons' => [],
|
||||
'generated_at' => now(),
|
||||
'metadata' => ['provider' => $request->provider],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$request->forceFill([
|
||||
'status' => AiEditRequest::STATUS_SUCCEEDED,
|
||||
'safety_state' => 'passed',
|
||||
'safety_reasons' => [],
|
||||
'failure_code' => null,
|
||||
'failure_message' => null,
|
||||
'completed_at' => now(),
|
||||
])->save();
|
||||
|
||||
$usageLedger->recordDebitForRequest($request->fresh(), $result->costUsd, [
|
||||
'source' => 'poll_job',
|
||||
'poll_attempt' => $this->pollAttempt,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($result->status === 'processing') {
|
||||
$maxPolls = $runtimeConfig->maxPolls();
|
||||
if ($this->pollAttempt < $maxPolls) {
|
||||
self::dispatch($request->id, $this->providerTaskId, $this->pollAttempt + 1)
|
||||
->delay(now()->addSeconds(20))
|
||||
->onQueue($runtimeConfig->queueName());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$request->forceFill([
|
||||
'status' => $result->status === 'blocked' ? AiEditRequest::STATUS_BLOCKED : AiEditRequest::STATUS_FAILED,
|
||||
'safety_state' => $result->safetyState ?? $request->safety_state,
|
||||
'safety_reasons' => $result->safetyReasons !== [] ? $result->safetyReasons : $request->safety_reasons,
|
||||
'failure_code' => $result->failureCode,
|
||||
'failure_message' => $result->failureMessage,
|
||||
'completed_at' => now(),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user